feat(policies): PolicyDescriptor + nested test body + XACML PERMIT#16
Merged
Conversation
Closes the third contract gap with the admin UI. The Policies page
needs name+description rows for the list and a nested
(subject, action, resource, environment) body for the dry-run tester;
this controller returned bare strings and accepted a flat request
with `name` in the path.
SPI
---
- PolicyDecision: ALLOW -> PERMIT. Adopt the XACML 3.0 vocabulary
(PERMIT / DENY / NOT_APPLICABLE) so the admin UI's Tag rendering
and downstream auditors see the standard names.
- Policy interface: new `default String description() { return null; }`
hook. Backward-compatible — existing policies don't need to change
unless they want to surface human-readable text in the admin UI.
- DefaultPolicyEvaluator: new `registeredPolicies()` returning the
Policy objects (not just names) so the admin controller can read
descriptions without changing the PolicyEvaluator contract.
Test
----
- DefaultPolicyEvaluatorTest renamed `returnsAllowFromMatchingPolicy`
-> `returnsPermitFromMatchingPolicy` and updated all PolicyDecision
references.
Controller rewrite
------------------
- `GET /admin/api/v1/policies` returns
`[{name, description}]` sorted by name — matches the admin UI's
PolicyDescriptor interface.
- `POST /admin/api/v1/policies/test` (no `name` in the path) takes
the admin UI's nested body:
{
policyName,
subject: {userId, tenantId?, attributes?},
action,
resource: {type, id?, attributes?},
environment
}
The subject `userId` / `tenantId` and the resource fields feed
PolicyContext.builder; subject attributes and action aren't
consumed by the current PolicyContext (subject attributes flow
through environment for now) — that's queued as a follow-up.
- Response: `{effect, reason, matchedRules}` matching the UI's
PolicyTestResponse. `reason` / `matchedRules` are placeholders
today because Policy still returns only a PolicyDecision enum
with no detail; promoting the SPI to return a richer decision
is queued as a follow-up so this PR stays focused on the wire
contract.
This was referenced May 27, 2026
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third PR in the admin-ui ↔ backend contract-gap series (after #14 menus, #15 audit-logs). The Policies page needs name+description rows for the list and a nested
(subject, action, resource, environment)body for the dry-run tester; this controller returned bare strings and accepted a flat request withnamein the path.Summary
SPI changes
PolicyDecision:ALLOW→PERMIT. Adopt the XACML 3.0 vocabulary (PERMIT/DENY/NOT_APPLICABLE) so the admin UI's Tag rendering and downstream auditors see the standard names.Policyinterface: newdefault String description() { return null; }hook. Backward-compatible — existing policies don't need to change unless they want to surface human-readable text in the UI.DefaultPolicyEvaluator: newregisteredPolicies()returning thePolicyobjects (not just names) so the admin controller can read descriptions without changing thePolicyEvaluatorcontract.Test
DefaultPolicyEvaluatorTest— renamedreturnsAllowFromMatchingPolicy→returnsPermitFromMatchingPolicyand updated allPolicyDecisionreferences.Controller rewrite
GET /admin/api/v1/policies→[{name, description}]sorted by name. Matches the admin UI'sPolicyDescriptorinterface.POST /admin/api/v1/policies/test(nonamein the path) takes the admin UI's nested body:{ "policyName": "...", "subject": { "userId": "...", "tenantId": "...", "attributes": { ... } }, "action": "...", "resource": { "type": "...", "id": "...", "attributes": { ... } }, "environment": { ... } }{effect, reason, matchedRules}matching the UI'sPolicyTestResponse.Honest caveats (queued follow-ups, not in this PR)
actionaren't consumed by the currentPolicyContext— it hasresourceAttributesandenvironmentAttributesbut no subject-attribute slot. Subject attributes flow through environment for now; promoting them to first-class lives in a follow-up.reasonandmatchedRulesare placeholders today becausePolicy.evaluatestill returns only aPolicyDecisionenum with no detail. Promoting the SPI to return a richer decision (PolicyEvaluation { decision, reason, matchedRules }) is queued as a follow-up so this PR stays focused on the wire contract.Test plan
./gradlew buildgreen (55 tasks, all tests pass — only the access-core test needed updating for ALLOW→PERMIT)What's next in this series
Final closures: Tenants (status enum + createdAt +
PUT /{id}/status), Diagnostics + Settings field rename.