← Knowledge library

Access & roles

Role

A role is a flat set of permissions granted to a user inside one organizational boundary. It carries no semantics of its own — "Doctor", "Doctor Read Only" and "Doctor Scheduler" are three unrelated permission sets, not variations on a job…

3.1 · versioned documentation · 8 min read

A role is a flat set of permissions granted to a user inside one organizational boundary. It carries no semantics of its own — "Doctor", "Doctor Read Only" and "Doctor Scheduler" are three unrelated permission sets, not variations on a job title. A user can hold roles in many organizations but only one role per organization chain. RolePermission join rows expand a role into its effective permission list.

Two layers back the concept. The Django model (care/security/models/role.py) is storage: name, description, flags, and the contexts array. The Pydantic resource specs (care/emr/resources/role/spec.py, care/emr/resources/permissions.py) define the API read/write schemas, bind contexts to an enum, type the permissions field, and hold every create/update validation rule.

Source:

Models

Model Purpose
RoleModel A named role grouping a set of permissions
RolePermission Join table linking a RoleModel to a PermissionModel

Both extend BaseModel (see Base model), so they get external_id, created_date/modified_date, and soft-delete via deleted — but none of the created_by/updated_by/history/meta fields that EMRBaseModel adds.

RoleModel fields

User-created roles can be deleted by anyone holding the right permission. System roles (is_system=True) are exempt: the API blocks creating, updating, or deleting them.

Identity & description

Field Type Required Default Notes
name CharField(1024) yes — Unique across non-deleted rows (see Meta). The spec rejects empty/blank values and any name that already exists case-insensitively
description TextField no "" Free text
contexts ArrayField[CharField(24)] yes [] Boundary types the role applies to. The column stores opaque strings; the spec constrains each element to the RoleContext enum (see RoleContext values)

Status flags

Field Type Required Default Notes
is_system BooleanField no False Set by the platform. These roles can't be deleted, and the create/update spec rejects any payload carrying is_system=True as well as any update to an existing system role
temp_deleted BooleanField no False Stages a role for removal independently of deleted. Not exposed in the API specs
is_archived BooleanField no False Hides the role without deleting it. Read/write through the specs

Meta / constraints

name is unique only among non-deleted rows, enforced by a partial constraint:

UniqueConstraint(fields=["name"], condition=Q(deleted=False), name="unique_name_if_not_deleted")

RoleCreateSpec checks the same uniqueness case-insensitively (name__iexact) before the request ever reaches the DB constraint.

Related models

RolePermission

Each row connects a role to one permission; a role accumulates its grants across many rows.

role         → FK RoleModel (CASCADE, required)
permission   → FK PermissionModel (CASCADE, required)
temp_deleted → BooleanField (default False)

temp_deleted excludes a grant from the role without deleting the row — permission lookups filter on rolepermission__temp_deleted=False.

PermissionModel

The permission a RolePermission points at (Permission).

slug         → CharField(1024) unique, indexed
name         → CharField(1024)
description  → TextField (default "")
context      → CharField(1024)   # one of the PermissionContext values
temp_deleted → BooleanField (default False)

The set of available permissions lives in code, not as fixed DB rows. PermissionController (care/security/permissions/base.py) registers them by aggregating per-domain enum.Enum handlers such as PatientPermissions and FacilityPermissions, where each member's value is a Permission(name, description, context, roles) dataclass.

Enum / value tables

RoleContext values

Each element of contexts is validated against this enum (care/security/roles/role.py). These name the organizational boundary a role can apply to — a different axis from PermissionContext.

Value Meaning
FACILITY Role applies within a facility
GOVT_ORG Role applies within a government organization
ROLE_ORG Role applies within a role (user-group) organization

PermissionContext values

PermissionModel.context / Permission.context is one of these (care/security/permissions/constants.py). The permissions mixins below filter grants by context to resolve a user's effective permissions.

Value
GENERIC
FACILITY
PATIENT
QUESTIONNAIRE
ORGANIZATION
FACILITY_ORGANIZATION
ENCOUNTER

PermissionEnum (write field for permissions)

RoleCreateSpec.permissions is typed against PermissionController.get_enum(), a str enum built at runtime from every registered permission name across all handlers. Values are permission identifiers (slugs) like can_create_patient, can_write_patient, can_list_patients, can_view_clinical_data. The set is the union of PermissionController.internal_permission_handlers plus anything plugins register, so it varies by deployment.

Built-in (is_system) roles

RoleController (care/security/roles/role.py) seeds these with is_system=True. None can be created, updated, or deleted through the API.

Role Contexts
Doctor FACILITY, GOVT_ORG
Nurse FACILITY, GOVT_ORG
Staff FACILITY, GOVT_ORG
Volunteer FACILITY, GOVT_ORG
Pharmacist FACILITY
Administrator FACILITY, GOVT_ORG
Facility Admin FACILITY
Admin FACILITY, GOVT_ORG
Admin (role org) ROLE_ORG
Manager (role org) ROLE_ORG
Member (role org) ROLE_ORG

Resource specs (API schema)

Every spec extends EMRResource (care/emr/resources/base.py), which supplies serialize (DB → Pydantic, via perform_extra_serialization) and de_serialize (Pydantic → DB, via perform_extra_deserialization). RoleBaseSpec sets __exclude__ = ["permissions"] so each subclass declares permissions itself instead of inheriting it.

Spec class Role Fields
RoleBaseSpec shared base id, name, description, is_system, is_archived, contexts
RoleCreateSpec write · create & update base fields + permissions: list[PermissionEnum]
RoleReadSpec read · detail/list base fields + permissions: list[PermissionSpec]
RoleReadMinimalSpec read · minimal/embedded base fields only
PermissionSpec nested (read) name, description, slug (SlugType), context

Field shapes & bindings

Field Spec type Notes
id UUID4 | None The role's external_id on read (set in perform_extra_serialization); ignored on write
name str | None Required and non-blank on create; case-insensitive uniqueness enforced
description str | None Optional
is_system bool | None (default False) Must be falsy on write — True is rejected
is_archived bool | None (default False) —
contexts list[RoleContext] Bound to the RoleContext enum
permissions (write) list[PermissionEnum] (default []) Dynamic str enum of every registered permission name; must contain ≥ 1 entry
permissions (read) list[PermissionSpec] Resolved server-side from the role's active grants
PermissionSpec.slug SlugType str, length 5–50, pattern ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$

Validation (RoleCreateSpec.validate_role, mode="after")

The validator runs on both create and update; it reads the is_update flag and the target object from the serializer context.

Server-side serialization behaviour

Permission resolution mixins

care/emr/resources/permissions.py defines mixins that other resource specs inherit to expose the calling user's effective permissions on an object — not the role's own permission list. They populate mapping["permissions"] in perform_extra_user_serialization, but only when an authenticated user is passed to serialize.

Mixin Resolves Context filter
PermissionsMixin base — adds permissions: list[str] —
PatientPermissionsMixin roles the user holds on a patient permission__context in ("PATIENT", "FACILITY")
EncounterPermissionsMixin roles the user holds on an encounter permission__context in ("ENCOUNTER", "PATIENT")
FacilityPermissionsMixin roles on facility root + sub-orgs; also exposes root_org_permissions and child_org_permissions (child set excludes can_update_facility) none (root) / excludes can_update_facility (child)

Methods & save behaviour

RoleModel resolves its effective permissions through two cached helpers backed by the Django cache (Redis) with a 7-day TTL:

Both queries join through RolePermission and exclude temp_deleted=True grants.

Cache invalidation signal

invalidate_role_permissions_cache, a @receiver([post_save, post_delete], sender=RolePermission) handler, clears both cache keys for the affected role_id on every create, update, or delete of a RolePermission. ORM writes invalidate the cache automatically; raw SQL writes skip the signal and leave stale caches behind.

API integration notes

Related

Source evidence

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