Naalya Handbook
Roles & Permissions Admin

Roles & Permissions Admin

The management side of authorization — the permission registry, plus how roles are created, cloned, edited, and assigned to users.

There are two halves to authorization on this backend. One half enforces permissions at request time — guards, the CASL ability factory, turning "only your campus" into SQL. The other half — this section — is the management side: what permissions even exist, how a school's roles get created, how an admin edits a role, and how a role lands on a user. If you've read RBAC & Scopes you already know how a permission is evaluated; here you'll learn how one gets into the system in the first place.

The chain

The mental model is a short chain. A permission is just an (action, resource) pair — READ STUDENT, MANAGE ROLE. A role is a named bundle of those pairs. A role assignment glues a role onto a specific user, optionally narrowed to one campus. Nobody is ever granted a raw permission directly; you always go through a role, and you always assign the role to a user. That indirection is the whole design, and it's what makes the rest of this section click.

the chain, in order
permission registry   →   role (bundle of permissions)   →   assignment (role + user + scope)
 (what's assignable)        POST /roles, PUT /roles/:id/permissions     POST /users/:id/roles

This is the admin side — enforcement lives elsewhere

This section covers managing roles. How a stored permission becomes an allow/deny decision (the guard pipeline, CASL abilities, scope-to-SQL) is RBAC & Scopes. Read that first if "ability factory" or "condition" is new to you — these pages lean on those ideas without re-explaining them.

The permission registry

Not every (action, resource) combination is a real permission. You can't grant DELETE AUDIT_LOG because audit logs are read-only. The set of valid permissions is defined declaratively, in code, in libs/shared/src/permission/permission-registry/. Each resource is registered with a kind that expands to a fixed list of actions, so you describe a resource once instead of spelling out every action by hand.

Two kinds exist, and each maps to a set of actions:

libs/shared/src/permission/permission-registry/registry.types.ts
const KIND_ACTIONS: Record<ResourceKind, Action[]> = {
  full: [Action.READ, Action.LIST, Action.CREATE, Action.UPDATE, Action.DELETE, Action.MANAGE],
  readonly: [Action.READ, Action.LIST],
};

interface ResourceConfig { resource: Resource; kind: ResourceKind; label: string; actions?: Action[] }

So registering a resource as full mints six permissions for it; readonly mints two. A single resource config is tiny — Role is a full resource, AuditLog is readonly:

libs/shared/src/permission/permission-registry/administration-permissions.registry.ts
export const rolePermissions: ResourceConfig = {
  resource: Resource.ROLE,
  kind: 'full',
  label: 'Role',
};

The configs are split across themed files — user, academic, administration, operations, observability — and index.ts gathers them into one RESOURCE_CONFIGS object keyed by Resource. Here's the lay of the land:

GroupA few of the resources
UserUSER, STAFF, STUDENT, GUARDIAN (full); SCHOOL_DASHBOARD (readonly)
AcademicGRADE, CLASS, ACADEMIC_REPORT, ACADEMIC_YEAR, SUBJECT, ENROLLMENT, CURRICULUM, CBT_EXAM, GRADING_CONFIG, SCHEME_OF_WORK, LESSON_PLAN, TEMPLATE (full)
AdministrationROLE, CAMPUS, DEPARTMENT, APPLICATION, SCHOOL (full)
BursaryTRANSACTION, PAYMENT_GATEWAY, PAYMENT_ITEM (full)
OperationsJOB_VACANCY, KNOWLEDGE_BASE, API_KEY, ENQUIRY, NOTICE_BOARD, FEEDBACK, SOCIAL_CONNECTION (full)
ObservabilityAUDIT_LOG, ANALYTICS (readonly)

The enum holds 35 resources today. Adding one is a three-step rule: the Resource enum, a ResourceConfig in the right themed file, and a grant in the relevant SYSTEM_ROLES seed arrays (SUPER_ADMIN's MANAGE ALL covers it implicitly).

Two shapes

At module load, permission.registry.ts walks RESOURCE_CONFIGS and flattens everything into two shapes you'll see used everywhere. PERMISSION_REGISTRY is a flat array of every valid permission, each carrying an auto-generated description and tags (good for search). PERMISSION_REGISTRY_MAP is the same data keyed resource → action → definition (good for grouping in a UI).

The flat list is also the single source of truth for validation. The helper isValidPermission is what every role-write goes through, and it encodes one special case:

libs/shared/src/permission/permission.utils.ts
function isValidPermission(action: Action, resource: Resource): boolean {
  if (action === Action.MANAGE && resource === Resource.ALL) return true;   // super-admin grant

  return PERMISSION_REGISTRY.some(
    (permission) => permission.action === action && permission.resource === resource,
  );
}

MANAGE ALL is always valid — that's the wildcard the Super Admin role carries. Everything else must physically exist in the registry. A readonly resource simply has no CREATE/UPDATE/DELETE rows, so those pairs are rejected without any special-casing.

The registry is the allow-list — invented pairs get a 400

When a client posts a permission for a role, isValidPermission runs over every pair. If even one isn't in PERMISSION_REGISTRY (and isn't MANAGE ALL), the whole request fails with a BadRequestException. You can't sneak a typo'd or fictional permission into the database — adding a new assignable permission means adding a ResourceConfig, not posting clever JSON.

The registry endpoint

The frontend's role editor needs to show admins the menu of assignable permissions. That's served by a single read-only endpoint on the auth controller, which just hands back both shapes:

apps/server/src/app/auth/auth.controller.ts
@Get('permissions/registry')
@ApiOperation({ operationId: 'getPermissionsRegistry', summary: 'Get permissions' /* ... */ })
getPermissionsRegistry(): PermissionsRegistryResponseDto {
  return {
    permissions: PERMISSION_REGISTRY,      // flat list, for search
    map: PERMISSION_REGISTRY_MAP,          // keyed by resource → action, for grouping
  };
}

GET /api/v1/auth/permissions/registry (operationId getPermissionsRegistry) requires only authentication — any logged-in user can fetch the catalog. The flat permissions array drives a searchable list; the map drives a grouped-by-resource layout. Nothing here touches the database; the registry is built once in memory from the code.

Where to go next

The rest of this section follows the chain from a fresh school's cloned roles to a scoped assignment on a user.

On this page