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} — {@link InfoMixin} (manifest/build/version/git metadata) - * <li>{@code /loggers}, {@code /loggers/{name}} — {@link LoggersMixin} (runtime JUL level get/set) * <li>{@code /healthz}, {@code /readyz}, {@code /livez} — {@link HealthMixin} (read-through health view) * <li>{@code /threaddump}, {@code /heapdump} — {@link DumpsMixin} (deny-by-default diagnostics) * </ul> * * <p> + * <b>{@code /info} and {@code /loggers} are off by default</b> — 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})} — 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 — {@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 - * — 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} — 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} — 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("{}"); } }
