← Knowledge library

Access & roles

Permission

A permission is the ability to perform one action in one context — read it as "Action on Resource", like "Can Create Patient" in the PATIENT context. Permissions are the atoms of MANSOQ's access control: code declares them, the sync command…

3.1 · versioned documentation · 7 min read

A permission is the ability to perform one action in one context — read it as "Action on Resource", like "Can Create Patient" in the PATIENT context. Permissions are the atoms of MANSOQ's access control: code declares them, the sync command writes them to the database, roles group them, and permission associations bind them to resources. You rarely touch this table directly; you reference its rows by slug when building roles.

The Django PermissionModel is only storage; two other places do the real work. The permission registry (care/security/permissions/) declares every permission as a Python enum member carrying a name, description, context, and the default roles that hold it. The Pydantic resource specs (care/emr/resources/role/spec.py) define the read API schema. This doc covers all three.

Source:

Models

Model Purpose
PermissionModel A single, named permission (action) that can be granted to a user in a context

PermissionModel extends BaseModel, the lowest-level MANSOQ base — not EMRBaseModel. That means no created_by/updated_by, no history, no meta columns. The inherited fields:

Field Type Notes
external_id UUIDField default=uuid4, unique, indexed. Opaque public identifier — never expose the integer pk
created_date DateTimeField auto_now_add; nullable, indexed
modified_date DateTimeField auto_now; nullable, indexed
deleted BooleanField default=False, indexed. Soft-delete flag; the default manager hides deleted=True rows

PermissionModel fields

Field Type Required Default Notes
slug CharField(1024) yes — unique, indexed, and the API lookup_field. The enum member name (e.g. can_create_patient). Code, specs, and the API all reference a permission by slug, not external_id
name CharField(1024) yes — Display name (e.g. "Can Create Patient"), copied from the registry Permission.name
description TextField no "" Free text from the registry Permission.description; often empty
context CharField(1024) yes — Resource context the permission applies to. No DB choices, but the sync command only writes a PermissionContext value (from Permission.context.value), listed below
temp_deleted BooleanField no False Staging flag for the sync command. It marks permissions absent from the declared set before hard-deleting them — separate from the inherited deleted soft-delete flag

context values (PermissionContext)

context is a free-form CharField in the database, but the registry only writes one of these enum values (constants.py):

Value Meaning
GENERIC Not scoped to a specific resource type
FACILITY Scoped to a facility
PATIENT Scoped to a patient
QUESTIONNAIRE Scoped to a questionnaire
ORGANIZATION Scoped to a (govt/role) organization
FACILITY_ORGANIZATION Scoped to a facility organization
ENCOUNTER Scoped to an encounter

The value is load-bearing: when resolving access for a serialized resource, the mixins below filter on permission__context__in=[...], so only permissions in the matching context count.

Related models

Registry declaration — Permission dataclass

Permissions are authored in code, not the database. Each one is a member of a *Permissions enum, one per resource area (PatientPermissions, EncounterPermissions, and so on). The member name becomes the slug; the member value is a Permission dataclass (constants.py):

Field Type Maps to PermissionModel Notes
name str name Display name
description str description Free text
context PermissionContext context (.value) One of the enum values above
roles list[Role] — (drives RolePermission) Default system roles that receive this permission on sync

Example (care/security/permissions/patient.py):

can_create_patient = Permission(
    "Can Create Patient", "", PermissionContext.PATIENT,
    [STAFF_ROLE, DOCTOR_ROLE, NURSE_ROLE, ADMINISTRATOR, ADMIN_ROLE, FACILITY_ADMIN_ROLE],
)

PermissionController (base.py) aggregates every handler enum:

Sync command (sync_permissions_roles)

The management command sync_permissions_roles is the only writer of this table. It is idempotent, Redis-locked, and runs in a single transaction:

  1. Mark every PermissionModel temp_deleted=True.
  2. For each declared permission, upsert by slug — setting name, description, context (from Permission.context.value) and clearing temp_deleted.
  3. Hard-delete any row still temp_deleted=True (no longer declared).
  4. Upsert system roles, then rebuild RolePermission rows from each Permission.roles list, using the same mark-then-prune pattern.

RolePermission

Permissions reach users only through roles. RolePermission is the join table (role FK, permission FK, temp_deleted) connecting a RoleModel to a PermissionModel. A user's effective access is the union of permissions across their role bindings, filtered to active grants (rolepermission__temp_deleted=False).

Resource specs (API schema)

The Pydantic specs build on EMRResource (base.py), whose serialize (DB → spec) and de_serialize (spec → DB) drive read and write, with perform_extra_serialization/perform_extra_deserialization hooks for side effects.

Spec class Role File Notes
PermissionSpec read · list + detail role/spec.py The only permission-facing spec. Served read-only by PermissionViewSet
PermissionsMixin read augmentation permissions.py Adds the requesting user's computed permissions list to other resources' serialized output

No PermissionCreateSpec or PermissionUpdateSpec exists: permissions are reference data, never client-writable. Write specs exist for roles instead — see Role.

PermissionSpec

__model__ = PermissionModel. A flat read schema with no extra serialization hooks; it inherits the base mapping that sets id = external_id:

Field Type Notes
name str From PermissionModel.name
description str From PermissionModel.description
slug SlugType str, min_length=5, max_length=50, must match ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$ (URL-safe; starts and ends alphanumeric). See slug_type.py
context str One of the PermissionContext values above
id UUID4 Added by serialize as external_id (inherited base behaviour)

Served by PermissionViewSet, an EMRModelReadOnlyViewSet (list + retrieve only) with lookup_field="slug", filterable by name (icontains). No create, update, or delete endpoint exists.

Computed permissions on other resources (PermissionsMixin)

permissions.py defines mixins that other read specs inherit, so a serialized resource carries the current user's effective permission slugs for that object. For authenticated users, PermissionsMixin.perform_extra_user_serialization calls add_permissions(mapping, user, obj) to populate a permissions: list[str] field:

Mixin Output fields Resolution
PatientPermissionsMixin permissions Roles on the patient; permission slugs where context in ["PATIENT", "FACILITY"]
EncounterPermissionsMixin permissions Roles on the encounter; permission slugs where context in ["ENCOUNTER", "PATIENT"]
FacilityPermissionsMixin permissions, root_org_permissions, child_org_permissions Union of root-org and sub-org role permissions; child_org_permissions excludes the can_update_facility slug

The frontend reads these lists to gate UI by capability, skipping a separate authorization round-trip.

Methods & save behaviour

PermissionModel adds no custom save(), delete() override, validators, or signals — everything comes from BaseModel, including soft-delete (delete() setting deleted=True) and the default manager that filters out soft-deleted rows. The lifecycle runs externally through sync_permissions_roles (above), which stages with temp_deleted and hard-deletes undeclared rows.

PermissionSpec runs no extra serialization or deserialization: just the base serialize flow plus the inherited id = external_id mapping.

API integration notes

Related

Source evidence

References are pinned to the private MANSOQ source repository. Repository access is required to open these links.