diff --git a/.gitignore b/.gitignore index ad85f6b..3406d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,12 @@ build/ .project .settings/ kls_database.db + +# Requirements map: generated evidence (see .reqmap/raw/per_test.sh) +.reqmap/raw/* +!.reqmap/raw/per_test.sh +.reqmap/coverage.json +.reqmap/tests.json +.reqmap/stack.json +.reqmap/gaps.json +.reqmap/viewer.html diff --git a/.reqmap/GAPS.md b/.reqmap/GAPS.md new file mode 100644 index 0000000..82e71d7 --- /dev/null +++ b/.reqmap/GAPS.md @@ -0,0 +1,161 @@ +# Gap analysis: cerbos-sdk-java + +Code at `6ba1f6844beb`, requirements model at `6ba1f6844beb`, generated 2026-09-28. +Inputs: tests.json yes, coverage.json yes with per-test contexts (JaCoCo, one slice per test method), churn window 90 days. +Cerbos Hub (`dev.cerbos.sdk.hub`, its tests and `service_config.json`) is out of scope. Its requirements, tests and coverage are not part of this report. +The working tree has one uncommitted test file (`CheckResourcesRequestBuilderTest.java`), which is included in both tests.json and coverage.json. + +## Summary + +29 of 47 requirements have a mapped test and 18 have none. Line coverage of the SDK is 65.0% (353/543) and branch coverage is 43.3% (39/90). The gaps cluster in client configuration. Every integration test builds a plaintext client, so none of the TLS, CA, mutual TLS or insecure-mode code runs under test, and two of those paths behave in surprising ways. That is the most important gap. + +| Status | Requirements | +|---|---| +| implemented-tested | 29 | +| implemented-untested | 18 | +| tested-only | 0 | +| documented-only | 0 | + +| | High risk | Medium risk | Low risk | +|---|---|---|---| +| Confirmed gaps | 12 | 13 | 8 | + +## Confirmed gaps + +### REQ-CLIENT-CONFIG-002 to 005: TLS, insecure mode, custom CA and mutual TLS (UNTESTED, high risk) + +Every fixture calls `withPlaintext()`, so the TLS branch of `CerbosClientBuilder.build()` never runs (lines 85-111 are uncovered apart from the plaintext arm). Nothing fails if TLS stops working, if the CA file is ignored, or if a client certificate is not presented. Two behaviours recorded in the model are also unchecked: a CA certificate silently overrides insecure mode, and a certificate without its key (or a key without its certificate) is ignored without an error. + +Evidence: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:35-63`, `:85-111`; no tests; 19 test contexts execute the builder, all through the plaintext arm. Fixture certificates already exist in `src/test/resources/certificates`. + +Suggested tests (Cerbos container started with TLS using the fixture certificates): + +| Case | plaintext | insecure | CA cert | client cert/key | Expect | +|---|---|---|---|---|---| +| `tlsWithCustomCa` | off | off | fixture CA | none | check succeeds | +| `tlsWithoutCaFails` | off | off | unset | none | CerbosException, UNAVAILABLE | +| `insecureTrustsAnyCert` | off | on | unset | none | check succeeds | +| `caOverridesInsecure` | off | on | wrong CA | none | CerbosException (documents current behaviour) | +| `mutualTls` | off | off | fixture CA | both set | check succeeds against a server that requires client certs | +| `certWithoutKeyIgnored` | off | off | fixture CA | cert only | builds, no client cert presented | +| `unreadableCaFails` | off | off | missing file | none | InvalidClientConfigurationException "Failed to set CA trust root" | + +Add to: `src/test/java/dev/cerbos/sdk/CerbosBlockingClientTest.java` (or a new `CerbosTlsClientTest`), following the container setup in its `@BeforeAll`. + +### REQ-CLIENT-CONFIG-010: Admin client uses Basic credentials (UNTESTED_VALUES, high risk) + +The tests prove that the right credentials work. Nothing checks that a null username or password fails at build time, or that wrong credentials are rejected by the server as a CerbosException. + +Evidence: `CerbosClientBuilder.java:138-145`, `AdminApiCredentials.java:15-34`; line 141 (the null check) is uncovered. + +| Case | username | password | Expect | +|---|---|---|---| +| `adminClientNullUsername` | null | "x" | InvalidClientConfigurationException | +| `adminClientNullPassword` | "x" | null | InvalidClientConfigurationException | +| `adminClientWrongPassword` | "cerbos" | "wrong" | CerbosException, UNAUTHENTICATED | + +Add to: `CerbosBlockingAdminClientTest.java`, reusing its container. + +### REQ-CLIENT-CONFIG-014: RPC failures raised as CerbosException (SCENARIO_NO_TEST, high risk) + +The batch check error path (`CheckResourcesRequestBuilder.java:92-94`) never runs, and neither do the catch blocks in most admin methods (list, enable, disable, purge, reload). No test checks what the exception carries. The model records that its cause is the StatusRuntimeException's cause, which is usually null, so the original status exception is lost. + +Evidence: `CerbosException.java:10-26`, `CheckResourcesRequestBuilder.java:92-94`, `CerbosBlockingAdminClient.java:116-118`; tests cover single check, plan and delete errors only. + +| Case | Operation | Expect | +|---|---|---| +| `batchCheckInvalidRequest` | batch check with a resource missing its kind | CerbosException with INVALID_ARGUMENT | +| `cerbosExceptionCarriesStatus` | any failing call | `getStatus().getCode()` matches; message starts "RPC exception" | + +Add to: `CerbosClientTests.java` next to `partialCheckRequest`. + +### REQ-CHECK-006: Batch check many resources in one request (SCENARIO_NO_TEST, high risk) + +`addResourceAndActions` is never called, and no test passes aux data to the batch builder, so the rule that batch aux data replaces client aux data is unchecked. + +Evidence: `CheckResourcesRequestBuilder.java:51-56` uncovered; tested by `checkResources` only. + +| Case | Add method | Aux data source | Expect | +|---|---|---|---| +| `batchAddResourceAndActions` | addResourceAndActions | client | per-resource results match single checks | +| `batchAuxDataOverridesClient` | addResources(ResourceAction) | batch argument | decision uses the batch JWT | + +Add to: `CerbosClientTests.java`, following `checkResources`. + +### REQ-ADMIN-009: Purge old store revisions (UNTESTED_VALUES, high risk) + +Only `keepLast` 0 is tested. A positive value (keep that many revisions) and a negative value (treated as 0, which purges everything) are both unchecked. Purging the wrong number of revisions loses history. + +Evidence: `CerbosBlockingAdminClient.java:206-218`, line 209 uncovered; `purgeStoreRevisions`. + +| Case | keepLast | Expect | +|---|---|---| +| `purgeKeepsLastN` | 1 after two updates | affected rows equal revisions minus 1 | +| `purgeNegativeKeepsNone` | -1 | same result as 0 | + +Add to: `CerbosBlockingAdminClientTest.java`, following `purgeStoreRevisions`. + +### REQ-CHECK-002: Allowed only on an explicit ALLOW (UNTESTED_VALUES, high risk) + +`isAllowed` returns false when there is no result entry at all (`CheckResult.java:46-47`). That branch has never run. It is the fail-closed case, and a regression that returns true or throws would go unnoticed. + +Evidence: `CheckResult.java:45-52`, line 47 uncovered. + +Suggested test: `isAllowedWithNoEntryIsFalse`: build `CheckResult` from an empty entry, expect `isAllowed("view")` false. Add to a new `CheckResultTest.java` (pure unit test). + +### REQ-CHECK-005: Pass a JWT as auxiliary data (SCENARIO_NO_TEST, high risk) + +The JWT path is tested without a key set id only. Nothing checks that a key set id is sent. + +Suggested test: `checkWithJWTAndKeySetId`: configure a second key set in the container, pass `AuxData.withJWT(token, "ks2")`, expect the decision that depends on a claim. Add to `CerbosClientTests.java`, following `checkWithJWT`. + +### REQ-PLAN-003 and REQ-PLAN-004: Plan outcome kinds (UNTESTED_VALUES, high risk) + +No test gets an ALWAYS_ALLOWED plan, so `isAlwaysAllowed()` has only ever returned false. The empty-operand condition for ALWAYS_ALLOWED and ALWAYS_DENIED is also unchecked. + +Evidence: `PlanResourcesResult.java:39-53`, one arm of each comparison untaken. + +Suggested test: `planResourcesAlwaysAllowed`: plan an action the principal's role always has, expect `isAlwaysAllowed()` true, the other two false, and `getCondition()` present with an empty operand. Add to `CerbosClientTests.java`, following `planResources`. + +### Medium risk + +Each of these has a real hole with a concrete consequence. Outlines are one line each. + +- **REQ-ADMIN-003, send policies in batches.** The 17-policy fixture goes through the off-by-one batching (9 then 8), but nothing asserts batch sizes. Test with a fake stub: 10 policies should mean 2 calls today (9 + 1). Lines 93-94 and 108-109 (error paths) never run. +- **REQ-ADMIN-002, validate before queuing.** No test passes an invalid policy or schema. `addPolicyRejectsInvalid`: expect ValidationException and nothing sent. Include a list with an invalid third item to pin down the partial-queue behaviour. +- **REQ-ADMIN-005, list policy ids.** `listActivePolicies` (include_disabled false) is never called, and neither is the version filter. `listActivePoliciesExcludesDisabled`: disable one, expect 16. +- **REQ-ADMIN-012, reload the store.** No test. `reloadStoreWaits` with wait true and false, expecting no exception. +- **REQ-CLIENT-CONFIG-008, per-call deadline.** Runs on every call, never asserted. `timeoutExceededRaisesDeadline`: 1 ms timeout, expect CerbosException DEADLINE_EXCEEDED. +- **REQ-CLIENT-CONFIG-011, admin credentials from env.** No test. Needs env injection. Test the missing-variable case, which should raise InvalidClientConfigurationException. +- **REQ-CLIENT-CONFIG-013, audit annotations.** Annotations are set but never asserted, and `testBuild` has no assertions (NO_ASSERTIONS confirmed). Assert that the built request carries the annotations. +- **REQ-CHECK-003, unexpected result count.** No test for zero or several results on a single check. +- **REQ-CHECK-004, decisions as a map.** No test. +- **REQ-CHECK-007, find a batch result.** The predicate overload is never used. Add pass and fail cases, and two kinds sharing one id. +- **REQ-CHECK-008, decision metadata.** The include_meta off case is not asserted. `getMeta()` on an action with no meta entry throws (model note), so pin that down. +- **REQ-CHECK-010, validation errors on check results.** Only the plan path is tested. Mirror `planResourcesValidation` for check. +- **REQ-CHECK-012, attribute types.** Only strings are tested. Add double, bool, list and map attributes to a policy condition. + +### Low risk + +REQ-CLIENT-CONFIG-001 (missing target), 006 (authority override), 007 (interceptors), 009 (playground header, PlaygroundIT has no assertions), 012 (custom headers configured but never checked), REQ-CHECK-011 (`_NEW_` default id), REQ-CHECK-013 (request and call ids), REQ-TEST-SUPPORT-001 (image tag). All untested. They are cheap unit tests if anyone wants them, but a regression in any of them is unlikely to cause harm. + +## Contradictions + +- `CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` asserts 17 policies and `getPolicyNonExistent` asserts that `resource.foo.vdefault` does not exist, but the `@BeforeAll` setup loads 18 policies including `resource.foo.vdefault`. Both pass only because JUnit happens to run `deletePolicyWithoutDependents` first. Run on their own, they fail with "expected 17 but was 18" and "expected 0 but was 1". + +## Model corrections + +None. Every UNTESTED candidate with incidental coverage (REQ-CLIENT-CONFIG-002, 008, REQ-ADMIN-002, REQ-CHECK-003) was checked. The tests that run that code assert something else, so the gaps are real and the mappings stand. + +## Follow-ups + +- Make the admin tests independent of method order: reset the store in `@BeforeEach`, or add `@TestMethodOrder` and say why. `.reqmap/raw/per_test.sh` depends on this too. It currently builds coverage for the two failing slices from the `.exec` file. +- Hub was dropped from scope for this run. The Hub model from the first pass recorded two real bugs worth keeping in mind: `Utils.filesFromDirectory` returns nothing because `Path.endsWith(".yaml")` compares path segments, and `uploadFilesFromDirectory` lets every file through for the same reason. +- The open questions in REQUIREMENTS.md are still unanswered. Several (CA versus insecure precedence, batch sizes) decide whether the suggested tests should assert current behaviour or the intended behaviour. + +## Appendix: triaged as noise + +| Category | Requirement | Reason | +|---|---|---| +| UNCOVERED_CODE, PARTIAL_BRANCHES (105) | various | Same cause as a confirmed gap listed above (mostly StatusRuntimeException catch blocks under REQ-CLIENT-CONFIG-014 and the TLS branches). Counted once there. | +| UNTESTED_VALUES, UNTESTED_PAIRS, SCENARIO_NO_TEST | REQ-ADMIN-005, REQ-CHECK-007 | Independent guard clauses. Covering each value once is enough, and missing pairs add little. | diff --git a/.reqmap/REQUIREMENTS.md b/.reqmap/REQUIREMENTS.md new file mode 100644 index 0000000..97e621c --- /dev/null +++ b/.reqmap/REQUIREMENTS.md @@ -0,0 +1,732 @@ +# Requirements: cerbos-sdk-java + +Reverse engineered from the code at `6ba1f6844beb` (2026-09-28). Generated from `requirements.json`; edit that file and re-render instead of editing this one. + +In scope: `src/main/java/dev/cerbos/sdk/**` +Out of scope: `src/main/proto/**`, `generated dev/cerbos/api/** code`, `.github/**`, `src/test/java/dev/cerbos/sdk/PlaygroundIT.java`, `src/main/java/dev/cerbos/sdk/hub/** (Cerbos Hub store client)`, `src/main/resources/service_config.json (Hub gRPC service config)`, `src/test/java/dev/cerbos/sdk/hub/**` + +## Summary + +| Feature | implemented-tested | implemented-untested | tested-only | documented-only | unevidenced | retired | +|---|---|---|---|---|---|---| +| PDP and admin client configuration | 3 | 11 | 0 | 0 | 0 | 0 | +| Authorisation checks | 8 | 5 | 0 | 0 | 0 | 0 | +| Query planning | 6 | 0 | 0 | 0 | 0 | 0 | +| Admin API | 10 | 2 | 0 | 0 | 0 | 0 | +| Testcontainers support | 2 | 0 | 0 | 0 | 0 | 0 | +| **Total** | 29 | 18 | 0 | 0 | 0 | 0 | + +## PDP and admin client configuration (`CLIENT-CONFIG`) + +Building PDP and admin clients: transport security, timeouts, credentials, headers, audit annotations and RPC error reporting. + +| ID | Requirement | Kind | Risk | Status | Confidence | +|---|---|---|---|---|---| +| REQ-CLIENT-CONFIG-001 | Reject a missing server target | validation | low | implemented-untested | medium | +| REQ-CLIENT-CONFIG-002 | TLS by default, plaintext on request | configuration | high | implemented-untested | medium | +| REQ-CLIENT-CONFIG-003 | Insecure mode trusts any server certificate | configuration | high | implemented-untested | medium | +| REQ-CLIENT-CONFIG-004 | Custom CA certificate | configuration | high | implemented-untested | medium | +| REQ-CLIENT-CONFIG-005 | Mutual TLS client certificate | configuration | high | implemented-untested | medium | +| REQ-CLIENT-CONFIG-006 | Authority override | configuration | low | implemented-untested | medium | +| REQ-CLIENT-CONFIG-007 | Caller-supplied gRPC interceptors | integration | low | implemented-untested | medium | +| REQ-CLIENT-CONFIG-008 | Per-call deadline | non-functional | medium | implemented-untested | medium | +| REQ-CLIENT-CONFIG-009 | Playground instance header | integration | low | implemented-untested | medium | +| REQ-CLIENT-CONFIG-010 | Admin client uses Basic credentials | authorisation | high | implemented-tested | high | +| REQ-CLIENT-CONFIG-011 | Admin credentials from environment | configuration | medium | implemented-untested | medium | +| REQ-CLIENT-CONFIG-012 | Custom request headers | integration | low | implemented-untested | medium | +| REQ-CLIENT-CONFIG-013 | Audit annotations on requests | integration | medium | implemented-tested | medium | +| REQ-CLIENT-CONFIG-014 | RPC failures raised as CerbosException | error-handling | high | implemented-tested | high | + +### REQ-CLIENT-CONFIG-001 Reject a missing server target + +When the target address is null or blank, building a PDP or admin client fails with InvalidClientConfigurationException ("Invalid target [...]") and no connection is opened. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:31-33` (isEmptyString); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:80-83` (CerbosClientBuilder.buildChannel) +- Conditions: target in {null, blank, non-blank} + +| target | Expected | Tests | +|---|---|---| +| null | InvalidClientConfigurationException | 0 | +| blank | InvalidClientConfigurationException | 0 | +| non-blank | client built | 0 | + +### REQ-CLIENT-CONFIG-002 TLS by default, plaintext on request + +The client connects over TLS and verifies the server against the JVM's default trust store, unless the caller asks for plaintext, in which case it connects with no transport security. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:35-38` (withPlaintext); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:85-111` (CerbosClientBuilder.buildChannel) +- Conditions: plaintext in {on, off} + +| plaintext | Expected | Tests | +|---|---|---| +| on | unencrypted gRPC channel | 0 | +| off | TLS channel with default trust | 0 | + +Every integration test builds its client withPlaintext(), so the TLS branch never runs in the suite. The fixture certificates in src/test/resources/certificates are not used by any test. + +### REQ-CLIENT-CONFIG-003 Insecure mode trusts any server certificate + +When the caller enables insecure mode on a TLS connection, the client accepts any server certificate without verifying it. Insecure mode has no effect when plaintext is also enabled. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:40-43` (withInsecure); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:89-92` +- Conditions: plaintext in {on, off}; insecure in {on, off}; ca_certificate in {set, unset} + +| plaintext | insecure | ca_certificate | Expected | Tests | +|---|---|---|---|---| +| off | on | unset | any server certificate accepted | 0 | +| on | on | unset | plaintext; insecure ignored | 0 | +| off | on | set | CA trust manager is set after the insecure one, so the CA certificate is what gets used | 0 | + +With both insecure mode and a CA certificate, the code calls trustManager() twice, and the CA certificate call comes second. On gRPC's TlsChannelCredentials.Builder the later call replaces the earlier one. +- Open question: When both withInsecure() and withCaCertificate() are set, should insecure mode win? The code currently gives the CA certificate precedence. + +### REQ-CLIENT-CONFIG-004 Custom CA certificate + +When the caller supplies a CA certificate, the client trusts servers signed by that CA. If the certificate cannot be loaded, building the client fails with InvalidClientConfigurationException ("Failed to set CA trust root"). + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:50-53` (withCaCertificate); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:94-100` +- Conditions: ca_certificate in {unset, valid, unreadable} + +| ca_certificate | Expected | Tests | +|---|---|---| +| valid | server verified against the supplied CA | 0 | +| unreadable | InvalidClientConfigurationException: Failed to set CA trust root | 0 | + +### REQ-CLIENT-CONFIG-005 Mutual TLS client certificate + +When the caller supplies both a client certificate and a private key, the client presents them to the server. If only one of the two is supplied, both are silently ignored. If loading fails, building fails with InvalidClientConfigurationException ("Failed to set TLS credentials"). + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:55-63`; `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:102-108` +- Conditions: tls_certificate in {set, unset}; tls_key in {set, unset}; load_result in {ok, error} + +| tls_certificate | tls_key | load_result | Expected | Tests | +|---|---|---|---|---| +| set | set | ok | client certificate presented | 0 | +| set | unset | | no client certificate, no error | 0 | +| unset | set | | no client certificate, no error | 0 | +| set | set | error | InvalidClientConfigurationException: Failed to set TLS credentials | 0 | +- Open question: Should supplying a certificate without a key (or the reverse) be a configuration error instead of being silently ignored? + +### REQ-CLIENT-CONFIG-006 Authority override + +When the caller sets a non-blank authority, the client uses it as the TLS/HTTP2 authority instead of the target host name. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:45-48` (withAuthority); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:113-115` +- Conditions: authority in {null, blank, non-blank} + +| authority | Expected | Tests | +|---|---|---| +| non-blank | authority overridden | 0 | +| blank | target host used | 0 | + +### REQ-CLIENT-CONFIG-007 Caller-supplied gRPC interceptors + +The client runs any gRPC client interceptors the caller registers on every call it makes. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:75-78` (withClientInterceptors); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:117-119` + +### REQ-CLIENT-CONFIG-008 Per-call deadline + +Every PDP and admin call must complete within the configured timeout, 1 second by default, or it fails with DEADLINE_EXCEEDED. The caller can change the timeout when building the client. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:24-24`; `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:65-68` (withTimeout); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:63-66` (withClient); `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:42-45` (withClient) +- Conditions: timeout in {default (1000 ms), custom} + +| timeout | Expected | Tests | +|---|---|---| +| default (1000 ms) | deadline 1000 ms per call | 0 | +| custom | deadline as configured | 0 | + +Integration tests run with the default timeout but never check it. + +### REQ-CLIENT-CONFIG-009 Playground instance header + +When the caller sets a non-blank playground instance id, every PDP call carries it in a playground-instance header. Admin clients never send this header. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:70-73` (withPlaygroundInstance); `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:124-130` (buildBlockingClient); `src/main/java/dev/cerbos/sdk/PlaygroundInstanceCredentials.java:13-31`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:40-52` +- Conditions: playground_instance in {null, blank, non-blank} + +| playground_instance | Expected | Tests | +|---|---|---| +| non-blank | header playground-instance sent | 0 | +| blank | no header | 0 | + +PlaygroundIT exercises this, but it is a manual main() with no assertions, so it doesn't count as a test. + +### REQ-CLIENT-CONFIG-010 Admin client uses Basic credentials + +An admin client sends the given username and password as an HTTP Basic authorization header on every admin call. If either value is null, building the admin client fails with InvalidClientConfigurationException ("username and password must not be null"). Empty strings are accepted. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:138-145` (buildBlockingAdminClient); `src/main/java/dev/cerbos/sdk/AdminApiCredentials.java:15-34`; `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:29-34` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas` +- Conditions: username in {null, empty, non-empty}; password in {null, empty, non-empty} + +| username | password | Expected | Tests | +|---|---|---|---| +| non-empty | non-empty | admin calls authenticated | 1 | +| null | non-empty | InvalidClientConfigurationException | 0 | +| non-empty | null | InvalidClientConfigurationException | 0 | +| empty | empty | client built; server decides | 0 | + +The admin tests authenticate as cerbos/cerbosAdmin. Their passing shows the header is accepted, but no test covers rejection. + +### REQ-CLIENT-CONFIG-011 Admin credentials from environment + +When no credentials are passed, the admin client reads CERBOS_USERNAME and CERBOS_PASSWORD from the environment. If either variable is missing, building fails with InvalidClientConfigurationException. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java:132-136` (buildBlockingAdminClient()) +- Conditions: CERBOS_USERNAME in {set, unset}; CERBOS_PASSWORD in {set, unset} + +| CERBOS_USERNAME | CERBOS_PASSWORD | Expected | Tests | +|---|---|---|---| +| set | set | client built | 0 | +| unset | set | InvalidClientConfigurationException | 0 | + +### REQ-CLIENT-CONFIG-012 Custom request headers + +The caller can derive a PDP or admin client that sends extra headers, given as a map or as gRPC Metadata, on every call. The original client is left unchanged. Passing null Metadata removes the extra headers. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:84-98` (withHeaders); `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:53-67` (withHeaders); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:63-66`; `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:42-45` + +Both test fixtures configure a wibble: wobble header, but no test checks that it arrives. Header names must be valid ASCII metadata keys, otherwise gRPC throws IllegalArgumentException. + +### REQ-CLIENT-CONFIG-013 Audit annotations on requests + +The caller can derive a PDP client that attaches annotations to the request context of every check, batch check and plan request, where they show up in the Cerbos audit log. Passing null removes them. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:108-115` (withRequestAnnotations); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:130-145` (check); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:207-209` (plan); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:242-244` (plan); `src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java:28-41` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CheckResourcesRequestBuilderTest.java::CheckResourcesRequestBuilderTest::testBuild` + +CerbosBlockingClientTest sets foo=bar annotations but never asserts them. testBuild only builds a batch builder with no annotations and makes no assertions. check() sets the request context twice with the same value, which is redundant but harmless. + +### REQ-CLIENT-CONFIG-014 RPC failures raised as CerbosException + +When the Cerbos server rejects or fails any PDP or admin call, the client throws an unchecked CerbosException. It carries the numeric gRPC status code and the status description, and its message has the form "RPC exception []". + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosException.java:10-26`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:153-155`; `src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java:92-94`; `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:116-118` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialCheckRequest` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialPlanRequest` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents` +- Conditions: operation in {check, batch check, plan, admin call} + +| operation | Expected | Tests | +|---|---|---| +| check | CerbosException with INVALID_ARGUMENT for a principal without roles and a resource without id | 1 | +| plan | CerbosException with INVALID_ARGUMENT | 1 | +| admin call | CerbosException | 1 | +| batch check | CerbosException | 0 | + +The exception's cause is the cause of the gRPC StatusRuntimeException, not the StatusRuntimeException itself, and that cause is usually null. Callers therefore lose the original exception and its trailers. +- Open question: Should CerbosException keep the StatusRuntimeException as its cause, so callers can read trailers and error details? + +## Authorisation checks (`CHECK`) + +Single and batch permission checks, the request builders for principals and resources, and reading decisions, metadata and outputs. + +| ID | Requirement | Kind | Risk | Status | Confidence | +|---|---|---|---|---|---| +| REQ-CHECK-001 | Check one resource for several actions | behaviour | high | implemented-tested | high | +| REQ-CHECK-002 | Allowed only on an explicit ALLOW | authorisation | high | implemented-tested | high | +| REQ-CHECK-003 | Single check with an unexpected result count | error-handling | medium | implemented-untested | medium | +| REQ-CHECK-004 | All decisions as a map | behaviour | medium | implemented-untested | medium | +| REQ-CHECK-005 | Pass a JWT as auxiliary data | integration | high | implemented-tested | high | +| REQ-CHECK-006 | Batch check many resources in one request | behaviour | high | implemented-tested | high | +| REQ-CHECK-007 | Find a batch result by resource id | behaviour | medium | implemented-tested | high | +| REQ-CHECK-008 | Decision metadata on request | behaviour | medium | implemented-tested | high | +| REQ-CHECK-009 | Policy outputs per result | behaviour | medium | implemented-tested | high | +| REQ-CHECK-010 | Schema validation errors on check results | validation | medium | implemented-untested | medium | +| REQ-CHECK-011 | New resources default to id _NEW_ | data | low | implemented-untested | medium | +| REQ-CHECK-012 | Principal and resource attributes | data | medium | implemented-tested | high | +| REQ-CHECK-013 | Request ids and call ids | data | low | implemented-untested | medium | + +### REQ-CHECK-001 Check one resource for several actions + +A caller can ask whether a principal may perform one or more actions on a single resource. The SDK sends one CheckResources request with a fresh request id and returns a decision for each requested action. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:127-156` (CerbosBlockingClient.check) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithJWT` + +### REQ-CHECK-002 Allowed only on an explicit ALLOW + +isAllowed(action) returns true only when the server's effect for that action is ALLOW. It returns false for DENY, for an action that is not in the response, and when the result has no entry at all. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResult.java:45-52` (CheckResult.isAllowed) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` +- Conditions: effect in {ALLOW, DENY, action absent, no result entry} + +| effect | Expected | Tests | +|---|---|---| +| ALLOW | true | 2 | +| DENY | false | 2 | +| action absent | false (fail closed) | 1 | +| no result entry | false | 0 | + +checkResources asks for defer on XX225 even though that resource did not request the action, and asserts false. That covers the absent-action case. + +### REQ-CHECK-003 Single check with an unexpected result count + +When the server's response to a single check does not contain exactly one result, the SDK returns a result with no entry. isAllowed is false for every action, getAll is empty and there are no validation errors, but getMeta() and getOutputs() throw NullPointerException. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:149-152`; `src/main/java/dev/cerbos/sdk/CheckResult.java:46-48`; `src/main/java/dev/cerbos/sdk/CheckResult.java:59-62`; `src/main/java/dev/cerbos/sdk/CheckResult.java:75-94`; `src/main/java/dev/cerbos/sdk/CheckResult.java:101-107` (getMeta/getOutputs) +- Conditions: result_count in {0, 1, >1} + +| result_count | Expected | Tests | +|---|---|---| +| 1 | normal result | 1 | +| 0 | all actions denied; getMeta/getOutputs throw NullPointerException | 0 | +| >1 | all actions denied; getMeta/getOutputs throw NullPointerException | 0 | +- Open question: Should getMeta() and getOutputs() return empty values when there is no result entry, as the other accessors do? + +### REQ-CHECK-004 All decisions as a map + +getAll() returns an unmodifiable map from each action in the result to true when the action is allowed and false otherwise. It returns an empty map when there is no result entry. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResult.java:59-68` (CheckResult.getAll) + +### REQ-CHECK-005 Pass a JWT as auxiliary data + +A caller can attach a JWT, with an optional key set id, as auxiliary data. The SDK sends it with every check, batch check and plan request so policies can use its claims. A client without auxiliary data sends an empty AuxData. + +- Implemented by: `src/main/java/dev/cerbos/sdk/builders/AuxData.java:17-37`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:74-76` (with(AuxData)); `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:128-129`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:164-170`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:195-196` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithJWT` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` +- Conditions: aux_data in {none, jwt, jwt with key set id} + +| aux_data | Expected | Tests | +|---|---|---| +| jwt | JWT claims available to policy; defer allowed | 2 | +| none | empty AuxData sent | 1 | +| jwt with key set id | JWT and key set id sent | 0 | + +### REQ-CHECK-006 Batch check many resources in one request + +A caller can check several resources, each with its own actions, for one principal in a single request. Resources are added either as a resource plus actions or as ResourceAction objects. The batch uses the client's auxiliary data unless the caller passes auxiliary data for that batch, which then replaces it. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:164-182` (batch); `src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java:28-95`; `src/main/java/dev/cerbos/sdk/builders/ResourceAction.java:16-62` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` + - `src/test/java/dev/cerbos/sdk/CheckResourcesRequestBuilderTest.java::CheckResourcesRequestBuilderTest::testBuild` +- Conditions: add_method in {addResources(ResourceAction), addResourceAndActions}; aux_data_source in {client, batch argument} + +| add_method | aux_data_source | Expected | Tests | +|---|---|---|---| +| addResources(ResourceAction) | client | one request, per-resource results | 1 | +| addResourceAndActions | client | one request, per-resource results | 0 | +| addResources(ResourceAction) | batch argument | batch aux data used instead of client aux data | 0 | + +testBuild (in the uncommitted CheckResourcesRequestBuilderTest) only constructs the builder and asserts nothing. The request id is generated when the builder is created, so calling check() twice on one builder sends the same request id both times. + +### REQ-CHECK-007 Find a batch result by resource id + +find(resourceId) returns the first result whose resource id matches, or empty when there is none. An optional predicate on the resource (for example its kind) can narrow the match. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResourcesResult.java:25-46` (CheckResourcesResult.find); `src/main/java/dev/cerbos/sdk/CheckResourcesResult.java:21-23` (results) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` +- Conditions: id_match in {match, no match}; predicate in {none, passes, fails} + +| id_match | predicate | Expected | Tests | +|---|---|---|---| +| match | none | result returned | 1 | +| no match | none | empty | 1 | +| match | passes | result returned | 0 | +| match | fails | empty | 0 | + +With two resources of different kinds that share an id, find(id) with no predicate returns whichever comes first. + +### REQ-CHECK-008 Decision metadata on request + +When the caller asks for metadata, each result exposes the effective derived roles and, for each action, the policy that matched. getInfoForAction returns empty for an action with no metadata. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java:76-79` (withIncludeMeta); `src/main/java/dev/cerbos/sdk/CheckResult.java:113-149` (CheckResult.Meta) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` +- Conditions: include_meta in {on, off}; action_in_meta in {yes, no} + +| include_meta | action_in_meta | Expected | Tests | +|---|---|---|---| +| on | yes | derived roles and matched policy returned | 1 | +| on | no | getInfoForAction empty | 1 | +| off | | metadata empty | 0 | + +The single-resource check() cannot request metadata. Only the batch builder has withIncludeMeta(). + +### REQ-CHECK-009 Policy outputs per result + +Each result exposes the outputs produced by policy rules as a map keyed by the rule's source identifier, along with a count. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResult.java:105-107`; `src/main/java/dev/cerbos/sdk/CheckResult.java:151-179` (CheckResult.Outputs) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` + +asMap() collects into an unmodifiable map, so two outputs with the same source would throw IllegalStateException. + +### REQ-CHECK-010 Schema validation errors on check results + +A single result reports whether the server found schema validation errors and lists them. A batch result reports whether any of its results has validation errors. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CheckResult.java:75-94`; `src/main/java/dev/cerbos/sdk/CheckResourcesResult.java:48-50` +- Conditions: validation_errors in {none, some} + +| validation_errors | Expected | Tests | +|---|---|---| +| none | hasValidationErrors false | 0 | +| some | hasValidationErrors true, errors listed | 0 | + +Only the plan path has a validation-error test (planResourcesValidation). + +### REQ-CHECK-011 New resources default to id _NEW_ + +A resource created with a kind but no id is sent with the id "_NEW_". + +- Implemented by: `src/main/java/dev/cerbos/sdk/builders/Resource.java:19-21`; `src/main/java/dev/cerbos/sdk/builders/ResourceAction.java:24-26` + +### REQ-CHECK-012 Principal and resource attributes + +Principals and resources carry an id, roles (principals only, and additive across calls), policy version, scope and attributes. Attribute values can be strings, numbers (sent as doubles), booleans, lists or maps, nested to any depth. + +- Implemented by: `src/main/java/dev/cerbos/sdk/builders/AttributeValue.java:16-56`; `src/main/java/dev/cerbos/sdk/builders/Principal.java:13-51`; `src/main/java/dev/cerbos/sdk/builders/Resource.java:12-49` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources` +- Conditions: attribute_type in {string, double, bool, list, map} + +| attribute_type | Expected | Tests | +|---|---|---| +| string | sent as string value | 1 | +| double | sent as number | 0 | +| bool | sent as bool | 0 | +| list | sent as list | 0 | +| map | sent as struct | 0 | + +Tests only use string attributes and a single role. Scope is never set in the check tests. + +### REQ-CHECK-013 Request ids and call ids + +Each check, batch and plan request gets a random UUID request id. Results expose the request id and the Cerbos call id that the server returned. + +- Implemented by: `src/main/java/dev/cerbos/sdk/RequestId.java:10-15`; `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:133-133`; `src/main/java/dev/cerbos/sdk/CheckResult.java:31-37`; `src/main/java/dev/cerbos/sdk/CheckResourcesResult.java:56-62`; `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:67-73` + +## Query planning (`PLAN`) + +PlanResources calls that tell the caller which resources a principal may act on, and how to read the plan. + +| ID | Requirement | Kind | Risk | Status | Confidence | +|---|---|---|---|---|---| +| REQ-PLAN-001 | Plan a query filter for one action | behaviour | high | implemented-tested | high | +| REQ-PLAN-002 | Plan a query filter for several actions | behaviour | high | implemented-tested | high | +| REQ-PLAN-003 | Plan outcome kinds | authorisation | high | implemented-tested | high | +| REQ-PLAN-004 | Plan condition is always present | behaviour | medium | implemented-tested | medium | +| REQ-PLAN-005 | Schema validation errors on plans | validation | medium | implemented-tested | high | +| REQ-PLAN-006 | Plan result identifies the plan | data | low | implemented-tested | high | + +### REQ-PLAN-001 Plan a query filter for one action + +A caller can ask which resources of a kind a principal may perform one action on. The SDK sends a PlanResources request with the resource kind, policy version, scope and attributes (any resource id is dropped) and returns the plan for that action. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:194-217` (plan(String)); `src/main/java/dev/cerbos/sdk/builders/Resource.java:51-59` (toPlanResource); `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:22-25` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources` + +Uses the deprecated single action field of the request. + +### REQ-PLAN-002 Plan a query filter for several actions + +A caller can plan for several actions at once. The SDK sends them as the request's action list, and the result lists the same actions. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java:229-252` (plan(Iterable)); `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:27-29` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions` + +### REQ-PLAN-003 Plan outcome kinds + +A plan result is exactly one of always allowed, always denied or conditional, as reported by the server's filter kind. + +- Implemented by: `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:39-49` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation` +- Conditions: filter_kind in {ALWAYS_ALLOWED, ALWAYS_DENIED, CONDITIONAL} + +| filter_kind | Expected | Tests | +|---|---|---| +| CONDITIONAL | isConditional true, others false | 2 | +| ALWAYS_DENIED | isAlwaysDenied true, others false | 1 | +| ALWAYS_ALLOWED | isAlwaysAllowed true, others false | 0 | + +### REQ-PLAN-004 Plan condition is always present + +getCondition() always returns a present value. For a conditional plan it is the filter expression tree. For always-allowed and always-denied plans it is an empty operand, not an empty Optional. + +- Implemented by: `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:51-53` (getCondition) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions` +- Conditions: filter_kind in {CONDITIONAL, ALWAYS_ALLOWED, ALWAYS_DENIED} + +| filter_kind | Expected | Tests | +|---|---|---| +| CONDITIONAL | present, expression tree | 2 | +| ALWAYS_DENIED | present, empty operand | 0 | +| ALWAYS_ALLOWED | present, empty operand | 0 | +- Open question: Should getCondition() return Optional.empty() when the plan is not conditional? Using Optional suggests that was the intent. + +### REQ-PLAN-005 Schema validation errors on plans + +A plan result reports whether the server found schema validation errors and lists them. In the tested case, a request that fails validation is planned as always denied. + +- Implemented by: `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:55-61` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources` +- Conditions: validation_errors in {none, some} + +| validation_errors | Expected | Tests | +|---|---|---| +| none | hasValidationErrors false | 1 | +| some | hasValidationErrors true, 2 errors, always denied | 1 | + +Whether invalid input produces always-denied depends on the server's schema enforcement setting (reject in the test config), not on the SDK. + +### REQ-PLAN-006 Plan result identifies the plan + +A plan result reports the resource kind, policy version, request id and Cerbos call id that the server returned. + +- Implemented by: `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:31-37`; `src/main/java/dev/cerbos/sdk/PlanResourcesResult.java:67-73` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions` + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation` + +## Admin API (`ADMIN`) + +Managing policies and schemas in a mutable Cerbos policy store: bulk add or update, list, get, enable, disable, delete, purge and reload. + +| ID | Requirement | Kind | Risk | Status | Confidence | +|---|---|---|---|---|---| +| REQ-ADMIN-001 | Add or update policies from JSON or objects | behaviour | high | implemented-tested | high | +| REQ-ADMIN-002 | Validate policies and schemas before queuing | validation | medium | implemented-untested | medium | +| REQ-ADMIN-003 | Send policies and schemas in batches | non-functional | medium | implemented-tested | medium | +| REQ-ADMIN-004 | Add or update JSON schemas | behaviour | high | implemented-tested | high | +| REQ-ADMIN-005 | List policy ids with filters | behaviour | medium | implemented-tested | high | +| REQ-ADMIN-006 | Get policies by id | behaviour | medium | implemented-tested | high | +| REQ-ADMIN-007 | Enable and disable policies | behaviour | high | implemented-tested | high | +| REQ-ADMIN-008 | Delete policies | behaviour | high | implemented-tested | high | +| REQ-ADMIN-009 | Purge old store revisions | data | high | implemented-tested | high | +| REQ-ADMIN-010 | List and get schemas | behaviour | medium | implemented-tested | high | +| REQ-ADMIN-011 | Delete schemas | behaviour | medium | implemented-tested | high | +| REQ-ADMIN-012 | Reload the policy store | behaviour | medium | implemented-untested | medium | + +### REQ-ADMIN-001 Add or update policies from JSON or objects + +An admin can queue policies as JSON text, a JSON reader or policy objects, then send them all to the policy store with one addOrUpdate call. Nothing is sent until addOrUpdate is called. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:75-77`; `src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java:40-79` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithFilter` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy` + +The admin test loads 17 policies (YAML converted to JSON) through this builder in its @BeforeAll setup. listPoliciesWithoutFilter then asserts that all 17 are present. + +### REQ-ADMIN-002 Validate policies and schemas before queuing + +Each policy or schema is validated against the Cerbos proto rules as it is queued. An invalid one is rejected with a checked ValidationException that lists the violations, and it is not queued. + +- Implemented by: `src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java:59-63`; `src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java:72-79`; `src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java:42-48`; `src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java:71-78`; `src/main/java/dev/cerbos/sdk/validation/Validator.java:15-25`; `src/main/java/dev/cerbos/sdk/validation/ValidationException.java:13-28` +- Conditions: input in {valid, violates rules, validator error} + +| input | Expected | Tests | +|---|---|---| +| valid | queued | 1 | +| violates rules | ValidationException with violations; not queued | 0 | +| validator error | ValidationException with cause and no violations | 0 | + +When a list of policies or schemas is passed, the ones before the first invalid item are already queued when the exception is thrown. + +### REQ-ADMIN-003 Send policies and schemas in batches + +addOrUpdate sends queued policies (and, separately, schemas) in several requests. The first request holds 9 items and each later one holds up to 10. With nothing queued, no request is sent. If a request fails, the earlier batches stay applied and the rest are not sent. + +- Implemented by: `src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java:86-112` (addOrUpdate); `src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java:85-111` (addOrUpdate) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` +- Conditions: queued_count in {0, 1-9, 10, >10} + +| queued_count | Expected | Tests | +|---|---|---| +| 0 | no request | 0 | +| 1-9 | one request | 0 | +| 10 | two requests (9 + 1) | 0 | +| >10 | 17 policies sent as 9 + 8 | 1 | + +The loop flushes when i % 10 == 0, before adding item i, which gives batches of 9, 10, 10, ... The requests are not atomic across batches. +- Open question: Was the intended batch size 10 for every batch? The first batch holds 9 because of an off-by-one in the flush check. + +### REQ-ADMIN-004 Add or update JSON schemas + +An admin can queue schemas as an id plus JSON definition (text or reader) or as schema objects, then send them with addOrUpdate. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:225-227`; `src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java:42-78` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getSchema` + +### REQ-ADMIN-005 List policy ids with filters + +An admin can list policy ids, either active only or including disabled ones, optionally filtered by regular expressions on name, version and scope. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:89-119` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithFilter` +- Conditions: include_disabled in {true, false}; name_regex in {set, unset}; version_regex in {set, unset}; scope_regex in {set, unset} + +| include_disabled | name_regex | version_regex | scope_regex | Expected | Tests | +|---|---|---|---|---|---| +| true | unset | unset | unset | all 17 ids | 1 | +| true | set | unset | set | 3 matching ids in order | 1 | +| false | unset | unset | unset | disabled policies excluded | 0 | +| true | unset | set | unset | filtered by version | 0 | + +### REQ-ADMIN-006 Get policies by id + +An admin can fetch policies by id. Only policies that exist are returned, so an unknown id produces an empty list rather than an error. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:128-138` (getPolicy) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicyNonExistent` +- Conditions: policy_exists in {yes, no} + +| policy_exists | Expected | Tests | +|---|---|---| +| yes | one policy returned | 1 | +| no | empty list | 1 | + +### REQ-ADMIN-007 Enable and disable policies + +An admin can disable or re-enable policies by id. Each call returns the number of policies it changed. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:147-176` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::enableAndDisablePolicy` + +### REQ-ADMIN-008 Delete policies + +An admin can delete policies by id and gets back the number deleted. Deleting a policy that other policies depend on fails with CerbosException. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:185-195` (deletePolicy) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithoutDependents` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents` +- Conditions: has_dependents in {yes, no} + +| has_dependents | Expected | Tests | +|---|---|---| +| no | returns 1 | 1 | +| yes | CerbosException | 1 | + +The SDK does not enforce the dependents rule. The server does. + +### REQ-ADMIN-009 Purge old store revisions + +An admin can purge policy store revisions and gets back the number of rows removed. A positive keepLast keeps that many recent revisions. Zero or a negative value sends no keepLast, so the server purges every revision. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:206-218` (purgeStoreRevisions) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::purgeStoreRevisions` +- Conditions: keep_last in {negative, 0, positive} + +| keep_last | Expected | Tests | +|---|---|---| +| 0 | all revisions purged, count > 1 | 1 | +| negative | treated as 0 | 0 | +| positive | that many revisions kept | 0 | + +### REQ-ADMIN-010 List and get schemas + +An admin can list all schema ids and fetch schemas by id. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:235-261` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getSchema` + +### REQ-ADMIN-011 Delete schemas + +An admin can delete schemas by id and gets back the number deleted. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:270-280` (deleteSchema) +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deleteSchema` + +### REQ-ADMIN-012 Reload the policy store + +An admin can ask the server to reload its policy store, and can choose to wait until the reload finishes. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java:288-294` (storeReload) +- Conditions: wait in {true, false} + +| wait | Expected | Tests | +|---|---|---| +| true | returns after reload completes | 0 | +| false | returns immediately | 0 | + +## Testcontainers support (`TEST-SUPPORT`) + +The CerbosContainer helper for running a Cerbos PDP in integration tests. + +| ID | Requirement | Kind | Risk | Status | Confidence | +|---|---|---|---|---|---| +| REQ-TEST-SUPPORT-001 | Cerbos test container image | configuration | low | implemented-tested | medium | +| REQ-TEST-SUPPORT-002 | Container readiness and gRPC target | integration | low | implemented-tested | medium | + +### REQ-TEST-SUPPORT-001 Cerbos test container image + +CerbosContainer starts the ghcr.io/cerbos/cerbos image, tagged latest by default or with a caller-supplied tag. A custom image must declare compatibility with ghcr.io/cerbos/cerbos, otherwise construction fails. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosContainer.java:13-32` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` +- Conditions: image in {default, custom tag, compatible custom image, incompatible image} + +| image | Expected | Tests | +|---|---|---| +| custom tag | ghcr.io/cerbos/cerbos: started | 1 | +| default | ghcr.io/cerbos/cerbos:latest | 0 | +| incompatible image | IllegalStateException from assertCompatibleWith | 0 | + +Both integration fixtures use the "dev" tag. Their tests exercise the container without asserting anything about it. + +### REQ-TEST-SUPPORT-002 Container readiness and gRPC target + +The container exposes HTTP port 3592 and gRPC port 3593, counts as ready once the log shows "Starting gRPC server", and gives a target of 127.0.0.1: for building clients. + +- Implemented by: `src/main/java/dev/cerbos/sdk/CerbosContainer.java:16-17`; `src/main/java/dev/cerbos/sdk/CerbosContainer.java:30-44` +- Tested by: + - `src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT` + - `src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter` + +Evidence comes from fixture use only. + +## Open questions + +- REQ-CLIENT-CONFIG-003: When both withInsecure() and withCaCertificate() are set, should insecure mode win? The code currently gives the CA certificate precedence. +- REQ-CLIENT-CONFIG-005: Should supplying a certificate without a key (or the reverse) be a configuration error instead of being silently ignored? +- REQ-CLIENT-CONFIG-014: Should CerbosException keep the StatusRuntimeException as its cause, so callers can read trailers and error details? +- REQ-CHECK-003: Should getMeta() and getOutputs() return empty values when there is no result entry, as the other accessors do? +- REQ-PLAN-004: Should getCondition() return Optional.empty() when the plan is not conditional? Using Optional suggests that was the intent. +- REQ-ADMIN-003: Was the intended batch size 10 for every batch? The first batch holds 9 because of an off-by-one in the flush check. diff --git a/.reqmap/raw/per_test.sh b/.reqmap/raw/per_test.sh new file mode 100755 index 0000000..23c8202 --- /dev/null +++ b/.reqmap/raw/per_test.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Per-test-method JaCoCo slices, each tagged with its tests.json id, merged into .reqmap/coverage.json. +# CerbosClientTests is abstract, so its methods run through CerbosBlockingClientTest. +# Cerbos Hub (dev.cerbos.sdk.hub) is out of scope: its tests are not run and its files are dropped. +set -euo pipefail +S="$HOME/.claude/skills/test-coverage-map/scripts" +OUT=.reqmap/raw/slices +rm -rf "$OUT" && mkdir -p "$OUT" +jq -r '.tests[].id' .reqmap/tests.json | while read -r id; do + file=${id%%::*} + rest=${id#*::} + method=${rest##*::} + cls=${rest%%::*} + pkg=$(dirname "${file#src/test/java/}" | tr / .) + case "$cls" in + CerbosClientTests) run="$pkg.CerbosBlockingClientTest" ;; + *) run="$pkg.$cls" ;; + esac + inner=${rest#"$cls"}; inner=${inner%"::$method"}; inner=${inner#::} + [ -n "$inner" ] && run="$run\$$inner" + slug=$(echo "$id" | tr '/.:$' '____') + rm -f build/jacoco/test.exec + if ./gradlew test --rerun --tests "$run.$method" jacocoTestReport -q --console=plain >"$OUT/$slug.log" 2>&1; then + echo "ok $id" + else + # The test failed in isolation (some admin tests depend on method order), but the code + # it ran is still in test.exec, so build the report from that and keep the slice. + echo "FAIL $id (see $OUT/$slug.log); report built from test.exec" + ./gradlew jacocoTestReport -x test -q --console=plain >>"$OUT/$slug.log" 2>&1 + fi + python3 "$S/normalise_coverage.py" build/reports/jacoco/test/jacocoTestReport.xml \ + --root . --context "$id" -o "$OUT/$slug.json" +done +python3 "$S/merge_coverage.py" "$OUT/*.json" --root . -o "$OUT/merged.all" +jq '.files |= with_entries(select(.key | startswith("src/main/java/dev/cerbos/sdk/hub/") | not))' \ + "$OUT/merged.all" > "$OUT/merged.nohub" +python3 "$S/merge_coverage.py" "$OUT/merged.nohub" --root . -o .reqmap/coverage.json +python3 "$S/cov_query.py" summary --limit 20 diff --git a/.reqmap/requirements.json b/.reqmap/requirements.json new file mode 100644 index 0000000..71adc93 --- /dev/null +++ b/.reqmap/requirements.json @@ -0,0 +1,2903 @@ +{ + "schema": 1, + "kind": "requirements", + "git_sha": "6ba1f6844beb2a18d69e71daafc18013f1eb62c9", + "generated_at": "2026-09-28", + "scope": { + "name": "cerbos-sdk-java", + "include": [ + "src/main/java/dev/cerbos/sdk/**" + ], + "excluded": [ + "src/main/proto/**", + "generated dev/cerbos/api/** code", + ".github/**", + "src/test/java/dev/cerbos/sdk/PlaygroundIT.java", + "src/main/java/dev/cerbos/sdk/hub/** (Cerbos Hub store client)", + "src/main/resources/service_config.json (Hub gRPC service config)", + "src/test/java/dev/cerbos/sdk/hub/**" + ] + }, + "features": [ + { + "id": "CLIENT-CONFIG", + "name": "PDP and admin client configuration", + "summary": "Building PDP and admin clients: transport security, timeouts, credentials, headers, audit annotations and RPC error reporting." + }, + { + "id": "CHECK", + "name": "Authorisation checks", + "summary": "Single and batch permission checks, the request builders for principals and resources, and reading decisions, metadata and outputs." + }, + { + "id": "PLAN", + "name": "Query planning", + "summary": "PlanResources calls that tell the caller which resources a principal may act on, and how to read the plan." + }, + { + "id": "ADMIN", + "name": "Admin API", + "summary": "Managing policies and schemas in a mutable Cerbos policy store: bulk add or update, list, get, enable, disable, delete, purge and reload." + }, + { + "id": "TEST-SUPPORT", + "name": "Testcontainers support", + "summary": "The CerbosContainer helper for running a Cerbos PDP in integration tests." + } + ], + "requirements": [ + { + "id": "REQ-CLIENT-CONFIG-001", + "feature": "CLIENT-CONFIG", + "title": "Reject a missing server target", + "statement": "When the target address is null or blank, building a PDP or admin client fails with InvalidClientConfigurationException (\"Invalid target [...]\") and no connection is opened.", + "kind": "validation", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 31, + 33 + ], + "symbol": "isEmptyString" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 80, + 83 + ], + "symbol": "CerbosClientBuilder.buildChannel" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "target", + "values": [ + "null", + "blank", + "non-blank" + ], + "evidence": { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 81, + 83 + ] + } + } + ], + "scenarios": [ + { + "given": { + "target": "null" + }, + "expect": "InvalidClientConfigurationException", + "tests": [] + }, + { + "given": { + "target": "blank" + }, + "expect": "InvalidClientConfigurationException", + "tests": [] + }, + { + "given": { + "target": "non-blank" + }, + "expect": "client built", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-002", + "feature": "CLIENT-CONFIG", + "title": "TLS by default, plaintext on request", + "statement": "The client connects over TLS and verifies the server against the JVM's default trust store, unless the caller asks for plaintext, in which case it connects with no transport security.", + "kind": "configuration", + "risk": "high", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 35, + 38 + ], + "symbol": "withPlaintext" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 85, + 111 + ], + "symbol": "CerbosClientBuilder.buildChannel" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "plaintext", + "values": [ + "on", + "off" + ], + "evidence": { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 86, + 111 + ] + } + } + ], + "scenarios": [ + { + "given": { + "plaintext": "on" + }, + "expect": "unencrypted gRPC channel", + "tests": [] + }, + { + "given": { + "plaintext": "off" + }, + "expect": "TLS channel with default trust", + "tests": [] + } + ], + "notes": "Every integration test builds its client withPlaintext(), so the TLS branch never runs in the suite. The fixture certificates in src/test/resources/certificates are not used by any test.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-003", + "feature": "CLIENT-CONFIG", + "title": "Insecure mode trusts any server certificate", + "statement": "When the caller enables insecure mode on a TLS connection, the client accepts any server certificate without verifying it. Insecure mode has no effect when plaintext is also enabled.", + "kind": "configuration", + "risk": "high", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 40, + 43 + ], + "symbol": "withInsecure" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 89, + 92 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "plaintext", + "values": [ + "on", + "off" + ] + }, + { + "name": "insecure", + "values": [ + "on", + "off" + ] + }, + { + "name": "ca_certificate", + "values": [ + "set", + "unset" + ] + } + ], + "scenarios": [ + { + "given": { + "plaintext": "off", + "insecure": "on", + "ca_certificate": "unset" + }, + "expect": "any server certificate accepted", + "tests": [] + }, + { + "given": { + "plaintext": "on", + "insecure": "on", + "ca_certificate": "unset" + }, + "expect": "plaintext; insecure ignored", + "tests": [] + }, + { + "given": { + "plaintext": "off", + "insecure": "on", + "ca_certificate": "set" + }, + "expect": "CA trust manager is set after the insecure one, so the CA certificate is what gets used", + "tests": [] + } + ], + "notes": "With both insecure mode and a CA certificate, the code calls trustManager() twice, and the CA certificate call comes second. On gRPC's TlsChannelCredentials.Builder the later call replaces the earlier one.", + "questions": [ + "When both withInsecure() and withCaCertificate() are set, should insecure mode win? The code currently gives the CA certificate precedence." + ], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-004", + "feature": "CLIENT-CONFIG", + "title": "Custom CA certificate", + "statement": "When the caller supplies a CA certificate, the client trusts servers signed by that CA. If the certificate cannot be loaded, building the client fails with InvalidClientConfigurationException (\"Failed to set CA trust root\").", + "kind": "configuration", + "risk": "high", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 50, + 53 + ], + "symbol": "withCaCertificate" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 94, + 100 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "ca_certificate", + "values": [ + "unset", + "valid", + "unreadable" + ] + } + ], + "scenarios": [ + { + "given": { + "ca_certificate": "valid" + }, + "expect": "server verified against the supplied CA", + "tests": [] + }, + { + "given": { + "ca_certificate": "unreadable" + }, + "expect": "InvalidClientConfigurationException: Failed to set CA trust root", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-005", + "feature": "CLIENT-CONFIG", + "title": "Mutual TLS client certificate", + "statement": "When the caller supplies both a client certificate and a private key, the client presents them to the server. If only one of the two is supplied, both are silently ignored. If loading fails, building fails with InvalidClientConfigurationException (\"Failed to set TLS credentials\").", + "kind": "configuration", + "risk": "high", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 55, + 63 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 102, + 108 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "tls_certificate", + "values": [ + "set", + "unset" + ] + }, + { + "name": "tls_key", + "values": [ + "set", + "unset" + ] + }, + { + "name": "load_result", + "values": [ + "ok", + "error" + ] + } + ], + "scenarios": [ + { + "given": { + "tls_certificate": "set", + "tls_key": "set", + "load_result": "ok" + }, + "expect": "client certificate presented", + "tests": [] + }, + { + "given": { + "tls_certificate": "set", + "tls_key": "unset" + }, + "expect": "no client certificate, no error", + "tests": [] + }, + { + "given": { + "tls_certificate": "unset", + "tls_key": "set" + }, + "expect": "no client certificate, no error", + "tests": [] + }, + { + "given": { + "tls_certificate": "set", + "tls_key": "set", + "load_result": "error" + }, + "expect": "InvalidClientConfigurationException: Failed to set TLS credentials", + "tests": [] + } + ], + "notes": "", + "questions": [ + "Should supplying a certificate without a key (or the reverse) be a configuration error instead of being silently ignored?" + ], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-006", + "feature": "CLIENT-CONFIG", + "title": "Authority override", + "statement": "When the caller sets a non-blank authority, the client uses it as the TLS/HTTP2 authority instead of the target host name.", + "kind": "configuration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 45, + 48 + ], + "symbol": "withAuthority" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 113, + 115 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "authority", + "values": [ + "null", + "blank", + "non-blank" + ] + } + ], + "scenarios": [ + { + "given": { + "authority": "non-blank" + }, + "expect": "authority overridden", + "tests": [] + }, + { + "given": { + "authority": "blank" + }, + "expect": "target host used", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-007", + "feature": "CLIENT-CONFIG", + "title": "Caller-supplied gRPC interceptors", + "statement": "The client runs any gRPC client interceptors the caller registers on every call it makes.", + "kind": "integration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 75, + 78 + ], + "symbol": "withClientInterceptors" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 117, + 119 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-008", + "feature": "CLIENT-CONFIG", + "title": "Per-call deadline", + "statement": "Every PDP and admin call must complete within the configured timeout, 1 second by default, or it fails with DEADLINE_EXCEEDED. The caller can change the timeout when building the client.", + "kind": "non-functional", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 24, + 24 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 65, + 68 + ], + "symbol": "withTimeout" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 63, + 66 + ], + "symbol": "withClient" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 42, + 45 + ], + "symbol": "withClient" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "timeout", + "values": [ + "default (1000 ms)", + "custom" + ] + } + ], + "scenarios": [ + { + "given": { + "timeout": "default (1000 ms)" + }, + "expect": "deadline 1000 ms per call", + "tests": [] + }, + { + "given": { + "timeout": "custom" + }, + "expect": "deadline as configured", + "tests": [] + } + ], + "notes": "Integration tests run with the default timeout but never check it.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-009", + "feature": "CLIENT-CONFIG", + "title": "Playground instance header", + "statement": "When the caller sets a non-blank playground instance id, every PDP call carries it in a playground-instance header. Admin clients never send this header.", + "kind": "integration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 70, + 73 + ], + "symbol": "withPlaygroundInstance" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 124, + 130 + ], + "symbol": "buildBlockingClient" + }, + { + "file": "src/main/java/dev/cerbos/sdk/PlaygroundInstanceCredentials.java", + "lines": [ + 13, + 31 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 40, + 52 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "playground_instance", + "values": [ + "null", + "blank", + "non-blank" + ] + } + ], + "scenarios": [ + { + "given": { + "playground_instance": "non-blank" + }, + "expect": "header playground-instance sent", + "tests": [] + }, + { + "given": { + "playground_instance": "blank" + }, + "expect": "no header", + "tests": [] + } + ], + "notes": "PlaygroundIT exercises this, but it is a manual main() with no assertions, so it doesn't count as a test.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-010", + "feature": "CLIENT-CONFIG", + "title": "Admin client uses Basic credentials", + "statement": "An admin client sends the given username and password as an HTTP Basic authorization header on every admin call. If either value is null, building the admin client fails with InvalidClientConfigurationException (\"username and password must not be null\"). Empty strings are accepted.", + "kind": "authorisation", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 138, + 145 + ], + "symbol": "buildBlockingAdminClient" + }, + { + "file": "src/main/java/dev/cerbos/sdk/AdminApiCredentials.java", + "lines": [ + 15, + 34 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 29, + 34 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas" + ], + "documented_in": [], + "conditions": [ + { + "name": "username", + "values": [ + "null", + "empty", + "non-empty" + ] + }, + { + "name": "password", + "values": [ + "null", + "empty", + "non-empty" + ] + } + ], + "scenarios": [ + { + "given": { + "username": "non-empty", + "password": "non-empty" + }, + "expect": "admin calls authenticated", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ] + }, + { + "given": { + "username": "null", + "password": "non-empty" + }, + "expect": "InvalidClientConfigurationException", + "tests": [] + }, + { + "given": { + "username": "non-empty", + "password": "null" + }, + "expect": "InvalidClientConfigurationException", + "tests": [] + }, + { + "given": { + "username": "empty", + "password": "empty" + }, + "expect": "client built; server decides", + "tests": [] + } + ], + "notes": "The admin tests authenticate as cerbos/cerbosAdmin. Their passing shows the header is accepted, but no test covers rejection.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-011", + "feature": "CLIENT-CONFIG", + "title": "Admin credentials from environment", + "statement": "When no credentials are passed, the admin client reads CERBOS_USERNAME and CERBOS_PASSWORD from the environment. If either variable is missing, building fails with InvalidClientConfigurationException.", + "kind": "configuration", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosClientBuilder.java", + "lines": [ + 132, + 136 + ], + "symbol": "buildBlockingAdminClient()" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "CERBOS_USERNAME", + "values": [ + "set", + "unset" + ] + }, + { + "name": "CERBOS_PASSWORD", + "values": [ + "set", + "unset" + ] + } + ], + "scenarios": [ + { + "given": { + "CERBOS_USERNAME": "set", + "CERBOS_PASSWORD": "set" + }, + "expect": "client built", + "tests": [] + }, + { + "given": { + "CERBOS_USERNAME": "unset", + "CERBOS_PASSWORD": "set" + }, + "expect": "InvalidClientConfigurationException", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-012", + "feature": "CLIENT-CONFIG", + "title": "Custom request headers", + "statement": "The caller can derive a PDP or admin client that sends extra headers, given as a map or as gRPC Metadata, on every call. The original client is left unchanged. Passing null Metadata removes the extra headers.", + "kind": "integration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 84, + 98 + ], + "symbol": "withHeaders" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 53, + 67 + ], + "symbol": "withHeaders" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 63, + 66 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 42, + 45 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "Both test fixtures configure a wibble: wobble header, but no test checks that it arrives. Header names must be valid ASCII metadata keys, otherwise gRPC throws IllegalArgumentException.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-013", + "feature": "CLIENT-CONFIG", + "title": "Audit annotations on requests", + "statement": "The caller can derive a PDP client that attaches annotations to the request context of every check, batch check and plan request, where they show up in the Cerbos audit log. Passing null removes them.", + "kind": "integration", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 108, + 115 + ], + "symbol": "withRequestAnnotations" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 130, + 145 + ], + "symbol": "check" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 207, + 209 + ], + "symbol": "plan" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 242, + 244 + ], + "symbol": "plan" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java", + "lines": [ + 28, + 41 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CheckResourcesRequestBuilderTest.java::CheckResourcesRequestBuilderTest::testBuild" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "CerbosBlockingClientTest sets foo=bar annotations but never asserts them. testBuild only builds a batch builder with no annotations and makes no assertions. check() sets the request context twice with the same value, which is redundant but harmless.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CLIENT-CONFIG-014", + "feature": "CLIENT-CONFIG", + "title": "RPC failures raised as CerbosException", + "statement": "When the Cerbos server rejects or fails any PDP or admin call, the client throws an unchecked CerbosException. It carries the numeric gRPC status code and the status description, and its message has the form \"RPC exception []\".", + "kind": "error-handling", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosException.java", + "lines": [ + 10, + 26 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 153, + 155 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java", + "lines": [ + 92, + 94 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 116, + 118 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialCheckRequest", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialPlanRequest", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents" + ], + "documented_in": [], + "conditions": [ + { + "name": "operation", + "values": [ + "check", + "batch check", + "plan", + "admin call" + ] + } + ], + "scenarios": [ + { + "given": { + "operation": "check" + }, + "expect": "CerbosException with INVALID_ARGUMENT for a principal without roles and a resource without id", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialCheckRequest" + ] + }, + { + "given": { + "operation": "plan" + }, + "expect": "CerbosException with INVALID_ARGUMENT", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::partialPlanRequest" + ] + }, + { + "given": { + "operation": "admin call" + }, + "expect": "CerbosException", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents" + ] + }, + { + "given": { + "operation": "batch check" + }, + "expect": "CerbosException", + "tests": [] + } + ], + "notes": "The exception's cause is the cause of the gRPC StatusRuntimeException, not the StatusRuntimeException itself, and that cause is usually null. Callers therefore lose the original exception and its trailers.", + "questions": [ + "Should CerbosException keep the StatusRuntimeException as its cause, so callers can read trailers and error details?" + ], + "retired": false + }, + { + "id": "REQ-CHECK-001", + "feature": "CHECK", + "title": "Check one resource for several actions", + "statement": "A caller can ask whether a principal may perform one or more actions on a single resource. The SDK sends one CheckResources request with a fresh request id and returns a decision for each requested action.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 127, + 156 + ], + "symbol": "CerbosBlockingClient.check" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithJWT" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-002", + "feature": "CHECK", + "title": "Allowed only on an explicit ALLOW", + "statement": "isAllowed(action) returns true only when the server's effect for that action is ALLOW. It returns false for DENY, for an action that is not in the response, and when the result has no entry at all.", + "kind": "authorisation", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 45, + 52 + ], + "symbol": "CheckResult.isAllowed" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "effect", + "values": [ + "ALLOW", + "DENY", + "action absent", + "no result entry" + ], + "evidence": { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 46, + 51 + ] + } + } + ], + "scenarios": [ + { + "given": { + "effect": "ALLOW" + }, + "expect": "true", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "effect": "DENY" + }, + "expect": "false", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "effect": "action absent" + }, + "expect": "false (fail closed)", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "effect": "no result entry" + }, + "expect": "false", + "tests": [] + } + ], + "notes": "checkResources asks for defer on XX225 even though that resource did not request the action, and asserts false. That covers the absent-action case.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-003", + "feature": "CHECK", + "title": "Single check with an unexpected result count", + "statement": "When the server's response to a single check does not contain exactly one result, the SDK returns a result with no entry. isAllowed is false for every action, getAll is empty and there are no validation errors, but getMeta() and getOutputs() throw NullPointerException.", + "kind": "error-handling", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 149, + 152 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 46, + 48 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 59, + 62 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 75, + 94 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 101, + 107 + ], + "symbol": "getMeta/getOutputs" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "result_count", + "values": [ + "0", + "1", + ">1" + ] + } + ], + "scenarios": [ + { + "given": { + "result_count": "1" + }, + "expect": "normal result", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT" + ] + }, + { + "given": { + "result_count": "0" + }, + "expect": "all actions denied; getMeta/getOutputs throw NullPointerException", + "tests": [] + }, + { + "given": { + "result_count": ">1" + }, + "expect": "all actions denied; getMeta/getOutputs throw NullPointerException", + "tests": [] + } + ], + "notes": "", + "questions": [ + "Should getMeta() and getOutputs() return empty values when there is no result entry, as the other accessors do?" + ], + "retired": false + }, + { + "id": "REQ-CHECK-004", + "feature": "CHECK", + "title": "All decisions as a map", + "statement": "getAll() returns an unmodifiable map from each action in the result to true when the action is allowed and false otherwise. It returns an empty map when there is no result entry.", + "kind": "behaviour", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 59, + 68 + ], + "symbol": "CheckResult.getAll" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-005", + "feature": "CHECK", + "title": "Pass a JWT as auxiliary data", + "statement": "A caller can attach a JWT, with an optional key set id, as auxiliary data. The SDK sends it with every check, batch check and plan request so policies can use its claims. A client without auxiliary data sends an empty AuxData.", + "kind": "integration", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/builders/AuxData.java", + "lines": [ + 17, + 37 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 74, + 76 + ], + "symbol": "with(AuxData)" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 128, + 129 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 164, + 170 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 195, + 196 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "aux_data", + "values": [ + "none", + "jwt", + "jwt with key set id" + ] + } + ], + "scenarios": [ + { + "given": { + "aux_data": "jwt" + }, + "expect": "JWT claims available to policy; defer allowed", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "aux_data": "none" + }, + "expect": "empty AuxData sent", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT" + ] + }, + { + "given": { + "aux_data": "jwt with key set id" + }, + "expect": "JWT and key set id sent", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-006", + "feature": "CHECK", + "title": "Batch check many resources in one request", + "statement": "A caller can check several resources, each with its own actions, for one principal in a single request. Resources are added either as a resource plus actions or as ResourceAction objects. The batch uses the client's auxiliary data unless the caller passes auxiliary data for that batch, which then replaces it.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 164, + 182 + ], + "symbol": "batch" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java", + "lines": [ + 28, + 95 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/builders/ResourceAction.java", + "lines": [ + 16, + 62 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources", + "src/test/java/dev/cerbos/sdk/CheckResourcesRequestBuilderTest.java::CheckResourcesRequestBuilderTest::testBuild" + ], + "documented_in": [], + "conditions": [ + { + "name": "add_method", + "values": [ + "addResources(ResourceAction)", + "addResourceAndActions" + ] + }, + { + "name": "aux_data_source", + "values": [ + "client", + "batch argument" + ] + } + ], + "scenarios": [ + { + "given": { + "add_method": "addResources(ResourceAction)", + "aux_data_source": "client" + }, + "expect": "one request, per-resource results", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "add_method": "addResourceAndActions", + "aux_data_source": "client" + }, + "expect": "one request, per-resource results", + "tests": [] + }, + { + "given": { + "add_method": "addResources(ResourceAction)", + "aux_data_source": "batch argument" + }, + "expect": "batch aux data used instead of client aux data", + "tests": [] + } + ], + "notes": "testBuild (in the uncommitted CheckResourcesRequestBuilderTest) only constructs the builder and asserts nothing. The request id is generated when the builder is created, so calling check() twice on one builder sends the same request id both times.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-007", + "feature": "CHECK", + "title": "Find a batch result by resource id", + "statement": "find(resourceId) returns the first result whose resource id matches, or empty when there is none. An optional predicate on the resource (for example its kind) can narrow the match.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesResult.java", + "lines": [ + 25, + 46 + ], + "symbol": "CheckResourcesResult.find" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesResult.java", + "lines": [ + 21, + 23 + ], + "symbol": "results" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "id_match", + "values": [ + "match", + "no match" + ] + }, + { + "name": "predicate", + "values": [ + "none", + "passes", + "fails" + ] + } + ], + "scenarios": [ + { + "given": { + "id_match": "match", + "predicate": "none" + }, + "expect": "result returned", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "id_match": "no match", + "predicate": "none" + }, + "expect": "empty", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "id_match": "match", + "predicate": "passes" + }, + "expect": "result returned", + "tests": [] + }, + { + "given": { + "id_match": "match", + "predicate": "fails" + }, + "expect": "empty", + "tests": [] + } + ], + "notes": "With two resources of different kinds that share an id, find(id) with no predicate returns whichever comes first.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-008", + "feature": "CHECK", + "title": "Decision metadata on request", + "statement": "When the caller asks for metadata, each result exposes the effective derived roles and, for each action, the policy that matched. getInfoForAction returns empty for an action with no metadata.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesRequestBuilder.java", + "lines": [ + 76, + 79 + ], + "symbol": "withIncludeMeta" + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 113, + 149 + ], + "symbol": "CheckResult.Meta" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "include_meta", + "values": [ + "on", + "off" + ] + }, + { + "name": "action_in_meta", + "values": [ + "yes", + "no" + ] + } + ], + "scenarios": [ + { + "given": { + "include_meta": "on", + "action_in_meta": "yes" + }, + "expect": "derived roles and matched policy returned", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "include_meta": "on", + "action_in_meta": "no" + }, + "expect": "getInfoForAction empty", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ] + }, + { + "given": { + "include_meta": "off" + }, + "expect": "metadata empty", + "tests": [] + } + ], + "notes": "The single-resource check() cannot request metadata. Only the batch builder has withIncludeMeta().", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-009", + "feature": "CHECK", + "title": "Policy outputs per result", + "statement": "Each result exposes the outputs produced by policy rules as a map keyed by the rule's source identifier, along with a count.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 105, + 107 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 151, + 179 + ], + "symbol": "CheckResult.Outputs" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "asMap() collects into an unmodifiable map, so two outputs with the same source would throw IllegalStateException.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-010", + "feature": "CHECK", + "title": "Schema validation errors on check results", + "statement": "A single result reports whether the server found schema validation errors and lists them. A batch result reports whether any of its results has validation errors.", + "kind": "validation", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 75, + 94 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesResult.java", + "lines": [ + 48, + 50 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "validation_errors", + "values": [ + "none", + "some" + ] + } + ], + "scenarios": [ + { + "given": { + "validation_errors": "none" + }, + "expect": "hasValidationErrors false", + "tests": [] + }, + { + "given": { + "validation_errors": "some" + }, + "expect": "hasValidationErrors true, errors listed", + "tests": [] + } + ], + "notes": "Only the plan path has a validation-error test (planResourcesValidation).", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-011", + "feature": "CHECK", + "title": "New resources default to id _NEW_", + "statement": "A resource created with a kind but no id is sent with the id \"_NEW_\".", + "kind": "data", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/builders/Resource.java", + "lines": [ + 19, + 21 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/builders/ResourceAction.java", + "lines": [ + 24, + 26 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-012", + "feature": "CHECK", + "title": "Principal and resource attributes", + "statement": "Principals and resources carry an id, roles (principals only, and additive across calls), policy version, scope and attributes. Attribute values can be strings, numbers (sent as doubles), booleans, lists or maps, nested to any depth.", + "kind": "data", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/builders/AttributeValue.java", + "lines": [ + 16, + 56 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/builders/Principal.java", + "lines": [ + 13, + 51 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/builders/Resource.java", + "lines": [ + 12, + 49 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "attribute_type", + "values": [ + "string", + "double", + "bool", + "list", + "map" + ] + } + ], + "scenarios": [ + { + "given": { + "attribute_type": "string" + }, + "expect": "sent as string value", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT" + ] + }, + { + "given": { + "attribute_type": "double" + }, + "expect": "sent as number", + "tests": [] + }, + { + "given": { + "attribute_type": "bool" + }, + "expect": "sent as bool", + "tests": [] + }, + { + "given": { + "attribute_type": "list" + }, + "expect": "sent as list", + "tests": [] + }, + { + "given": { + "attribute_type": "map" + }, + "expect": "sent as struct", + "tests": [] + } + ], + "notes": "Tests only use string attributes and a single role. Scope is never set in the check tests.", + "questions": [], + "retired": false + }, + { + "id": "REQ-CHECK-013", + "feature": "CHECK", + "title": "Request ids and call ids", + "statement": "Each check, batch and plan request gets a random UUID request id. Results expose the request id and the Cerbos call id that the server returned.", + "kind": "data", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/RequestId.java", + "lines": [ + 10, + 15 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 133, + 133 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResult.java", + "lines": [ + 31, + 37 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CheckResourcesResult.java", + "lines": [ + 56, + 62 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 67, + 73 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-PLAN-001", + "feature": "PLAN", + "title": "Plan a query filter for one action", + "statement": "A caller can ask which resources of a kind a principal may perform one action on. The SDK sends a PlanResources request with the resource kind, policy version, scope and attributes (any resource id is dropped) and returns the plan for that action.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 194, + 217 + ], + "symbol": "plan(String)" + }, + { + "file": "src/main/java/dev/cerbos/sdk/builders/Resource.java", + "lines": [ + 51, + 59 + ], + "symbol": "toPlanResource" + }, + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 22, + 25 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "Uses the deprecated single action field of the request.", + "questions": [], + "retired": false + }, + { + "id": "REQ-PLAN-002", + "feature": "PLAN", + "title": "Plan a query filter for several actions", + "statement": "A caller can plan for several actions at once. The SDK sends them as the request's action list, and the result lists the same actions.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingClient.java", + "lines": [ + 229, + 252 + ], + "symbol": "plan(Iterable)" + }, + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 27, + 29 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-PLAN-003", + "feature": "PLAN", + "title": "Plan outcome kinds", + "statement": "A plan result is exactly one of always allowed, always denied or conditional, as reported by the server's filter kind.", + "kind": "authorisation", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 39, + 49 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation" + ], + "documented_in": [], + "conditions": [ + { + "name": "filter_kind", + "values": [ + "ALWAYS_ALLOWED", + "ALWAYS_DENIED", + "CONDITIONAL" + ] + } + ], + "scenarios": [ + { + "given": { + "filter_kind": "CONDITIONAL" + }, + "expect": "isConditional true, others false", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions" + ] + }, + { + "given": { + "filter_kind": "ALWAYS_DENIED" + }, + "expect": "isAlwaysDenied true, others false", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation" + ] + }, + { + "given": { + "filter_kind": "ALWAYS_ALLOWED" + }, + "expect": "isAlwaysAllowed true, others false", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-PLAN-004", + "feature": "PLAN", + "title": "Plan condition is always present", + "statement": "getCondition() always returns a present value. For a conditional plan it is the filter expression tree. For always-allowed and always-denied plans it is an empty operand, not an empty Optional.", + "kind": "behaviour", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 51, + 53 + ], + "symbol": "getCondition" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions" + ], + "documented_in": [], + "conditions": [ + { + "name": "filter_kind", + "values": [ + "CONDITIONAL", + "ALWAYS_ALLOWED", + "ALWAYS_DENIED" + ] + } + ], + "scenarios": [ + { + "given": { + "filter_kind": "CONDITIONAL" + }, + "expect": "present, expression tree", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions" + ] + }, + { + "given": { + "filter_kind": "ALWAYS_DENIED" + }, + "expect": "present, empty operand", + "tests": [] + }, + { + "given": { + "filter_kind": "ALWAYS_ALLOWED" + }, + "expect": "present, empty operand", + "tests": [] + } + ], + "notes": "", + "questions": [ + "Should getCondition() return Optional.empty() when the plan is not conditional? Using Optional suggests that was the intent." + ], + "retired": false + }, + { + "id": "REQ-PLAN-005", + "feature": "PLAN", + "title": "Schema validation errors on plans", + "statement": "A plan result reports whether the server found schema validation errors and lists them. In the tested case, a request that fails validation is planned as always denied.", + "kind": "validation", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 55, + 61 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources" + ], + "documented_in": [], + "conditions": [ + { + "name": "validation_errors", + "values": [ + "none", + "some" + ] + } + ], + "scenarios": [ + { + "given": { + "validation_errors": "none" + }, + "expect": "hasValidationErrors false", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources" + ] + }, + { + "given": { + "validation_errors": "some" + }, + "expect": "hasValidationErrors true, 2 errors, always denied", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation" + ] + } + ], + "notes": "Whether invalid input produces always-denied depends on the server's schema enforcement setting (reject in the test config), not on the SDK.", + "questions": [], + "retired": false + }, + { + "id": "REQ-PLAN-006", + "feature": "PLAN", + "title": "Plan result identifies the plan", + "statement": "A plan result reports the resource kind, policy version, request id and Cerbos call id that the server returned.", + "kind": "data", + "risk": "low", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 31, + 37 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/PlanResourcesResult.java", + "lines": [ + 67, + 73 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResources", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesMultipleActions", + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::planResourcesValidation" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-001", + "feature": "ADMIN", + "title": "Add or update policies from JSON or objects", + "statement": "An admin can queue policies as JSON text, a JSON reader or policy objects, then send them all to the policy store with one addOrUpdate call. Nothing is sent until addOrUpdate is called.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 75, + 77 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java", + "lines": [ + 40, + 79 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithFilter", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "The admin test loads 17 policies (YAML converted to JSON) through this builder in its @BeforeAll setup. listPoliciesWithoutFilter then asserts that all 17 are present.", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-002", + "feature": "ADMIN", + "title": "Validate policies and schemas before queuing", + "statement": "Each policy or schema is validated against the Cerbos proto rules as it is queued. An invalid one is rejected with a checked ValidationException that lists the violations, and it is not queued.", + "kind": "validation", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java", + "lines": [ + 59, + 63 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java", + "lines": [ + 72, + 79 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java", + "lines": [ + 42, + 48 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java", + "lines": [ + 71, + 78 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/validation/Validator.java", + "lines": [ + 15, + 25 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/validation/ValidationException.java", + "lines": [ + 13, + 28 + ] + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "input", + "values": [ + "valid", + "violates rules", + "validator error" + ] + } + ], + "scenarios": [ + { + "given": { + "input": "valid" + }, + "expect": "queued", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ] + }, + { + "given": { + "input": "violates rules" + }, + "expect": "ValidationException with violations; not queued", + "tests": [] + }, + { + "given": { + "input": "validator error" + }, + "expect": "ValidationException with cause and no violations", + "tests": [] + } + ], + "notes": "When a list of policies or schemas is passed, the ones before the first invalid item are already queued when the exception is thrown.", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-003", + "feature": "ADMIN", + "title": "Send policies and schemas in batches", + "statement": "addOrUpdate sends queued policies (and, separately, schemas) in several requests. The first request holds 9 items and each later one holds up to 10. With nothing queued, no request is sent. If a request fails, the earlier batches stay applied and the rest are not sent.", + "kind": "non-functional", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdatePolicyRequestBuilder.java", + "lines": [ + 86, + 112 + ], + "symbol": "addOrUpdate" + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java", + "lines": [ + 85, + 111 + ], + "symbol": "addOrUpdate" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ], + "documented_in": [], + "conditions": [ + { + "name": "queued_count", + "values": [ + "0", + "1-9", + "10", + ">10" + ] + } + ], + "scenarios": [ + { + "given": { + "queued_count": "0" + }, + "expect": "no request", + "tests": [] + }, + { + "given": { + "queued_count": "1-9" + }, + "expect": "one request", + "tests": [] + }, + { + "given": { + "queued_count": "10" + }, + "expect": "two requests (9 + 1)", + "tests": [] + }, + { + "given": { + "queued_count": ">10" + }, + "expect": "17 policies sent as 9 + 8", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ] + } + ], + "notes": "The loop flushes when i % 10 == 0, before adding item i, which gives batches of 9, 10, 10, ... The requests are not atomic across batches.", + "questions": [ + "Was the intended batch size 10 for every batch? The first batch holds 9 because of an off-by-one in the flush check." + ], + "retired": false + }, + { + "id": "REQ-ADMIN-004", + "feature": "ADMIN", + "title": "Add or update JSON schemas", + "statement": "An admin can queue schemas as an id plus JSON definition (text or reader) or as schema objects, then send them with addOrUpdate.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 225, + 227 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/AddOrUpdateSchemaRequestBuilder.java", + "lines": [ + 42, + 78 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getSchema" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-005", + "feature": "ADMIN", + "title": "List policy ids with filters", + "statement": "An admin can list policy ids, either active only or including disabled ones, optionally filtered by regular expressions on name, version and scope.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 89, + 119 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithFilter" + ], + "documented_in": [], + "conditions": [ + { + "name": "include_disabled", + "values": [ + "true", + "false" + ] + }, + { + "name": "name_regex", + "values": [ + "set", + "unset" + ] + }, + { + "name": "version_regex", + "values": [ + "set", + "unset" + ] + }, + { + "name": "scope_regex", + "values": [ + "set", + "unset" + ] + } + ], + "scenarios": [ + { + "given": { + "include_disabled": "true", + "name_regex": "unset", + "version_regex": "unset", + "scope_regex": "unset" + }, + "expect": "all 17 ids", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ] + }, + { + "given": { + "include_disabled": "true", + "name_regex": "set", + "version_regex": "unset", + "scope_regex": "set" + }, + "expect": "3 matching ids in order", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithFilter" + ] + }, + { + "given": { + "include_disabled": "false", + "name_regex": "unset", + "version_regex": "unset", + "scope_regex": "unset" + }, + "expect": "disabled policies excluded", + "tests": [] + }, + { + "given": { + "include_disabled": "true", + "name_regex": "unset", + "version_regex": "set", + "scope_regex": "unset" + }, + "expect": "filtered by version", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-006", + "feature": "ADMIN", + "title": "Get policies by id", + "statement": "An admin can fetch policies by id. Only policies that exist are returned, so an unknown id produces an empty list rather than an error.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 128, + 138 + ], + "symbol": "getPolicy" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicyNonExistent" + ], + "documented_in": [], + "conditions": [ + { + "name": "policy_exists", + "values": [ + "yes", + "no" + ] + } + ], + "scenarios": [ + { + "given": { + "policy_exists": "yes" + }, + "expect": "one policy returned", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicy" + ] + }, + { + "given": { + "policy_exists": "no" + }, + "expect": "empty list", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getPolicyNonExistent" + ] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-007", + "feature": "ADMIN", + "title": "Enable and disable policies", + "statement": "An admin can disable or re-enable policies by id. Each call returns the number of policies it changed.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 147, + 176 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::enableAndDisablePolicy" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-008", + "feature": "ADMIN", + "title": "Delete policies", + "statement": "An admin can delete policies by id and gets back the number deleted. Deleting a policy that other policies depend on fails with CerbosException.", + "kind": "behaviour", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 185, + 195 + ], + "symbol": "deletePolicy" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithoutDependents", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents" + ], + "documented_in": [], + "conditions": [ + { + "name": "has_dependents", + "values": [ + "yes", + "no" + ] + } + ], + "scenarios": [ + { + "given": { + "has_dependents": "no" + }, + "expect": "returns 1", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithoutDependents" + ] + }, + { + "given": { + "has_dependents": "yes" + }, + "expect": "CerbosException", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deletePolicyWithDependents" + ] + } + ], + "notes": "The SDK does not enforce the dependents rule. The server does.", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-009", + "feature": "ADMIN", + "title": "Purge old store revisions", + "statement": "An admin can purge policy store revisions and gets back the number of rows removed. A positive keepLast keeps that many recent revisions. Zero or a negative value sends no keepLast, so the server purges every revision.", + "kind": "data", + "risk": "high", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 206, + 218 + ], + "symbol": "purgeStoreRevisions" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::purgeStoreRevisions" + ], + "documented_in": [], + "conditions": [ + { + "name": "keep_last", + "values": [ + "negative", + "0", + "positive" + ], + "evidence": { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 208, + 210 + ] + } + } + ], + "scenarios": [ + { + "given": { + "keep_last": "0" + }, + "expect": "all revisions purged, count > 1", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::purgeStoreRevisions" + ] + }, + { + "given": { + "keep_last": "negative" + }, + "expect": "treated as 0", + "tests": [] + }, + { + "given": { + "keep_last": "positive" + }, + "expect": "that many revisions kept", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-010", + "feature": "ADMIN", + "title": "List and get schemas", + "statement": "An admin can list all schema ids and fetch schemas by id.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 235, + 261 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listSchemas", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::getSchema" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-011", + "feature": "ADMIN", + "title": "Delete schemas", + "statement": "An admin can delete schemas by id and gets back the number deleted.", + "kind": "behaviour", + "risk": "medium", + "confidence": "high", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 270, + 280 + ], + "symbol": "deleteSchema" + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::deleteSchema" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-ADMIN-012", + "feature": "ADMIN", + "title": "Reload the policy store", + "statement": "An admin can ask the server to reload its policy store, and can choose to wait until the reload finishes.", + "kind": "behaviour", + "risk": "medium", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosBlockingAdminClient.java", + "lines": [ + 288, + 294 + ], + "symbol": "storeReload" + } + ], + "tested_by": [], + "documented_in": [], + "conditions": [ + { + "name": "wait", + "values": [ + "true", + "false" + ] + } + ], + "scenarios": [ + { + "given": { + "wait": "true" + }, + "expect": "returns after reload completes", + "tests": [] + }, + { + "given": { + "wait": "false" + }, + "expect": "returns immediately", + "tests": [] + } + ], + "notes": "", + "questions": [], + "retired": false + }, + { + "id": "REQ-TEST-SUPPORT-001", + "feature": "TEST-SUPPORT", + "title": "Cerbos test container image", + "statement": "CerbosContainer starts the ghcr.io/cerbos/cerbos image, tagged latest by default or with a caller-supplied tag. A custom image must declare compatibility with ghcr.io/cerbos/cerbos, otherwise construction fails.", + "kind": "configuration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosContainer.java", + "lines": [ + 13, + 32 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ], + "documented_in": [], + "conditions": [ + { + "name": "image", + "values": [ + "default", + "custom tag", + "compatible custom image", + "incompatible image" + ] + } + ], + "scenarios": [ + { + "given": { + "image": "custom tag" + }, + "expect": "ghcr.io/cerbos/cerbos: started", + "tests": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT" + ] + }, + { + "given": { + "image": "default" + }, + "expect": "ghcr.io/cerbos/cerbos:latest", + "tests": [] + }, + { + "given": { + "image": "incompatible image" + }, + "expect": "IllegalStateException from assertCompatibleWith", + "tests": [] + } + ], + "notes": "Both integration fixtures use the \"dev\" tag. Their tests exercise the container without asserting anything about it.", + "questions": [], + "retired": false + }, + { + "id": "REQ-TEST-SUPPORT-002", + "feature": "TEST-SUPPORT", + "title": "Container readiness and gRPC target", + "statement": "The container exposes HTTP port 3592 and gRPC port 3593, counts as ready once the log shows \"Starting gRPC server\", and gives a target of 127.0.0.1: for building clients.", + "kind": "integration", + "risk": "low", + "confidence": "medium", + "implemented_by": [ + { + "file": "src/main/java/dev/cerbos/sdk/CerbosContainer.java", + "lines": [ + 16, + 17 + ] + }, + { + "file": "src/main/java/dev/cerbos/sdk/CerbosContainer.java", + "lines": [ + 30, + 44 + ] + } + ], + "tested_by": [ + "src/test/java/dev/cerbos/sdk/CerbosClientTests.java::CerbosClientTests::checkWithoutJWT", + "src/test/java/dev/cerbos/sdk/CerbosBlockingAdminClientTest.java::CerbosBlockingAdminClientTest::listPoliciesWithoutFilter" + ], + "documented_in": [], + "conditions": [], + "scenarios": [], + "notes": "Evidence comes from fixture use only.", + "questions": [], + "retired": false + } + ] +} diff --git a/build.gradle.kts b/build.gradle.kts index f72b501..70ee2e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,9 +10,10 @@ plugins { idea `maven-publish` id("com.google.protobuf") version "0.10.0" - id("com.palantir.git-version") version "5.1.0" - id("org.jreleaser") version "1.26.0" - id("com.gradleup.shadow") version "9.6.1" + id("com.palantir.git-version") version "5.0.0" + id("org.jreleaser") version "1.24.0" + id("com.gradleup.shadow") version "9.4.1" + jacoco } val projectVersion: String by lazy { @@ -91,6 +92,19 @@ tasks.withType { tasks.getByName("test") { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + xml.required = true + html.required = true + } + // Measure the SDK only, not the classes protoc generates from the vendored protos. + classDirectories.setFrom( + files(classDirectories.files.map { fileTree(it) { include("dev/cerbos/sdk/**") } }) + ) } tasks.shadowJar {