Skip to content

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

HeaderTypeDescription
AuthorizationBearer <jwt>Must be a valid, non-expired access token. The user identity is extracted from this token.
x-org-idUUID v7The organization to load abilities for. Must be an organization the user is a member of.
x-agency-idUUID v7The agency context. Must match the agency embedded in the JWT.

Note: x-agency-id is validated against the agency stored in the JWT. Passing a mismatched agency will result in a 403.

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

FieldTypeRequiredDescription
actionstringYesThe CASL action, e.g. read, manage, create.
subjectstringYesThe CASL subject string, e.g. identity.user, ai.chat, platform.admin.
conditionsRecord<string, unknown> | nullNoMongoDB-style filter object constraining the rule to matching records. null or absent = no conditions.
invertedbooleanNoWhen 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 VariableReplaced 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

StatusError CodeWhen
400 Bad RequestMISSING_ORG_HEADERThe x-org-id header is absent or blank. The @OrgId() decorator throws before the controller runs.
401 UnauthorizedUNAUTHORIZEDThe Authorization header is missing, expired, or contains an invalid JWT.
403 ForbiddenORG_ACCESS_DENIEDThe 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 resolved

Empty 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 IdentityPermissionsProvider resolves 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).