This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit ad28481527110acb0989f1772a31212e074a5560
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 15:33:14 2026 -0400

    READY-395: Don't mount unauthenticated /info and /loggers actuator 
endpoints by default
---
 .../management/BasicActuatorGroup.java             | 26 +++++--
 .../management/BasicActuatorGroup_Test.java        | 86 ++++++++++++++++------
 2 files changed, 82 insertions(+), 30 deletions(-)

diff --git 
a/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/management/BasicActuatorGroup.java
 
b/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/management/BasicActuatorGroup.java
index 28784b2af8..7f6a6b5e03 100644
--- 
a/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/management/BasicActuatorGroup.java
+++ 
b/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/management/BasicActuatorGroup.java
@@ -28,13 +28,26 @@ import org.apache.juneau.rest.server.servlet.*;
  * Mounts the management endpoints by composing their {@code *Mixin} flavors 
via
  * {@link Rest#mixins() @Rest(mixins=...)} on top of {@link 
BasicRestServletGroup}:
  * <ul>
- *     <li>{@code /info} &mdash; {@link InfoMixin} (manifest/build/version/git 
metadata)
- *     <li>{@code /loggers}, {@code /loggers/{name}} &mdash; {@link 
LoggersMixin} (runtime JUL level get/set)
  *     <li>{@code /healthz}, {@code /readyz}, {@code /livez} &mdash; {@link 
HealthMixin} (read-through health view)
  *     <li>{@code /threaddump}, {@code /heapdump} &mdash; {@link DumpsMixin} 
(deny-by-default diagnostics)
  * </ul>
  *
  * <p>
+ * <b>{@code /info} and {@code /loggers} are off by default</b> &mdash; unlike 
health, they are not mounted
+ * on the bare group, since {@link InfoMixin} discloses the full manifest and 
{@link LoggersMixin}'s read side
+ * discloses logger topology, neither behind any auth. Turn either back on 
(either approach suffices):
+ * <ul>
+ *     <li><b>A-la-carte (recommended):</b> mount {@link InfoMixin} / {@link 
LoggersMixin} on a resource you
+ *             control, e.g. a {@code BasicActuatorGroup} subclass with
+ *             {@code @Rest(mixins={InfoMixin.class, LoggersMixin.class})} 
&mdash; mixins declared on a subclass are
+ *             additive to the mixins inherited from this class, so {@code 
HealthMixin}/{@code DumpsMixin} stay mounted
+ *             too.
+ *     <li><b>Zero-code:</b> not currently supported &mdash; {@code 
@Rest(mixins=...)} is a compile-time class
+ *             list, and the {@code RestContext.Builder} injection point that 
would have let a system property add a
+ *             mixin at init time was removed prior to 10.0.0 (see {@link 
RestInit}).  Use the a-la-carte subclass above.
+ * </ul>
+ *
+ * <p>
  * <b>Path prefix:</b> defaults to {@code /actuator} and is configurable via 
the {@code juneau.actuator.path}
  * system property (resolved through the standard {@code $S{...}} SVL var).  
Mount the group at a custom prefix
  * either by setting that property or by subclassing with your own {@link 
Rest#path() @Rest(path=...)}.
@@ -46,10 +59,9 @@ import org.apache.juneau.rest.server.servlet.*;
  * (which this module does not depend on), so add {@code MetricsMixin} 
a-la-carte when that module is present.
  *
  * <p>
- * <b>Exposure policy:</b> the read endpoints ({@code /info}, {@code /loggers} 
read, health) are on; the
- * mutating/sensitive ones ({@code /loggers} write, {@code /threaddump}, 
{@code /heapdump}) are deny-by-default
- * &mdash; the dumps via {@link DumpsSettings}, and logger-writes should be 
guarded by the consumer.  No auth
- * provider is auto-wired (explicit-over-magic).
+ * <b>Exposure policy:</b> of the mounted endpoints, health is on; the 
mutating/sensitive ones
+ * ({@code /threaddump}, {@code /heapdump}) are deny-by-default via {@link 
DumpsSettings}.  No auth provider is
+ * auto-wired (explicit-over-magic).
  *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ManagementSurface";>Management 
Surface</a>
@@ -63,8 +75,6 @@ import org.apache.juneau.rest.server.servlet.*;
        title="Management",
        description="Actuator-style management surface.",
        mixins={
-               InfoMixin.class,
-               LoggersMixin.class,
                HealthMixin.class,
                DumpsMixin.class
        }
diff --git 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/management/BasicActuatorGroup_Test.java
 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/management/BasicActuatorGroup_Test.java
index 223f36149c..e24a494771 100644
--- 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/management/BasicActuatorGroup_Test.java
+++ 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/management/BasicActuatorGroup_Test.java
@@ -31,9 +31,9 @@ import org.apache.juneau.rest.server.servlet.*;
 import org.junit.jupiter.api.*;
 
 /**
- * Tests for {@link BasicActuatorGroup} &mdash; verifies the management 
endpoints are reachable under the
- * configured prefix, the deny-by-default gating on the diagnostics is 
honored, and the standalone flavors
- * remain independently mountable.
+ * Tests for {@link BasicActuatorGroup} &mdash; verifies that {@code /info} 
and {@code /loggers} are off by
+ * default (not disclosed to an unauthenticated client), that health and the 
deny-by-default diagnostics behave
+ * as before, and that a-la-carte subclassing restores {@code /info} and 
{@code /loggers}.
  */
 @SuppressWarnings({
        "resource" // Closeable MockRestClient fixtures; lifecycle managed by 
the test/framework, not a real leak.
@@ -58,9 +58,6 @@ class BasicActuatorGroup_Test extends TestBase {
                @Bean public DumpsSettings dumpsSettings() {
                        return 
DumpsSettings.create().enableThreadDump().enableHeapDump().build();
                }
-               @Bean public LoggersSettings loggersSettings() {
-                       return LoggersSettings.create().enableWrite().build();
-               }
        }
 
        @Rest(children={ActuatorChild.class})
@@ -73,22 +70,21 @@ class BasicActuatorGroup_Test extends TestBase {
                Logger.getLogger(LName).setLevel(null);
        }
 
-       @Test void a01_infoReachable() throws Exception {
+       @Test void a01_infoOffByDefault() throws Exception {
                var c = MockRestClient.buildLax(A.class);
-               
c.get("/actuator/info").accept("application/json").run().assertStatus(200)
-                       
.assertContent().asString().isContains("Implementation-Version", "10.0.0");
+               
c.get("/actuator/info").accept("application/json").run().assertStatus(404);
        }
 
-       @Test void a02_loggersReadReachable() throws Exception {
+       @Test void a02_loggersReadOffByDefault() throws Exception {
                var c = MockRestClient.buildLax(A.class);
-               
c.get("/actuator/loggers").accept("application/json").run().assertStatus(200)
-                       .assertContent().asString().isContains("ROOT");
+               
c.get("/actuator/loggers").accept("application/json").run().assertStatus(404);
        }
 
-       @Test void a03_loggersWriteRoundTrip() throws Exception {
+       @Test void a03_loggersWriteOffByDefault() throws Exception {
                var c = MockRestClient.buildLax(A.class);
-               c.put("/actuator/loggers/" + LName, 
"FINE").accept("application/json").run().assertStatus(200);
-               assertEquals(Level.FINE, Logger.getLogger(LName).getLevel());
+               // The entire LoggersMixin (read and write) is unmounted by 
default, so the write side is also
+               // unreachable regardless of the child's LoggersSettings bean.
+               c.put("/actuator/loggers/" + LName, 
"FINE").accept("application/json").run().assertStatus(404);
        }
 
        @Test void a04_healthReachable() throws Exception {
@@ -124,16 +120,62 @@ class BasicActuatorGroup_Test extends TestBase {
                c.get("/actuator/heapdump").run().assertStatus(403);
        }
 
-       @Test void b03_loggersWriteDeniedByDefaultInGroup() throws Exception {
-               var c = MockRestClient.buildLax(B.class);
-               // Reads still work; the mutating set-level is denied without 
an opt-in LoggersSettings bean.
-               
c.get("/actuator/loggers").accept("application/json").run().assertStatus(200);
+       /**
+        * A-la-carte re-enablement: a subclass that adds {@link InfoMixin} and 
{@link LoggersMixin} restores both
+        * endpoints, on top of the {@link 
org.apache.juneau.rest.server.health.HealthMixin}/{@link DumpsMixin}
+        * inherited from {@link BasicActuatorGroup} (mixins declared on a 
subclass are additive, not a replacement).
+        */
+       @Rest(path="/actuator", mixins={InfoMixin.class, LoggersMixin.class})
+       public static class EnabledChild extends BasicActuatorGroup {
+               private static final long serialVersionUID = 1L;
+               @Bean public ManifestFile manifest() throws IOException { 
return BasicActuatorGroup_Test.manifest(); }
+       }
+
+       @Rest(children={EnabledChild.class})
+       public static class C extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test void c01_infoReachableWhenMixinAddedALaCarte() throws Exception {
+               var c = MockRestClient.buildLax(C.class);
+               
c.get("/actuator/info").accept("application/json").run().assertStatus(200)
+                       
.assertContent().asString().isContains("Implementation-Version", "10.0.0");
+       }
+
+       @Test void c02_loggersReadReachableWhenMixinAddedALaCarte() throws 
Exception {
+               var c = MockRestClient.buildLax(C.class);
+               
c.get("/actuator/loggers").accept("application/json").run().assertStatus(200)
+                       .assertContent().asString().isContains("ROOT");
+       }
+
+       @Test void c03_loggersWriteStillDeniedByDefaultWhenMixinAddedALaCarte() 
throws Exception {
+               var c = MockRestClient.buildLax(C.class);
+               // The read side is reachable once the mixin is mounted, but 
the mutating set-level endpoint keeps its
+               // own independent deny-by-default policy (no LoggersSettings 
bean registered on EnabledChild).
                c.put("/actuator/loggers/" + LName, 
"FINE").run().assertStatus(403);
        }
 
-       @Test void b02_infoStillReachableWithoutManifest() throws Exception {
-               // No manifest bean -> /info degrades to an empty map but stays 
reachable (200).
-               var c = MockRestClient.buildLax(B.class);
+       @Test void c04_healthAndDumpsStillMountedWhenMixinAddedALaCarte() 
throws Exception {
+               // Confirms subclass mixins are additive: adding Info/Loggers 
didn't drop the inherited Health/Dumps.
+               var c = MockRestClient.buildLax(C.class);
+               
c.get("/actuator/healthz").accept("application/json").run().assertStatus(200);
+               c.get("/actuator/threaddump").run().assertStatus(403);
+       }
+
+       /** A-la-carte {@link InfoMixin} with no manifest bean registered. */
+       @Rest(path="/actuator", mixins={InfoMixin.class})
+       public static class D01_BareEnabledChild extends BasicActuatorGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Rest(children={D01_BareEnabledChild.class})
+       public static class D01_D extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test void d01_infoStillReachableWithoutManifest() throws Exception {
+               // No manifest bean -> /info degrades to an empty map but stays 
reachable (200), once mounted a-la-carte.
+               var c = MockRestClient.buildLax(D01_D.class);
                
c.get("/actuator/info").accept("application/json").run().assertStatus(200).assertContent().asString().is("{}");
        }
 }

Reply via email to