The Access Control List (ACL) module manages roles , permissions , and role assignments to users. The AclController is mounted at /acl and restricted to UserType.MORIA & UserType.ORGANIZATION via a controller-level @Roles guard. Each endpoint is also protected by specific @Permissions(...) — FE must ensure the logged-in admin holds a role that has granted those permissions.
Property Value Base URL {HOST}/v1Auth Bearer JWT (header Authorization) or cookie access_token Content-Type application/jsonError envelope { "message": string | string[], "statusCode": number, "error": string }Validation Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 Related modules users, organizations, authentication, onboarding Document version v1 · 2026-05-20 Audience Internal FE devs (mobile + web)
A MORIA or ORGANIZATION admin can create a custom role for their organization, set a combination of permissions, and assign it to users. The endpoints are split into two groups: roles (/acl/roles) and permissions (/acl/permissions). Assign/unassign operations on a user’s role use the /acl/assign-role endpoint via the unassign_role flag.
Method Path Auth Summary POST /v1/acl/rolesbearer Create a new role for the organization GET /v1/acl/rolesbearer List roles with role_type / user_type filters GET /v1/acl/roles/:role_idbearer Detail of one role PATCH /v1/acl/roles/:role_idbearer Update role fields or reset the permission list DELETE /v1/acl/roles/:role_idbearer Delete a role PATCH /v1/acl/assign-rolebearer Assign / unassign a role to a user GET /v1/acl/permissionsbearer List all available permissions GET /v1/acl/permissions/:permission_idbearer Detail of one permission GET /v1/acl/roles/:role_id/permissionsbearer List permissions held by a role DELETE /v1/acl/roles/:role_id/permissionsbearer Detach permissions from a role
Auth + scope notes
All controller-level endpoints are gated by @Roles(MORIA, ORGANIZATION) — INDIVIDUAL users always get 403.
For create role : a moria_admin must send the target organization_id. A regular organization admin may only create a role for their own organization (the server compares with admin.organization.id).
For list role : MORIA can read across organizations (filter via organization_id in the query); ORGANIZATION is always scoped to its own organization.
Permission strings (e.g. create-role, read-permission) are used as-is — no translation needed.
Create a new role together with the list of permissions to be attached. For a regular organization admin, the organization_id in the body must match the caller’s organization — otherwise the response is 403 Forbidden.
bearer
MORIA, ORGANIZATION
create-role
RESOURCE_CREATED
Field Type Required Notes namestring ✓ Unique role name (e.g. organization_admin) descriptionstring ✓ Short role description organization_idstring (UUID) optional Required when sent by a moria_admin; for an organization admin must match their organization permissionsstring[] ✓ List of permission names to be assigned (e.g. ["update-role", "read-role", "invite-individual-user"])
"name" : " organization_admin " ,
"description" : " This is the organization admin role " ,
"organization_id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
" invite-individual-user " ,
" invite-organization-admin "
"message" : " role created successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"name" : " organization_admin " ,
"display_name" : " Organization Admin " ,
"organization_id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
"user_type" : " organization " ,
"description" : " This is the organization admin role " ,
"created_by" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"updated_by" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad RequestValidation failed (required field empty, organization_id not a UUID, body contains an unknown field) 401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenOrganization admin sent an organization_id belonging to another organization · missing create-role permission
Creates a new row in the roles table and links it to rows in the role_permissions pivot table.
Emits BusinessEvent RESOURCE_CREATED (impact HIGH) with resourceId = the new role’s id.
List roles within the caller’s organization scope (or cross-organization for MORIA). Supports filters by role_type (custom/default) and user_type.
bearer
MORIA, ORGANIZATION
read-role
Param Type Default Notes pagenumber 1Page number limitnumber 10Records per page organization_idstring (UUID) optional Only used when caller is MORIA. For ORGANIZATION, the server always uses the admin’s own org role_typeenum RoleType optional custom or defaultuser_typeenum UserType optional moria, organization, individual
"message" : " roles fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"name" : " organization_admin " ,
"display_name" : " Organization Admin " ,
"organization_id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
"user_type" : " organization " ,
"description" : " Organization admin role " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad RequestQuery param invalid (e.g. role_type not a known enum value) 401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenCaller role is not MORIA/ORGANIZATION or lacks read-role permission
Detail of one role by ID. role_id is validated as a UUID via ParseUUIDPipe.
bearer
MORIA, ORGANIZATION
read-role
Param Type Notes role_idUUID Role ID
"message" : " role fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"name" : " organization_admin " ,
"display_name" : " Organization Admin " ,
"organization_id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
"user_type" : " organization " ,
"description" : " Organization admin role " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad Requestrole_id is not a UUID401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing read-role permission 404 Not FoundRole not found
Update the name, description, or reset the permission list of a role. All fields are optional — send only what changes. If permissions is sent, the old permission list is entirely replaced by the new value.
bearer
MORIA, ORGANIZATION
update-role
RESOURCE_UPDATED
Param Type Notes role_idUUID Role ID
Field Type Notes namestring New role name descriptionstring New description permissionsstring[] New permission list (replace, not append)
"name" : " organization_hr " ,
"description" : " This is the organization human resource role " ,
"message" : " role updated successfully " ,
"role" : { "..." : " same shape as GET /:role_id " }
Status When it occurs 400 Bad RequestValidation failed 401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing update-role permission 404 Not FoundRole not found
Delete a role. Soft-delete via deleted_at from AuditableEntity; users still assigned to this role must be reassigned manually before deletion.
bearer
MORIA, ORGANIZATION
delete-role
RESOURCE_DELETED
Param Type Notes role_idUUID Role ID
"message" : " role deleted successfully "
Status When it occurs 400 Bad Requestrole_id is not a UUID401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing delete-role permission 404 Not FoundRole not found
Assign a role to a user, or (if unassign_role: true) detach a role from a user. Only organization/MORIA admins may call this. The permission gate accepts either assign-role or unassign-role.
bearer
MORIA, ORGANIZATION
assign-role, unassign-role
RESOURCE_UPDATED
Field Type Required Notes user_idstring (UUID) optional ID of the user being assigned / unassigned role_idstring (UUID) optional Target role ID (used when assigning) unassign_roleboolean optional true → the service runs unassign (ignoring role_id) · default false (assign)
"user_id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"role_id" : " 03be5259-f281-478e-a8d0-e7e825e525f2 " ,
"message" : " role assigned successfully " ,
"role" : { "..." : " RoleDto shape, now reflecting the new assignment " }
Status When it occurs 400 Bad RequestInvalid UUID / body contains an unknown field 401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing assign-role/unassign-role permission 404 Not FoundUser or role not found
List every permission available on the platform. Used by FE to build a role builder UI (e.g. a checkbox per permission). Standard pagination.
bearer
MORIA, ORGANIZATION
read-permission
Param Type Default Notes pagenumber 1Page number limitnumber 10Records per page
"message" : " permissions fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"display_name" : " Create User " ,
"description" : " Allows creating new users " ,
"category_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing read-permission permission
Detail of one permission by ID. permission_id is validated as a UUID.
bearer
MORIA, ORGANIZATION
read-permission
Param Type Notes permission_idUUID Permission ID
"message" : " permission fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"display_name" : " Create User " ,
"description" : " Allows creating new users " ,
"category_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad Requestpermission_id is not a UUID401 UnauthorizedInvalid Bearer/cookie token 404 Not FoundPermission not found
List permissions attached to a role. Used by FE to display a role summary on the detail screen.
bearer
MORIA, ORGANIZATION
read-permission
Param Type Notes role_idUUID Role ID
"message" : " organization permissions fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"display_name" : " Create User " ,
"description" : " Allows creating new users " ,
"category_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad Requestrole_id is not a UUID401 UnauthorizedInvalid Bearer/cookie token 404 Not FoundRole not found
Detach one or more permissions from a role. The body contains the list of permission names to revoke.
bearer
MORIA, ORGANIZATION
delete-permission
RESOURCE_UPDATED
Param Type Notes role_idUUID Role ID
Field Type Required Notes permissionsstring[] ✓ List of permission names to revoke
"message" : " permissions removed successfully "
Status When it occurs 400 Bad Requestrole_id is not a UUID · permissions is empty401 UnauthorizedInvalid Bearer/cookie token 403 ForbiddenMissing delete-permission permission 404 Not FoundRole not found
custom — role created by an organization/MORIA admin
default — built-in platform role (e.g. moria_admin, organization_admin) — cannot be deleted
moria · organization · individual
create-role, read-role, update-role, delete-role
assign-role, unassign-role
read-permission, delete-permission
invite-individual-user, invite-organization-admin, invite-moria-admin
"message" : " you can't create role for another organization " ,
message can be a string or an array of strings (multi-field validation errors).
400 body/query/param validation
401 token expired / missing
403 role/permission mismatch or cross-organization
404 role / permission not found
500 internal — show a generic toast