Skip to content

Commit 68c6bf0

Browse files
committed
Merge branch 'feat/cedar-enforcement-logging'
2 parents a65b98e + 015be4f commit 68c6bf0

36 files changed

Lines changed: 787 additions & 60 deletions

.github/workflows/labs64io-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
pip install -q -e ".[dev]"
4141
pytest -q
4242
43-
# RFC-05 P1 — unified Cedar schema & policy gates. Edge + domain policies
43+
# Edge + domain policies
4444
# validate against ONE shared schema (no-drift, F3), and a cross-tenant
4545
# isolation gate proves the tenant guard denies across-tenant access (F4/F8).
4646
cedar:

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Guidance for AI agents working in this repository. Read this before making chang
44

55
## What this is
66

7-
Shared cross-service libraries for the Labs64.IO Ecosystem. Currently: the auth-context libraries (Java + Python), which parse, enforce and propagate the trusted gateway header contract, plus the unified Cedar authorization assets (RFC-05): the shared schema/policies in `auth-policy-cedar/` and the `@Authorize` Cedar domain PEP in the Java starter (`io.labs64.authcontext.cedar`, feature-flagged via `labs64.auth.cedar.*`, engine = optional `com.cedarpolicy:cedar-java:uber` dependency).
7+
Shared cross-service libraries for the Labs64.IO Ecosystem. Currently: the auth-context libraries (Java + Python), which parse, enforce and propagate the trusted gateway header contract, plus the unified Cedar authorization assets: the shared schema/policies in `auth-policy-cedar/` and the `@Authorize` Cedar domain PEP in the Java starter (`io.labs64.authcontext.cedar`, feature-flagged via `labs64.auth.cedar.*`, engine = optional `com.cedarpolicy:cedar-java:uber` dependency).
88

99
## Critical guardrails
1010

@@ -22,7 +22,7 @@ Shared cross-service libraries for the Labs64.IO Ecosystem. Currently: the auth-
2222
|---|---|
2323
| `auth-context-java/` | `io.labs64:auth-context-spring-boot-starter` (incl. `@Authorize` Cedar domain PEP) |
2424
| `auth-context-python/` | `auth-context-python` (package `auth_context`) |
25-
| `auth-policy-cedar/` | THE shared Cedar schema + reference policies + `validate.sh` CI gate (RFC-05) |
25+
| `auth-policy-cedar/` | THE shared Cedar schema + reference policies + `validate.sh` CI gate |
2626
| `test-vectors/` | Canonical cross-language behavior vectors (headers + Cedar request construction) |
2727

2828
## Commands

auth-context-java/INTEGRATION.md

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ paths:
8686
### Domain (Tier-2) resource authorization
8787

8888
Declare `resource: <Type>` to have the operation authorized in-process by the
89-
module's `@Authorize` PEP against a typed Cedar resource (RFC-05 P3/P4). This is
89+
module's `@Authorize` PEP against a typed Cedar resource. This is
9090
the OpenAPI-native source for the generated domain policy set
9191
(`auth-policy-domain.cedar`): the preprocessor emits one `permit` keyed on the
9292
operationId, conditioned on the same `tenant`/`scopes`, plus one structural
@@ -132,7 +132,7 @@ Add three things to your `pom.xml`:
132132
<openapi.source>${project.basedir}/../your-api-module/src/main/resources/openapi/openapi-your-module.yaml</openapi.source>
133133
<openapi.generated>${project.build.directory}/generated-openapi/openapi-your-module.yaml</openapi.generated>
134134
<auth-policy.generated>${project.build.directory}/generated-resources/auth-policy.json</auth-policy.generated>
135-
<!-- Cedar policies generated from x-labs64-auth (RFC-05 P2/P3/P4).
135+
<!-- Cedar policies generated from x-labs64-auth.
136136
auth-policy.module MUST equal the module's gateway path prefix (e.g.
137137
payment-gateway) — it is baked into the Cedar resource ids.
138138
edge → auth-policy.cedar (Tier 1, served to the ACS/traefik)
@@ -360,11 +360,20 @@ endpoint in-cluster to build its edge authorization table.
360360
The same controller serves the build-generated **Tier-1 edge** Cedar policy set
361361
(`auth-policy.cedar`, emitted when the preprocessor is invoked with
362362
`--cedar-output` + `--module`) at `GET /.well-known/auth-policy.cedar`
363-
(text/plain; 404 when the module does not generate it). The authproxy fetches
364-
it alongside the JSON under live discovery — the identical distribution model
365-
— and evaluates it according to its `CEDAR_MODE` (RFC-05 P2). The `--module`
366-
name must equal the module's gateway path prefix (e.g. `payment-gateway`),
367-
because it is baked into the Cedar resource ids.
363+
(text/plain; 404 when the module does not generate it). Each generated permit
364+
also carries `@path`/`@method`/`@public`/`@tenantRequired`/`@scopes`
365+
annotations — the same fields `auth-policy.json`'s routes carry — so this one
366+
file doubles as the OpenAPI-template routing table. **The traefik-authproxy's
367+
live-discovery path fetches only `auth-policy.cedar`** (not the JSON document)
368+
and rebuilds its routing table from those annotations
369+
(`policy_store.parse_cedar_document`), then evaluates it for the
370+
authorization decision according to `CEDAR_MODE`. `auth-policy.json` is still
371+
served and still on the classpath — it remains the routing source for the
372+
**signed-bundle** policy path (`policy_bundle.py`, which packages
373+
`modules/<name>.json` today) — but a module integrating only for live
374+
discovery must serve `auth-policy.cedar` for the ACS to route to it at all.
375+
The `--module` name must equal the module's gateway path prefix (e.g.
376+
`payment-gateway`), because it is baked into the Cedar resource ids.
368377

369378
The **Tier-2 domain** set (`auth-policy-domain.cedar`, from
370379
`--cedar-domain-output`) is **not** served over the well-known endpoint — it is
@@ -432,7 +441,7 @@ paths:
432441

433442
### Generated outputs
434443

435-
**auth-policy.json**:
444+
**auth-policy.json** (signed-bundle routing source):
436445
```json
437446
{
438447
"version": 1,
@@ -447,6 +456,21 @@ paths:
447456
}
448457
```
449458

459+
**auth-policy.cedar** (live-discovery routing + decision source — same fields, as annotations):
460+
```
461+
@id("auditflow::publishEvent")
462+
@path("/audit/publish")
463+
@method("POST")
464+
@public("false")
465+
@tenantRequired("true")
466+
@scopes("audit-event:write")
467+
permit(
468+
principal,
469+
action == Labs64IO::Action::"invoke",
470+
resource == Labs64IO::ApiOperation::"auditflow::publishEvent"
471+
) when { (context has tenant) && (context.scopes.contains("audit-event:write")) };
472+
```
473+
450474
**Generated AuditEventApi.java**:
451475
```java
452476
@RequireTenant
@@ -486,3 +510,24 @@ Add your paths to `labs64.auth-context.public-paths` or ensure the gateway sends
486510
### Annotations not enforced
487511

488512
Ensure `auth-context-spring-boot-starter` is on the classpath and `labs64.auth-context.enabled` is not set to `false`.
513+
514+
## Reading @Authorize enforcement logs
515+
516+
Each `@Authorize` decision emits a non-sensitive summary on `io.labs64.authcontext.cedar.LoggingDecisionListener`:
517+
518+
```
519+
cedar-domain outcome=<enforced|shadow>-<allow|deny> decision=<allow|deny|error> \
520+
mode=<enforce|shadow> action=<a> resourceType=<t> reasons=<policyIds|-> requestId=<id>
521+
```
522+
523+
INFO for a clean allow, WARN for deny/error.
524+
525+
Sensitive fields (user, tenant, resolved resource id, raw error) ride the dedicated `io.labs64.authcontext.cedar.detail` logger at DEBUG, off by default. Enable it during the Cedar testing phase, e.g. in `application.yaml`:
526+
527+
```yaml
528+
logging:
529+
level:
530+
io.labs64.authcontext.cedar.detail: DEBUG
531+
```
532+
533+
The detail line emits `cedar-detail requestId=<id> user=<user> tenant=<tenant> resource=<type>::<id>[ error=<err>]`, shareable with the summary for joining via `requestId`.

auth-context-java/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
<artifactId>jackson-dataformat-yaml</artifactId>
6565
<optional>true</optional>
6666
</dependency>
67-
<!-- Cedar domain PEP (RFC-05 P3): optional — modules opt in by adding this
67+
<!-- Cedar domain PEP: optional — modules opt in by adding this
6868
dependency themselves (uber classifier bundles the native FFI for
6969
linux/macos/windows, ~27 MB) plus labs64.auth.cedar.enabled=true.
7070
The cedar autoconfiguration is @ConditionalOnClass-guarded. -->

auth-context-java/src/main/java/io/labs64/authcontext/autoconfigure/AuthContextAutoConfiguration.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@
66
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
77
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
88
import org.springframework.boot.context.properties.EnableConfigurationProperties;
9+
import java.io.BufferedReader;
10+
import java.io.IOException;
11+
import java.io.InputStreamReader;
12+
import java.io.UncheckedIOException;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.List;
15+
916
import org.springframework.boot.web.servlet.FilterRegistrationBean;
1017
import org.springframework.context.annotation.Bean;
1118
import org.springframework.core.Ordered;
19+
import org.springframework.core.io.Resource;
20+
import org.springframework.core.io.ResourceLoader;
1221
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
1322

1423
import io.labs64.authcontext.authorization.RequireScopesInterceptor;
@@ -20,6 +29,7 @@
2029
import io.labs64.authcontext.web.AuthContextProperties;
2130
import io.labs64.authcontext.web.AuthContextWebMvcConfigurer;
2231
import io.labs64.authcontext.web.AuthPolicyController;
32+
import io.labs64.authcontext.web.PublicPathMatcher;
2333

2434
@AutoConfiguration
2535
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@@ -33,12 +43,31 @@ public AuthContextParser authContextParser() {
3343
return new AuthContextParser();
3444
}
3545

46+
@Bean
47+
@ConditionalOnMissingBean
48+
public PublicPathMatcher publicPathMatcher(AuthContextProperties properties, ResourceLoader resourceLoader) {
49+
Resource resource = resourceLoader.getResource(properties.getPublicPathsLocation());
50+
if (!resource.exists()) {
51+
// No generated list (module has no public OpenAPI operations, or the
52+
// build step is not wired): fall back to configured prefixes only.
53+
return PublicPathMatcher.empty();
54+
}
55+
try (BufferedReader reader = new BufferedReader(
56+
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
57+
List<String> lines = reader.lines().toList();
58+
return PublicPathMatcher.fromLines(lines);
59+
} catch (IOException e) {
60+
throw new UncheckedIOException(
61+
"Failed to read generated public paths from " + properties.getPublicPathsLocation(), e);
62+
}
63+
}
64+
3665
@Bean
3766
@ConditionalOnMissingBean
3867
public FilterRegistrationBean<AuthContextFilter> authContextFilter(AuthContextProperties properties,
39-
AuthContextParser parser) {
68+
AuthContextParser parser, PublicPathMatcher publicPathMatcher) {
4069
FilterRegistrationBean<AuthContextFilter> registration = new FilterRegistrationBean<>(
41-
new AuthContextFilter(properties, parser));
70+
new AuthContextFilter(properties, parser, publicPathMatcher));
4271
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
4372
return registration;
4473
}

auth-context-java/src/main/java/io/labs64/authcontext/cedar/AuthorizationDecision.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
/**
88
* One Cedar domain authorization decision — published to every
9-
* {@link AuthorizationDecisionListener} for auditability (RFC-05 P9/F9:
10-
* allow AND deny, with the matched policy ids, deterministic and replayable).
9+
* {@link AuthorizationDecisionListener} for auditability.
1110
*
1211
* @param action Cedar action id (unqualified)
1312
* @param resourceType Cedar resource type (unqualified)
@@ -30,4 +29,21 @@ public record AuthorizationDecision(String action, String resourceType, String r
3029
public AuthorizationDecision {
3130
reasons = reasons == null ? List.of() : List.copyOf(reasons);
3231
}
32+
33+
/** Three-valued decision for logs: {@code allow} / {@code deny} / {@code error}. */
34+
public String decision() {
35+
return error != null ? "error" : allowed ? "allow" : "deny";
36+
}
37+
38+
/**
39+
* Enforcement outcome verb: {@code enforced-allow} / {@code enforced-deny} /
40+
* {@code shadow-allow} / {@code shadow-deny}. An errored decision is a deny
41+
* (fail-closed), so its outcome carries the blocking action, while
42+
* {@link #decision()} still reports {@code error} as the reason.
43+
*/
44+
public String outcome() {
45+
String phase = enforced ? "enforced" : "shadow";
46+
String result = (allowed && error == null) ? "allow" : "deny";
47+
return phase + "-" + result;
48+
}
3349
}

auth-context-java/src/main/java/io/labs64/authcontext/cedar/Authorize.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import java.lang.annotation.Target;
88

99
/**
10-
* Fine-grained Cedar domain authorization on a handler method (RFC-05 P3).
10+
* Fine-grained Cedar domain authorization on a handler method.
1111
*
1212
* <p>Additive to the coarse {@code @RequireScopes}/{@code @RequireTenant}
1313
* pre-filters: those stay as fast gates, Cedar is the authoritative

auth-context-java/src/main/java/io/labs64/authcontext/cedar/AuthorizeInterceptor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import jakarta.servlet.http.HttpServletResponse;
2323

2424
/**
25-
* The {@code @Authorize} PEP (RFC-05 P3): runs after the coarse
25+
* The {@code @Authorize} PEP: runs after the coarse
2626
* {@code @RequireTenant}/{@code @RequireScopes} pre-filters, resolves the
2727
* domain resource through the module's {@link CedarEntityResolver}, asks the
2828
* Cedar PDP, publishes the decision to every
@@ -65,10 +65,13 @@ public boolean preHandle(@NonNull final HttpServletRequest request,
6565
// The AuthContextFilter fails closed before us on protected paths;
6666
// this is defense in depth for misconfigured public paths.
6767
if (service.isEnforcing()) {
68+
logger.warn("cedar-domain outcome=enforced-deny decision=deny mode=enforce "
69+
+ "action={} reason=no-auth-context requestId=-", annotation.action());
6870
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
6971
return false;
7072
}
71-
logger.warn("cedar-domain action={} skipped: no AuthContext (shadow)", annotation.action());
73+
logger.warn("cedar-domain outcome=shadow-deny decision=deny mode=shadow "
74+
+ "action={} reason=no-auth-context requestId=-", annotation.action());
7275
return true;
7376
}
7477

auth-context-java/src/main/java/io/labs64/authcontext/cedar/CedarAuthorizationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import io.labs64.authcontext.core.AuthHeaders;
3333

3434
/**
35-
* In-process Cedar domain PDP (RFC-05 P3, Tier 2).
35+
* In-process Cedar domain PDP.
3636
*
3737
* <p>Loads the module's domain policy set once at startup and evaluates
3838
* {@code @Authorize} checks against it. Request construction follows the

auth-context-java/src/main/java/io/labs64/authcontext/cedar/CedarAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
import com.cedarpolicy.BasicAuthorizationEngine;
1717

1818
/**
19-
* Cedar domain PEP wiring (RFC-05 P3). Doubly opt-in: the module must add the
19+
* Cedar domain PEP wiring. Doubly opt-in: the module must add the
2020
* optional {@code com.cedarpolicy:cedar-java} (uber) dependency AND set
2121
* {@code labs64.auth.cedar.enabled=true}. Registered after the coarse
2222
* interceptors so {@code @RequireScopes}/{@code @RequireTenant} stay the fast
23-
* pre-filter (RFC-05 §4.6).
23+
* pre-filter.
2424
*/
2525
@AutoConfiguration
2626
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)

0 commit comments

Comments
 (0)