diff --git a/.claude/skills/run-integration-tests/SKILL.md b/.claude/skills/run-integration-tests/SKILL.md index a5946ef4..c6e3cbcc 100644 --- a/.claude/skills/run-integration-tests/SKILL.md +++ b/.claude/skills/run-integration-tests/SKILL.md @@ -53,7 +53,7 @@ certificate validation and look like a dead container: ## Reading logs -- **File logging is deployment-supplied, and the shipped default is OFF.** The artifact ships `quarkus.log.file.enable=false`; each gateway service switches it on with `QUARKUS_LOG_FILE_ENABLE=true` plus `LOG_FILE_PATH=/logs/.log`, and `/logs` is bind-mounted to `${LOG_TARGET_DIR:-integration-tests/target/quarkus-logs}` (a dedicated subdirectory, writable by the container's uid 1001 — the container can NOT write `/quarkus.log` on the read-only root FS). A service missing the enable flag silently produces no file; `ItProfileConfigBindingWiringTest` guards that pairing, and `ManagementPlainHttpOptOutIT` reads one of those files. +- **File logging is deployment-supplied, and the shipped default is OFF.** The artifact ships `quarkus.log.file.enabled=false`; each gateway service switches it on with `QUARKUS_LOG_FILE_ENABLED=true` plus `LOG_FILE_PATH=/logs/.log`, and `/logs` is bind-mounted to `${LOG_TARGET_DIR:-integration-tests/target/quarkus-logs}` (a dedicated subdirectory, writable by the container's uid 1001 — the container can NOT write `/quarkus.log` on the read-only root FS). A service missing the enable flag silently produces no file; `ItProfileConfigBindingWiringTest` guards the enable flag, and nothing more — the path and the `/logs` mount come from the Compose file and are not asserted there. `ManagementPlainHttpOptOutIT` reads one of those files. - Use `docker compose -f integration-tests/docker-compose.yml logs api-sheriff` for the app's real stdout (stack traces, config resolution). - On a **CI** startup failure, `start-integration-container.sh` now dumps `docker compose logs api-sheriff` + `/q/health` into `integration-tests/target/failsafe-reports/` (`api-sheriff-app.log`, `api-sheriff-health.json`), which the workflow uploads as an artifact. Download with `gh run download --repo cuioss/API-Sheriff` — the GitHub job log itself does NOT contain the app container stdout. diff --git a/CLAUDE.md b/CLAUDE.md index 7b8355c3..177ef174 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,19 @@ docker build -f api-sheriff/src/main/docker/Dockerfile.native -t api-sheriff:lat 1. Quality gate (canonical `quality-gate` command above) 2. Full verify (canonical `verify` command above) +**"Zero warnings" is now enforced by the compiler, not just asked for.** The reactor-wide +`maven-compiler-plugin` configuration sets `true` **and** +`true`, so javac runs with `-Werror`: a compiler warning — a +deprecated API, an unchecked cast — **fails the build** in all six modules rather than scrolling past +in the log. The failure reaches the executor's structured payload, with the offending file and line +on the `warnings[]` row and the `-Werror` cause naming the file on `errors[]`. Read both arrays; the +line number lives on the warning row. + +Answer such a failure by **migrating off the warned construct**, the way every site this gate was +turned on over was retired. A `@SuppressWarnings` added to get back to green hollows the gate out +while leaving it reporting success, which is worse than not having it — and it collides with the +Pre-1.0 rule below that forbids carrying deprecated code at all. + **Documentation-only commits skip both.** A commit whose entire footprint is prose or agent instructions cannot change build output, so a Maven run proves nothing and only burns minutes. Skip when **every** changed file is one of: diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/AuthenticationStage.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/AuthenticationStage.java index 6e7348b2..95029684 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/AuthenticationStage.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/AuthenticationStage.java @@ -66,9 +66,6 @@ */ public final class AuthenticationStage { - private static final String REQUIRE_NONE = "none"; - private static final String REQUIRE_BEARER = "bearer"; - private static final String REQUIRE_SESSION = "session"; private static final String BEARER_PREFIX = "Bearer "; private final Provider tokenValidator; @@ -112,20 +109,21 @@ public void process(PipelineRequest request) { Objects.requireNonNull(request, "request"); RouteRuntime route = requireSelectedRoute(request); AuthConfig auth = route.getEffectiveAuth(); - String require = auth.require(); - if (REQUIRE_NONE.equals(require)) { - return; + // The `case null` label is load-bearing, not defensive: it makes this an ENHANCED switch, + // which javac is required to check for exhaustiveness. Without it a constant-only switch + // statement is a legacy switch — a fourth Require constant would compile clean and fall + // through silently, leaving the posture unenforced while the route still reports itself + // AUTHENTICATED. `require` is non-null by AuthConfig's canonical constructor, so this arm + // is unreachable; its job is to make the omission a compile error rather than a bypass. + switch (auth.require()) { + case NONE -> { + // Anonymous surface: nothing to enforce. + } + case BEARER -> validateBearer(request, auth, route); + case SESSION -> requireSessionStage(route).process(request); + case null -> throw new IllegalStateException( + "Route " + route.getId() + " reached authentication with a null auth posture"); } - if (REQUIRE_BEARER.equals(require)) { - validateBearer(request, auth, route); - return; - } - if (REQUIRE_SESSION.equals(require)) { - requireSessionStage(route).process(request); - return; - } - throw new IllegalStateException( - "Route " + route.getId() + " reached authentication with unsupported require '" + require + "'"); } private SessionAuthenticationStage requireSessionStage(RouteRuntime route) { diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/RouteTableBuilder.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/RouteTableBuilder.java index b7a18624..4dc3cf1a 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/RouteTableBuilder.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/RouteTableBuilder.java @@ -35,6 +35,7 @@ import de.cuioss.sheriff.gateway.config.model.GatewayConfig; import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedAsset; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; @@ -107,9 +108,8 @@ public final class RouteTableBuilder { private static final List STANDARD_ALLOWED_METHODS = List.copyOf(EnumSet.allOf(HttpMethod.class)); - /** The {@link AuthConfig#require()} value meaning no authentication is required; also the - * display fallback for an absent anchor name in {@link #logPosture}. */ - private static final String NONE = "none"; + /** The display fallback for an absent anchor name in {@link #logPosture}. */ + private static final String NO_ANCHOR_NAME = "none"; /** The default {@code websocket.idle_timeout_seconds} applied when a WebSocket route omits it. */ private static final int DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECONDS = 300; @@ -411,7 +411,7 @@ private static ResolvedAsset resolveAsset(RouteConfig route, AssetConfig asset, * auth requires authentication */ public static AccessLevel effectiveAccessLevel(@Nullable AnchorConfig anchor, AuthConfig effectiveAuth) { - if (!NONE.equals(effectiveAuth.require())) { + if (effectiveAuth.require() != Require.NONE) { return AccessLevel.AUTHENTICATED; } return anchor == null ? AccessLevel.PUBLIC : anchor.access(); @@ -425,7 +425,7 @@ public static AccessLevel effectiveAccessLevel(@Nullable AnchorConfig anchor, Au * placeholder would report a partial-disable posture for every route that merely omits the knob. */ private static void logPosture(ResolvedRoute route, SecurityProfile globalProfile) { - String anchorName = route.anchor() != null ? route.anchor() : NONE; + String anchorName = route.anchor() != null ? route.anchor() : NO_ANCHOR_NAME; SecurityFilterConfig securityFilter = route.effectiveSecurityFilter(); SecurityProfile effectiveProfile = SecurityProfile .parse(securityFilter == null ? null : securityFilter.profile()) diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java index 43e9b5f6..440cda54 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/load/ConfigLoader.java @@ -413,18 +413,24 @@ private static void validate(Schema schema, JsonNode node, String file, List errors) { - if (node instanceof ObjectNode object) { - List names = new ArrayList<>(); - object.fieldNames().forEachRemaining(names::add); - for (String name : names) { - substituteChild(object.get(name), schemaTree, file, pointer + "/" + name, errors, - resolved -> object.set(name, resolved)); + switch (node) { + case ObjectNode object -> { + List names = new ArrayList<>(); + object.fieldNames().forEachRemaining(names::add); + for (String name : names) { + substituteChild(object.get(name), schemaTree, file, pointer + "/" + name, errors, + resolved -> object.set(name, resolved)); + } + } + case ArrayNode array -> { + for (int index = 0; index < array.size(); index++) { + int position = index; + substituteChild(array.get(index), schemaTree, file, pointer + "/" + index, errors, + resolved -> array.set(position, resolved)); + } } - } else if (node instanceof ArrayNode array) { - for (int index = 0; index < array.size(); index++) { - int position = index; - substituteChild(array.get(index), schemaTree, file, pointer + "/" + index, errors, - resolved -> array.set(position, resolved)); + default -> { + // A scalar node has no children to walk; substituteChild already resolved it. } } } diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/AuthConfig.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/AuthConfig.java index ba147647..cf5a7c76 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/AuthConfig.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/AuthConfig.java @@ -24,10 +24,16 @@ * The {@code auth} block, declarable at endpoint level (mandatory default * posture) and per route (wholesale override). *

- * {@code require} is one of {@code none} / {@code bearer} / {@code session}; the - * value set is enforced by the configuration validator. {@code required_scopes} - * is valid at either level; because override is wholesale, a route-level block - * that omits it drops endpoint-level scope enforcement for that route. + * {@code require} is a {@link Require} posture ({@code none} / {@code bearer} / + * {@code session}); the value set is declared in the JSON schemas and refused there + * before binding. {@code required_scopes} is valid at either level; because override + * is wholesale, a route-level block that omits it drops endpoint-level scope + * enforcement for that route. + *

+ * Thread safety. This immutable record is thread-safe and may be shared + * freely across request threads: {@link Require} is an enum, and the canonical constructor + * defensively copies {@code requiredScopes} into an unmodifiable list, so no caller can + * mutate an instance after construction. * * @param require the authentication requirement (mandatory) * @param requiredScopes the scopes enforced for this posture, empty when none @@ -35,7 +41,7 @@ * @since 1.0 */ @Builder -public record AuthConfig(String require, List requiredScopes) { +public record AuthConfig(Require require, List requiredScopes) { /** * Canonical constructor defensively copying {@code requiredScopes} into an diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/Require.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/Require.java new file mode 100644 index 00000000..c91ea3e6 --- /dev/null +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/Require.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2026 CUI-OpenSource-Software (info@cuioss.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.cuioss.sheriff.gateway.config.model; + +import java.util.Locale; + +/** + * The authentication posture an {@link AuthConfig auth} block requires. + *

+ * The value set is declared in {@code gateway.schema.json} and + * {@code endpoint.schema.json}, so an unknown value is refused during schema + * validation, before binding ever reaches this type. Modelling the posture as an + * enum rather than a {@link String} is therefore a type-safety change and + * carries no behavioural delta at the configuration boundary: it replaces the three + * duplicated {@code REQUIRE_*} string-constant sets that had drifted across the + * validator, the authentication stage and the edge route with one shared type, and + * lets the posture dispatch be a switch over a closed set. + *

+ * Compile-time exhaustiveness is not automatic. A switch statement whose labels + * are all enum constants is a legacy switch, which javac neither requires to be exhaustive + * nor warns about — adding a fourth constant would compile clean and fall through silently. + * A dispatch that must not miss a posture therefore carries a {@code case null} arm, which + * makes it an enhanced switch and obliges javac to reject a non-exhaustive one. See + * {@code AuthenticationStage#process}, where a missed posture would leave a route + * unenforced while still reporting itself authenticated. + *

+ * The constants are uppercase per Java convention; the case-insensitive YAML binding + * ({@code MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS}) maps the lowercase + * {@code none} / {@code bearer} / {@code session} configuration values onto them. + * + * @author API Sheriff Team + * @since 1.0 + */ +public enum Require { + + /** No authentication required: the surface is anonymous. */ + NONE, + /** A validated bearer token is required; the gateway needs a configured issuer. */ + BEARER, + /** An authenticated session is required; the gateway needs an OIDC block. */ + SESSION; + + /** + * The configuration spelling of this posture — the lowercase form as it appears in + * {@code gateway.yaml}. + *

+ * Overridden so that operator-facing text renders the posture the way the operator + * wrote it: validation errors and the route-posture log line interpolate this value + * with {@code %s}, and reporting {@code BEARER} for a file that says {@code bearer} + * would make the message harder to trace back to the offending line. Binding is + * unaffected — Jackson reads enums by constant name (case-insensitively here) and + * does not consult {@code toString()}. + * + * @return the lowercase configuration value for this posture + */ + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java index 7fbc7228..c9b162d1 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java @@ -51,6 +51,7 @@ import de.cuioss.sheriff.gateway.config.model.MatchConfig.HeaderMatcher; import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteConfig; @@ -129,9 +130,6 @@ public final class ConfigValidator { private static final int BROAD_PREFIX_IPV4 = 8; private static final int BROAD_PREFIX_IPV6 = 32; private static final String WILDCARD_ORIGIN = "*"; - private static final String REQUIRE_NONE = "none"; - private static final String REQUIRE_BEARER = "bearer"; - private static final String REQUIRE_SESSION = "session"; // java:S1075 — a fixed JSON-pointer into the config document (schema key), not a customizable URI/filesystem path. @SuppressWarnings("java:S1075") private static final String OIDC_USER_INFO_PATH_POINTER = "/oidc/user_info/path"; @@ -762,9 +760,9 @@ private static void validateAnchorAuthFloor(GatewayConfig gateway, List endpoints, List errors) { - Set requires = new HashSet<>(); + Set requires = EnumSet.noneOf(Require.class); for (EndpointConfig endpoint : endpoints) { for (RouteConfig route : endpoint.routes()) { requires.add(effectiveRequire(gateway, endpoint, route)); } } - if (requires.contains(REQUIRE_BEARER) && lacksConfiguredIssuer(gateway)) { + if (requires.contains(Require.BEARER) && lacksConfiguredIssuer(gateway)) { errors.add(new ConfigError(GATEWAY_FILE, "/token_validation", "effective auth 'bearer' requires token_validation with at least one issuer")); } - if (requires.contains(REQUIRE_SESSION) && gateway.oidc() == null) { + if (requires.contains(Require.SESSION) && gateway.oidc() == null) { errors.add(new ConfigError(GATEWAY_FILE, "/oidc", "effective auth 'session' requires an oidc block")); } } @@ -838,19 +836,19 @@ private static void validateAccessAuthMatrix(GatewayConfig gateway, List errors) { AuthConfig anchorAuth = anchor.auth(); - String require = anchorAuth == null ? REQUIRE_NONE : anchorAuth.require(); - if (REQUIRE_NONE.equals(require)) { + Require require = anchorAuth == null ? Require.NONE : anchorAuth.require(); + if (require == Require.NONE) { errors.add(new ConfigError(GATEWAY_FILE, pointer, "anchor '%s' is access: authenticated but declares no non-'none' auth floor" .formatted(anchor.name()))); return; } - if (REQUIRE_BEARER.equals(require) && lacksConfiguredIssuer(gateway)) { + if (require == Require.BEARER && lacksConfiguredIssuer(gateway)) { errors.add(new ConfigError(GATEWAY_FILE, pointer, "anchor '%s' access: authenticated bearer floor requires token_validation with at least one issuer" .formatted(anchor.name()))); } - if (REQUIRE_SESSION.equals(require) && gateway.oidc() == null) { + if (require == Require.SESSION && gateway.oidc() == null) { errors.add(new ConfigError(GATEWAY_FILE, pointer, "anchor '%s' access: authenticated session floor requires an oidc block".formatted(anchor.name()))); } @@ -1363,7 +1361,7 @@ private static void validateWebSocketRoute(GatewayConfig gateway, EndpointConfig List errors) { WebSocketConfig websocket = route.websocket(); List origins = websocket == null ? List.of() : websocket.allowedOrigins(); - if (REQUIRE_BEARER.equals(effectiveRequire(gateway, endpoint, route)) && origins.isEmpty()) { + if (effectiveRequire(gateway, endpoint, route) == Require.BEARER && origins.isEmpty()) { errors.add(new ConfigError(endpointFile(endpoint), ENDPOINT_ROUTES_POINTER, "websocket route '%s' with effective auth 'bearer' must declare a non-empty allowed_origins allowlist (fail-closed)" .formatted(route.id()))); @@ -1431,9 +1429,9 @@ private static Protocol effectiveProtocol(RouteConfig route) { return anchor == null ? null : anchor.auth(); } - private static String effectiveRequire(GatewayConfig gateway, EndpointConfig endpoint, RouteConfig route) { + private static Require effectiveRequire(GatewayConfig gateway, EndpointConfig endpoint, RouteConfig route) { AuthConfig auth = effectiveAuth(gateway, endpoint, route); - return auth == null ? REQUIRE_NONE : auth.require(); + return auth == null ? Require.NONE : auth.require(); } private static Set effectiveAllowedMethods(GatewayConfig gateway, EndpointConfig endpoint, diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java index 8d45052c..a3ea3064 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java @@ -57,6 +57,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedAsset; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -167,7 +168,6 @@ public class GatewayEdgeRoute { * header-block limit ({@link EdgeHardeningOptions}) even at the configurable budget ceiling. */ private static final int COOKIE_HEADER_OVERHEAD_BYTES = 512; - private static final String REQUIRE_SESSION = "session"; private static final String COOKIE_HEADER = "Cookie"; private static final String LOCATION_HEADER = "Location"; private static final String SET_COOKIE_HEADER = "Set-Cookie"; @@ -358,7 +358,11 @@ public GatewayEdgeRoute(RouteTable routeTable, GatewayConfig gatewayConfig, this.forwardPolicyStage = new ForwardPolicyStage(resolver, peerGate, emitMode); this.responseStage = new ResponseStage(); this.originValidationStage = new OriginValidationStage(); - this.webSocketRelayStage = new WebSocketRelayStage(upstreamFailureMapper, gatewayEventCounter); + // One WebSocketClient for the whole edge: HttpClient.webSocket(...) is deprecated in favour of + // the dedicated client, and the dialer carries no per-route state — the upstream host, port, + // TLS flag and URI all ride on the per-dial WebSocketConnectOptions. + this.webSocketRelayStage = new WebSocketRelayStage(vertx.createWebSocketClient(), + upstreamFailureMapper, gatewayEventCounter); this.grpcStatusMapper = new GrpcStatusMapper(); // Bind the boot-shared cui-http counter to Micrometer so the per-UrlSecurityFailureType @@ -457,7 +461,7 @@ private boolean needsReservedBodyRead(RoutingContext ctx) { if (!"POST".equalsIgnoreCase(ctx.request().method().name())) { return false; } - String host = ctx.request().authority() != null ? ctx.request().authority().host() : ctx.request().host(); + String host = authorityHost(ctx.request()); return reservedPathRegistry.match(host, ctx.request().path()) .filter(kind -> kind == ReservedEndpoint.BACKCHANNEL_LOGOUT) .isPresent(); @@ -717,7 +721,7 @@ private void process(RoutingContext ctx) { // Fixed CSRF defence (D7): every unsafe-method require:session request must prove same-origin // provenance before the session runtime resolves it. A bearer-only gateway has no session // routes and never reaches this guard. - if (bffRuntime.isActive() && REQUIRE_SESSION.equals(route.getEffectiveAuth().require())) { + if (bffRuntime.isActive() && route.getEffectiveAuth().require() == Require.SESSION) { bffRuntime.csrfDefence().enforce(request); } authenticationStage.process(request); @@ -736,12 +740,10 @@ private void process(RoutingContext ctx) { // Protocol-dispatch seam: a WebSocket route validates its handshake Origin and hands the // upgrade to the opaque relay; a gRPC route dispatches over the forced-h2 GrpcDispatchStage // and relays response trailers. Every other protocol takes the HTTP dispatch path. - if (route.getProtocol() == Protocol.WEBSOCKET) { - dispatchWebSocket(ctx, request, route, forward); - } else if (route.getProtocol() == Protocol.GRPC) { - dispatchGrpc(ctx, request, route, forward); - } else { - dispatchAndRelay(ctx, request, route, forward); + switch (route.getProtocol()) { + case WEBSOCKET -> dispatchWebSocket(ctx, request, route, forward); + case GRPC -> dispatchGrpc(ctx, request, route, forward); + default -> dispatchAndRelay(ctx, request, route, forward); } } catch (GatewayException rejected) { handleGatewayRejection(ctx, request, rejected); @@ -788,14 +790,18 @@ private void handleGatewayRejection(RoutingContext ctx, @Nullable PipelineReques if (rejected.getEventType().category() != EventCategory.UPSTREAM) { gatewayEventCounter.increment(rejected.getEventType()); } - if (rejected.getEventType() == EventType.SECURITY_FILTER_VIOLATION) { + switch (rejected.getEventType()) { // Security-relevant WARN (D4): the failure-type detail only, never the raw payload — // rejected.getMessage() already carries a sanitized description (see GatewayException). - LOGGER.warn(ApiSheriffLogMessages.WARN.SECURITY_FILTER_VIOLATION, routeLabel(ctx), rejected.getMessage()); - } else if (rejected.getEventType() == EventType.PASSTHROUGH_HOST_SMUGGLED) { + case SECURITY_FILTER_VIOLATION -> LOGGER.warn(ApiSheriffLogMessages.WARN.SECURITY_FILTER_VIOLATION, + routeLabel(ctx), rejected.getMessage()); // Security-relevant WARN: a terminated Host named a reserved passthrough SNI. The // message is a fixed disposition (never the raw Host value). - LOGGER.warn(ApiSheriffLogMessages.WARN.PASSTHROUGH_HOST_SMUGGLED, rejected.getMessage()); + case PASSTHROUGH_HOST_SMUGGLED -> LOGGER.warn(ApiSheriffLogMessages.WARN.PASSTHROUGH_HOST_SMUGGLED, + rejected.getMessage()); + default -> { + // Every other rejection is rendered without a security WARN; it is metered above. + } } recordError(ctx, rejected.getEventType()); renderRejection(ctx, request, rejected.getEventType()); @@ -1143,13 +1149,33 @@ private static PipelineRequest buildPipelineRequest(HttpServerRequest raw, HttpM .requestPath(rawPath) .queryParameters(toListMap(raw.params())) .headers(toListMap(raw.headers())) - .host(raw.authority() != null ? raw.authority().host() : raw.host()) + .host(authorityHost(raw)) .peerAddress(raw.remoteAddress() != null ? raw.remoteAddress().hostAddress() : null) .declaredContentLength(contentLength) .bodyPresent(bodyPresent) .build(); } + /** + * The request's authority host, or {@code null} when the request declares no authority. + *

+ * Reads {@link HttpServerRequest#authority()} directly instead of the deprecated + * {@code HttpServerRequest#host()}. The two are not interchangeable: {@code host()} + * returns the raw {@code Host} header, so it carries the port ({@code example.com:8443}) where + * {@code authority().host()} does not ({@code example.com}). Substituting {@code host()} back + * would leak the port into the reserved-path match and the security-validated request host. + *

+ * Dropping the former {@code host()} fallback is nonetheless behaviour-preserving: it applied + * only when {@code authority()} was {@code null}, which happens only when the {@code Host} + * header is absent — and there {@code host()} is {@code null} too. A malformed header makes + * authority parsing throw rather than return {@code null}, so that path never reached the + * fallback either. Both host-reading sites share this one seam so the two cannot drift apart. + */ + private static @Nullable String authorityHost(HttpServerRequest raw) { + var authority = raw.authority(); + return authority == null ? null : authority.host(); + } + private static Map> toListMap(MultiMap multiMap) { Map> map = new LinkedHashMap<>(); for (String name : multiMap.names()) { diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java index 6ede56a3..9869b59c 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java @@ -27,12 +27,12 @@ import de.cuioss.tools.logging.CuiLogger; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.ServerWebSocket; import io.vertx.core.http.UpgradeRejectedException; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketBase; +import io.vertx.core.http.WebSocketClient; import io.vertx.core.http.WebSocketConnectOptions; import io.vertx.core.http.WebSocketFrame; import io.vertx.ext.web.RoutingContext; @@ -43,8 +43,11 @@ * HTTP {@link DispatchStage} for a {@code protocol: websocket} route once the pipeline and the * {@code OriginValidationStage} have accepted the handshake. *

- * Dial-before-upgrade. The upstream WebSocket is dialed first, over the route's - * shared Vert.x {@link HttpClient}. Only when the upstream confirms {@code 101} is the client + * Dial-before-upgrade. The upstream WebSocket is dialed first, over the edge-wide + * Vert.x {@link WebSocketClient} — the dedicated dialer that replaced the deprecated + * {@code HttpClient.webSocket(WebSocketConnectOptions)}. One client serves every route because the + * dialer carries no per-route state: host, port, TLS and URI all ride on the per-dial + * {@link WebSocketConnectOptions}. Only when the upstream confirms {@code 101} is the client * upgrade completed ({@link io.vertx.core.http.HttpServerRequest#toWebSocket()}); the two legs are * then relayed opaquely. If the upstream is unreachable or times out, the failure is mapped to * {@code 502}/{@code 504} before the client upgrade, so no half-open upgrade is ever left @@ -88,14 +91,18 @@ public final class WebSocketRelayStage { private static final short CLOSE_NORMAL = 1000; private static final short CLOSE_INTERNAL_ERROR = 1011; + private final WebSocketClient webSocketClient; private final UpstreamFailureMapper failureMapper; private final GatewayEventCounter eventCounter; /** + * @param webSocketClient the edge-wide dialer for every upstream WebSocket handshake * @param failureMapper the shared mapper turning an upstream dial failure into the error contract * @param eventCounter the shared in-process event counter */ - public WebSocketRelayStage(UpstreamFailureMapper failureMapper, GatewayEventCounter eventCounter) { + public WebSocketRelayStage(WebSocketClient webSocketClient, UpstreamFailureMapper failureMapper, + GatewayEventCounter eventCounter) { + this.webSocketClient = Objects.requireNonNull(webSocketClient, "webSocketClient"); this.failureMapper = Objects.requireNonNull(failureMapper, "failureMapper"); this.eventCounter = Objects.requireNonNull(eventCounter, "eventCounter"); } @@ -128,10 +135,9 @@ public void relay(RoutingContext ctx, RouteRuntime route, Map fo Objects.requireNonNull(requestUri, "requestUri"); Objects.requireNonNull(releaseAdmission, "releaseAdmission"); Map retainedSecurityHeaders = Map.copyOf(securityHeaders); - HttpClient client = route.getHttpClient(); - if (client == null) { - throw new IllegalStateException("WebSocket dispatch requires an upstream client"); - } + // No per-route HttpClient guard here: the handshake is dialed by the edge-wide WebSocketClient, + // so the route's own client is not an input to this path. DispatchStage keeps that guard for the + // HTTP leg, which does consume it. The resolved upstream below IS this path's input. ResolvedUpstream upstream = route.getUpstream(); if (upstream == null) { throw new IllegalStateException("WebSocket dispatch requires a resolved upstream"); @@ -142,7 +148,7 @@ public void relay(RoutingContext ctx, RouteRuntime route, Map fo .setSsl(HTTPS.equalsIgnoreCase(upstream.scheme())) .setURI(requestUri); forwardHeaders.forEach(options::addHeader); - ctx.vertx().runOnContext(v -> client.webSocket(options) + ctx.vertx().runOnContext(v -> webSocketClient.connect(options) .onSuccess(upstreamWs -> onUpstreamConnected(ctx, route, upstreamWs, releaseAdmission)) .onFailure(failure -> onUpstreamFailure(ctx, route, failure, retainedSecurityHeaders))); } diff --git a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java index c7b89741..0aa8a402 100644 --- a/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java +++ b/api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java @@ -34,6 +34,7 @@ import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; import de.cuioss.sheriff.gateway.config.model.RateLimitConfig; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.RouteConfig; import de.cuioss.sheriff.gateway.config.model.SecurityDefaultsConfig; import de.cuioss.sheriff.gateway.config.model.SecurityFilterConfig; @@ -105,7 +106,8 @@ HttpMethod.class, Protocol.class, AnchorType.class, - AccessLevel.class + AccessLevel.class, + Require.class }) public final class ConfigModelReflection { diff --git a/api-sheriff/src/main/resources/application.properties b/api-sheriff/src/main/resources/application.properties index 140ed276..523a37ee 100644 --- a/api-sheriff/src/main/resources/application.properties +++ b/api-sheriff/src/main/resources/application.properties @@ -235,7 +235,7 @@ quarkus.log.category."de.cuioss.sheriff".level=INFO # # The shipped default is `false` and there is deliberately no profile branch turning it off again. # Whether a container writes a log file is a deployment decision, exactly like the ports and the -# trust material above: a deployment that wants one sets QUARKUS_LOG_FILE_ENABLE=true and points +# trust material above: a deployment that wants one sets QUARKUS_LOG_FILE_ENABLED=true and points # LOG_FILE_PATH at a mounted directory (integration-tests/docker-compose.yml does both). # # Shipping `true` here was what forced the branch: the default path below resolves to the @@ -243,7 +243,7 @@ quarkus.log.category."de.cuioss.sheriff".level=INFO # Flipping the default removes both the branch and the hazard — an unconfigured run writes no file # at all rather than being talked out of writing one at `/`. The remaining keys describe HOW the # file is written once a deployment asks for one; they are inert while the feature is off. -quarkus.log.file.enable=false +quarkus.log.file.enabled=false quarkus.log.file.path=${LOG_FILE_PATH:/quarkus.log} quarkus.log.file.level=INFO quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS z} %-5p [%c{3.}] (%t) %s%e%n diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/auth/AuthenticationStageTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/auth/AuthenticationStageTest.java index 2a803e81..f4040f41 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/auth/AuthenticationStageTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/auth/AuthenticationStageTest.java @@ -38,6 +38,7 @@ import de.cuioss.sheriff.gateway.bff.session.SessionRecord; import de.cuioss.sheriff.gateway.config.model.AuthConfig; import de.cuioss.sheriff.gateway.config.model.HttpMethod; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.events.EventType; import de.cuioss.sheriff.gateway.events.GatewayException; import de.cuioss.sheriff.gateway.pipeline.PipelineRequest; @@ -69,7 +70,7 @@ void passesRequireNoneWithoutResolvingValidator() { AuthenticationStage stage = new AuthenticationStage(() -> { throw new AssertionError("require:none must not resolve the token validator"); }); - PipelineRequest request = request(authConfig("none", List.of()), Map.of()); + PipelineRequest request = request(authConfig(Require.NONE, List.of()), Map.of()); // Act + Assert assertDoesNotThrow(() -> stage.process(request)); @@ -81,7 +82,7 @@ void acceptsValidBearerToken() { // Arrange TestTokenHolder holder = TestTokenGenerators.accessTokens().next(); AuthenticationStage stage = stageFor(holder); - PipelineRequest request = bearerRequest(holder.getRawToken(), authConfig("bearer", List.of())); + PipelineRequest request = bearerRequest(holder.getRawToken(), authConfig(Require.BEARER, List.of())); // Act + Assert assertDoesNotThrow(() -> stage.process(request)); @@ -92,7 +93,7 @@ void acceptsValidBearerToken() { void rejectsMissingBearerToken() { // Arrange AuthenticationStage stage = stageFor(TestTokenGenerators.accessTokens().next()); - PipelineRequest request = request(authConfig("bearer", List.of()), Map.of()); + PipelineRequest request = request(authConfig(Require.BEARER, List.of()), Map.of()); // Act GatewayException thrown = assertThrows(GatewayException.class, () -> stage.process(request)); @@ -107,7 +108,7 @@ void rejectsMissingBearerToken() { void rejectsInvalidBearerToken() { // Arrange AuthenticationStage stage = stageFor(TestTokenGenerators.accessTokens().next()); - PipelineRequest request = bearerRequest("not.a.valid.jwt", authConfig("bearer", List.of())); + PipelineRequest request = bearerRequest("not.a.valid.jwt", authConfig(Require.BEARER, List.of())); // Act GatewayException thrown = assertThrows(GatewayException.class, () -> stage.process(request)); @@ -123,7 +124,7 @@ void rejectsMissingScope() { // Arrange TestTokenHolder holder = TestTokenGenerators.accessTokens().next(); AuthenticationStage stage = stageFor(holder); - PipelineRequest request = bearerRequest(holder.getRawToken(), authConfig("bearer", List.of(ABSENT_SCOPE))); + PipelineRequest request = bearerRequest(holder.getRawToken(), authConfig(Require.BEARER, List.of(ABSENT_SCOPE))); // Act GatewayException thrown = assertThrows(GatewayException.class, () -> stage.process(request)); @@ -138,7 +139,7 @@ void dispatchesSessionRouteToWiredSessionStage() { // Arrange — a session stage wired with a live session; a require:session request carrying the // session cookie must be dispatched here and complete, recording the mediated bearer. AuthenticationStage stage = new AuthenticationStage(failingValidatorProvider(), sessionStage()); - PipelineRequest request = sessionRequest(authConfig("session", List.of())); + PipelineRequest request = sessionRequest(authConfig(Require.SESSION, List.of())); // Act + Assert assertDoesNotThrow(() -> stage.process(request)); @@ -152,7 +153,7 @@ void dispatchesSessionRouteToWiredSessionStage() { void rejectsSessionRouteWithoutWiredSessionRuntime() { // Arrange — a stage built without a session runtime (non-BFF gateway). AuthenticationStage stage = stageFor(TestTokenGenerators.accessTokens().next()); - PipelineRequest request = sessionRequest(authConfig("session", List.of())); + PipelineRequest request = sessionRequest(authConfig(Require.SESSION, List.of())); // Act IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> stage.process(request)); @@ -203,7 +204,7 @@ private static PipelineRequest sessionRequest(AuthConfig auth) { return request; } - private static AuthConfig authConfig(String require, List requiredScopes) { + private static AuthConfig authConfig(Require require, List requiredScopes) { return AuthConfig.builder().require(require).requiredScopes(requiredScopes).build(); } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java index 592e3602..abd06275 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java @@ -37,6 +37,7 @@ import de.cuioss.sheriff.gateway.bff.session.SessionRecord; import de.cuioss.sheriff.gateway.config.model.AuthConfig; import de.cuioss.sheriff.gateway.config.model.HttpMethod; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.events.EventType; import de.cuioss.sheriff.gateway.events.GatewayException; import de.cuioss.sheriff.gateway.pipeline.PipelineRequest; @@ -420,7 +421,7 @@ private static SessionRecord rebind(SessionRecord session, String accessToken) { } private static AuthConfig authConfig(List requiredScopes) { - return AuthConfig.builder().require("session").requiredScopes(requiredScopes).build(); + return AuthConfig.builder().require(Require.SESSION).requiredScopes(requiredScopes).build(); } private static Map> navigationHeaders() { diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/DocumentedSetsContractTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/DocumentedSetsContractTest.java index 3dfff568..d82f5d4b 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/DocumentedSetsContractTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/DocumentedSetsContractTest.java @@ -36,9 +36,11 @@ import de.cuioss.sheriff.gateway.asset.AssetResponseEnvelope; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.SecurityProfile; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.networknt.schema.Error; import com.networknt.schema.InputFormat; @@ -53,12 +55,13 @@ * Binds the operator-facing documents and the bundled JSON Schema back to the code that * authoritatively defines the sets they enumerate. *

- * Why this test exists. Four shipped surfaces restate a set whose definition lives + * Why this test exists. Six shipped surfaces restate a set whose definition lives * in Java: three of them list the built-in asset extensions carried by - * {@link AssetResponseEnvelope#builtInExtensions()}, and one lists the inbound-filter mode set - * carried by {@link SecurityProfile}. A restated list has no mechanical tie to its source, so adding - * a mapping or a mode leaves every restatement silently stale — the documentation still reads as - * authoritative while describing a gateway that no longer exists. The project's own review policy + * {@link AssetResponseEnvelope#builtInExtensions()}, one lists the inbound-filter mode set + * carried by {@link SecurityProfile}, and the two bundled JSON Schemas each list the authentication + * posture set carried by {@link Require}. A restated list has no mechanical tie to its source, so + * adding a mapping, a mode or a posture leaves every restatement silently stale — the documentation + * still reads as authoritative while describing a gateway that no longer exists. The project's own review policy * treats a hardcoded list mirroring a set defined elsewhere as a defect unless it is derived from * that source at build or run time; deriving these at build time would mean generating prose, so * this contract test is the sanctioned alternative: the lists stay hand-written and readable, and @@ -87,9 +90,11 @@ * against the bundled schema through the same {@code com.networknt} code path {@code ConfigLoader} * boots with, and must produce zero errors. *

- * No vacuous pass. Every extraction is anchored on a literal sentence fragment held - * in a named constant. When an anchor cannot be located, or locates an empty set, the test fails - * naming both the document and the anchor rather than asserting over nothing. A guard that stops + * No vacuous pass. Every extraction is anchored on a named constant — a literal + * sentence fragment for the prose surfaces, and a JSON pointer for the posture set, which the schema + * already models as an array and so needs no text scraping. When an anchor cannot be located, or + * locates an empty set, the test fails naming both the document and the anchor rather than asserting + * over nothing. A guard that stops * matching after a rewrite must break loudly; one that quietly matches nothing is worse than absent. * The exhibit guards carry the same discipline in two parts: each extracted exhibit must be non-empty * and must still carry the budget key whose absence from the schema motivated the guard, and a @@ -110,7 +115,21 @@ class DocumentedSetsContractTest { private static final Path BFF_COOKIE_ADOC = repoRoot().resolve("doc/user/bff-cookie.adoc"); /** The bundled schema, read off the classpath so the assertion sees the shipped copy. */ - private static final String SCHEMA_RESOURCE = "/schema/gateway.schema.json"; + private static final String GATEWAY_SCHEMA_RESOURCE = "/schema/gateway.schema.json"; + + /** + * The bundled endpoint schema. It declares the same {@code auth} block as the gateway schema, so + * both copies restate the {@link Require} posture set and both are asserted against it. + */ + private static final String ENDPOINT_SCHEMA_RESOURCE = "/schema/endpoint.schema.json"; + + /** + * JSON pointer to the {@code require} enum array both bundled schemas declare on their shared + * {@code auth} definition. A pointer is used rather than a text anchor because the value being + * asserted is a JSON array the schema already models — extracting it structurally cannot be + * defeated by reformatting, and a moved definition fails naming this pointer. + */ + private static final String REQUIRE_ENUM_POINTER = "/$defs/auth/properties/require/enum"; /** * Anchor for {@code doc/configuration.adoc}'s bare extension enumeration. The stated count @@ -238,21 +257,33 @@ void userReadmeEnumeratesTheBuiltInExtensions() throws Exception { @DisplayName("the bundled gateway schema enumerates exactly the built-in asset extensions") void gatewaySchemaEnumeratesTheBuiltInExtensions() throws Exception { // Arrange - String schema = readSchema(); - int anchor = anchorIndex(schema, SCHEMA_EXTENSION_ANCHOR, SCHEMA_RESOURCE); + String schema = readSchema(GATEWAY_SCHEMA_RESOURCE); + int anchor = anchorIndex(schema, SCHEMA_EXTENSION_ANCHOR, GATEWAY_SCHEMA_RESOURCE); // Act — the schema lists the extensions bare (no backticks), comma separated int listStart = anchor + SCHEMA_EXTENSION_ANCHOR.length(); int listEnd = schema.indexOf(')', listStart); if (listEnd < 0) { - fail(SCHEMA_RESOURCE + ": the extension list opened by \"" + SCHEMA_EXTENSION_ANCHOR + fail(GATEWAY_SCHEMA_RESOURCE + ": the extension list opened by \"" + SCHEMA_EXTENSION_ANCHOR + "\" is never closed; the anchor no longer describes the schema"); } TokenList documented = separatedTokens(schema.substring(listStart, listEnd), ","); // Assert — the schema states no count of its own, so only the set and the number of entries // it listed to name that set are asserted - assertExtensionSet(documented, SCHEMA_RESOURCE); + assertExtensionSet(documented, GATEWAY_SCHEMA_RESOURCE); + } + + @Test + @DisplayName("the bundled gateway schema enumerates exactly the Require posture set") + void gatewaySchemaEnumeratesTheRequirePostures() throws Exception { + assertRequirePostures(GATEWAY_SCHEMA_RESOURCE); + } + + @Test + @DisplayName("the bundled endpoint schema enumerates exactly the Require posture set") + void endpointSchemaEnumeratesTheRequirePostures() throws Exception { + assertRequirePostures(ENDPOINT_SCHEMA_RESOURCE); } @Test @@ -409,6 +440,65 @@ private static void assertExtensionsMatch(TokenList documented, int statedCount, + " together"); } + /** + * Asserts one bundled schema's {@code auth.require} enum array against {@link Require} — the set + * it names, and how many entries it listed to name it. + *

+ * The comparison is against {@link Require#toString()} rather than against the constant names + * because that method is the configuration spelling: the constants are uppercase per Java + * convention and the schema declares the lowercase form an operator writes. Binding to the + * spelling the type itself publishes means a renamed constant and a schema that was not updated + * with it fail here, and so does a {@code toString()} that stops producing the config spelling. + *

+ * The listed-entry count is asserted alongside the set for the same reason the extension guards + * assert it: a {@link Set} cannot observe a duplicate, so an array naming {@code bearer} twice + * collapses into exactly the set a correct array produces and would otherwise pass. + * + * @param resource the classpath resource of the schema to assert + * @throws IOException when the bundled schema cannot be read + */ + private static void assertRequirePostures(String resource) throws IOException { + TokenList declared = schemaRequireEnum(resource); + + assertFalse(declared.tokens().isEmpty(), resource + ": " + REQUIRE_ENUM_POINTER + " resolved to" + + " an empty enum array — the guard would pass vacuously"); + assertEquals(postureNames(), sorted(declared.tokens()), + resource + " enumerates the auth.require posture set, which is authoritatively defined" + + " by the Require enum, and has drifted from it. The schema is what refuses an" + + " unknown posture before binding ever reaches the type, so a posture the enum" + + " declares and the schema omits is unreachable configuration, and one the schema" + + " declares and the enum omits fails the boot bind instead of the validation"); + assertEquals(Require.values().length, declared.rawCount(), + resource + " lists a different number of postures than Require declares. The count is" + + " taken over the raw array entries rather than over the de-duplicated set, so a" + + " posture listed twice fails here even though the set equality above still holds"); + } + + /** + * The {@code auth.require} enum array of a bundled schema, read structurally through + * {@link #REQUIRE_ENUM_POINTER}. + * + * @param resource the classpath resource of the schema to read + * @return the de-duplicated posture values and the number of entries that produced them + * @throws IOException when the bundled schema cannot be read + */ + private static TokenList schemaRequireEnum(String resource) throws IOException { + JsonNode array = new ObjectMapper().readTree(readSchema(resource)).at(REQUIRE_ENUM_POINTER); + if (!array.isArray()) { + return fail(resource + ": nothing resolves at " + REQUIRE_ENUM_POINTER + ", so this contract" + + " guard no longer reaches the posture enumeration it protects. Restore the auth" + + " definition, or update REQUIRE_ENUM_POINTER in DocumentedSetsContractTest to match" + + " where the schema now declares it."); + } + Set tokens = new LinkedHashSet<>(); + int rawCount = 0; + for (JsonNode value : array) { + tokens.add(value.asText()); + rawCount++; + } + return new TokenList(tokens, rawCount); + } + /** * The body of the first AsciiDoc {@code [source,yaml]} listing block following an anchor. * @@ -495,13 +585,13 @@ private static Schema gatewaySchema() { builder -> builder.schemaRegistryConfig(SchemaRegistryConfig.builder() .errorMessageKeyword(ERROR_MESSAGE_KEYWORD) .build())); - try (InputStream in = DocumentedSetsContractTest.class.getResourceAsStream(SCHEMA_RESOURCE)) { + try (InputStream in = DocumentedSetsContractTest.class.getResourceAsStream(GATEWAY_SCHEMA_RESOURCE)) { if (in == null) { - return fail("the bundled schema " + SCHEMA_RESOURCE + " is not on the test classpath"); + return fail("the bundled schema " + GATEWAY_SCHEMA_RESOURCE + " is not on the test classpath"); } return registry.getSchema(in); } catch (IOException e) { - return fail("cannot read the bundled schema " + SCHEMA_RESOURCE + ": " + e.getMessage()); + return fail("cannot read the bundled schema " + GATEWAY_SCHEMA_RESOURCE + ": " + e.getMessage()); } } @@ -671,6 +761,19 @@ private static TokenList separatedTokens(String segment, String separator) { return new TokenList(tokens, rawCount); } + /** + * The configuration spellings {@link Require} publishes, sorted for comparison. + * + * @return the lowercase posture values as an operator writes them + */ + private static Set postureNames() { + Set names = new TreeSet<>(); + for (Require posture : Require.values()) { + names.add(posture.toString()); + } + return names; + } + private static Set modeNames() { Set names = new TreeSet<>(); for (SecurityProfile profile : SecurityProfile.values()) { @@ -687,10 +790,17 @@ private static String read(Path document) throws IOException { return Files.readString(document); } - private static String readSchema() throws IOException { - try (InputStream in = DocumentedSetsContractTest.class.getResourceAsStream(SCHEMA_RESOURCE)) { + /** + * One bundled schema's text, read off the classpath so every assertion sees the shipped copy. + * + * @param resource the classpath resource of the schema to read + * @return the schema document + * @throws IOException when the resource cannot be read + */ + private static String readSchema(String resource) throws IOException { + try (InputStream in = DocumentedSetsContractTest.class.getResourceAsStream(resource)) { if (in == null) { - return fail("the bundled schema " + SCHEMA_RESOURCE + " is not on the test classpath"); + return fail("the bundled schema " + resource + " is not on the test classpath"); } return new String(in.readAllBytes(), StandardCharsets.UTF_8); } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/RouteTableBuilderTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/RouteTableBuilderTest.java index 54db0291..c9953282 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/RouteTableBuilderTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/RouteTableBuilderTest.java @@ -43,6 +43,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.MatchConfig.HeaderMatcher; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedAsset; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; @@ -67,7 +68,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link RouteTableBuilder}: enabled-only merge, longest-prefix @@ -153,7 +153,7 @@ private static ResolvedTopology topologyWithBasePath(String alias, String basePa private static EndpointConfig.EndpointConfigBuilder endpoint(String id, String alias) { return EndpointConfig.builder().id(id).enabled(true).baseUrl(alias) - .auth(new AuthConfig("none", List.of())); + .auth(new AuthConfig(Require.NONE, List.of())); } private static EndpointConfig.EndpointConfigBuilder anchoredEndpoint(String id, String alias, String anchorName) { @@ -419,24 +419,24 @@ class EffectiveAuth { @DisplayName("Should apply a route-level auth override wholesale") void shouldApplyRouteAuthOverride() { RouteConfig secured = RouteConfig.builder().id("secured").match(match("/secured", HttpMethod.GET)) - .auth(new AuthConfig("bearer", List.of("read"))).build(); + .auth(new AuthConfig(Require.BEARER, List.of("read"))).build(); EndpointConfig endpoint = endpoint("orders", "ORDERS").routes(List.of(secured)).build(); RouteTable table = builder.build(gateway().build(), List.of(endpoint), topologyWith("ORDERS")); - assertEquals("bearer", find(table, "secured").effectiveAuth().require()); + assertEquals(Require.BEARER, find(table, "secured").effectiveAuth().require()); } @Test @DisplayName("Should inherit the endpoint default auth when the route omits it") void shouldInheritEndpointAuth() { EndpointConfig endpoint = EndpointConfig.builder().id("orders").enabled(true).baseUrl("ORDERS") - .auth(new AuthConfig("session", List.of())) + .auth(new AuthConfig(Require.SESSION, List.of())) .routes(List.of(route("r", HttpMethod.GET))).build(); RouteTable table = builder.build(gateway().build(), List.of(endpoint), topologyWith("ORDERS")); - assertEquals("session", find(table, "r").effectiveAuth().require()); + assertEquals(Require.SESSION, find(table, "r").effectiveAuth().require()); } } @@ -491,7 +491,7 @@ class AnchorResolution { @DisplayName("Should materialize the anchor auth floor when endpoint and route both omit auth") void shouldMaterializeAnchorAuthFloor() { GatewayConfig config = gateway() - .anchors(Map.of("api", anchor("api", "/api", new AuthConfig("bearer", List.of()), null, null, null))) + .anchors(Map.of("api", anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), null, null, null))) .build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api") .routes(List.of(routeWithPrefix("r", "/api/orders", HttpMethod.GET))).build(); @@ -499,7 +499,7 @@ void shouldMaterializeAnchorAuthFloor() { RouteTable table = builder.build(config, List.of(endpoint), topologyWith("ORDERS")); ResolvedRoute resolved = find(table, "r"); - assertEquals("bearer", resolved.effectiveAuth().require(), "the anchor auth floor should materialize"); + assertEquals(Require.BEARER, resolved.effectiveAuth().require(), "the anchor auth floor should materialize"); assertEquals("api", resolved.anchor(), "the resolving anchor name should be retained"); } @@ -507,15 +507,15 @@ void shouldMaterializeAnchorAuthFloor() { @DisplayName("Should let a route auth override replace the anchor floor wholesale between non-none postures") void shouldLetRouteAuthReplaceAnchorFloor() { GatewayConfig config = gateway() - .anchors(Map.of("api", anchor("api", "/api", new AuthConfig("bearer", List.of()), null, null, null))) + .anchors(Map.of("api", anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), null, null, null))) .build(); RouteConfig route = RouteConfig.builder().id("r").match(match("/api/orders", HttpMethod.GET)) - .auth(new AuthConfig("session", List.of())).build(); + .auth(new AuthConfig(Require.SESSION, List.of())).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api").routes(List.of(route)).build(); RouteTable table = builder.build(config, List.of(endpoint), topologyWith("ORDERS")); - assertEquals("session", find(table, "r").effectiveAuth().require()); + assertEquals(Require.SESSION, find(table, "r").effectiveAuth().require()); } @Test @@ -536,7 +536,7 @@ void shouldThrowWhenNoEffectiveAuthResolves() { @DisplayName("Should materialize the anchor security_filter when the route omits it") void shouldMaterializeAnchorSecurityFilter() { GatewayConfig config = gateway().anchors(Map.of("api", - anchor("api", "/api", new AuthConfig("bearer", List.of()), filter("strict"), null, null))).build(); + anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), filter("strict"), null, null))).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api") .routes(List.of(routeWithPrefix("r", "/api/orders", HttpMethod.GET))).build(); @@ -550,7 +550,7 @@ void shouldMaterializeAnchorSecurityFilter() { @DisplayName("Should let the route security_filter replace the anchor block wholesale") void shouldLetRouteSecurityFilterReplaceAnchor() { GatewayConfig config = gateway().anchors(Map.of("api", - anchor("api", "/api", new AuthConfig("bearer", List.of()), filter("strict"), null, null))).build(); + anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), filter("strict"), null, null))).build(); RouteConfig route = RouteConfig.builder().id("r").match(match("/api/orders", HttpMethod.GET)) .securityFilter(filter("lenient")).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api").routes(List.of(route)).build(); @@ -566,7 +566,7 @@ void shouldLetRouteSecurityFilterReplaceAnchor() { @DisplayName("Should materialize the anchor allowed_methods when the endpoint declares none") void shouldMaterializeAnchorAllowedMethods() { GatewayConfig config = gateway().anchors(Map.of("api", anchor("api", "/api", - new AuthConfig("bearer", List.of()), null, List.of(HttpMethod.GET), null))).build(); + new AuthConfig(Require.BEARER, List.of()), null, List.of(HttpMethod.GET), null))).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api") .routes(List.of(routeWithPrefix("r", "/api/orders", HttpMethod.GET))).build(); @@ -580,7 +580,7 @@ void shouldMaterializeAnchorAllowedMethods() { @DisplayName("Should let the endpoint allowed_methods replace the anchor list wholesale") void shouldLetEndpointReplaceAnchorAllowedMethods() { GatewayConfig config = gateway().anchors(Map.of("api", anchor("api", "/api", - new AuthConfig("bearer", List.of()), null, List.of(HttpMethod.GET), null))).build(); + new AuthConfig(Require.BEARER, List.of()), null, List.of(HttpMethod.GET), null))).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api") .allowedMethods(List.of(HttpMethod.POST)) .routes(List.of(routeWithPrefix("r", "/api/orders", HttpMethod.POST))).build(); @@ -597,7 +597,7 @@ void shouldMaterializeAnchorSecurityHeadersElseGateway() { SecurityHeadersConfig gatewayHeaders = SecurityHeadersConfig.builder() .contentTypeNosniff(true).build(); GatewayConfig config = gateway().securityHeaders(gatewayHeaders).anchors(Map.of("api", - anchor("api", "/api", new AuthConfig("bearer", List.of()), null, null, anchorHeaders))).build(); + anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), null, null, anchorHeaders))).build(); EndpointConfig anchored = anchoredEndpoint("orders", "ORDERS", "api") .routes(List.of(routeWithPrefix("anchored", "/api/orders", HttpMethod.GET))).build(); EndpointConfig plain = endpoint("public", "PUBLIC") @@ -616,8 +616,8 @@ void shouldMaterializeAnchorSecurityHeadersElseGateway() { @DisplayName("Should let a per-route anchor override the endpoint default membership") void shouldLetRouteAnchorOverrideEndpointAnchor() { GatewayConfig config = gateway().anchors(Map.of( - "api", anchor("api", "/api", new AuthConfig("bearer", List.of()), null, null, null), - "bff", anchor("bff", "/bff", new AuthConfig("session", List.of()), null, null, null))).build(); + "api", anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), null, null, null), + "bff", anchor("bff", "/bff", new AuthConfig(Require.SESSION, List.of()), null, null, null))).build(); RouteConfig routeOnBff = RouteConfig.builder().id("r").anchor("bff") .match(match("/bff/home", HttpMethod.GET)).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api").routes(List.of(routeOnBff)).build(); @@ -626,7 +626,7 @@ void shouldLetRouteAnchorOverrideEndpointAnchor() { ResolvedRoute resolved = find(table, "r"); assertEquals("bff", resolved.anchor(), "the per-route anchor override should win"); - assertEquals("session", resolved.effectiveAuth().require()); + assertEquals(Require.SESSION, resolved.effectiveAuth().require()); } @Test @@ -651,7 +651,7 @@ class PostureLogging { @DisplayName("Should emit a per-route effective-posture INFO line during assembly") void shouldEmitEffectivePostureInfo() { GatewayConfig config = gateway() - .anchors(Map.of("api", anchor("api", "/api", new AuthConfig("bearer", List.of()), filter("strict"), + .anchors(Map.of("api", anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), filter("strict"), null, null))) .build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api") @@ -718,7 +718,7 @@ void shouldLogDeclaredRouteProfile() { @DisplayName("Should WARN when a route replaces an anchor-provided security_filter wholesale") void shouldWarnOnWeakeningSecurityFilterOverride() { GatewayConfig config = gateway() - .anchors(Map.of("api", anchor("api", "/api", new AuthConfig("bearer", List.of()), filter("strict"), + .anchors(Map.of("api", anchor("api", "/api", new AuthConfig(Require.BEARER, List.of()), filter("strict"), null, null))) .build(); RouteConfig route = RouteConfig.builder().id("r").match(match("/api/orders", HttpMethod.GET)) @@ -873,7 +873,7 @@ void shouldKeepDeclaredEmptyDistinctFromAbsent() { @DisplayName("Asset terminal-action materialization (ADR-0014)") class AssetTerminalAction { - private AnchorConfig assetAnchor(String name, String prefix, AccessLevel access, String require) { + private AnchorConfig assetAnchor(String name, String prefix, AccessLevel access, Require require) { return AnchorConfig.builder() .name(name) .pathPrefix(prefix) @@ -900,7 +900,7 @@ void shouldMaterializeDirectoryAsset() { AssetConfig asset = AssetConfig.builder().source(AssetConfig.Source.DIRECTORY) .directory("/srv/assets").build(); EndpointConfig endpoint = EndpointConfig.builder().id("web").enabled(true).baseUrl("WEB") - .anchor("assets").auth(new AuthConfig("none", List.of())) + .anchor("assets").auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(assetRoute("bundle", "/assets", "assets", asset))).build(); RouteTable table = builder.build(config, List.of(endpoint), topologyWith("WEB")); @@ -920,7 +920,7 @@ void shouldMaterializeDirectoryAsset() { @DisplayName("Should materialize an upstream asset action resolving its alias through the topology") void shouldMaterializeUpstreamAsset() { GatewayConfig config = gateway().anchors(Map.of("assets", - assetAnchor("assets", "/assets", AccessLevel.AUTHENTICATED, "bearer"))).build(); + assetAnchor("assets", "/assets", AccessLevel.AUTHENTICATED, Require.BEARER))).build(); AssetConfig asset = AssetConfig.builder().source(AssetConfig.Source.UPSTREAM) .upstream("SECONDARY").build(); EndpointConfig endpoint = EndpointConfig.builder().id("web").enabled(true).baseUrl("WEB") @@ -949,7 +949,7 @@ void shouldTreatRouteAuthOverrideAsAuthenticatedUnderPublicAccessAnchor() { .directory("/srv/assets").build(); RouteConfig route = RouteConfig.builder().id("bundle").anchor("assets") .match(match("/assets", HttpMethod.GET)) - .auth(new AuthConfig("bearer", List.of())) + .auth(new AuthConfig(Require.BEARER, List.of())) .asset(asset).build(); EndpointConfig endpoint = EndpointConfig.builder().id("web").enabled(true).baseUrl("WEB") .anchor("assets").auth(null).routes(List.of(route)).build(); @@ -957,7 +957,7 @@ void shouldTreatRouteAuthOverrideAsAuthenticatedUnderPublicAccessAnchor() { RouteTable table = builder.build(config, List.of(endpoint), topologyWith("WEB")); ResolvedRoute resolved = find(table, "bundle"); - assertEquals("bearer", resolved.effectiveAuth().require(), + assertEquals(Require.BEARER, resolved.effectiveAuth().require(), "the route-level override should strengthen the public anchor's absent auth floor"); assertEquals(AccessLevel.AUTHENTICATED, resolved.asset().access(), "a route whose effective auth requires bearer must be governed AUTHENTICATED " @@ -972,7 +972,7 @@ void shouldRejectUnresolvableUpstreamAssetAlias() { AssetConfig asset = AssetConfig.builder().source(AssetConfig.Source.UPSTREAM) .upstream("MISSING").build(); EndpointConfig endpoint = EndpointConfig.builder().id("web").enabled(true).baseUrl("WEB") - .anchor("assets").auth(new AuthConfig("none", List.of())) + .anchor("assets").auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(assetRoute("cdn", "/assets", "assets", asset))).build(); ResolvedTopology topology = topologyWith("WEB"); List endpoints = List.of(endpoint); @@ -1123,9 +1123,9 @@ private static AnchorConfig accessAnchor(AccessLevel access) { } @ParameterizedTest(name = "require ''{0}'' resolves to AUTHENTICATED even under a public anchor") - @ValueSource(strings = {"bearer", "session"}) + @EnumSource(value = Require.class, mode = EnumSource.Mode.EXCLUDE, names = "NONE") @DisplayName("Should treat any non-'none' effective auth as authenticated regardless of anchor access") - void shouldTreatNonNoneRequireAsAuthenticated(String require) { + void shouldTreatNonNoneRequireAsAuthenticated(Require require) { // The strengthened-floor case: the anchor stays access: public, but the route's own floor // must still govern the surface as authenticated (ADR-0007 permits strengthening). AccessLevel access = RouteTableBuilder.effectiveAccessLevel( @@ -1139,7 +1139,7 @@ void shouldTreatNonNoneRequireAsAuthenticated(String require) { @DisplayName("Should fall back to the anchor's declared access for an effectively-unauthenticated route") void shouldFallBackToAnchorAccessWhenUnauthenticated(AccessLevel declared) { AccessLevel access = RouteTableBuilder.effectiveAccessLevel( - accessAnchor(declared), new AuthConfig("none", List.of())); + accessAnchor(declared), new AuthConfig(Require.NONE, List.of())); assertEquals(declared, access); } @@ -1148,7 +1148,7 @@ void shouldFallBackToAnchorAccessWhenUnauthenticated(AccessLevel declared) { @DisplayName("Should default to PUBLIC for an unanchored, effectively-unauthenticated route") void shouldDefaultToPublicWhenUnanchoredAndUnauthenticated() { AccessLevel access = RouteTableBuilder.effectiveAccessLevel( - null, new AuthConfig("none", List.of())); + null, new AuthConfig(Require.NONE, List.of())); assertEquals(AccessLevel.PUBLIC, access); } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.java index a89ccf98..6028aa06 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/load/ConfigLoaderTest.java @@ -42,6 +42,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.IssuerConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.RouteConfig; import de.cuioss.sheriff.gateway.config.model.SecurityDefaultsConfig; import de.cuioss.sheriff.gateway.config.model.UpstreamDefaultsConfig; @@ -464,7 +465,7 @@ void bindsAnchorsBlockAndInjectsTheMapKeyAsName() throws Exception { assertNotNull(api, "the anchor keyed 'api' should bind"); assertEquals("api", api.name(), "the map key is injected as the anchor name"); assertEquals("/api", api.pathPrefix()); - assertEquals("bearer", api.auth().require()); + assertEquals(Require.BEARER, api.auth().require()); assertEquals("strict", api.securityFilter().profile()); assertEquals(List.of(HttpMethod.GET, HttpMethod.POST), api.allowedMethods()); assertEquals(AnchorType.PROXY, api.type(), "the required type axis binds (case-insensitive from 'proxy')"); @@ -673,7 +674,7 @@ void acceptsTheMinimalProfileAtEveryEnumSite() throws Exception { loaded.gateway().anchors().get("api").securityFilter().profile()); assertEquals("minimal", loaded.endpoints().getFirst().routes().getFirst() .securityFilter().profile()); - assertEquals("none", loaded.endpoints().getFirst().auth().require(), + assertEquals(Require.NONE, loaded.endpoints().getFirst().auth().require(), "auth.require: none is a different knob and survives the profile rename"); } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java index 3b23749c..fdb70830 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java @@ -67,7 +67,7 @@ class ConfigModelContractTest { // --- Shared fixtures --------------------------------------------------- private static AuthConfig auth() { - return new AuthConfig("bearer", List.of("read")); + return new AuthConfig(Require.BEARER, List.of("read")); } private static AnchorConfig anchorConfig() { @@ -358,7 +358,7 @@ static Stream valueObjects() { new UpstreamDefaultsConfig(true, true), new UpstreamDefaultsConfig(false, true)), voCase("EndpointConfig", endpointConfig(), endpointConfig(), EndpointConfig.builder() .id("other").baseUrl("svc").auth(auth()).build()), - voCase("AuthConfig", auth(), auth(), new AuthConfig("none", List.of())), + voCase("AuthConfig", auth(), auth(), new AuthConfig(Require.NONE, List.of())), voCase("RouteConfig", routeConfig(), routeConfig(), RouteConfig.builder().id("other").match(matchConfig()).build()), voCase("ResolvedRoute", resolvedRoute(), resolvedRoute(), @@ -423,8 +423,9 @@ class BuilderEquivalence { @Test void authConfigBuilderMatchesConstructor() { - AuthConfig viaCtor = new AuthConfig("bearer", List.of("read")); - AuthConfig viaBuilder = AuthConfig.builder().require("bearer").requiredScopes(List.of("read")).build(); + AuthConfig viaCtor = new AuthConfig(Require.BEARER, List.of("read")); + AuthConfig viaBuilder = AuthConfig.builder().require(Require.BEARER) + .requiredScopes(List.of("read")).build(); assertEquals(viaCtor, viaBuilder); } @@ -571,7 +572,7 @@ void resolvedRouteNormalizesAbsentComponents() { @Test void collectionBearingRecordsNormalizeNullToEmpty() { - assertTrue(new AuthConfig("none", null).requiredScopes().isEmpty()); + assertTrue(new AuthConfig(Require.NONE, null).requiredScopes().isEmpty()); assertTrue(new TokenValidationConfig(null).issuers().isEmpty()); assertTrue(new ForwardedConfig(null, null, null).trustedProxies().isEmpty()); assertTrue(new ForwardConfig(null, null, null, null, null).setHeaders().isEmpty()); @@ -906,7 +907,7 @@ void anchorConfigExposesEveryPolicyBlock() { void resolvedRouteCarriesTheMaterializedEffectivePosture() { ResolvedRoute cfg = resolvedRoute(); assertEquals("api", cfg.anchor()); - assertEquals("bearer", cfg.effectiveAuth().require()); + assertEquals(Require.BEARER, cfg.effectiveAuth().require()); assertEquals(List.of(HttpMethod.GET, HttpMethod.POST), cfg.effectiveAllowedMethods()); assertNotNull(cfg.effectiveSecurityFilter()); assertNotNull(cfg.effectiveSecurityHeaders()); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/topology/TopologyResolverTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/topology/TopologyResolverTest.java index 9ec55ba9..bcb8260b 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/topology/TopologyResolverTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/topology/TopologyResolverTest.java @@ -33,6 +33,7 @@ import de.cuioss.sheriff.gateway.config.load.EnvSecretResolver; import de.cuioss.sheriff.gateway.config.model.AuthConfig; import de.cuioss.sheriff.gateway.config.model.EndpointConfig; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.topology.TopologyResolver.TopologyResolutionException; @@ -62,7 +63,7 @@ private static EndpointConfig endpointFor(String alias) { .id(alias.toLowerCase(Locale.ROOT)) .enabled(true) .baseUrl(alias) - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .build(); } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorRouteDisjointnessTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorRouteDisjointnessTest.java index 59a9a049..d1bb6cf2 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorRouteDisjointnessTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorRouteDisjointnessTest.java @@ -30,6 +30,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.MatchConfig.HeaderMatcher; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteConfig; @@ -97,7 +98,7 @@ private List validateRoutes(MatchConfig first, MatchConfig second) .id("orders") .enabled(true) .baseUrl("ORDERS") - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(route("first", first), route("second", second))) .build(); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java index 3f7eb17f..763d1726 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java @@ -47,6 +47,7 @@ import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedTopology; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteConfig; @@ -144,7 +145,7 @@ private static EndpointConfig endpoint(String id, String alias, List .id(id) .enabled(true) .baseUrl(alias) - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .allowedMethods(allowedMethods) .routes(List.of(routes)) .build(); @@ -157,7 +158,7 @@ private static void assertHasError(List errors, String pointerConta + messageContains + "', but got: " + errors); } - private static AnchorConfig anchor(String name, String prefix, String require) { + private static AnchorConfig anchor(String name, String prefix, @Nullable Require require) { // The ADR-0007 anchor rules (prefix disjointness, namespace membership, auth floor) are // orthogonal to the ADR-0013 access->auth matrix, so these fixtures stay matrix-consistent // by construction: an anchor with no auth floor is access: public (public + no auth block is @@ -173,7 +174,7 @@ private static AnchorConfig anchor(String name, String prefix, String require) { } private static AnchorConfig matrixAnchor(String name, String prefix, AnchorType type, AccessLevel access, - String require) { + @Nullable Require require) { return AnchorConfig.builder() .name(name) .pathPrefix(prefix) @@ -245,7 +246,7 @@ void shouldAcceptDirectoryAssetUnderAssetAnchor() { GatewayConfig gateway = gatewayWithAnchors(Map.of("assets", matrixAnchor("assets", "/assets", AnchorType.ASSET, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("web", "WEB", "assets", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), assetRoute("bundle", "/assets", "assets", directoryAsset("/srv/assets"), HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("WEB")); @@ -259,7 +260,7 @@ void shouldAcceptUpstreamAssetWithResolvableAlias() { GatewayConfig gateway = gatewayWithAnchors(Map.of("assets", matrixAnchor("assets", "/assets", AnchorType.ASSET, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("web", "WEB", "assets", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), assetRoute("cdn", "/assets", "assets", upstreamAsset("SECONDARY"), HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("WEB", "SECONDARY")); @@ -273,7 +274,7 @@ void shouldRejectAssetAnchorWithoutAssetBlock() { GatewayConfig gateway = gatewayWithAnchors(Map.of("assets", matrixAnchor("assets", "/assets", AnchorType.ASSET, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("web", "WEB", "assets", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), anchoredRoute("noasset", "/assets", "assets", HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("WEB")); @@ -287,7 +288,7 @@ void shouldRejectAssetBlockUnderProxyAnchor() { GatewayConfig gateway = gatewayWithAnchors(Map.of("api", matrixAnchor("api", "/api", AnchorType.PROXY, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("api-ep", "API", "api", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), assetRoute("mixed", "/api", "api", directoryAsset("/srv/assets"), HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("API")); @@ -301,7 +302,7 @@ void shouldRejectAssetBlockOnUnanchoredRoute() { GatewayConfig gateway = validGateway().build(); EndpointConfig endpoint = EndpointConfig.builder() .id("plain").enabled(true).baseUrl("PLAIN") - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(assetRoute("loose", "/loose", null, directoryAsset("/srv/assets"), HttpMethod.GET))) .build(); @@ -316,7 +317,7 @@ void shouldRejectUnresolvableUpstreamAssetAlias() { GatewayConfig gateway = gatewayWithAnchors(Map.of("assets", matrixAnchor("assets", "/assets", AnchorType.ASSET, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("web", "WEB", "assets", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), assetRoute("cdn", "/assets", "assets", upstreamAsset("MISSING"), HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("WEB")); @@ -330,7 +331,7 @@ void shouldRejectDirectoryAssetWithoutRoot() { GatewayConfig gateway = gatewayWithAnchors(Map.of("assets", matrixAnchor("assets", "/assets", AnchorType.ASSET, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("web", "WEB", "assets", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), assetRoute("bundle", "/assets", "assets", directoryAsset(null), HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("WEB")); @@ -586,7 +587,7 @@ void shouldRejectUnresolvedAliasForEnabledEndpoint() { void shouldRejectBearerWithoutIssuer() { EndpointConfig endpoint = EndpointConfig.builder() .id("orders").enabled(true).baseUrl("ORDERS") - .auth(new AuthConfig("bearer", List.of())) + .auth(new AuthConfig(Require.BEARER, List.of())) .routes(List.of(route("r", HttpMethod.GET))) .build(); @@ -601,7 +602,7 @@ void shouldRejectBearerWithoutIssuer() { void shouldRejectSessionWithoutOidc() { EndpointConfig endpoint = EndpointConfig.builder() .id("orders").enabled(true).baseUrl("ORDERS") - .auth(new AuthConfig("session", List.of())) + .auth(new AuthConfig(Require.SESSION, List.of())) .routes(List.of(route("r", HttpMethod.GET))) .build(); @@ -632,7 +633,7 @@ void shouldAcceptMillisecondPrecisionTimeout() { .build(); EndpointConfig endpoint = EndpointConfig.builder() .id("orders").enabled(true).baseUrl("ORDERS") - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(route)) .build(); @@ -879,7 +880,7 @@ void shouldAcceptBearerWithIssuer() { .build(); EndpointConfig endpoint = EndpointConfig.builder() .id("orders").enabled(true).baseUrl("ORDERS") - .auth(new AuthConfig("bearer", List.of())) + .auth(new AuthConfig(Require.BEARER, List.of())) .routes(List.of(route("r", HttpMethod.GET))) .build(); @@ -950,7 +951,7 @@ void shouldRejectOverlappingAnchorPrefixes() { void shouldRejectUndefinedAnchorReference() { GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", null))); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "ghost", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), anchoredRoute("r", "/other", null, HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -963,7 +964,7 @@ void shouldRejectUndefinedAnchorReference() { void shouldRejectRoutePathOutsideDeclaredAnchor() { GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", null))); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), anchoredRoute("r", "/billing", "api", HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -976,7 +977,7 @@ void shouldRejectRoutePathOutsideDeclaredAnchor() { void shouldRejectUndeclaredSquatter() { GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", null))); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", null, - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), anchoredRoute("r", "/api/secret", null, HttpMethod.GET)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -987,10 +988,10 @@ void shouldRejectUndeclaredSquatter() { @Test @DisplayName("Rule 5: Should reject an effective 'none' auth that weakens a non-none anchor floor") void shouldRejectWeakenedAuthFloor() { - GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", "bearer"))); + GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", Require.BEARER))); RouteConfig weakening = RouteConfig.builder().id("r").anchor("api") .match(match("/api/x", HttpMethod.GET)) - .auth(new AuthConfig("none", List.of())).build(); + .auth(new AuthConfig(Require.NONE, List.of())).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api", null, weakening); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -1015,7 +1016,7 @@ void shouldRejectRouteWithoutAnyAuthSource() { void shouldAcceptEndpointWhereEveryRouteSuppliesOwnAuth() { GatewayConfig gateway = validGateway().build(); RouteConfig selfAuth = RouteConfig.builder().id("r").match(match("/r", HttpMethod.GET)) - .auth(new AuthConfig("none", List.of())).build(); + .auth(new AuthConfig(Require.NONE, List.of())).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", null, null, selfAuth); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -1028,7 +1029,7 @@ void shouldAcceptEndpointWhereEveryRouteSuppliesOwnAuth() { @DisplayName("Rule 6: Should catch a route overriding to an auth-less anchor that the endpoint anchor would mask") void shouldCatchRouteAnchorOverrideToAuthLessAnchor() { GatewayConfig gateway = gatewayWithAnchors(Map.of( - "secured", anchor("secured", "/api", "bearer"), + "secured", anchor("secured", "/api", Require.BEARER), "open", anchor("open", "/open", null))); RouteConfig override = RouteConfig.builder().id("r").anchor("open") .match(match("/open/x", HttpMethod.GET)).build(); @@ -1048,7 +1049,7 @@ void shouldCatchRouteAnchorOverrideToAuthLessAnchor() { @Test @DisplayName("Rule 7: Should carry an anchor-provided bearer posture into the effective-auth completeness check") void shouldPropagateAnchorAuthIntoEffectiveAuthCheck() { - GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", "bearer"))); + GatewayConfig gateway = gatewayWithAnchors(Map.of("api", anchor("api", "/api", Require.BEARER))); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api", null, anchoredRoute("r", "/api/x", "api", HttpMethod.GET)); @@ -1061,7 +1062,7 @@ void shouldPropagateAnchorAuthIntoEffectiveAuthCheck() { @DisplayName("Should accept a well-formed anchored configuration") void shouldAcceptValidAnchoredConfig() { GatewayConfig gateway = validGateway() - .anchors(Map.of("api", anchor("api", "/api", "bearer"))) + .anchors(Map.of("api", anchor("api", "/api", Require.BEARER))) .tokenValidation(new TokenValidationConfig(List.of( IssuerConfig.builder().name("main").issuer("https://idp.example").build()))) .build(); @@ -1080,7 +1081,7 @@ void shouldAggregateAnchorViolations() { "api", anchor("api", "/api", null), "apiv1", anchor("apiv1", "/api/v1", null))); EndpointConfig squatter = anchoredEndpoint("s", "S", null, - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), anchoredRoute("sr", "/api/secret", null, HttpMethod.GET)); List errors = validator.validate(gateway, List.of(squatter), topologyWith("S")); @@ -1141,7 +1142,7 @@ private static RouteConfig grpcRoute(String id, String prefix, String anchorName void shouldExemptGrpcRouteFromDeclaredAnchorContainment() { GatewayConfig gateway = gatewayWithAnchors(Map.of("grpc", anchor("grpc", "/grpc", null))); EndpointConfig endpoint = anchoredEndpoint("echo", "ECHO", "grpc", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), grpcRoute("grpc-echo", ECHO_PATH, "grpc", null)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ECHO")); @@ -1156,7 +1157,7 @@ void shouldExemptGrpcRouteFromDeclaredAnchorContainment() { void shouldAcceptTwoGrpcRoutesUnderOneAnchorOnBareServicePaths() { GatewayConfig gateway = gatewayWithAnchors(Map.of("grpc", anchor("grpc", "/grpc", null))); EndpointConfig endpoint = anchoredEndpoint("echo", "ECHO", "grpc", - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), grpcRoute("grpc-echo", ECHO_PATH, "grpc", null), grpcRoute("grpc-bearer", SECURE_ECHO_PATH, "grpc", null)); @@ -1171,7 +1172,7 @@ void shouldAcceptTwoGrpcRoutesUnderOneAnchorOnBareServicePaths() { void shouldExemptGrpcRouteFromUndeclaredSquatterRule() { GatewayConfig gateway = gatewayWithAnchors(Map.of("root", anchor("root", "/", null))); EndpointConfig endpoint = anchoredEndpoint("echo", "ECHO", null, - new AuthConfig("none", List.of()), + new AuthConfig(Require.NONE, List.of()), grpcRoute("grpc-echo", ECHO_PATH, null, null)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ECHO")); @@ -1188,7 +1189,7 @@ void shouldStillEnforceContainmentForNonGrpcRoute() { RouteConfig websocket = RouteConfig.builder().id("ws").anchor("api") .protocol(Protocol.WEBSOCKET) .match(match("/billing", HttpMethod.GET)) - .auth(new AuthConfig("none", List.of())).build(); + .auth(new AuthConfig(Require.NONE, List.of())).build(); EndpointConfig endpoint = anchoredEndpoint("orders", "ORDERS", "api", null, websocket); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ORDERS")); @@ -1199,9 +1200,9 @@ void shouldStillEnforceContainmentForNonGrpcRoute() { @Test @DisplayName("Auth floor stays enforced for a gRPC route: effective 'none' still weakens a non-none anchor floor") void shouldStillEnforceAuthFloorForGrpcRoute() { - GatewayConfig gateway = gatewayWithAnchors(Map.of("grpc", anchor("grpc", "/grpc", "bearer"))); + GatewayConfig gateway = gatewayWithAnchors(Map.of("grpc", anchor("grpc", "/grpc", Require.BEARER))); EndpointConfig endpoint = anchoredEndpoint("echo", "ECHO", "grpc", null, - grpcRoute("grpc-echo", ECHO_PATH, "grpc", new AuthConfig("none", List.of()))); + grpcRoute("grpc-echo", ECHO_PATH, "grpc", new AuthConfig(Require.NONE, List.of()))); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("ECHO")); @@ -1229,7 +1230,7 @@ void shouldRejectBffAnchorThatIsNotAuthenticated() { @DisplayName("Rule public+auth: Should reject an access: public anchor declaring an auth block for any non-bff type") void shouldRejectPublicAnchorDeclaringAuthBlock(AnchorType type) { GatewayConfig gateway = gatewayWithAnchors(Map.of( - "open", matrixAnchor("open", "/open", type, AccessLevel.PUBLIC, "bearer"))); + "open", matrixAnchor("open", "/open", type, AccessLevel.PUBLIC, Require.BEARER))); List errors = validator.validate(gateway, List.of(), topologyWith()); @@ -1237,9 +1238,9 @@ void shouldRejectPublicAnchorDeclaringAuthBlock(AnchorType type) { } @ParameterizedTest - @ValueSource(strings = {"none", "bearer", "session"}) + @EnumSource(Require.class) @DisplayName("Rule public+auth: Should reject an access: public anchor for every auth-floor value in the vocabulary") - void shouldRejectPublicAnchorForEveryAuthFloorValue(String require) { + void shouldRejectPublicAnchorForEveryAuthFloorValue(Require require) { GatewayConfig gateway = gatewayWithAnchors(Map.of( "open", matrixAnchor("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, require))); @@ -1251,17 +1252,18 @@ void shouldRejectPublicAnchorForEveryAuthFloorValue(String require) { static Stream authenticatedAnchorsWithoutBackedFloor() { return Stream.of( Arguments.of("no auth floor at all", null, "declares no non-'none' auth floor"), - Arguments.of("an explicit 'none' floor", "none", "declares no non-'none' auth floor"), - Arguments.of("a bearer floor with no token_validation issuer", "bearer", + Arguments.of("an explicit 'none' floor", Require.NONE, "declares no non-'none' auth floor"), + Arguments.of("a bearer floor with no token_validation issuer", Require.BEARER, "access: authenticated bearer floor requires token_validation with at least one issuer"), - Arguments.of("a session floor with no oidc block", "session", + Arguments.of("a session floor with no oidc block", Require.SESSION, "access: authenticated session floor requires an oidc block")); } @ParameterizedTest(name = "{0}") @MethodSource("authenticatedAnchorsWithoutBackedFloor") @DisplayName("Rules authenticated→floor and authenticated backing: Should reject an access: authenticated anchor without a backed auth floor") - void shouldRejectAuthenticatedAnchorWithoutBackedFloor(String label, String require, String expectedDetail) { + void shouldRejectAuthenticatedAnchorWithoutBackedFloor(String label, @Nullable Require require, + String expectedDetail) { GatewayConfig gateway = gatewayWithAnchors(Map.of( "secure", matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, require))); @@ -1274,7 +1276,7 @@ void shouldRejectAuthenticatedAnchorWithoutBackedFloor(String label, String requ @DisplayName("Should accept a type 'bff' anchor that is access: authenticated with a backed bearer floor") void shouldAcceptAuthenticatedBffWithBackedBearerFloor() { GatewayConfig gateway = gatewayWithAnchorAndIssuer( - matrixAnchor("portal", "/portal", AnchorType.BFF, AccessLevel.AUTHENTICATED, "bearer")); + matrixAnchor("portal", "/portal", AnchorType.BFF, AccessLevel.AUTHENTICATED, Require.BEARER)); List errors = validator.validate(gateway, List.of(), topologyWith()); @@ -1300,7 +1302,7 @@ void shouldAcceptPublicAnchorWithoutAuthBlock(AnchorType type) { void shouldAggregateMatrixViolationsInOnePass() { GatewayConfig gateway = gatewayWithAnchors(Map.of( "portal", matrixAnchor("portal", "/portal", AnchorType.BFF, AccessLevel.PUBLIC, null), - "open", matrixAnchor("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, "bearer"), + "open", matrixAnchor("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, Require.BEARER), "secure", matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, null))); List errors = validator.validate(gateway, List.of(), topologyWith()); @@ -1340,7 +1342,7 @@ void shouldReportEveryViolationInOnePass() { GatewayConfig gateway = GatewayConfig.builder().version(2).build(); EndpointConfig endpoint = EndpointConfig.builder() .id("orders").enabled(true).baseUrl("MISSING") - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .allowedMethods(List.of(HttpMethod.GET)) .routes(List.of(route("orders-post", HttpMethod.POST))) .build(); @@ -1369,7 +1371,7 @@ private static GatewayConfig gatewayWithIssuer() { private static EndpointConfig webSocketEndpoint(String alias, RouteConfig route) { return EndpointConfig.builder() .id("ws-ep").enabled(true).baseUrl(alias) - .auth(new AuthConfig("none", List.of())) + .auth(new AuthConfig(Require.NONE, List.of())) .routes(List.of(route)) .build(); } @@ -1386,7 +1388,7 @@ private static RouteConfig webSocketRoute(String id, @Nullable WebSocketConfig w } private static AuthConfig bearer() { - return new AuthConfig("bearer", List.of()); + return new AuthConfig(Require.BEARER, List.of()); } @Test @@ -1643,8 +1645,6 @@ void shouldAcceptInBoundsMaxCookieSize(int maxCookieSize) { class SecurityProfileMinimalRefusal { private static final String MINIMAL_PROFILE = "minimal"; - private static final String REQUIRE_NONE = "none"; - private static final String REQUIRE_BEARER = "bearer"; private static final String REFUSAL_MESSAGE = "resolves inbound-filter profile 'minimal'"; private static RouteConfig profiledRoute(String id, String prefix, String anchorName, String profile, @@ -1660,7 +1660,7 @@ private static RouteConfig profiledRoute(String id, String prefix, String anchor } private static AnchorConfig anchorWithProfile(String name, String prefix, AnchorType type, - AccessLevel access, String require, String profile) { + AccessLevel access, @Nullable Require require, String profile) { return AnchorConfig.builder() .name(name) .pathPrefix(prefix) @@ -1699,7 +1699,7 @@ private static GatewayConfig gatewayWithGlobalProfile(AnchorConfig anchorConfig, void shouldRejectMinimalOnAuthenticatedAnchor() { // Arrange — the anchor's bearer floor makes every route under it effectively authenticated. GatewayConfig gateway = gatewayWithAnchorAndIssuer( - matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER)); + matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER)); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profiledRoute("secure-read", "/secure/read", "secure", MINIMAL_PROFILE, null)); @@ -1717,7 +1717,7 @@ void shouldRejectMinimalOnBffAnchor() { // Arrange — a bff anchor is required to be access: authenticated (ADR-0013), so a matrix-clean // bff fixture necessarily trips both refusal dimensions; the anchor-type one must be named. GatewayConfig gateway = gatewayWithAnchorAndIssuer( - matrixAnchor("shell", "/shell", AnchorType.BFF, AccessLevel.AUTHENTICATED, REQUIRE_BEARER)); + matrixAnchor("shell", "/shell", AnchorType.BFF, AccessLevel.AUTHENTICATED, Require.BEARER)); EndpointConfig endpoint = anchoredEndpoint("bff", "BFF", "shell", null, profiledRoute("shell-view", "/shell/view", "shell", MINIMAL_PROFILE, null)); @@ -1737,9 +1737,9 @@ void shouldRejectMinimalOnRouteStrengtheningPublicAnchorFloor() { GatewayConfig gateway = gatewayWithAnchorAndIssuer( matrixAnchor("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, null)); EndpointConfig endpoint = anchoredEndpoint("public-api", "API", "open", - new AuthConfig(REQUIRE_NONE, List.of()), + new AuthConfig(Require.NONE, List.of()), profiledRoute("open-secured", "/open/secured", "open", MINIMAL_PROFILE, - new AuthConfig(REQUIRE_BEARER, List.of()))); + new AuthConfig(Require.BEARER, List.of()))); // Act List errors = validator.validate(gateway, List.of(endpoint), topologyWith("API")); @@ -1755,7 +1755,7 @@ void shouldAcceptMinimalOnPublicUnauthenticatedRoute() { GatewayConfig gateway = gatewayWithAnchors(Map.of("open", matrixAnchor("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, null))); EndpointConfig endpoint = anchoredEndpoint("public-api", "API", "open", - new AuthConfig(REQUIRE_NONE, List.of()), + new AuthConfig(Require.NONE, List.of()), profiledRoute("open-read", "/open/read", "open", MINIMAL_PROFILE, null)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("API")); @@ -1769,7 +1769,7 @@ void shouldRejectGlobalMinimalInheritedByAuthenticatedRoute() { // Arrange — the route declares no security_filter at all; 'minimal' reaches it through the // gateway-wide fallback, which is the same violation as declaring it per route. GatewayConfig gateway = gatewayWithGlobalProfile( - matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER), + matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER), MINIMAL_PROFILE); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profiledRoute("secure-read", "/secure/read", "secure", null, null)); @@ -1791,7 +1791,7 @@ void shouldAcceptGlobalMinimalOnPublicRoutesOnly() { null, null)) .build(); EndpointConfig endpoint = anchoredEndpoint("public-api", "API", "open", - new AuthConfig(REQUIRE_NONE, List.of()), + new AuthConfig(Require.NONE, List.of()), profiledRoute("open-read", "/open/read", "open", null, null)); List errors = validator.validate(gateway, List.of(endpoint), topologyWith("API")); @@ -1805,7 +1805,7 @@ void shouldRejectAnchorDeclaredMinimalInheritedByAuthenticatedRoute() { // Arrange — the middle leg of the resolution chain: the route declares no security_filter, // so 'minimal' reaches it from the anchor's own block rather than per route or gateway-wide. GatewayConfig gateway = gatewayWithAnchorAndIssuer(anchorWithProfile("secure", "/secure", - AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER, MINIMAL_PROFILE)); + AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER, MINIMAL_PROFILE)); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profiledRoute("secure-read", "/secure/read", "secure", null, null)); @@ -1824,7 +1824,7 @@ void shouldAcceptAnchorDeclaredMinimalOnPublicUnauthenticatedRoute() { GatewayConfig gateway = gatewayWithAnchors(Map.of("open", anchorWithProfile("open", "/open", AnchorType.PROXY, AccessLevel.PUBLIC, null, MINIMAL_PROFILE))); EndpointConfig endpoint = anchoredEndpoint("public-api", "API", "open", - new AuthConfig(REQUIRE_NONE, List.of()), + new AuthConfig(Require.NONE, List.of()), profiledRoute("open-read", "/open/read", "open", null, null)); // Act @@ -1841,7 +1841,7 @@ void shouldReplaceAnchorFilterBlockWholesaleRatherThanMergeProfile() { // declaring 'minimal'. The block is replaced wholesale, so the profile falls back to the // gateway-wide 'strict' and never to the anchor's 'minimal' — a merge would refuse here. GatewayConfig gateway = gatewayWithGlobalProfile(anchorWithProfile("secure", "/secure", - AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER, MINIMAL_PROFILE), "strict"); + AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER, MINIMAL_PROFILE), "strict"); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profileLessFilterRoute("secure-read", "/secure/read", "secure")); @@ -1857,7 +1857,7 @@ void shouldReplaceAnchorFilterBlockWholesaleRatherThanMergeProfile() { @DisplayName("Should accept a non-'minimal' profile on an authenticated route, case-insensitively") void shouldAcceptNonMinimalProfileOnAuthenticatedRoute(String profile) { GatewayConfig gateway = gatewayWithAnchorAndIssuer( - matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER)); + matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER)); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profiledRoute("secure-read", "/secure/read", "secure", profile, null)); @@ -1874,7 +1874,7 @@ void shouldAggregateRefusalWithUnrelatedViolation() { .version(2) .anchors(Map.of("secure", matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, - REQUIRE_BEARER))) + Require.BEARER))) .tokenValidation(new TokenValidationConfig(List.of( IssuerConfig.builder().name("main").issuer("https://idp.example").build()))) .build(); @@ -1894,7 +1894,7 @@ void shouldAggregateRefusalWithUnrelatedViolation() { @DisplayName("Should name the remedy and echo no configured scalar value") void shouldNameRemedyWithoutEchoingConfiguredScalars() { GatewayConfig gateway = gatewayWithAnchorAndIssuer( - matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, REQUIRE_BEARER)); + matrixAnchor("secure", "/secure", AnchorType.PROXY, AccessLevel.AUTHENTICATED, Require.BEARER)); EndpointConfig endpoint = anchoredEndpoint("api", "API", "secure", null, profiledRoute("secure-read", "/secure/read", "secure", MINIMAL_PROFILE, null)); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgePipelineTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgePipelineTest.java index dd4a0578..8b44ac4f 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgePipelineTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgePipelineTest.java @@ -38,6 +38,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -106,8 +107,8 @@ void setUp() throws Exception { .issuerConfig(TestTokenGenerators.accessTokens().next().getIssuerConfig()).build(); RouteTable routeTable = new RouteTable(List.of( - route("secure", "/secure", "bearer", upstreamPort, HttpMethod.GET), - route("echo", "/echo", "none", upstreamPort, HttpMethod.GET, HttpMethod.POST), + route("secure", "/secure", Require.BEARER, upstreamPort, HttpMethod.GET), + route("echo", "/echo", Require.NONE, upstreamPort, HttpMethod.GET, HttpMethod.POST), minimalModeRoute(upstreamPort))); GatewayConfig gatewayConfig = GatewayConfig.builder() @@ -366,14 +367,14 @@ private static ResolvedRoute minimalModeRoute(int upstreamPort) { .id("open") .protocol(Protocol.HTTP) .match(MatchConfig.builder().pathPrefix("/open").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET, HttpMethod.POST)) .effectiveSecurityFilter(filter) .upstream(new ResolvedUpstream("http", "localhost", upstreamPort, "")) .build(); } - private static ResolvedRoute route(String id, String pathPrefix, String require, int upstreamPort, + private static ResolvedRoute route(String id, String pathPrefix, Require require, int upstreamPort, HttpMethod... methods) { return ResolvedRoute.builder() .id(id) diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java index 70933a67..7c1ff375 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java @@ -65,6 +65,7 @@ import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -279,7 +280,7 @@ private static ResolvedRoute rejectEverythingRoute() { .id("auth-proxy") .protocol(Protocol.HTTP) .match(MatchConfig.builder().pathPrefix("/auth").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .effectiveSecurityFilter(SecurityFilterConfig.builder() .allowedPaths(List.of("/auth/never-matches")).build()) @@ -700,7 +701,7 @@ private static ResolvedRoute sessionRoute() { .id("s") .protocol(Protocol.HTTP) .match(MatchConfig.builder().pathPrefix("/s").build()) - .effectiveAuth(AuthConfig.builder().require("session").build()) + .effectiveAuth(AuthConfig.builder().require(Require.SESSION).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .upstream(new ResolvedUpstream("https", "s.example", 443, "")) .build(); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java index 5f7a50e5..b9b0ef9a 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java @@ -51,6 +51,7 @@ import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.OidcConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -147,7 +148,7 @@ void registersCatchAllRoute() { void bootsSessionAuthRoute() { // Arrange RouteTable sessionTable = new RouteTable(List.of( - route("s", Protocol.HTTP, "session"))); + route("s", Protocol.HTTP, Require.SESSION))); // Act + Assert — a require:session route now assembles at boot; its stage-4 runtime is the // SessionAuthenticationStage (D4), which replaced the boot-time CONFIG_INVALID rejection. This @@ -161,7 +162,7 @@ void bootsSessionAuthRoute() { void bootsGrpcProtocol() { // Arrange RouteTable grpcTable = new RouteTable(List.of( - route("g", Protocol.GRPC, "none"))); + route("g", Protocol.GRPC, Require.NONE))); // Act + Assert — GRPC is now registered, so a gRPC route assembles cleanly at boot (the boot // rejection was removed with the gRPC processor). @@ -174,7 +175,7 @@ void bootsGrpcProtocol() { void bootsWebSocketProtocol() { // Arrange RouteTable webSocketTable = new RouteTable(List.of( - route("w", Protocol.WEBSOCKET, "none"))); + route("w", Protocol.WEBSOCKET, Require.NONE))); // Act + Assert — WEBSOCKET is now registered, so a WebSocket route assembles cleanly at boot // (the boot rejection was removed with the WebSocket processor). @@ -187,7 +188,7 @@ void bootsWebSocketProtocol() { void bootsSessionAuthWebSocketRoute() { // Arrange RouteTable webSocketTable = new RouteTable(List.of( - route("w", Protocol.WEBSOCKET, "session"))); + route("w", Protocol.WEBSOCKET, Require.SESSION))); // Act + Assert — session auth no longer gates boot, so a session-auth WebSocket route // assembles exactly like any other WebSocket route. @@ -792,7 +793,7 @@ private static ResolvedRoute webSocketRoute(int upstreamPort) { .id("w") .protocol(Protocol.WEBSOCKET) .match(MatchConfig.builder().pathPrefix("/w").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .upstream(new ResolvedUpstream("http", "localhost", upstreamPort, "")) .build(); @@ -867,7 +868,7 @@ public Iterator iterator() { } } - private static ResolvedRoute route(String id, Protocol protocol, String require) { + private static ResolvedRoute route(String id, Protocol protocol, Require require) { return ResolvedRoute.builder() .id(id) .protocol(protocol) diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GrpcDispatchStageTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GrpcDispatchStageTest.java index 14f8dcbb..9d7dcf46 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GrpcDispatchStageTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GrpcDispatchStageTest.java @@ -42,6 +42,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -353,7 +354,7 @@ private static ResolvedRoute route(String id, Protocol protocol, ResolvedUpstrea .id(id) .protocol(protocol) .match(MatchConfig.builder().pathPrefix("/" + id).build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.POST)) .upstream(upstream) .build(); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssemblerTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssemblerTest.java index 1eecf08f..fb69986d 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssemblerTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssemblerTest.java @@ -44,6 +44,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedAsset; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; @@ -102,8 +103,8 @@ void shouldReuseSecurityConfigurationForSharedShape() { SecurityConfiguration.builder().build()); }; RouteTable table = new RouteTable(List.of( - route("r1", Protocol.HTTP, "none", sharedFilter, upstream("a.example")), - route("r2", Protocol.HTTP, "none", sharedFilter, upstream("a.example")))); + route("r1", Protocol.HTTP, Require.NONE, sharedFilter, upstream("a.example")), + route("r2", Protocol.HTTP, Require.NONE, sharedFilter, upstream("a.example")))); List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -117,9 +118,9 @@ void shouldReuseSecurityConfigurationForSharedShape() { @DisplayName("Should build distinct SecurityConfigurations for different security-filter shapes") void shouldBuildDistinctSecurityConfigurationsForDifferentShapes() { RouteTable table = new RouteTable(List.of( - route("r1", Protocol.HTTP, "none", + route("r1", Protocol.HTTP, Require.NONE, SecurityFilterConfig.builder().allowedPaths(List.of("/a")).build(), upstream("a.example")), - route("r2", Protocol.HTTP, "none", + route("r2", Protocol.HTTP, Require.NONE, SecurityFilterConfig.builder().allowedPaths(List.of("/b")).build(), upstream("a.example")))); List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -133,9 +134,9 @@ void shouldBuildDistinctSecurityConfigurationsForDifferentShapes() { @DisplayName("Should reuse one client for routes sharing an upstream tuple and split by tuple") void shouldReuseClientForSharedUpstreamTuple() { RouteTable table = new RouteTable(List.of( - route("r1", Protocol.HTTP, "none", null, upstream("a.example")), - route("r2", Protocol.HTTP, "none", null, upstream("a.example")), - route("r3", Protocol.HTTP, "none", null, upstream("b.example")))); + route("r1", Protocol.HTTP, Require.NONE, null, upstream("a.example")), + route("r2", Protocol.HTTP, Require.NONE, null, upstream("a.example")), + route("r3", Protocol.HTTP, Require.NONE, null, upstream("b.example")))); List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -151,8 +152,8 @@ void shouldReuseClientForSharedUpstreamTuple() { @DisplayName("Should preserve the route-table order") void shouldPreserveRouteTableOrder() { RouteTable table = new RouteTable(List.of( - route("first", Protocol.HTTP, "none", null, upstream("a.example")), - route("second", Protocol.GRAPHQL, "bearer", null, upstream("b.example")))); + route("first", Protocol.HTTP, Require.NONE, null, upstream("a.example")), + route("second", Protocol.GRAPHQL, Require.BEARER, null, upstream("b.example")))); List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -166,11 +167,11 @@ void shouldAssembleSessionRoutes() { // A require:session route now assembles like any other route — its stage-4 runtime is the // SessionAuthenticationStage (D4), which replaced the boot-time CONFIG_INVALID rejection. RouteTable sessionTable = new RouteTable(List.of( - route("s", Protocol.HTTP, "session", null, upstream("a.example")))); + route("s", Protocol.HTTP, Require.SESSION, null, upstream("a.example")))); List sessionRuntimes = assertDoesNotThrow( () -> assembler.assemble(sessionTable, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory), "a require:session route assembles now the boot-time rejection is removed"); - assertEquals("session", sessionRuntimes.getFirst().getEffectiveAuth().require(), + assertEquals(Require.SESSION, sessionRuntimes.getFirst().getEffectiveAuth().require(), "the assembled route keeps its require:session posture for the stage-4 runtime to dispatch on"); // A session-auth WebSocket route likewise assembles — session auth no longer gates boot, so @@ -178,12 +179,12 @@ void shouldAssembleSessionRoutes() { // runtime it produced rather than on the absence of a throw: an assemble() that quietly // dropped the route would return an empty list and satisfy a bare no-throw assertion. RouteTable webSocketSessionTable = new RouteTable(List.of( - route("sw", Protocol.WEBSOCKET, "session", null, upstream("a.example")))); + route("sw", Protocol.WEBSOCKET, Require.SESSION, null, upstream("a.example")))); RouteRuntime webSocketSession = assembler.assemble(webSocketSessionTable, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory).getFirst(); assertEquals("sw", webSocketSession.getId(), "the session-auth WebSocket route reaches the assembled table"); - assertEquals("session", webSocketSession.getEffectiveAuth().require(), + assertEquals(Require.SESSION, webSocketSession.getEffectiveAuth().require(), "and keeps its require:session posture for the stage-4 runtime to dispatch on"); // A gRPC route with non-session auth assembles cleanly — and asks for a forced-h2 upstream @@ -194,7 +195,7 @@ void shouldAssembleSessionRoutes() { // or not forced-h2 was ever requested. List grpcTargets = new ArrayList<>(); RouteTable grpcTable = new RouteTable(List.of( - route("g", Protocol.GRPC, "none", null, upstream("a.example")))); + route("g", Protocol.GRPC, Require.NONE, null, upstream("a.example")))); RouteRuntime grpc = assembler.assemble(grpcTable, securityConfigFactory, capturingClientFactory(grpcTargets), guardFactory, assetSourceFactory).getFirst(); assertEquals("g", grpc.getId(), "the gRPC route reaches the assembled table"); @@ -209,11 +210,11 @@ void shouldAssembleSessionRoutes() { // protocol rather than passing for every route the assembler builds. List webSocketTargets = new ArrayList<>(); RouteTable webSocketNoneTable = new RouteTable(List.of( - route("w", Protocol.WEBSOCKET, "none", null, upstream("a.example")))); + route("w", Protocol.WEBSOCKET, Require.NONE, null, upstream("a.example")))); RouteRuntime webSocketNone = assembler.assemble(webSocketNoneTable, securityConfigFactory, capturingClientFactory(webSocketTargets), guardFactory, assetSourceFactory).getFirst(); assertEquals("w", webSocketNone.getId(), "the WebSocket route reaches the assembled table"); - assertEquals("none", webSocketNone.getEffectiveAuth().require(), + assertEquals(Require.NONE, webSocketNone.getEffectiveAuth().require(), "and carries its declared require:none posture"); assertEquals(1, webSocketTargets.size(), "the WebSocket route resolves exactly one upstream client"); assertFalse(webSocketTargets.getFirst().forcedHttp2(), @@ -236,7 +237,8 @@ private RouteRuntimeAssembler.UpstreamClientFactory capturingClientFactory( @Test @DisplayName("Should carry the required scopes from the effective auth") void shouldCarryRequiredScopes() { - AuthConfig auth = AuthConfig.builder().require("bearer").requiredScopes(List.of("read", "write")).build(); + AuthConfig auth = AuthConfig.builder().require(Require.BEARER) + .requiredScopes(List.of("read", "write")).build(); RouteTable table = new RouteTable(List.of(ResolvedRoute.builder() .id("scoped").protocol(Protocol.HTTP).match(MatchConfig.builder().pathPrefix("/s").build()) .effectiveAuth(auth).effectiveAllowedMethods(List.of(HttpMethod.GET)) @@ -255,7 +257,7 @@ void shouldCarryEffectiveForward() { Map.of("X-Gateway", "api-sheriff")); RouteTable table = new RouteTable(List.of(ResolvedRoute.builder() .id("fwd").protocol(Protocol.HTTP).match(MatchConfig.builder().pathPrefix("/f").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .upstream(upstream("a.example")).effectiveForward(forward).build())); @@ -271,7 +273,7 @@ void shouldCarryEffectiveForwardDenyLists() { ForwardConfig forward = new ForwardConfig(null, List.of("Cookie"), null, List.of("debug"), Map.of()); RouteTable table = new RouteTable(List.of(ResolvedRoute.builder() .id("deny").protocol(Protocol.HTTP).match(MatchConfig.builder().pathPrefix("/d").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .upstream(upstream("a.example")).effectiveForward(forward).build())); @@ -285,7 +287,7 @@ void shouldCarryEffectiveForwardDenyLists() { @DisplayName("Should default an absent forward block to the forward-all posture") void shouldDefaultAbsentForwardToForwardAll() { RouteTable table = new RouteTable(List.of( - route("r1", Protocol.HTTP, "none", null, upstream("a.example")))); + route("r1", Protocol.HTTP, Require.NONE, null, upstream("a.example")))); List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -303,7 +305,7 @@ void shouldAssembleAssetRouteWithoutClientOrGuard() { ResolvedRoute assetRoute = ResolvedRoute.builder() .id("bundle").protocol(Protocol.HTTP) .match(MatchConfig.builder().pathPrefix("/assets").build()) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(List.of(HttpMethod.GET)) .asset(ResolvedAsset.directory("/srv/assets", AccessLevel.PUBLIC)) .build(); @@ -323,7 +325,7 @@ void shouldAssembleAssetRouteWithoutClientOrGuard() { @DisplayName("Should assemble the null (no-asset) proxy path into an upstream/client/guard runtime without throwing (S3655 guard)") void shouldAssembleNoAssetProxyPathWithoutThrowing() { RouteTable table = new RouteTable(List.of( - route("proxy-only", Protocol.HTTP, "none", null, upstream("a.example")))); + route("proxy-only", Protocol.HTTP, Require.NONE, null, upstream("a.example")))); List runtimes = assertDoesNotThrow( () -> assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory), @@ -348,7 +350,7 @@ void shouldResolvePostureForBlockLessRoute() { SecurityProfile.STRICT.preset()); }; RouteTable table = new RouteTable(List.of( - route("block-less", Protocol.HTTP, "none", null, upstream("a.example")))); + route("block-less", Protocol.HTTP, Require.NONE, null, upstream("a.example")))); // Act List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, @@ -373,9 +375,9 @@ void shouldSetResolvedProfileExplicitly(SecurityProfile resolved) { securityConfigFactory = _ -> new RouteRuntimeAssembler.SecurityPosture(resolved, SecurityProfile.limitsProfile(resolved, resolved).preset()); RouteTable table = new RouteTable(List.of( - route("declared", Protocol.HTTP, "none", + route("declared", Protocol.HTTP, Require.NONE, SecurityFilterConfig.builder().build(), upstream("a.example")), - route("block-less", Protocol.HTTP, "none", null, upstream("a.example")))); + route("block-less", Protocol.HTTP, Require.NONE, null, upstream("a.example")))); // Act List runtimes = assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, @@ -403,9 +405,9 @@ void shouldSplitPostureCacheOnDeclaredVersusAbsentBlock() { }; SecurityFilterConfig declared = SecurityFilterConfig.builder().allowedPaths(List.of("/shared")).build(); RouteTable table = new RouteTable(List.of( - route("r1", Protocol.HTTP, "none", declared, upstream("a.example")), - route("r2", Protocol.HTTP, "none", declared, upstream("a.example")), - route("r3", Protocol.HTTP, "none", null, upstream("a.example")))); + route("r1", Protocol.HTTP, Require.NONE, declared, upstream("a.example")), + route("r2", Protocol.HTTP, Require.NONE, declared, upstream("a.example")), + route("r3", Protocol.HTTP, Require.NONE, null, upstream("a.example")))); // Act assembler.assemble(table, securityConfigFactory, clientFactory, guardFactory, assetSourceFactory); @@ -415,7 +417,7 @@ void shouldSplitPostureCacheOnDeclaredVersusAbsentBlock() { "the shared declared shape resolves once and the absent block resolves once more"); } - private static ResolvedRoute route(String id, Protocol protocol, String require, + private static ResolvedRoute route(String id, Protocol protocol, Require require, @Nullable SecurityFilterConfig filter, ResolvedUpstream upstream) { return ResolvedRoute.builder() .id(id) diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java index 9ef6327e..5f6bc4ce 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java @@ -42,6 +42,7 @@ import de.cuioss.sheriff.gateway.config.model.HttpMethod; import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.ResolvedRoute; import de.cuioss.sheriff.gateway.config.model.ResolvedUpstream; import de.cuioss.sheriff.gateway.config.model.RouteTable; @@ -95,7 +96,7 @@ class WebSocketRelayStageTest { private HttpServer upstreamServer; private HttpServer frontServer; private WebSocketClient wsClient; - private HttpClient relayUpstreamClient; + private WebSocketClient relayUpstreamClient; private int frontPort; private int upstreamPort; private int deadPort; @@ -114,7 +115,7 @@ void setUp() throws Exception { ws.textMessageHandler(ws::writeTextMessage); }).listen(0).toCompletionStage().toCompletableFuture().get(15, TimeUnit.SECONDS); upstreamPort = upstreamServer.actualPort(); - relayUpstreamClient = vertx.createHttpClient(); + relayUpstreamClient = vertx.createWebSocketClient(); // A definitely-closed port for the unreachable-upstream case. HttpServer throwaway = vertx.createHttpServer().requestHandler(req -> req.response().end()) @@ -126,14 +127,14 @@ void setUp() throws Exception { .issuerConfig(TestTokenGenerators.accessTokens().next().getIssuerConfig()).build(); RouteTable routeTable = new RouteTable(List.of( - wsRoute("wsopen", "/ws-open", "none", upstreamPort, Set.of(), null, positiveListNamingNothing()), - wsRoute("wsforwardall", "/ws-forward-all", "none", upstreamPort, Set.of(), null, null), - wsRoute("wsorigin", "/ws-origin", "none", upstreamPort, Set.of(ALLOWED_ORIGIN), null, + wsRoute("wsopen", "/ws-open", Require.NONE, upstreamPort, Set.of(), null, positiveListNamingNothing()), + wsRoute("wsforwardall", "/ws-forward-all", Require.NONE, upstreamPort, Set.of(), null, null), + wsRoute("wsorigin", "/ws-origin", Require.NONE, upstreamPort, Set.of(ALLOWED_ORIGIN), null, positiveListNamingNothing()), - wsRoute("wssecure", "/ws-secure", "bearer", upstreamPort, Set.of(ALLOWED_ORIGIN), null, + wsRoute("wssecure", "/ws-secure", Require.BEARER, upstreamPort, Set.of(ALLOWED_ORIGIN), null, positiveListNamingNothing()), - wsRoute("wsidle", "/ws-idle", "none", upstreamPort, Set.of(), 1, positiveListNamingNothing()), - wsRoute("wsdead", "/ws-dead", "none", deadPort, Set.of(), null, positiveListNamingNothing()))); + wsRoute("wsidle", "/ws-idle", Require.NONE, upstreamPort, Set.of(), 1, positiveListNamingNothing()), + wsRoute("wsdead", "/ws-dead", Require.NONE, deadPort, Set.of(), null, positiveListNamingNothing()))); GatewayConfig gatewayConfig = GatewayConfig.builder() .version(1) @@ -427,10 +428,9 @@ private HttpServer startRelayOnlyServer(int upstreamTargetPort, Runnable release .id("relay-only") .protocol(Protocol.WEBSOCKET) .upstream(new ResolvedUpstream("http", "localhost", upstreamTargetPort, "")) - .httpClient(relayUpstreamClient) .effectiveWebSocketIdleTimeoutSeconds(300) .build(); - WebSocketRelayStage stage = new WebSocketRelayStage( + WebSocketRelayStage stage = new WebSocketRelayStage(relayUpstreamClient, new UpstreamFailureMapper(new GatewayEventCounter()), new GatewayEventCounter()); Router router = Router.router(vertx); router.route().handler(ctx -> { @@ -492,7 +492,7 @@ private static SecurityHeadersConfig securityHeaders() { * made these fixtures silently change meaning when the absent state flipped from nothing-crosses * to forward-all, so the posture is now stated at each call site. */ - private static ResolvedRoute wsRoute(String id, String pathPrefix, String require, int upstreamPort, + private static ResolvedRoute wsRoute(String id, String pathPrefix, Require require, int upstreamPort, Set allowedOrigins, @Nullable Integer idleTimeoutSeconds, @Nullable ForwardConfig forward) { return ResolvedRoute.builder() .id(id) diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigFailFastTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigFailFastTest.java index ae7badf7..c77b95c6 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigFailFastTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigFailFastTest.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.math.BigInteger; import java.net.URISyntaxException; import java.nio.file.Path; @@ -56,7 +55,7 @@ private static ConfigProducer producerFor(String resourceDir) throws URISyntaxEx assertNotNull(resource, resourceDir + " fixture must be on the test classpath"); ConfigProducer producer = new ConfigProducer(); producer.configDir = Path.of(resource.toURI()).toString(); - producer.frameworkBodyLimit = new MemorySize(BigInteger.valueOf(FRAMEWORK_LIMIT_BYTES)); + producer.frameworkBodyLimit = MemorySize.of(FRAMEWORK_LIMIT_BYTES); return producer; } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java index bb0aa072..68db0824 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; @@ -176,7 +175,7 @@ private ConfigProducer producerForGateway(String gatewayYaml) throws IOException Files.writeString(configDir.resolve("gateway.yaml"), gatewayYaml); ConfigProducer producer = new ConfigProducer(); producer.configDir = configDir.toString(); - producer.frameworkBodyLimit = new MemorySize(BigInteger.valueOf(FRAMEWORK_LIMIT_BYTES)); + producer.frameworkBodyLimit = MemorySize.of(FRAMEWORK_LIMIT_BYTES); return producer; } diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ShippedApplicationPropertiesTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ShippedApplicationPropertiesTest.java index 1b9aa68f..13413663 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ShippedApplicationPropertiesTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ShippedApplicationPropertiesTest.java @@ -110,18 +110,18 @@ void packagedFileDeclaresNoProfileKey() throws Exception { void detectorFlagsProfileKeysAndIgnoresPercentInsideValues() { List lines = List.of( "# a comment naming %it.sheriff.token.issuers.it-static.url", - "! a bang comment naming %dev.quarkus.log.file.enable", + "! a bang comment naming %dev.quarkus.log.file.enabled", "", "quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS z} %-5p [%c{3.}] (%t) %s%e%n", "quarkus.arc.exclude-types=de.cuioss.sheriff.token.quarkus.health.*", "quarkus.log.file.path=${LOG_FILE_PATH:/quarkus.log}", "%it.sheriff.token.issuers.it-static.url=https://keycloak:8443/realms/it", - "%dev.quarkus.log.file.enable:false"); + "%dev.quarkus.log.file.enabled:false"); List offendingKeys = profileKeysIn(lines); assertEquals( - List.of("%it.sheriff.token.issuers.it-static.url", "%dev.quarkus.log.file.enable"), offendingKeys, + List.of("%it.sheriff.token.issuers.it-static.url", "%dev.quarkus.log.file.enabled"), offendingKeys, "the detector must flag a %-profile key whichever separator declares it, must not be fooled by a " + "% inside a VALUE (quarkus.log.file.format) or inside a comment, and must not truncate a " + "key at a colon that belongs to the value (quarkus.log.file.path)"); diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/routing/RouteRuntimeTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/routing/RouteRuntimeTest.java index 8db1e68e..44e9aa40 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/routing/RouteRuntimeTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/routing/RouteRuntimeTest.java @@ -31,6 +31,7 @@ import de.cuioss.sheriff.gateway.config.model.MatchConfig; import de.cuioss.sheriff.gateway.config.model.MatchConfig.HeaderMatcher; import de.cuioss.sheriff.gateway.config.model.Protocol; +import de.cuioss.sheriff.gateway.config.model.Require; import de.cuioss.sheriff.gateway.config.model.SecurityProfile; import org.junit.jupiter.api.DisplayName; @@ -168,7 +169,7 @@ private RouteRuntime.RouteRuntimeBuilder runtimeBuilder() { .id("r") .protocol(Protocol.HTTP) .matcher(RouteMatcher.from(MatchConfig.builder().pathPrefix("/r").build())) - .effectiveAuth(AuthConfig.builder().require("none").build()) + .effectiveAuth(AuthConfig.builder().require(Require.NONE).build()) .effectiveAllowedMethods(Set.of(HttpMethod.GET)); } } diff --git a/doc/development/README.adoc b/doc/development/README.adoc index 2ce8b407..5515d556 100644 --- a/doc/development/README.adoc +++ b/doc/development/README.adoc @@ -240,7 +240,7 @@ A profile branch is not a harmless convenience: * *It can mask a defect that only the unbranched production profile would expose.* A branch means the configuration the tests exercise is not the configuration production runs. Every green test is then evidence about the branch, not about the artifact. The `%dev` file-logging exemption was - exactly this: it existed to hide a shipped default (`quarkus.log.file.enable=true` with a path + exactly this: it existed to hide a shipped default (`quarkus.log.file.enabled=true` with a path resolving to the filesystem root) that was wrong for any run which did not override it. === The guard, and what it does not cover @@ -283,9 +283,9 @@ with the property, not by which is least work: services load through `QUARKUS_CONFIG_LOCATIONS`. The shipped file now binds no trust profile in any profile. . *A shipped-default flip*, when the branch existed only to correct a default that was wrong for the - unconfigured case. `%dev.quarkus.log.file.enable=false` disappeared by shipping - `quarkus.log.file.enable=false` and having deployments that want a log file set - `QUARKUS_LOG_FILE_ENABLE=true` beside their `LOG_FILE_PATH`. + unconfigured case. `%dev.quarkus.log.file.enabled=false` disappeared by shipping + `quarkus.log.file.enabled=false` and having deployments that want a log file set + `QUARKUS_LOG_FILE_ENABLED=true` beside their `LOG_FILE_PATH`. The trap to avoid is a fourth, non-shape: deleting the branch and leaving the default it was correcting. That is a regression wearing the invariant's clothes -- deleting the `%dev` line alone diff --git a/integration-tests/docker-compose.yml b/integration-tests/docker-compose.yml index 1357a869..359a4238 100644 --- a/integration-tests/docker-compose.yml +++ b/integration-tests/docker-compose.yml @@ -222,7 +222,7 @@ services: # File logging to mounted target directory. The shipped artifact ships file logging OFF, so a # deployment that wants a log file switches it on here — the enable flag and the path are one # decision and travel together. Every QUARKUS_PROFILE=it gateway instance sets both. - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus.log # Accept-time SNI passthrough split: gateway.yaml declares tls.passthrough_sni, so the Vert.x # SNI front listener owns the public TLS port (sheriff.tls.public-port=8443) and the terminated @@ -372,7 +372,7 @@ services: # Deployment-supplied benchmark-idp trust bucket, in lockstep with the primary api-sheriff # service — see that service's comment for the mechanism. - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-mtls.log # Terminate directly on the public port (no passthrough front on this instance). - QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/app/certificates/localhost.crt @@ -468,7 +468,7 @@ services: # Deployment-supplied benchmark-idp trust bucket, in lockstep with the primary api-sheriff # service — see that service's comment for the mechanism. - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-cookie.log # Terminate directly on the public port (no passthrough front on this instance). - QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/app/certificates/localhost.crt @@ -570,7 +570,7 @@ services: # Deployment-supplied benchmark-idp trust bucket, in lockstep with the primary api-sheriff # service — see that service's comment for the mechanism. - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-cookie-2.log # Terminate directly on the public port (no passthrough front on this instance). - QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/app/certificates/localhost.crt @@ -655,7 +655,7 @@ services: # Deployment-supplied benchmark-idp trust bucket, in lockstep with the primary api-sheriff # service — see that service's comment for the mechanism. - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-ws-admission.log # Terminate directly on the public port (no passthrough front on this instance). - QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/app/certificates/localhost.crt @@ -755,7 +755,7 @@ services: - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties # ManagementPlainHttpOptOutIT reads this container's log FILE for the ApiSheriff-115 downgrade # warning, so the enable flag is load-bearing here, not decorative. - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-plain-mgmt.log - QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/app/certificates/localhost.crt - QUARKUS_HTTP_SSL_CERTIFICATE_KEY_FILES=/app/certificates/localhost.key @@ -860,7 +860,7 @@ services: # Deployment-supplied benchmark-idp trust bucket, in lockstep with the primary api-sheriff # service — see that service's comment for the mechanism. - QUARKUS_CONFIG_LOCATIONS=/app/certificates/benchmark-idp-trust.properties - - QUARKUS_LOG_FILE_ENABLE=true + - QUARKUS_LOG_FILE_ENABLED=true - LOG_FILE_PATH=/logs/quarkus-passthrough-empty.log # NO QUARKUS_HTTP_SSL_PORT here — see the service comment. The overlaid gateway.yaml declares no # passthrough_sni, so no front listener is created and the terminated listener owns 8443. diff --git a/integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ItProfileConfigBindingWiringTest.java b/integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ItProfileConfigBindingWiringTest.java index edbf5790..5879795a 100644 --- a/integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ItProfileConfigBindingWiringTest.java +++ b/integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/ItProfileConfigBindingWiringTest.java @@ -80,7 +80,7 @@ class ItProfileConfigBindingWiringTest { private static final String BUCKET_PREFIX = "quarkus.tls.benchmark-idp.trust-store.p12."; /** The deployment switch for file logging, now that the shipped artifact defaults it to off. */ - private static final String LOG_FILE_ENABLED = "QUARKUS_LOG_FILE_ENABLE=true"; + private static final String LOG_FILE_ENABLED = "QUARKUS_LOG_FILE_ENABLED=true"; @Test @DisplayName("every it-profile gateway instance binds the mounted trust file") @@ -104,7 +104,7 @@ void everyItProfileInstanceEnablesFileLogging() throws Exception { // Arrange — same derived set, same reason: a seventh instance is covered without an edit here. List itServices = itProfileServices(); - // Act + Assert — the shipped artifact now defaults quarkus.log.file.enable to false, so a + // Act + Assert — the shipped artifact now defaults quarkus.log.file.enabled to false, so a // LOG_FILE_PATH on its own produces no file at all. The IT suite reads those files // (ManagementPlainHttpOptOutIT asserts on the ApiSheriff-115 downgrade warning inside the // plain-management container's log), and a missing switch would surface as a puzzling diff --git a/pom.xml b/pom.xml index 59ab3dbb..860529f7 100644 --- a/pom.xml +++ b/pom.xml @@ -160,6 +160,22 @@ maven-compiler-plugin 25 + true + + true