Matrix & Permissions API¶
These endpoints power the Matrix view — the permission heatmap showing which users (rows) have access to which resources (columns). All endpoints require Authorization: Bearer <JWT>.
Endpoints¶
GET /api/permissions¶
Main matrix data. Returns all permission assignments enriched with user attributes and business role (SOLL) mappings. This is the primary data source for the Matrix view.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
userLimit |
int | Limit to top N users by assignment count. 0 = return all users. Default: 25. |
filters |
JSON string | Server-side attribute filters applied at the SQL level. See Filter Architecture. Example: {"department":"HR","__userTag":"VIP"} |
Response
{
"data": [
{
"groupId": "uuid",
"groupDisplayName": "SG-Finance-Base",
"memberId": "uuid",
"memberDisplayName": "Jane Doe",
"memberupn": "jane.doe@contoso.com",
"membershipType": "Direct",
"department": "Finance",
"jobTitle": "Analyst",
"managedByAccessPackage": true
}
],
"totalUsers": 156,
"managedByPackages": [
{
"memberId": "uuid",
"groupId": "uuid",
"accessPackageIds": ["ap-001", "ap-007"]
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
data |
array | Flat list of membership rows. One row per (user, resource, membershipType) combination. |
data[].membershipType |
string | Direct, Indirect, or Eligible |
data[].managedByAccessPackage |
boolean | Whether this resource is included in any business role (SOLL column) |
totalUsers |
int | Total distinct users before the userLimit was applied |
managedByPackages |
array | SOLL mapping — which business role IDs govern each (member, group) pair |
Reads From: vw_ResourceUserPermissionAssignments materialized view → Principals + Resources
GET /api/access-package-groups¶
Business role → resource mappings used to build SOLL columns in the Matrix. Returns the list of business roles and the resources each one contains, together with catalog and category metadata.
Response
{
"accessPackages": [
{
"accessPackageId": "ap-001",
"accessPackageDisplayName": "Finance Base Access",
"catalogId": "cat-001",
"catalogDisplayName": "Corporate Catalog",
"groupId": "uuid",
"groupDisplayName": "SG-Finance-Base",
"roleName": "Member"
}
]
}
Reads From: ResourceRelationships (relationshipType='Contains') + Resources (resourceType='BusinessRole') + GovernanceCatalogs
GET /api/user-columns¶
Column discovery for Matrix user-side filters. Queries the Principals table to find all non-null columns and returns up to 500 distinct values per column so the frontend can render filter dropdowns.
Also returns virtual columns:
| Virtual Column | Description |
|---|---|
__userTag |
Injects a tag filter subquery when used in filters. Values are tag names. |
__groupTag |
Injects a group-side tag filter subquery. Values are tag names. |
Response
{
"columns": [
{
"name": "department",
"label": "Department",
"type": "string",
"values": ["Finance", "HR", "IT", "Legal"]
},
{
"name": "__userTag",
"label": "User Tag",
"type": "tag",
"values": ["VIP", "External", "Contractor"]
}
]
}
GET /api/resource-columns¶
Column discovery for the Resources table. Same response format as /api/user-columns. Used to build resource-side filter dropdowns on the Resources page.
Reads From: Resources table — discovers populated columns dynamically via db/columnCache.js (5-minute TTL cache).
GET /api/sync-log¶
Recent sync log entries from the GraphSyncLog table. Used by the Sync Log page.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 20 | Number of entries to return. Maximum: 100. |
Response
{
"data": [
{
"SyncType": "Principals",
"Status": "Success",
"StartTime": "2026-03-27T04:00:01Z",
"EndTime": "2026-03-27T04:01:45Z",
"RecordsProcessed": 3421,
"Message": null
}
]
}
Effective Access¶
These endpoints answer "can this principal reach this resource, and how?" — resolving direct grants plus everything reached through group membership (P1) and containment inheritance via Contains (P2). They are read-only, bounded (every expansion is capped and reports truncation explicitly rather than dropping results silently), and cached per completed sync. All three require Authorization: Bearer <JWT>.
The engine ships P1 (direct grants + grants reached via group membership) and P2 (capabilities inherited from ancestor nodes through containment). P3 (deny-aware / contested resolution) and P4 (bulk export) are deferred, so today every result comes from a grant-only AdditiveAllow policy — there is no deny outcome yet.
GET /api/resource/:id/effective-access¶
Effective access at a resource node for one principal, including capabilities inherited from ancestor nodes via Contains. One row per capability the principal effectively holds at the node.
Path / Query Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The focus resource (node) id. |
principalId |
query | yes | The principal to resolve. Returns 400 if omitted. |
policy |
query | no | Resolution policy name. Defaults to AdditiveAllow (the only policy shipped). An unknown name returns 400. |
Response
{
"nodeId": "uuid",
"principalId": "uuid",
"capabilities": [
{
"capabilityId": "read",
"capabilityResourceId": "uuid",
"effective": "allow",
"badge": "Direct"
}
],
"truncated": null
}
| Field | Type | Description |
|---|---|---|
capabilities[].effective |
string | allow when the principal holds the capability at the node (none capabilities are omitted). |
capabilities[].badge |
string | null | Reachability: Direct, Indirect, or Eligible. Direct only when the grant is declared at the focus node and held without a group hop; otherwise Indirect. |
truncated |
object | null | Non-null when a bound was hit. { "holders": N } if the group-membership expansion was capped, { "ancestors": N } if the containment ascent was capped (either key may be present). null when the result is complete. |
GET /api/principal/:id/effective-access¶
The principal-centric mirror of the above — same resolution and response shape, with the principal in the path and the focus node in the query.
Path / Query Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The principal to resolve. |
node |
query | yes | The focus resource (node) id. Returns 400 if omitted. |
policy |
query | no | As above. |
Response is identical to GET /api/resource/:id/effective-access ({ nodeId, principalId, capabilities[], truncated }).
GET /api/effective-access/resolve¶
The single-pair resolution primitive: the effective access of one principal on one resource (direct grants + grants reached via group membership). Lighter than the at-node forms — it does not walk containment.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
resourceId |
yes | The resource. Returns 400 if omitted. |
principalId |
yes | The principal. Returns 400 if omitted. |
policy |
no | Resolution policy name. Defaults to AdditiveAllow. Unknown name returns 400. |
Response
{
"resourceId": "uuid",
"principalId": "uuid",
"effective": "allow",
"badge": "Direct",
"decisiveAce": { "effect": "allow", "distance": 0, "explicit": true, "viaGroupId": null },
"truncated": null
}
| Field | Type | Description |
|---|---|---|
effective |
string | allow or none. (No deny — deny-aware resolution is P3.) |
badge |
string | null | Direct / Indirect / Eligible, or null when effective is none. |
decisiveAce |
object | null | The grant that drove the result — explicit: true and viaGroupId: null mean the principal holds it directly; a non-null viaGroupId is the group hop it was reached through. |
truncated |
object | null | { "holders": N } when the group-membership expansion was capped, else null. |
Note: the cache is keyed on the sync data version, so a completed sync invalidates every cached result at once.
Filter Architecture¶
The UI uses a hybrid filtering approach to balance performance and flexibility:
flowchart TD
F[Active Filters] --> US[User attribute filters\ndepartment, jobTitle, __userTag]
F --> RS[Relationship filters\ngroupDisplayName, membershipType]
US -->|Server-side SQL WHERE| DB[PostgreSQL]
RS -->|Client-side JS filter| Browser[Browser]
DB --> Browser
Server-Side Filters¶
Applied as SQL WHERE clauses before data reaches the browser. Efficient for large environments. Supported sources:
- All columns in the
Principalstable (discovered dynamically) __userTag— translates to a subquery againstGraphTagAssignments__groupTag— translates to a group-side tag subquery
Example filter JSON:
Multiple filters are combined with AND. Values are parameterized — no string interpolation.
Client-Side Filters¶
Applied in the browser after data loads. Used for fields that are properties of the relationship row rather than the user:
| Filter Field | Source |
|---|---|
membershipType |
vw_ResourceUserPermissionAssignments.membershipType |
groupDisplayName |
vw_ResourceUserPermissionAssignments.groupDisplayName |
| IST/SOLL toggle | Derived from managedByAccessPackage flag |
The (Blank) Sentinel¶
Tag filter dropdowns include a (Blank) option (internal sentinel: BLANK_TAG). When selected, the SQL filter becomes NOT EXISTS (SELECT 1 FROM GraphTagAssignments ...) — showing entities with no tags at all.
Matrix Rendering Notes¶
The frontend builds the matrix from the flat /api/permissions response:
- Row deduplication — multiple rows for the same user (e.g.
Direct+Eligible) are merged into a single user row with multi-type badges per cell. - Ownership rows — group ownership is its own resource (
resourceType='GroupOwnership'), so an owner appears as aDirectmembership on that ownership resource — a normal row, not a syntheticOwner-type split. - SOLL columns — built from
/api/access-package-groups. Each business role becomes a column. Resources within a role determine which cells are "managed". - AP coloring — each business role column gets a color from a 15-color palette defined in
MatrixColumnHeaders.jsx. - Staircase sort — default row order groups users by their leftmost AP bucket. Custom drag order persists via versioned localStorage.