Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
873f29f
feat(spring-data): add Spring Data JPA query plan adapter
alexolivier May 18, 2026
3416225
test(spring-data): cover new scenarios from PRs #222, #223, #234, #235
alexolivier May 18, 2026
92ad0f4
fix(spring-data): address review blockers + high-priority items
alexolivier May 18, 2026
66dca72
docs(spring-data): land review nice-to-haves (H5, N1, N3-N5)
alexolivier May 18, 2026
f86f5eb
add example
alexolivier May 18, 2026
ff5e4e0
fix(spring-data): consistent operator overrides, empty-intersection g…
alexolivier Jun 24, 2026
d218b5c
test(spring-data): assert 3-level nesting is correlated EXISTS; docum…
alexolivier Jun 24, 2026
8b9ad8e
feat(spring-data): hierarchy operators (overlaps / ancestorOf / desce…
alexolivier Jul 16, 2026
55b5c91
fix(spring-data): honor planner operand order; resolve outer attrs in…
alexolivier Jul 16, 2026
7f7a375
refactor(spring-data): decompose translator; normalize operand order …
alexolivier Jul 16, 2026
69cce68
fix(spring-data): defend against Hibernate 6 negation collapse; compa…
alexolivier Jul 16, 2026
d7dedc3
docs(spring-data): fix Page return type in README; document test suit…
alexolivier Jul 16, 2026
636e081
fix(spring-data): address review comments — protobuf version, Dockerf…
alexolivier Jul 16, 2026
1636b65
feat(spring-data): support size(string), size(coll) <op> N, field-to-…
alexolivier Jul 20, 2026
a6d1dee
feat(spring-data): arithmetic comparisons and field-to-field string m…
alexolivier Jul 20, 2026
ce647fa
fix(spring-data): close 3VL negation leaks, IEEE-double fidelity, joi…
alexolivier Jul 20, 2026
5be7a88
chore(spring-data): strip review-process comments, de-qualify imports
alexolivier Jul 20, 2026
8903133
refactor(spring-data): consolidate duplicated translator scaffolding
alexolivier Jul 20, 2026
cafe6ee
fix(spring-data): keep out-of-long-range whole-number constants as do…
alexolivier Jul 21, 2026
22ed6bb
refactor(spring-data): deepen tri-state algebra and comparison transl…
alexolivier Jul 21, 2026
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
36 changes: 36 additions & 0 deletions .github/workflows/spring-data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Spring Data Test

on:
pull_request:
paths:
- "spring-data/**"
- "policies/**"
- ".github/workflows/spring-data.yaml"
push:
tags:
- spring-data/v*

defaults:
run:
working-directory: spring-data

jobs:
test:
strategy:
matrix:
java-version: ["17", "21"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Setup JDK
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: temurin
java-version: ${{ matrix.java-version }}

- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0

- name: Build and test
run: gradle build --no-daemon
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Multi-language ORM adapters that translate Cerbos query plan responses into data
| langchain-chromadb | TypeScript | `@cerbos/langchain-chromadb` | ChromaDB |
| sqlalchemy | Python | `cerbos-sqlalchemy` | SQLAlchemy |
| elasticsearch-java | Java | `cerbos-elasticsearch` | Elasticsearch |
| spring-data | Java | `cerbos-spring-data` | Spring Data JPA |

## Commands

Expand All @@ -34,9 +35,12 @@ pdm run test # pytest
pdm run format # isort + black
```

### Java (Elasticsearch)
### Java (Elasticsearch, Spring Data)
```bash
docker run --rm -v "$(pwd)":/app -w /app gradle:8.12-jdk17 gradle build --no-daemon
# For tests that use testcontainers (cerbos PDP + DBs), mount the docker socket:
docker run --rm -v "$(pwd)":/app -v /var/run/docker.sock:/var/run/docker.sock \
-e TESTCONTAINERS_RYUK_DISABLED=true --network host \
-w /app gradle:8.12-jdk17 gradle build --no-daemon
```

## Testing
Expand Down Expand Up @@ -68,7 +72,7 @@ Conventional Commits: `feat(prisma):`, `fix(mongoose):`, `chore(deps):`. Scope i

Each adapter has its own GitHub Actions workflow triggered by changes in its directory or `/policies/`. Matrix tests across Node versions (20, 22, 24, 25) and relevant service versions.

Tag-based publishing: `prisma/v*` -> npm, `sqla/v*` -> PyPI, `elasticsearch-java/v*` -> Maven Central.
Tag-based publishing: `prisma/v*` -> npm, `sqla/v*` -> PyPI, `elasticsearch-java/v*` and `spring-data/v*` -> Maven Central.

## Working with Adapters

Expand Down
59 changes: 59 additions & 0 deletions policies/resource.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,24 @@ resourcePolicy:
match:
expr: request.resource.attr.aString == request.resource.attr.id

- actions:
- "not-equal-field-to-field"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: request.resource.attr.aString != request.resource.attr.id

- actions:
- "size-count-threshold"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: size(request.resource.attr.ownedBy) >= 2

- actions:
- "equal-bool-false"
effect: EFFECT_ALLOW
Expand Down Expand Up @@ -942,4 +960,45 @@ resourcePolicy:
expr: >
hierarchy(R.attr.scope).descendentOf(hierarchy(P.attr.scope))

# Operand-order conformance: the planner preserves policy source order, so these
# value-first shapes reach adapters with the constant as the FIRST operand. Directional
# operators must be mirrored by adapters or results are silently inverted.
- actions:
- "value-first-lt"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: 1 < R.attr.aNumber

- actions:
- "value-first-size"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: 0 < size(R.attr.ownedBy)

- actions:
- "value-first-intersect"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: hasIntersection(["user1", "userX"], R.attr.ownedBy)

# A residual resource attribute INSIDE a collection lambda body — adapters must resolve
# it against the outer entity, not the collection element.
- actions:
- "outer-attr-in-lambda"
effect: EFFECT_ALLOW
roles:
- USER
condition:
match:
expr: R.attr.tags.exists(tag, tag.name == "public" && R.attr.aBool)


5 changes: 5 additions & 0 deletions spring-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.gradle/
build/
bin/
.idea/
*.iml
38 changes: 38 additions & 0 deletions spring-data/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Domain glossary — spring-data adapter

Terms used by this adapter's code, tests, and reviews. Architecture vocabulary
(module / interface / seam / depth) follows the codebase-design convention.

- **Error→deny contract** — the adapter's semantic target: the filtered row set
equals what per-resource `check()` calls would allow. CEL evaluation errors
(null/missing attribute without a null overload) deny, so their SQL
translation must evaluate UNKNOWN — never FALSE — under every polarity.
- **TriPredicate** — the tri-state predicate algebra module enforcing that
contract structurally: it owns the UNKNOWN constant, the junction-barriered
negation (Hibernate 6 collapses `cb.not(cb.not(p))`), and the macro truth
tables. Inputs consumed in more than one polarity are `Supplier`s, so
"translate fresh per occurrence" cannot be violated by callers. `cb.not` has
exactly one call site: inside this module.
- **ComparisonTranslator / Resolved** — the single comparison-translation seam.
Every binary leaf comparison resolves each operand to a typed `Resolved` case
(`Constant`, `Field`, `ConstantAdd`, `FieldPlusConstant`, `Arithmetic`,
`Opaque`) and dispatches on the pair. New operand types (e.g. `timestamp()`)
are one resolver case + dispatch pairings — see the extension recipe in the
module Javadoc. Classification is structural; conversion is lazy, because
which error fires is part of the pinned interface.
- **NormalizedBinary** — planner operands arrive in policy source order
(`1 < R.attr.x` is value-first); this normalizes field-first and mirrors
directional operators (`lt`↔`gt`). Receiver-sensitive operators
(`contains`/`startsWith`/`endsWith`) are exempt — the receiver's position is
meaning, not noise. Overrides observe the mirrored operator name.
- **ChainSubquery** — the one correlated-subquery skeleton. It anchors
correlation at the scope that owns the relation and joins through every hop
of a multi-hop chain; all collection operators compose over it.
- **Differential oracle** — the adversarial conformance suite: hostile policy
shapes planned against a real PDP, translated, executed on H2, and the id set
compared row-by-row against `check()` with attributes mirroring the DB rows
exactly. DB NULL is a *missing* attribute on the check side. No
hand-computed expectations; a degeneracy guard prevents vacuous passes.
- **Double space** — all numeric work happens in IEEE doubles, because Cerbos
attribute numbers are CEL doubles and the wire plan erases `1` vs `1.0`.
Constants fold in Java; columns get a real `CAST(... AS DOUBLE)`.
8 changes: 8 additions & 0 deletions spring-data/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM gradle:8.12-jdk17 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
# Tests need a Docker daemon (Testcontainers spawns a Cerbos PDP) and the shared /policies
# directory, neither of which exists inside `docker build`. Build artifacts only; run the
# test suite via CI or `docker run` with the docker socket mounted (see CLAUDE.md).
RUN gradle build -x test --no-daemon
Loading
Loading