Skip to content

Commit e2836ee

Browse files
committed
Merge branch 'feat/rfc07-cerbos'
2 parents c493a2e + def7779 commit e2836ee

63 files changed

Lines changed: 1655 additions & 1652 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/labs64io-ci.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ jobs:
2626
cache: maven
2727
- name: Test auth-context-java
2828
run: cd auth-context-java && mvn -B -ntp test
29+
- name: Install auth-context-java (dependency for authz-queryplan-jpa)
30+
run: cd auth-context-java && mvn -B -ntp -DskipTests install
2931
- name: Test openapi-spring-boot-starter
3032
run: cd openapi-spring-boot-starter && mvn -B -ntp test
33+
- name: Test authz-queryplan-jpa
34+
run: cd authz-queryplan-jpa && mvn -B -ntp test
3135

3236
python:
3337
runs-on: ubuntu-latest
@@ -42,20 +46,12 @@ jobs:
4246
pip install -q -e ".[dev]"
4347
pytest -q
4448
45-
# Edge + domain policies
46-
# validate against ONE shared schema (no-drift, F3), and a cross-tenant
47-
# isolation gate proves the tenant guard denies across-tenant access (F4/F8).
48-
cedar:
49+
# Generate the reference Cerbos policy set
50+
# from the SAME x-labs64-auth (F3, no-drift) and run cerbos compile + the
51+
# truth-table suite — including the cross-tenant isolation invariant (F4/F8).
52+
cerbos:
4953
runs-on: ubuntu-latest
5054
steps:
5155
- uses: actions/checkout@v4
52-
- uses: dtolnay/rust-toolchain@stable
53-
- name: Cache cedar CLI
54-
uses: actions/cache@v4
55-
with:
56-
path: ~/.cargo/bin
57-
key: cedar-cli-4.11.2
58-
- name: Install cedar CLI
59-
run: command -v cedar || cargo install cedar-policy-cli --version 4.11.2 --locked
60-
- name: Cedar schema + cross-tenant isolation gate
61-
run: ./auth-policy-cedar/validate.sh
56+
- name: Cerbos compile + decision-equivalence truth-table gate
57+
run: ./auth-policy-cerbos/validate.sh

AGENTS.md

Lines changed: 7 additions & 6 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. Includes: the auth-context libraries (Java + Python), which parse, enforce and propagate the trusted gateway header contract, shared Spring Boot starters, 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).
7+
Shared cross-service libraries for the Labs64.IO Ecosystem. Includes: the auth-context libraries (Java + Python), which parse, enforce and propagate the trusted gateway header contract, shared Spring Boot starters, plus the authorization assets: the engine-neutral `@Authorize` domain PEP in the Java starter (`io.labs64.authcontext.authorization`) with a Cerbos PDP client (`io.labs64.authcontext.cerbos`), feature-flagged via `labs64.auth.authz.*` (decisions delegated to the central Cerbos PDP over gRPC), the Cerbos-policy generator backend in `OpenApiAuthPreprocessor` (`--cerbos-output`/`--routes-output`), the decision-equivalence gate in `auth-policy-cerbos/`, and the `authz-queryplan-jpa` Data-PEP translator.
88

99
## Critical guardrails
1010

@@ -14,16 +14,17 @@ Shared cross-service libraries for the Labs64.IO Ecosystem. Includes: the auth-c
1414
4. **No mandatory runtime dependencies in the Python package.** FastAPI/httpx integrations import lazily; the core must stay dependency-free.
1515
5. **Fail closed.** Non-public paths without a valid user identity return 401. Never weaken this default.
1616
6. **Value sanitization pattern `^[a-zA-Z0-9_.:-]+$`** must match the ACS (traefik-authproxy) exactly.
17-
7. **One Cedar schema.** `auth-policy-cedar/schema.cedarschema` is the single type system for BOTH authorization tiers (edge + every module's domain policies). Schema changes must keep `just cedar` green and stay in sync with `test-vectors/cedar-request-vectors.json`the AuthContext→Cedar request contract the Java/Python PEPs are pinned to.
17+
7. **OpenAPI is the single source of policy.** Cerbos policies are GENERATED from each module's `x-labs64-auth` by `OpenApiAuthPreprocessor` — never hand-edit generated policy YAML. Decision equivalence (edge + domain, incl. the cross-tenant isolation invariant) is proven by `auth-policy-cerbos/validate.sh` (`just cerbos`); keep it green.
1818

1919
## Layout
2020

2121
| Path | What |
2222
|---|---|
23-
| `auth-context-java/` | `io.labs64:auth-context-spring-boot-starter` |
23+
| `auth-context-java/` | `io.labs64:auth-context-spring-boot-starter` (auth-context + engine-neutral `@Authorize` PEP + Cerbos client) |
2424
| `auth-context-python/` | `auth-context-python` (package `auth_context`) |
25-
| `auth-policy-cedar/` | THE shared Cedar schema + reference policies + `validate.sh` CI gate |
26-
| `test-vectors/` | Canonical cross-language behavior vectors (headers + Cedar request construction) |
25+
| `authz-queryplan-jpa/` | `io.labs64:authz-queryplan-jpa` — Cerbos query-plan → JPA Specification (Data PEP) |
26+
| `auth-policy-cerbos/` | Reference OpenAPI → generated Cerbos policies + truth-table `validate.sh` CI gate |
27+
| `test-vectors/` | Canonical cross-language behavior vectors (header contract) |
2728
| `openapi-spring-boot-starter/` | `io.labs64:openapi-spring-boot-starter` |
2829

2930
## Commands
@@ -35,7 +36,7 @@ Shared cross-service libraries for the Labs64.IO Ecosystem. Includes: the auth-c
3536
| OpenAPI starter tests | `cd openapi-spring-boot-starter && mvn test` |
3637
| Python tests | `cd auth-context-python && .venv/bin/pytest` (after `just python-venv`) |
3738
| Install Java lib locally | `just install-java` |
38-
| Cedar schema/policy gate | `just cedar` (requires `cargo install cedar-policy-cli`) |
39+
| Cerbos policy + equivalence gate | `just cerbos` (runs `auth-policy-cerbos/validate.sh` via Docker) |
3940

4041
## Conventions
4142

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ CLI:
7777
cd auth-context-java
7878
mvn -q exec:java \
7979
-Dexec.mainClass=io.labs64.authcontext.openapi.OpenApiAuthPreprocessorCli \
80-
-Dexec.args="--input openapi.yaml --openapi-output target/generated/openapi.yaml --cedar-output target/generated/auth-policy.cedar --module commons"
80+
-Dexec.args="--input openapi.yaml --openapi-output target/generated/openapi.yaml --cerbos-output target/cerbos --routes-output target/routes.yaml --module commons"
8181
```
8282

8383
## Related

auth-context-java/INTEGRATION.md

Lines changed: 58 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ The pipeline has four stages:
1010
OpenAPI spec (with x-labs64-auth)
1111
→ OpenApiAuthPreprocessorCli (build-time)
1212
→ Cleaned OpenAPI (annotations injected)
13-
auth-policy.cedar (gateway route policy)
13+
Cerbos resource policies (YAML) + routing manifest
1414
→ openapi-generator (generates Java interfaces with annotations)
15-
→ auth-context-spring-boot-starter (runtime enforcement)
16-
→ GET /.well-known/auth-policy + /.well-known/auth-policy.cedar (runtime, served by the starter for the gateway ACS)
15+
→ auth-context-spring-boot-starter (runtime enforcement via @Authorize interceptors and central Cerbos PDP)
1716
```
1817

1918
1. **Author**: Add `x-labs64-auth` to each endpoint in your OpenAPI spec
20-
2. **Build-time**: The preprocessor strips the extension, injects Java annotations (`@RequireTenant`, `@RequireScopes`, `@PublicEndpoint`) into the generated OpenAPI, and emits `auth-policy.cedar`
19+
2. **Build-time**: The preprocessor strips the extension, injects Java annotations (`@RequireTenant`, `@RequireScopes`, `@PublicEndpoint`) into the generated OpenAPI, and emits Cerbos resource policies in YAML + routes manifest.
2120
3. **Code generation**: OpenAPI Generator produces Java API interfaces with the injected annotations
2221
4. **Runtime**: `auth-context-spring-boot-starter` auto-configures filters and interceptors that enforce the annotations
2322

@@ -86,9 +85,9 @@ paths:
8685
### Domain (Tier-2) resource authorization
8786

8887
Declare `resource: <Type>` to have the operation authorized in-process by the
89-
module's `@Authorize` PEP against a typed Cedar resource. This is
88+
module's `@Authorize` PEP against a typed Cerbos resource. This is
9089
the OpenAPI-native source for the generated domain policy set
91-
(`auth-policy-domain.cedar`): the preprocessor emits one `permit` keyed on the
90+
(`auth-policy-domain.cerbos`): the preprocessor emits one `permit` keyed on the
9291
operationId, conditioned on the same `tenant`/`scopes`, plus one structural
9392
cross-tenant guard per resource type. Operations without `resource` are
9493
edge-only (coarse reachability).
@@ -106,8 +105,8 @@ paths:
106105
```
107106

108107
The resource `<Type>` must be declared in the shared schema
109-
(`labs64.io-commons/auth-policy-cedar/schema.cedarschema`), and the module
110-
supplies the resource's `tenant` at request time via a `CedarEntityResolver`.
108+
(`labs64.io-commons/auth-policy-cerbos/schema.cerbosschema`), and the module
109+
supplies the resource's `tenant` at request time via a `CerbosEntityResolver`.
111110
Fine-grained resource-attribute rules (workflow status, ownership) are **not**
112111
expressible from OpenAPI — they stay in the service layer (and later Postgres
113112
RLS), by design.
@@ -131,14 +130,12 @@ Add three things to your `pom.xml`:
131130
<exec-maven-plugin.version>3.6.2</exec-maven-plugin.version>
132131
<openapi.source>${project.basedir}/../your-api-module/src/main/resources/openapi/openapi-your-module.yaml</openapi.source>
133132
<openapi.generated>${project.build.directory}/generated-openapi/openapi-your-module.yaml</openapi.generated>
134-
<auth-policy.generated>${project.build.directory}/generated-resources/auth-policy.cedar</auth-policy.generated>
135-
<!-- Cedar policies generated from x-labs64-auth.
136-
auth-policy.module MUST equal the module's gateway path prefix (e.g.
137-
payment-gateway) — it is baked into the Cedar resource ids.
138-
edge → auth-policy.cedar (Tier 1, served to the ACS/traefik)
139-
domain → auth-policy-domain.cedar (Tier 2, module @Authorize PEP) -->
140-
<auth-policy-cedar.generated>${project.build.directory}/generated-resources/auth-policy.cedar</auth-policy-cedar.generated>
141-
<auth-policy-cedar-domain.generated>${project.build.directory}/generated-resources/auth-policy-domain.cedar</auth-policy-cedar-domain.generated>
133+
<!-- Cerbos policies + routes manifest generated from x-labs64-auth.
134+
auth-policy.module must equal the module's gateway prefix
135+
(= policy-sources.yaml name); it is baked into the Cerbos resource
136+
kind (your-module -> your_module_api). Generated output is a build
137+
artifact only — the central Cerbos PDP owns the live policy set. -->
138+
<auth-policy-cerbos.output>${project.build.directory}/cerbos</auth-policy-cerbos.output>
142139
<auth-policy.module>your-module</auth-policy.module>
143140
</properties>
144141
```
@@ -187,10 +184,8 @@ The preprocessor must run **before** the OpenAPI generator. Add `exec-maven-plug
187184
<argument>${openapi.source}</argument>
188185
<argument>--openapi-output</argument>
189186
<argument>${openapi.generated}</argument>
190-
<argument>--cedar-output</argument>
191-
<argument>${auth-policy-cedar.generated}</argument>
192-
<argument>--cedar-domain-output</argument>
193-
<argument>${auth-policy-cedar-domain.generated}</argument>
187+
<argument>--cerbos-output</argument>
188+
<argument>${auth-policy-cerbos.output}</argument>
194189
<argument>--module</argument>
195190
<argument>${auth-policy.module}</argument>
196191
</arguments>
@@ -231,7 +226,7 @@ The preprocessor must run **before** the OpenAPI generator. Add `exec-maven-plug
231226
</executions>
232227
</plugin>
233228
234-
<!-- 3. Build helper: add generated resources (auth-policy.cedar) to classpath -->
229+
<!-- 3. Build helper: add generated resources to classpath -->
235230
<plugin>
236231
<groupId>org.codehaus.mojo</groupId>
237232
<artifactId>build-helper-maven-plugin</artifactId>
@@ -273,28 +268,23 @@ The preprocessor must run **before** the OpenAPI generator. Add `exec-maven-plug
273268

274269
Run `mvn clean compile` and check three outputs:
275270

276-
### 1. Generated auth-policy.cedar
271+
### 1. Generated Cerbos output
277272

278273
```bash
279-
cat target/generated-resources/auth-policy.cedar
274+
ls target/cerbos
275+
# Should contain routes.yaml and Cerbos resource policies (e.g., your_module_api.yaml)
276+
cat target/cerbos/routes.yaml
280277
```
281278

282279
Should contain route entries for each endpoint:
283280

284-
```json
285-
{
286-
"version": 1,
287-
"routes": [
288-
{
289-
"operationId": "publishEvent",
290-
"method": "POST",
291-
"path": "/audit/publish",
292-
"public": false,
293-
"tenantRequired": true,
294-
"scopes": ["audit-event:write"]
295-
}
296-
]
297-
}
281+
```yaml
282+
routes:
283+
/audit/publish:
284+
POST:
285+
module: auditflow
286+
operationId: publishEvent
287+
public: false
298288
```
299289

300290
### 2. Generated OpenAPI with annotations
@@ -347,48 +337,16 @@ labs64:
347337

348338
4. **`@PublicEndpoint`**: Bypasses both interceptors. The path must also be in `labs64.auth-context.public-paths` to pass the filter.
349339

350-
## The `/.well-known/auth-policy` endpoints
351-
352-
When `auth-policy.cedar` is on the classpath (Step 2c's `add-resource`), the
353-
starter auto-registers a controller serving it verbatim at
354-
`GET /.well-known/auth-policy`. The gateway's traefik-authproxy discovers
355-
modules via the `labs64.io/auth-policy=true` Service label and fetches this
356-
endpoint in-cluster to build its edge authorization table.
357-
358-
The same controller serves the build-generated **Tier-1 edge** Cedar policy set
359-
(`auth-policy.cedar`, emitted when the preprocessor is invoked with
360-
`--cedar-output` + `--module`) at `GET /.well-known/auth-policy.cedar`
361-
(text/plain; 404 when the module does not generate it). Each generated permit
362-
also carries `@path`/`@method`/`@public`/`@tenantRequired`/`@scopes`
363-
annotations — the same fields `auth-policy.cedar`'s routes carry — so this one
364-
file doubles as the OpenAPI-template routing table. **The traefik-authproxy's
365-
live-discovery path fetches only `auth-policy.cedar`** (not the JSON document)
366-
and rebuilds its routing table from those annotations
367-
(`policy_store.parse_cedar_document`), then evaluates it for the
368-
authorization decision according to `CEDAR_MODE`. `auth-policy.cedar` is still
369-
served and still on the classpath — it remains the routing source for the
370-
**signed-bundle** policy path (`policy_bundle.py`, which packages
371-
`modules/<name>.json` today) — but a module integrating only for live
372-
discovery must serve `auth-policy.cedar` for the ACS to route to it at all.
373-
The `--module` name must equal the module's gateway path prefix (e.g.
374-
`payment-gateway`), because it is baked into the Cedar resource ids.
375-
376-
The **Tier-2 domain** set (`auth-policy-domain.cedar`, from
377-
`--cedar-domain-output`) is **not** served over the well-known endpoint — it is
378-
consumed only in-process by this module's `@Authorize` PEP from the classpath
379-
(`labs64.auth.cedar.policy-location`, default `classpath:auth-policy-domain.cedar`).
380-
Both files are generated from the one `x-labs64-auth` source, so traefik and the
381-
module enforce the same OpenAPI-derived contract.
382-
383-
- Both paths are **unconditionally public** in `AuthContextFilter` — they
384-
cannot be disabled via `public-paths`, because the ACS must reach them
385-
before it can authorize anything.
386-
- It is **not** exposed through the external gateway: module IngressRoutes
387-
only publish API prefixes. The policy content is derived from the OpenAPI
388-
spec (whose `/v3/api-docs` is public), so no secrets are involved.
389-
- Scope matching at the edge and in `RequireScopesInterceptor` is **OR**
390-
(any listed scope suffices). The edge tenant check is presence-only —
391-
tenant validation stays a module concern via `AuthContext.tenantId()`.
340+
## Policy Distribution to the Central PDP
341+
342+
Services **no longer serve** authorization policies at runtime (the `/.well-known/auth-policy` endpoints have been deleted). Instead, the generated Cerbos policies and route manifests are collected from the build artifacts (`target/cerbos`) and distributed to the Central PDP via a GitOps CI pipeline.
343+
344+
1. **Build Time**: The OpenAPI preprocessor emits Cerbos YAML policies (`<module>_api.yaml` and `<DomainResource>.yaml`) and a `routes.yaml` manifest.
345+
2. **CI Pipeline**: A shared script (`build-cerbos-policies.sh`) aggregates the `target/cerbos` directories from all modules into a centralized policy distribution format.
346+
3. **Runtime**: The central Cerbos PDP loads these generated policies from a mounted ConfigMap. The Traefik Authproxy loads the `routes.yaml` manifests to understand the path-to-operation mappings.
347+
4. **Enforcement**: When a request arrives, the Traefik Authproxy queries the central Cerbos PDP for edge reachability (Tier 1). Then, inside your module, the `@Authorize` interceptors from `auth-context-spring-boot-starter` query the central Cerbos PDP for fine-grained domain authorization (Tier 2).
348+
349+
This disconnected topology guarantees zero engine footprint inside the Java application, as the policy engine evaluates entirely out-of-process.
392350

393351
### Accessing the auth context in controllers
394352

@@ -439,34 +397,22 @@ paths:
439397

440398
### Generated outputs
441399

442-
**auth-policy.cedar** (signed-bundle routing source):
443-
```json
444-
{
445-
"version": 1,
446-
"routes": [{
447-
"operationId": "publishEvent",
448-
"method": "POST",
449-
"path": "/audit/publish",
450-
"public": false,
451-
"tenantRequired": true,
452-
"scopes": ["audit-event:write"]
453-
}]
454-
}
455-
```
456-
457-
**auth-policy.cedar** (live-discovery routing + decision source — same fields, as annotations):
458-
```
459-
@id("auditflow::publishEvent")
460-
@path("/audit/publish")
461-
@method("POST")
462-
@public("false")
463-
@tenantRequired("true")
464-
@scopes("audit-event:write")
465-
permit(
466-
principal,
467-
action == Labs64IO::Action::"invoke",
468-
resource == Labs64IO::ApiOperation::"auditflow::publishEvent"
469-
) when { (context has tenant) && (context.scopes.contains("audit-event:write")) };
400+
**Generated Cerbos edge policy (`target/cerbos/auditflow_api.yaml`)**:
401+
```yaml
402+
apiVersion: api.cerbos.dev/v1
403+
resourcePolicy:
404+
version: default
405+
resource: auditflow_api
406+
rules:
407+
- actions: ["publishEvent"]
408+
effect: EFFECT_ALLOW
409+
roles: ["*"]
410+
condition:
411+
match:
412+
all:
413+
of:
414+
- expr: has(request.principal.attr.tenant)
415+
- expr: '"audit-event:write" in request.principal.attr.scopes'
470416
```
471417

472418
**Generated AuditEventApi.java**:
@@ -511,21 +457,21 @@ Ensure `auth-context-spring-boot-starter` is on the classpath and `labs64.auth-c
511457

512458
## Reading @Authorize enforcement logs
513459

514-
Each `@Authorize` decision emits a non-sensitive summary on `io.labs64.authcontext.cedar.LoggingDecisionListener`:
460+
Each `@Authorize` decision emits a non-sensitive summary on `io.labs64.authcontext.cerbos.LoggingDecisionListener`:
515461

516462
```
517-
cedar-domain outcome=<enforced|shadow>-<allow|deny> decision=<allow|deny|error> \
463+
cerbos-domain outcome=<enforced|shadow>-<allow|deny> decision=<allow|deny|error> \
518464
mode=<enforce|shadow> action=<a> resourceType=<t> reasons=<policyIds|-> requestId=<id>
519465
```
520466

521467
INFO for a clean allow, WARN for deny/error.
522468

523-
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`:
469+
Sensitive fields (user, tenant, resolved resource id, raw error) ride the dedicated `io.labs64.authcontext.cerbos.detail` logger at DEBUG, off by default. Enable it during the Cerbos testing phase, e.g. in `application.yaml`:
524470

525471
```yaml
526472
logging:
527473
level:
528-
io.labs64.authcontext.cedar.detail: DEBUG
474+
io.labs64.authcontext.cerbos.detail: DEBUG
529475
```
530476

531-
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`.
477+
The detail line emits `cerbos-detail requestId=<id> user=<user> tenant=<tenant> resource=<type>::<id>[ error=<err>]`, shareable with the summary for joining via `requestId`.

0 commit comments

Comments
 (0)