Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/run-integration-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>.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/<name>.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 <run-id> --repo cuioss/API-Sheriff` — the GitHub job log itself does NOT contain the app container stdout.

Expand Down
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<showDeprecation>true</showDeprecation>` **and**
`<failOnWarning>true</failOnWarning>`, 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> tokenValidator;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,9 +108,8 @@ public final class RouteTableBuilder {

private static final List<HttpMethod> 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;
Expand Down Expand Up @@ -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();
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,18 +413,24 @@ private static void validate(Schema schema, JsonNode node, String file, List<Con

private void substitute(JsonNode node, JsonNode schemaTree, String file, String pointer,
List<ConfigError> errors) {
if (node instanceof ObjectNode object) {
List<String> 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<String> 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.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@
* The {@code auth} block, declarable at endpoint level (mandatory default
* posture) and per route (wholesale override).
* <p>
* {@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.
* <p>
* <strong>Thread safety.</strong> 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
* @author API Sheriff Team
* @since 1.0
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
@Builder
public record AuthConfig(String require, List<String> requiredScopes) {
public record AuthConfig(Require require, List<String> requiredScopes) {

/**
* Canonical constructor defensively copying {@code requiredScopes} into an
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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 <em>type-safety</em> 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.
* <p>
* Compile-time exhaustiveness is <em>not</em> 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.
* <p>
* 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}.
* <p>
* 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);
}
}
Loading
Loading