Skip to content

ACL

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.

PropertyValue
Base URL{HOST}/v1
AuthBearer JWT (header Authorization) or cookie access_token
Content-Typeapplication/json
Error envelope{ "message": string | string[], "statusCode": number, "error": string }
ValidationGlobal ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400
Related modulesusers, organizations, authentication, onboarding
Document versionv1 · 2026-05-20
AudienceInternal 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.

MethodPathAuthSummary
POST/v1/acl/rolesbearerCreate a new role for the organization
GET/v1/acl/rolesbearerList roles with role_type / user_type filters
GET/v1/acl/roles/:role_idbearerDetail of one role
PATCH/v1/acl/roles/:role_idbearerUpdate role fields or reset the permission list
DELETE/v1/acl/roles/:role_idbearerDelete a role
PATCH/v1/acl/assign-rolebearerAssign / unassign a role to a user
GET/v1/acl/permissionsbearerList all available permissions
GET/v1/acl/permissions/:permission_idbearerDetail of one permission
GET/v1/acl/roles/:role_id/permissionsbearerList permissions held by a role
DELETE/v1/acl/roles/:role_id/permissionsbearerDetach permissions from a role

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
FieldTypeRequiredNotes
namestringUnique role name (e.g. organization_admin)
descriptionstringShort role description
organization_idstring (UUID)optionalRequired 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",
"permissions": [
"update-role",
"read-role",
"delete-role",
"invite-individual-user",
"invite-organization-admin"
]
}
{
"status": "success",
"statusCode": 201,
"message": "role created successfully",
"data": {
"role": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "organization_admin",
"display_name": "Organization Admin",
"organization_id": "03be5259-f281-478e-a8d0-e7e825e525f2",
"user_type": "organization",
"role_type": "custom",
"description": "This is the organization admin role",
"created_by": "770e8400-e29b-41d4-a716-446655440222",
"updated_by": "770e8400-e29b-41d4-a716-446655440222",
"deleted_by": null,
"created_at": "2026-05-20T08:30:00.000Z",
"updated_at": "2026-05-20T08:30:00.000Z"
}
}
}
StatusWhen 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
ParamTypeDefaultNotes
pagenumber1Page number
limitnumber10Records per page
organization_idstring (UUID)optionalOnly used when caller is MORIA. For ORGANIZATION, the server always uses the admin’s own org
role_typeenum RoleTypeoptionalcustom or default
user_typeenum UserTypeoptionalmoria, organization, individual
{
"status": "success",
"statusCode": 200,
"message": "roles fetched successfully",
"data": {
"limit": 10,
"count": 24,
"currentPage": 1,
"totalPages": 3,
"roles": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "organization_admin",
"display_name": "Organization Admin",
"organization_id": "03be5259-f281-478e-a8d0-e7e825e525f2",
"user_type": "organization",
"role_type": "custom",
"description": "Organization admin role",
"created_at": "2026-05-20T08:30:00.000Z",
"updated_at": "2026-05-20T08:30:00.000Z"
}
]
}
}
StatusWhen 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

GET /v1/acl/roles/:role_id bearer

Section titled “GET /v1/acl/roles/:role_id ”

Detail of one role by ID. role_id is validated as a UUID via ParseUUIDPipe.

bearer MORIA, ORGANIZATION read-role
ParamTypeNotes
role_idUUIDRole ID
{
"status": "success",
"statusCode": 200,
"message": "role fetched successfully",
"data": {
"role": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "organization_admin",
"display_name": "Organization Admin",
"organization_id": "03be5259-f281-478e-a8d0-e7e825e525f2",
"user_type": "organization",
"role_type": "custom",
"description": "Organization admin role",
"created_at": "2026-05-20T08:30:00.000Z",
"updated_at": "2026-05-20T08:30:00.000Z"
}
}
}
StatusWhen it occurs
400 Bad Requestrole_id is not a UUID
401 UnauthorizedInvalid Bearer/cookie token
403 ForbiddenMissing read-role permission
404 Not FoundRole not found

PATCH /v1/acl/roles/:role_id bearer

Section titled “PATCH /v1/acl/roles/:role_id ”

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
ParamTypeNotes
role_idUUIDRole ID
FieldTypeNotes
namestringNew role name
descriptionstringNew description
permissionsstring[]New permission list (replace, not append)
{
"name": "organization_hr",
"description": "This is the organization human resource role",
"permissions": [
"update-role",
"read-role",
"invite-individual-user"
]
}
{
"status": "success",
"statusCode": 200,
"message": "role updated successfully",
"data": {
"role": { "...": "same shape as GET /:role_id" }
}
}
StatusWhen it occurs
400 Bad RequestValidation failed
401 UnauthorizedInvalid Bearer/cookie token
403 ForbiddenMissing update-role permission
404 Not FoundRole not found

DELETE /v1/acl/roles/:role_id bearer

Section titled “DELETE /v1/acl/roles/:role_id ”

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
ParamTypeNotes
role_idUUIDRole ID
{
"status": "success",
"statusCode": 200,
"message": "role deleted successfully"
}
StatusWhen it occurs
400 Bad Requestrole_id is not a UUID
401 UnauthorizedInvalid Bearer/cookie token
403 ForbiddenMissing delete-role permission
404 Not FoundRole not found

PATCH /v1/acl/assign-role bearer

Section titled “PATCH /v1/acl/assign-role ”

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
FieldTypeRequiredNotes
user_idstring (UUID)optionalID of the user being assigned / unassigned
role_idstring (UUID)optionalTarget role ID (used when assigning)
unassign_rolebooleanoptionaltrue → the service runs unassign (ignoring role_id) · default false (assign)
{
"user_id": "770e8400-e29b-41d4-a716-446655440222",
"role_id": "03be5259-f281-478e-a8d0-e7e825e525f2",
"unassign_role": false
}
{
"status": "success",
"statusCode": 200,
"message": "role assigned successfully",
"data": {
"role": { "...": "RoleDto shape, now reflecting the new assignment" }
}
}
StatusWhen 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

GET /v1/acl/permissions bearer

Section titled “GET /v1/acl/permissions ”

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
ParamTypeDefaultNotes
pagenumber1Page number
limitnumber10Records per page
{
"status": "success",
"statusCode": 200,
"message": "permissions fetched successfully",
"data": {
"limit": 10,
"count": 100,
"currentPage": 1,
"totalPages": 10,
"permissions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "create-user",
"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"
}
]
}
}
StatusWhen it occurs
401 UnauthorizedInvalid Bearer/cookie token
403 ForbiddenMissing read-permission permission

GET /v1/acl/permissions/:permission_id bearer

Section titled “GET /v1/acl/permissions/:permission_id ”

Detail of one permission by ID. permission_id is validated as a UUID.

bearer MORIA, ORGANIZATION read-permission
ParamTypeNotes
permission_idUUIDPermission ID
{
"status": "success",
"statusCode": 200,
"message": "permission fetched successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "create-user",
"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"
}
}
StatusWhen it occurs
400 Bad Requestpermission_id is not a UUID
401 UnauthorizedInvalid Bearer/cookie token
404 Not FoundPermission not found

GET /v1/acl/roles/:role_id/permissions bearer

Section titled “GET /v1/acl/roles/:role_id/permissions ”

List permissions attached to a role. Used by FE to display a role summary on the detail screen.

bearer MORIA, ORGANIZATION read-permission
ParamTypeNotes
role_idUUIDRole ID
{
"status": "success",
"statusCode": 200,
"message": "organization permissions fetched successfully",
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "create-user",
"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"
}
]
}
StatusWhen it occurs
400 Bad Requestrole_id is not a UUID
401 UnauthorizedInvalid Bearer/cookie token
404 Not FoundRole not found

DELETE /v1/acl/roles/:role_id/permissions bearer

Section titled “DELETE /v1/acl/roles/:role_id/permissions ”

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
ParamTypeNotes
role_idUUIDRole ID
FieldTypeRequiredNotes
permissionsstring[]List of permission names to revoke
{
"permissions": [
"update-role",
"delete-role"
]
}
{
"status": "success",
"statusCode": 200,
"message": "permissions removed successfully"
}
StatusWhen it occurs
400 Bad Requestrole_id is not a UUID · permissions is empty
401 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",
"statusCode": 403,
"error": "Forbidden"
}

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