This guide explains how to use the Permission Resource check APIs and how to evaluate the server-produced access decisions on the client using the provided JavaScript library.
It covers:
-
The
/checkAPI (server-evaluated permission) -
The
/check-with-indexAPI (client-evaluable snapshot) -
Request payloads and detailed response structures
-
Scope and data-domain fallback behavior
-
Using the JavaScript client in a browser, React, or Vue
This document reflects the current and evolving server behavior. The JavaScript client library is available at runtime from Quarkus as /security/acl-client.js and is forward-compatible with the scoped access-matrix format described here.
|
1. APIs
1.1. POST /permission/check
Performs a server-side permission check for a single request using the caller identity and an optional data domain (organization/account/tenant/segment/owner).
{
"identity": "user-123",
"realm": "b2bi",
"area": "security",
"functionalDomain": "userProfile",
"action": "view",
"resourceId": "12345",
"orgRefName": "acme",
"accountNumber": "A1",
"tenantId": "t-001",
"dataSegment": 0,
"ownerId": "user-123",
"roles": ["user", "admin"],
"scope": "api"
}
{
"finalEffect": "ALLOW",
"winningRule": "SysAnyActionSecurity",
"explanations": [
{ "rule": "SysAnyActionSecurity", "effect": "ALLOW" }
]
}
Notes:
- This endpoint is authoritative and understands scripts/postconditions and any dynamic runtime evaluation.
- Use /check when you must account for scripts, context-enriched rules, or when you don’t have the precomputed snapshot.
1.2. POST /permission/check-with-index
Returns a precomputed permission snapshot for the supplied identity. This snapshot can be cached on the client and used for fast allow/deny decisions without server roundtrips.
Depending on server version/config, the response can include: - A legacy flat list of rules (for backward compatibility) - A scoped access matrix (recommended) keyed by data-domain scope → area → domain → action
{
"identity": "user-123",
"realm": "b2bi",
"orgRefName": "acme",
"accountNumber": "A1",
"tenantId": "t-001",
"dataSegment": 0,
"ownerId": "user-123"
}
| If your server version supports a nested dataDomain object, you may also send: |
{
"identity": "user-123",
"realm": "b2bi",
"dataDomain": {
"orgRefName": "acme",
"accountNumber": "A1",
"tenantId": "t-001",
"dataSegment": 0,
"ownerId": "user-123"
}
}
On servers that do not support nested dataDomain, this shape will produce:
400 Bad Request: Unrecognized field "dataDomain".
{
"enabled": true,
"version": 42,
"policyVersion": 123,
"sources": ["user:user-123", "role:user", "role:admin"],
"requiresServer": false,
"scopes": {
"org=acme|acct=A1|tenant=t-001|seg=0|owner=user-123": {
"requiresServer": false,
"matrix": {
"security": {
"userProfile": {
"view": { "effect": "ALLOW", "rule": "ViewOwnProfile", "priority": 5, "finalRule": true, "source": "role:user" }
},
"credential": {
"update": { "effect": "DENY", "rule": "NoUpdate", "priority": 10, "finalRule": true, "source": "role:user" }
}
},
"orders": {
"manage": { "*": { "effect": "DENY", "rule": "NoManage", "priority": 50, "finalRule": true } }
}
}
},
"org=acme|acct=A1|tenant=t-001|seg=*|owner=*": { "requiresServer": false, "matrix": { "*": { "*": { "*": { "effect": "DENY", "rule": "DefaultDeny", "priority": 999, "finalRule": false } } } } },
"org=*|acct=*|tenant=*|seg=*|owner=*": { "requiresServer": false, "matrix": { "security": { "*": { "*": { "effect": "ALLOW", "rule": "SysRoleAnyActionSecurity", "priority": 1, "finalRule": true } } } } }
},
"requestedScope": "org=acme|acct=A1|tenant=t-001|seg=0|owner=user-123",
"requestedFallback": [
"org=acme|acct=A1|tenant=t-001|seg=0|owner=*",
"org=acme|acct=A1|tenant=t-001|seg=*|owner=*",
"org=acme|acct=A1|tenant=*|seg=*|owner=*",
"org=acme|acct=*|tenant=*|seg=*|owner=*",
"org=*|acct=*|tenant=*|seg=*|owner=*"
],
"rules": [
{ "name": "SysRoleAnyActionSecurity", "uri": "system:security:*:*:*:*:*:*:*", "effect": "ALLOW", "priority": 1, "finalRule": true }
]
}
Interpretation:
- The client should prefer the scope that best matches its current data domain and then look up area → domain → action in that scope’s matrix, falling back through requestedFallback.
- Within a matrix, exact values beat wildcards; the matrix already encodes the winning outcome per triple.
- If requiresServer is true (globally or for a specific scope), the client should call /check for decisions in those affected areas.
1.3. Response example (legacy rules present and requiresServer=true)
{
"enabled": false,
"version": 0,
"policyVersion": 163044986023000,
"rules": [
{
"name": "users can't delete anything in security area",
"uri": "user:security:*:delete|*:*:*:*:*:*:*",
"effect": "DENY",
"priority": 10,
"finalRule": true
},
{
"name": "view your own resources",
"uri": "user:*:*:*|system-com:*:*:*:*:*:*",
"effect": "ALLOW",
"priority": 10,
"finalRule": true
},
{
"name": "view your own resources, limit to default dataSegment",
"uri": "user:*:*:*|*:*:*:*:*:*:*",
"effect": "ALLOW",
"priority": 10,
"finalRule": false
},
{
"name": "ViewSystemResources",
"uri": "user:*:*:view|system-com:*:*:*:*:system@system.com:*",
"effect": "ALLOW",
"priority": 10,
"finalRule": true
}
],
"sources": [
"user"
],
"requiresServer": true,
"scopes": {
"org=*|acct=*|tenant=*|seg=*|owner=system@system.com": {
"matrix": {
"*": {
"*": {
"view": {
"effect": "ALLOW",
"rule": "ViewSystemResources",
"priority": 10,
"finalRule": true,
"source": "user"
}
}
}
},
"requiresServer": false
},
"org=*|acct=*|tenant=*|seg=*|owner=*": {
"matrix": {
"security": {
"*": {
"delete": {
"effect": "DENY",
"rule": "users can't delete anything in security area",
"priority": 10,
"finalRule": true,
"source": "user"
}
}
},
"*": {
"*": {
"*": {
"effect": "ALLOW",
"rule": "view your own resources, limit to default dataSegment",
"priority": 10,
"finalRule": false,
"source": "user"
}
}
}
},
"requiresServer": false
}
},
"requestedScope": "org=acme|acct=A1|tenant=t-001|seg=0|owner=user-123",
"requestedFallback": [
"org=acme|acct=A1|tenant=t-001|seg=0|owner=*",
"org=acme|acct=A1|tenant=t-001|seg=*|owner=*",
"org=acme|acct=A1|tenant=*|seg=*|owner=*",
"org=acme|acct=*|tenant=*|seg=*|owner=*",
"org=*|acct=*|tenant=*|seg=*|owner=*"
]
}
Field semantics
-
enabled: false indicates the compiled index is disabled or unavailable. Clients should treat the snapshot as non-authoritative and prefer calling /permission/check for critical decisions; the matrix may still be present for some scopes but is not guaranteed complete.
-
version: 0 accompanies enabled=false. When enabled is true, version corresponds to the compiled index version and can be used for caching together with policyVersion.
-
policyVersion: the ruleset/policy timestamp or version for cache invalidation.
-
rules: legacy flat list preserved for backward compatibility. Clients should prefer the scoped matrix when available.
-
sources: identities included when the snapshot was materialized (e.g., user id and/or roles).
-
requiresServer (top-level): true means at least one rule could not be safely materialized (e.g., uses scripts/postconditions or dynamic filters). Clients should be prepared to call /permission/check for affected scopes/decisions.
-
scopes[<key>].requiresServer: per-scope flag. If true, client should not rely on that scope’s matrix for final decisions and should call /permission/check when evaluating in that scope.
-
requestedScope / requestedFallback: convenience keys provided when the request included data-domain values. Clients should attempt lookup starting at requestedScope, then walk requestedFallback in order.
2. Data-Domain Scope and Fallback
A scope key is a canonical string combining the data-domain dimensions:
org=<v>|acct=<v>|tenant=<v>|seg=<v>|owner=<v>
-
Values are specific strings or
*for wildcard. -
Fallback traversal order: owner → segment → tenant → account → org → global.
3. JavaScript Client Library
The JavaScript client provides helpers to evaluate the snapshot on the client.
-
Served by Quarkus at:
/security/acl-client.js -
Path in repo:
quantum-framework/src/main/resources/META-INF/resources/security/acl-client.js
ACLClient.scopeKeyFromDataDomain(dataDomain) // => scope key string
ACLClient.buildFallbackChain(scopeKey) // => [less-specific scope keys]
ACLClient.lookupAreaDomainAction(matrix, area, domain, action) // => Outcome | null
ACLClient.decide(snapshot, dataDomain, area, domain, action) // => 'ALLOW' | 'DENY'
ACLClient.decideOutcome(snapshot, dataDomain, area, domain, action) // => Outcome | null
ACLClient.interpretCheckResponse(check) // => { decision, scope, constraints, filterConstraintsPresent, filterConstraints }
ACLClient.interpretEvaluateResponse(res) // => { allow, deny, decisions, evalModeUsed, getDecision(area, domain, action) }
Outcome structure:
{
"effect": "ALLOW",
"rule": "<winning rule name>",
"priority": 0,
"finalRule": true,
"source": "role:user"
}
Allowed effect values are "ALLOW" or "DENY".
4. Single-resource check API: evalMode and SCOPED decisions
The /system/permissions/check endpoint accepts an optional evalMode parameter to control
how the server evaluates filter strings and postcondition scripts in single-resource checks. It also
supports sending a shallow snapshot of a concrete resource instance so that filters can be evaluated
in-memory when appropriate.
Eval modes:
-
LEGACY (default):
-
If a concrete resource instance is provided (with
modelClassandresource), the server attempts in-memory evaluation of rule filter strings; if filters match and postcondition is true/absent, the decision isEXACT. -
If no resource is provided and a matching rule carries filters and/or scripts, the server returns a
SCOPEDdecision: the outcome (ALLOW/DENY) is accompanied byscopedConstraintsenumerating the applied filters and/or scripts. -
List endpoints should continue to use
getFilters(…)/check-with-indexfor DB-side filtering;SCOPEDsimply surfaces constraints to the client for transparency.
-
-
AUTO: Same behavior as LEGACY; provided for client clarity.
-
STRICT:
-
With a resource present, the evaluator runs. If filter evaluation returns false, the rule is treated as NOT_APPLICABLE.
-
Without a resource for non-LIST actions, matching rules with filters/scripts produce a
SCOPEDcandidate (constraints listed) instead of being silently allowed/denied. Postconditions are not executed in-memory in this case. -
LIST is never suppressed; constraints are surfaced.
-
Request fields (additive):
-
identity(string, required) -
realm(string, optional) -
area,functionalDomain,action,resourceId(optional; may be wildcard*unless doing a specific check) -
modelClass(string, optional): fully qualified class name or resolvable entity name -
resource(object, optional): shallow JSON snapshot of the domain resource -
evalMode(string, optional):LEGACY|AUTO|STRICT(defaults toLEGACY)
Response fields (additive and backward compatible):
-
decision— canonical decision string, mirrorsfinalEffect("ALLOW"|"DENY"). -
decisionScope— one of:-
EXACT— fully evaluated in-memory (filters and scripts, when present). -
SCOPED— conditionally applies;scopedConstraintslists the filters/scripts that must hold. -
DEFAULT— no rule applied; the decision fell back to the default (naLabelwill beNA-ALLOWorNA-DENY).
-
-
evalModeUsed— echoes the server-applied eval mode. -
scopedConstraintsPresentandscopedConstraints[]— present whendecisionScope=SCOPED. -
filterConstraintsPresentandfilterConstraints[]— legacy-compatible constraint listing; kept for LIST and transition. -
notApplicable[]— rules that were considered but did not apply (with phase and reason).-
Winning rule metadata (additive):
-
-
winningRuleName,winningRulePriority,winningRuleFinal— the rule that produced the decision (forEXACT), or the selected candidate (forSCOPED). These are null forDEFAULT.
Example request enabling evaluator:
POST /system/permissions/check
{
"identity": "alice@end2endlogic.com",
"realm": "b2bi",
"area": "sales",
"functionalDomain": "order",
"action": "update",
"modelClass": "com.example.domain.Order",
"resource": { "id": "ORD-123", "customerId": "5f1e1a5e5e5e5e5e5e5e5e51" },
"evalMode": "STRICT"
}
Client usage to interpret the decision:
const res = await fetch('/system/permissions/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req) });
const check = await res.json();
const out = ACLClient.interpretCheckResponse(check);
if (out.scope === 'EXACT' && out.decision === 'ALLOW') {
// fully allowed
} else if (out.scope === 'SCOPED' && out.decision === 'ALLOW') {
// allowed subject to the following constraints (filters/scripts):
console.log(out.constraints);
} else {
// denied or default; inspect check.naLabel / check.notApplicable as needed
}
5. Evaluate API: per-action decisions with SCOPED semantics
The /system/permissions/fd/evaluate endpoint classifies an identity’s permissions across discovered
areas/domains/actions and now supports the same evalMode and optional modelClass/resource parameters as /check.
When the server cannot fully evaluate constraints for a given action (e.g., no resource supplied), it returns a
SCOPED decision for that action along with the enumerated constraints.
Request (additive parity with /check):
POST /system/permissions/fd/evaluate?useIndex=true
{
"identity": "alice@end2endlogic.com",
"realm": "b2bi",
// Optional narrowing
"area": "sales",
"functionalDomain": "order",
"action": "view",
// Optional single-resource evaluation controls
"modelClass": "com.example.domain.Order",
"resource": { "id": "ORD-123", "customerId": "5f1e1a5e5e5e5e5e5e5e5e51" },
"evalMode": "STRICT"
}
Response (additive):
{
"allow": { ... },
"deny": { ... },
"decisions": {
"sales": {
"order": {
"view": {
"effect": "ALLOW",
"decisionScope": "SCOPED",
"scopedConstraintsPresent": true,
"scopedConstraints": [
{ "type": "FILTER", "detail": "customerId:^[${accessibleCustomerIds}]", "joinOp": "AND" }
],
"naLabel": null,
"rule": "user-sales-order-view-filter",
"priority": 100,
"finalRule": false,
"source": null
}
}
}
},
"evalModeUsed": "STRICT"
}
Client usage:
const res = await fetch('/system/permissions/fd/evaluate?useIndex=true', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req) });
const body = await res.json();
const ev = ACLClient.interpretEvaluateResponse(body);
const decision = ev.getDecision('sales', 'order', 'view');
if (decision && decision.decisionScope === 'EXACT' && decision.effect === 'ALLOW') {
// fully allowed
} else if (decision && decision.decisionScope === 'SCOPED' && decision.effect === 'ALLOW') {
// allowed subject to constraints
console.log(decision.scopedConstraints);
}
5.1. Which rule produced the decision?
For actions resolved by the optimized index, decisions[area][domain][action] includes rule, priority,
finalRule, and source from the index outcome. For server-evaluated fallbacks (when index is disabled or does not
cover the specific combination), the server maps the winning rule metadata from the underlying check result, so
rule/priority/finalRule are also present for parity.
6. Behavior matrix (summary)
-
With resource present (any mode): evaluator runs. Filters=true and postcondition=true/absent ⇒
EXACT. Filters=false ⇒ rule isNOT_APPLICABLE. Failures to evaluate ⇒ fallback to SCOPED in STRICT, legacy-compatible otherwise. -
Without resource:
-
LEGACY/AUTO: matching rules with filters/scripts surface asSCOPEDcandidates; constraints listed. -
STRICT(non-LIST): also returnsSCOPED; postconditions are skipped; constraints listed. -
LIST is never suppressed; DB-side filters continue via
getFilters(…); constraints surfaced for transparency.
-
7. Migration notes
-
Prefer
evalModeover the deprecatedenableFilterEval. IfenableFilterEval=trueand bothmodelClassandresourceare supplied, the server treats it asAUTOfor backward compatibility. -
Existing clients that only use
finalEffectremain compatible. To opt in to richer semantics, readdecision,decisionScope, andscopedConstraints. -
For list/search pages, continue to rely on DB-side
getFilters(…)orcheck-with-index; do not pass a resource snapshot to/checkfor lists.
8. Troubleshooting / FAQs
Q: Why do I get SCOPED instead of EXACT?
* A: The server did not have a concrete resource to evaluate, or the predicate engine was not available. Use STRICT
with modelClass + resource for a fully evaluated decision when applicable.
Q: What is NA-ALLOW / NA-DENY?
* A: They are labels for default fallbacks when no rule could make a decision (decisionScope=DEFAULT). The value
mirrors the finalEffect chosen as the default.
Q: How do I know which rule produced the result?
* A: Inspect winningRuleName, winningRulePriority, winningRuleFinal on /check responses, or per-action
rule/priority/finalRule in /fd/evaluate responses.
8.1. Using in a Browser (no build tools)
<script src="/security/acl-client.js"></script>
<script>
async function canViewProfile() {
// 1) Get a snapshot for the user
const res = await fetch('/permission/check-with-index', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: 'user-123', realm: 'b2bi', orgRefName: 'acme', accountNumber: 'A1', tenantId: 't-001', dataSegment: 0, ownerId: 'user-123' })
});
const snapshot = await res.json();
// 2) Evaluate locally
const decision = ACLClient.decide(snapshot, { orgRefName: 'acme', accountNumber: 'A1', tenantId: 't-001', dataSegment: 0, ownerId: 'user-123' }, 'security', 'userProfile', 'view');
if (decision === 'ALLOW') {
// show UI
} else {
// hide or show alternative
}
}
</script>
8.2. Using in React
-
No npm package is required; include as an external script in
public/index.htmlor via dynamic import.
import { useEffect, useState } from 'react';
export default function ProfileButton() {
const [allowed, setAllowed] = useState(false);
useEffect(() => {
async function run() {
const res = await fetch('/permission/check-with-index', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: 'user-123', realm: 'b2bi' })
});
const snapshot = await res.json();
const effect = window.ACLClient.decide(snapshot, null, 'security', 'userProfile', 'view');
setAllowed(effect === 'ALLOW');
}
run();
}, []);
if (!allowed) return null;
return <button>View Profile</button>;
}
8.3. Using in Vue
<template>
<button v-if="allowed">View Profile</button>
</template>
<script>
export default {
data() { return { allowed: false }; },
async mounted() {
const res = await fetch('/permission/check-with-index', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: 'user-123', realm: 'b2bi' })
});
const snapshot = await res.json();
const effect = window.ACLClient.decide(snapshot, null, 'security', 'userProfile', 'view');
this.allowed = (effect === 'ALLOW');
}
}
</script>
9. cURL Examples
curl -sS -X POST \
-H 'Content-Type: application/json' \
http://localhost:8080/permission/check \
-d '{
"identity":"user-123",
"realm":"b2bi",
"area":"security",
"functionalDomain":"userProfile",
"action":"view",
"orgRefName":"acme",
"accountNumber":"A1",
"tenantId":"t-001",
"dataSegment":0,
"ownerId":"user-123"
}'
curl -sS -X POST \
-H 'Content-Type: application/json' \
http://localhost:8080/permission/check-with-index \
-d '{
"identity":"user-123",
"realm":"b2bi",
"orgRefName":"acme",
"accountNumber":"A1",
"tenantId":"t-001",
"dataSegment":0,
"ownerId":"user-123"
}'
10. Caching Guidance
-
Clients should cache the
/check-with-indexresponse keyed by(identity, realm, version, policyVersion). -
Refresh the snapshot when either
versionorpolicyVersionchanges.
11. Troubleshooting
-
If
requiresServeristrue(globally or per-scope), call/checkfor affected decisions. -
If no matrix entry is found in any scope fallback, default to
DENYfor safety. -
Ensure
effectcomparisons are case-insensitive on the client (String(effect).toUpperCase()).