Entity Detail API¶
These endpoints power the detail pages for users, resources, business roles, systems, org units, and identities. All endpoints require Authorization: Bearer <JWT>.
Users¶
Column Discovery¶
GET /api/user-columns-page¶
Column discovery for the Users page. Returns all populated columns from the Principals table with the alphabetically first page of distinct values each (500 by default — see MATRIX_VALUE_PAGE_SIZE in matrix.md), plus the virtual __userTag column if any user tags exist and the virtual __system column (see below). Used to build the filter UI on the Users page.
Same response format as GET /api/user-columns.
The virtual __system filter column¶
/api/user-columns-page, /api/group-columns (alias /api/resource-columns-page) and /api/resource-columns all offer a __system column whose values are the Systems.displayName of every connected system. The values come from the Systems table, not from distinct row values, so a system with no principals or resources is still offered as a filter value. On the ?schema=true fast path the column is returned with an empty value list.
Passing it back as a filter — filters={"__system":"<displayName>"} on /api/users, /api/groups or /api/resources — narrows the list to that system's rows. Systems.displayName is not unique, so two systems sharing a name both match.
The raw systemId column stays excluded from column discovery (db/columnCache.js) on purpose: surfacing it there would put an opaque integer field into every discovery-driven surface (the matrix attribute picker, the permissions grid, tags, the context-plugin attribute dropdown). __system is a list-page filter only.
User List¶
GET /api/users¶
Paginated list of principals with their tags.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | Full-text search on displayName and userPrincipalName (SQL LIKE %term%) |
|
limit |
int | 100 | Page size. Maximum: 500. |
offset |
int | 0 | Pagination offset. |
filters |
JSON string | Attribute filters. Same format as Matrix filters. Also accepts the virtual __userTag and __system keys. |
Response
{
"data": [
{
"id": "uuid",
"displayName": "Jane Doe",
"userPrincipalName": "jane.doe@contoso.com",
"department": "Finance",
"jobTitle": "Analyst",
"principalType": "User",
"tags": ["VIP", "External"]
}
],
"total": 3421
}
Reads From: Principals + GraphTagAssignments + GraphTags
User Detail¶
GET /api/user/:id¶
Single user attributes and counts (number of resource memberships, number of business role assignments).
Response
{
"id": "uuid",
"displayName": "Jane Doe",
"userPrincipalName": "jane.doe@contoso.com",
"department": "Finance",
"jobTitle": "Analyst",
"principalType": "User",
"extendedAttributes": {
"accountEnabled": true,
"lastSignInDateTime": "2026-03-20T08:14:00Z"
},
"membershipCount": 12,
"accessPackageCount": 3
}
Reads From: Principals
GET /api/user/:id/memberships¶
All resource memberships for a user, with membership type badges. Used by the Memberships section of the User detail page.
Response
{
"data": [
{
"groupId": "uuid",
"groupDisplayName": "SG-Finance-Base",
"membershipType": "Direct",
"managedByAccessPackage": true,
"accessPackageIds": ["ap-001"]
}
]
}
Reads From: vw_ResourceUserPermissionAssignments + Resources
GET /api/user/:id/access-packages¶
Business role (access package) assignments for a user. Only returns active governed assignments (state='Delivered').
Response
{
"data": [
{
"resourceId": "ap-001",
"displayName": "Finance Base Access",
"catalogDisplayName": "Corporate Catalog",
"assignmentStatus": "Delivered",
"assignedDateTime": "2026-01-10T09:00:00Z",
"expirationDateTime": null,
"policyId": "pol-001"
}
]
}
Reads From: ResourceAssignments (governed=true) + Resources (resourceType='BusinessRole') + GovernanceCatalogs
GET /api/user/:id/history¶
Version history for a user principal. Each entry represents a recorded change from the _history audit table, including the operation type and a JSONB snapshot of the row data.
Response
{
"history": [
{
"changedAt": "2026-01-15T10:23:00Z",
"operation": "U",
"rowData": { "displayName": "Jane Doe", "department": "Finance" },
"prevData": { "displayName": "Jane Doe", "department": "HR" }
}
]
}
Reads From: _history audit table (filtered by tableName = 'Principals')
Resources¶
Column Discovery¶
GET /api/group-columns¶
Column discovery for the Groups/Resources page. Returns populated columns from the Resources table with the alphabetically first page of distinct values each (500 by default — see MATRIX_VALUE_PAGE_SIZE in matrix.md), plus the virtual __groupTag column if any group tags exist and the virtual __system column (see below).
Reads From: Resources (via db/columnCache.js, 5-minute TTL)
Resource List¶
GET /api/groups¶
Paginated resource list with tags. The legacy path used by the Groups page; identical behavior to /api/resources with resourceType pre-filtered to group-like types.
Query Parameters: Same as GET /api/users.
Reads From: Resources + GraphTagAssignments + GraphTags
GET /api/resources¶
Paginated resource list with optional type and system filters. Used by the Resources page.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | Search displayName (SQL LIKE) |
|
resourceType |
string | Filter by resourceType (e.g. Group, EntraDirectoryRole, AppRole, BusinessRole) |
|
systemId |
string | Filter by originating system | |
limit |
int | 100 | Page size. Maximum: 500. |
offset |
int | 0 | Pagination offset. |
filters |
JSON string | Attribute filters. Also accepts the virtual __resourceTag and __system keys. |
Reads From: Resources + GraphTagAssignments + GraphTags
Resource Detail¶
GET /api/resources/:id¶
Single resource attributes including the extendedAttributes JSON column, plus counts (members, business roles that grant access to this resource).
Response
{
"id": "uuid",
"displayName": "SG-Finance-Base",
"resourceType": "Group",
"systemId": "sys-001",
"systemDisplayName": "EntraID",
"extendedAttributes": {
"mailEnabled": false,
"securityEnabled": true,
"groupTypes": []
},
"memberCount": 47,
"accessPackageCount": 2,
"tags": ["Finance", "Baseline"]
}
Reads From: Resources
GET /api/resources/:id/members¶
Paginated list of principals assigned to this resource, with membership type badges.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
membershipType |
string | Filter by type: Direct, Indirect, Eligible |
|
limit |
int | 100 | Page size. Maximum: 500. |
offset |
int | 0 | Pagination offset. |
Reads From: vw_ResourceUserPermissionAssignments
GET /api/resources/:id/history¶
Version history for a resource. Same format as GET /api/user/:id/history.
Reads From: Resources via_historyaudit table
GET /api/group/:id/access-packages¶
Business roles that include this resource as a contained resource (i.e. granting membership or ownership of this group as part of a business role).
Response
{
"data": [
{
"resourceId": "ap-001",
"displayName": "Finance Base Access",
"roleName": "Member",
"catalogDisplayName": "Corporate Catalog"
}
]
}
Reads From: ResourceRelationships (relationshipType='Contains') + Resources (resourceType='BusinessRole') + GovernanceCatalogs
Business Role Detail¶
GET /api/access-package/:id¶
Core attributes for a business role, including assignment counts, catalog info, and review status.
Review status distinguishes two "no decisions yet" states:
| Review Status | Meaning |
|---|---|
"Not required" |
No review policy is configured on this role |
"Pending first review" |
A review policy exists but no review instance has run yet |
"Active" |
Review instance exists with decisions |
Reads From: Resources (resourceType='BusinessRole') + GovernanceCatalogs + AssignmentPolicies + CertificationDecisions
GET /api/access-package/:id/assignments¶
Active governed assignments for a business role. Only assignments with state='Delivered' are returned.
Response
{
"data": [
{
"principalId": "uuid",
"principalDisplayName": "Jane Doe",
"userPrincipalName": "jane.doe@contoso.com",
"assignedDateTime": "2026-01-10T09:00:00Z",
"expirationDateTime": null,
"policyId": "pol-001",
"assignmentStatus": "Delivered"
}
],
"total": 48
}
Reads From: ResourceAssignments (governed=true) + Principals
GET /api/access-package/:id/resource-roles¶
Resources granted by this business role — what a user gets when the role is assigned.
Response
{
"data": [
{
"resourceId": "uuid",
"displayName": "SG-Finance-Base",
"resourceType": "Group",
"roleName": "Member",
"roleOriginSystem": "EntraID"
}
]
}
Reads From: ResourceRelationships (relationshipType='Contains') + Resources
GET /api/access-package/:id/policies¶
Assignment policies for a business role. Distinguishes auto-assigned (ABAC) policies from request-based (self-service) policies.
Response
{
"data": [
{
"policyId": "pol-001",
"displayName": "All Employees",
"policyType": "AutoAssignment",
"requestorScope": "AllMemberUsers",
"policyConditions": {
"department": "Finance"
}
}
]
}
Reads From: AssignmentPolicies
GET /api/access-package/:id/reviews¶
Certification decisions for this business role.
Response
{
"data": [
{
"reviewId": "rev-001",
"principalDisplayName": "Jane Doe",
"reviewerDisplayName": "John Smith",
"decision": "Approve",
"reviewedDateTime": "2026-03-01T14:30:00Z",
"isAutoReview": false,
"certificationScopeType": "BusinessRole"
}
]
}
isAutoReview: true indicates an AAD Access Review auto-applied decision (no human reviewer).
Reads From: CertificationDecisions
GET /api/access-package/:id/requests¶
Pending (open) assignment requests for a business role.
Reads From: AssignmentRequests + Principals
GET /api/access-package/:id/history¶
Version history for the business role resource record. Same format as GET /api/user/:id/history.
Reads From: Resources via_historyaudit table
Systems & Contexts¶
Systems¶
| Endpoint | Method | Description |
|---|---|---|
/api/systems |
GET |
All connected systems with resource counts, assignment counts, and resource type breakdown |
/api/systems/:id |
GET |
Single system detail |
/api/systems/:id |
PUT |
Update system metadata. Body: { displayName?, description?, enabled? } |
/api/systems/:id/owners |
GET |
List system owners (principals assigned as responsible for this system) |
/api/systems/:id/owners |
POST |
Add system owner. Body: { principalId } |
/api/systems/:id/owners/:userId |
DELETE |
Remove a system owner |
GET /api/systems response example:
{
"data": [
{
"systemId": "sys-001",
"displayName": "EntraID",
"enabled": true,
"resourceCount": 412,
"assignmentCount": 8903,
"resourceTypes": ["Group", "EntraDirectoryRole", "AppRole"]
}
]
}
Reads From: Systems + Resources + ResourceAssignments
Contexts¶
Contexts represent organizational and structural groupings (departments, OUs, manager hierarchies, tags, clusters, etc.). Each context groups exactly one kind of entity, named by its targetType (Identity, Principal, Resource or System); the contextType column discriminates between context types and variant says whether it was synced, generated or made by hand.
| Endpoint | Method | Description |
|---|---|---|
/api/contexts |
GET |
Flat list of contexts with hierarchy pointers |
/api/contexts/tree |
GET |
Pre-built nested tree structure (optionally ?root=<id> for one subtree) |
/api/contexts/:id |
GET |
Context detail with members, member counts and sub-context list |
/api/contexts/:id/members |
GET |
Paginated list of members — identities, principals, resources or systems, per the context's targetType |
GET /api/contexts/tree response excerpt (an array with one entry per root context):
[
{
"id": "ctx-001",
"variant": "generated",
"targetType": "Principal",
"displayName": "Finance",
"contextType": "Department",
"directMemberCount": 142,
"totalMemberCount": 165,
"children": [
{
"id": "ctx-007",
"variant": "manual",
"targetType": "Principal",
"displayName": "Finance - Controllers",
"contextType": "Team",
"directMemberCount": 23,
"totalMemberCount": 23,
"children": []
}
]
}
]
Reads From: Contexts + ContextMembers + the member table of the context's targetType
Identities¶
Identities are real persons aggregated from multiple accounts. Accounts are
attached either by a crawler's IdentityFilter (authoritative, score-less) or by
Account Linking (orphan accounts attached
with a linkConfidence).
GET /api/identities¶
Summary + paginated identity list.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | Search by display name, email, job title, or employee ID | |
minAccounts |
int | Only identities with at least this many linked accounts | |
confidence |
int | Only identities whose linkConfidence is ≥ this value |
|
sort |
string | displayName |
One of accountCount, confidence, displayName, department, linkedAt |
limit |
int | 50 | Page size. Maximum: 500. |
offset |
int | 0 | Pagination offset. |
Response
{
"available": true,
"summary": {
"totalIdentities": 1280,
"multiAccountIdentities": 412,
"totalAccounts": 1860,
"avgConfidence": 78.4,
"lastLinkedAt": "2026-06-05T02:00:00Z",
"accountTypeDistribution": [{ "accountType": "Admin", "cnt": 240 }]
},
"data": [
{
"id": "uuid",
"displayName": "Jane Doe",
"accountCount": 2,
"linkConfidence": 90,
"linkedAt": "2026-06-05T02:00:00Z",
"department": "Finance"
}
],
"total": 1280
}
Reads From: Identities + IdentityMembers + Principals + Systems
GET /api/identities/:id¶
A single identity with all of its linked accounts, per-account accountType,
linkConfidence, linkSignals, analystOverride, aggregate assignment counts
across accounts, and context count.
PUT /api/identities/:id/members/:userId/override¶
Record an analyst decision on a linked account. :userId is the account's
principalId. The Account Linking engine respects these on every re-run.
Permission: data.write.identity
Request Body
action must be one of:
| Action | Effect |
|---|---|
confirmed |
Lock the link — never overwritten by a re-run. |
rejected |
Unlink, and keep Account Linking from re-adding this account to any identity. |
moved |
Mark that the analyst moved the account to a different identity. |
DELETE /api/identities/:id/members/:userId/override¶
Clear a previously-set analyst override so Account Linking can manage the link again.
Permission: data.write.identity
Account Linking¶
Deterministic, dictionary-based correlation of orphan accounts to identities —
no LLM. See Account Linking for the engine
and dictionary. Config writes and run starts require the admin.crawlers
permission; reads are open to any signed-in user.
GET /api/account-linking/config¶
Returns the active config (rules dictionary + schedules), or the shipped
defaults ("defaults": true) when none has been saved.
PUT /api/account-linking/config¶
Upsert the single config row. Permission: admin.crawlers
Request Body
{
"rules": { "signals": [], "accountTypeRules": [], "linkThreshold": 50, "onlyLinkTypes": ["Admin","Guest","Secondary"] },
"schedules": [{ "frequency": "daily", "minute": 0, "enabled": true }],
"isActive": true
}
POST /api/account-linking/runs¶
Start a run. Returns 202 + the new AccountLinkingRuns row; the engine runs
in the background. Permission: admin.crawlers
GET /api/account-linking/runs¶
The 50 most recent runs, newest first.
GET /api/account-linking/runs/:id¶
Single-run status (for the polling UI): status, step, pct,
linksCreated, linksUpdated, skippedAnalystOverride, orphansRemaining.
User Preferences¶
Preferences are stored per-user in the GraphUserPreferences SQL table (auto-created on first access). The user is identified by the Entra ID oid claim from their JWT. In no-auth mode, the key anonymous is used as a fallback.
GET /api/preferences¶
Returns the current user's tab visibility preferences.
Response
Optional tabs that can be toggled: systems, identities, contexts, sync-log. All are hidden by default until the user enables them via the settings dropdown (user avatar, top right). Any other key is dropped on save.
PUT /api/preferences¶
Update the current user's tab visibility.
Request Body
Response: 204 No Content
Operations¶
Performance Metrics¶
Performance monitoring is opt-in. Enable by setting PERF_METRICS_ENABLED=true.
| Endpoint | Method | Description |
|---|---|---|
/api/perf |
GET |
Aggregated endpoint summaries — P50, P95, P99 latency per route |
/api/perf/recent |
GET |
Last N requests with per-SQL-query timing breakdowns |
/api/perf/slow |
GET |
Slowest N requests (by total duration) |
/api/perf/export |
GET |
Download full ring buffer as JSON for offline analysis |
/api/perf/clear |
POST |
Clear the ring buffer (admin only) |
/api/perf/toggle |
POST |
Enable/disable the collector at runtime (admin only) |
The ring buffer holds 1000 entries. When disabled (PERF_METRICS_ENABLED is unset or false), no overhead is incurred — the middleware is a no-op.
Server-Timing response headers are emitted on every request when enabled, making per-request SQL timing visible in browser DevTools (Network tab → Timing).