Appearance
My Abilities Endpoint
GET /identity/user/my-abilities exposes the authenticated user's fully computed CASL rules for a specific organization. The response is consumed by the frontend AbilityProvider to build the client-side MongoAbility instance that gates sidebar items, route access, and individual UI elements.
Request
http
GET /identity/user/my-abilities
Authorization: Bearer <jwt>
x-org-id: <uuid-v7>
x-agency-id: <uuid-v7>Required Headers
| Header | Type | Description |
|---|---|---|
Authorization | Bearer <jwt> | Must be a valid, non-expired access token. The user identity is extracted from this token. |
x-org-id | UUID v7 | The organization to load abilities for. Must be an organization the user is a member of. |
x-agency-id | UUID v7 | The agency context. Must match the agency embedded in the JWT. |
Note:
x-agency-idis validated against the agency stored in the JWT. Passing a mismatched agency will result in a403.
Response — 200 OK
json
{
"rules": [
{
"action": "read",
"subject": "identity.user",
"conditions": { "id": "019f1c5c-5682-70fc-bdff-3a496709dc59" }
},
{
"action": "update",
"subject": "identity.user",
"conditions": { "id": "019f1c5c-5682-70fc-bdff-3a496709dc59" }
},
{
"action": "manage",
"subject": "ai.chat",
"conditions": {
"userId": "019f1c5c-5682-70fc-bdff-3a496709dc59",
"orgId": "019d1c5c-5682-70fc-bdff-000000000001"
}
},
{
"action": "read",
"subject": "platform.admin"
}
]
}Response Schema (TypeScript / Zod)
Defined in packages/schemas/src/identity/users/my-abilities.schema.ts and exported from @daramex/schemas:
ts
import { z } from 'zod';
export const rawPermissionSchema = z.object({
action: z.string(),
subject: z.string(),
conditions: z.record(z.string(), z.unknown()).nullable().optional(),
inverted: z.boolean().optional(),
});
export const myAbilitiesResponseSchema = z.object({
rules: z.array(rawPermissionSchema),
});
export type IRawPermission = z.infer<typeof rawPermissionSchema>;
export type IMyAbilitiesResponse = z.infer<typeof myAbilitiesResponseSchema>;Rule Fields
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | The CASL action, e.g. read, manage, create. |
subject | string | Yes | The CASL subject string, e.g. identity.user, ai.chat, platform.admin. |
conditions | Record<string, unknown> | null | No | MongoDB-style filter object constraining the rule to matching records. null or absent = no conditions. |
inverted | boolean | No | When true, the rule is a deny rule — explicitly forbids the action even when another rule grants it. Defaults to false when absent. |
Interpolation — No Template Variables in Response
All persisted policies stored in the database may contain template variables such as ${user.id}, ${tenant.orgId}, and ${tenant.id}. These are resolved server-side before the response is sent.
The interpolatePermissions(rules, user, tenant) utility (in apps/api/src/core/authorization/interpolate-permissions.ts) performs a JSON stringify → regex replace → parse cycle:
| Template Variable | Replaced With |
|---|---|
${user.id} | The authenticated user's UUID |
${tenant.orgId} | The x-org-id header value |
${tenant.id} | The x-org-id header value (same as orgId in the tenant context) |
The response MUST NOT contain any string matching ${...}. If you observe template variables in a response, it indicates an interpolation bug.
Error Codes
| Status | Error Code | When |
|---|---|---|
400 Bad Request | MISSING_ORG_HEADER | The x-org-id header is absent or blank. The @OrgId() decorator throws before the controller runs. |
401 Unauthorized | UNAUTHORIZED | The Authorization header is missing, expired, or contains an invalid JWT. |
403 Forbidden | ORG_ACCESS_DENIED | The user is not a member of the specified org, or the org does not belong to the user's agency. |
Authorization Model
This endpoint uses JWT authentication only — it is intentionally NOT protected by @CheckPolicies / PoliciesGuard. Any authenticated user can call it, regardless of their current permission set. This is by design: the endpoint's purpose is to return the permission set, so it cannot gate on permissions it hasn't yet provided.
ts
// user.controller.ts — no @UseGuards(PoliciesGuard), no @CheckPolicies
@AuthUser() // ← requires valid JWT
@OrgId() // ← requires x-org-id header; validates org + agency membership
@Get('my-abilities')
async getMyAbilities(
@CurrentUser() user: IAuthUser,
@OrgIdParam() orgId: string,
@AgencyIdParam() agencyId: string,
): Promise<IMyAbilitiesResponse> {
return this.queryBus.execute(new GetMyAbilitiesQuery(user.id, agencyId, orgId));
}CQRS Flow
UserController.getMyAbilities()
│
└── QueryBus.execute(GetMyAbilitiesQuery { userId, agencyId, orgId })
│
└── GetMyAbilitiesHandler.execute()
├── IdentityPermissionsProvider.getUserPermissions(userId, orgId, agencyId)
│ ├── Validates org belongs to agency
│ ├── Validates user is an org member
│ ├── Loads role policies (via OrganizationMember → Role → Policy[])
│ ├── Loads user policies (UserPolicy[] for userId + orgId)
│ └── Returns IRawPermission[] (with ${...} placeholders)
│
└── interpolatePermissions(rules, { userId }, { orgId, tenantId: orgId })
└── Returns IRawPermission[] with all placeholders resolvedEmpty Rules (Zero Policies)
A user who is a valid org member but has no roles and no direct user policies receives:
json
{ "rules": [] }This is not an error — it is the correct default-deny state. The frontend treats empty rules identically to a failed request: all ability-gated items are hidden.
Performance
- Target: p95 < 200ms
- The
IdentityPermissionsProviderresolves all memberships, roles, and policies in a single DB round-trip via TypeORM eager relations. - Rules are computed per request — no server-side caching. Client-side caching is handled by the panel's TanStack Query layer (
staleTime: 2min,gcTime: 5min).
Related
- Frontend CASL Integration — how the panel consumes this endpoint
- RBAC and Custom Roles — how roles and policies are created and assigned
- CASL Authorization — the server-side authorization engine