Skip to main content
Version: 3.0

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 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

ModelPurpose
RoleModelA named role grouping a set of permissions
RolePermissionJoin 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

FieldTypeRequiredDefaultNotes
nameCharField(1024)yesUnique across non-deleted rows (see Meta). The spec rejects empty/blank values and any name that already exists case-insensitively
descriptionTextFieldno""Free text
contextsArrayField[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

FieldTypeRequiredDefaultNotes
is_systemBooleanFieldnoFalseSet 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_deletedBooleanFieldnoFalseStages a role for removal independently of deleted. Not exposed in the API specs
is_archivedBooleanFieldnoFalseHides 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.

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.

ValueMeaning
FACILITYRole applies within a facility
GOVT_ORGRole applies within a government organization
ROLE_ORGRole 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.

RoleContexts
DoctorFACILITY, GOVT_ORG
NurseFACILITY, GOVT_ORG
StaffFACILITY, GOVT_ORG
VolunteerFACILITY, GOVT_ORG
PharmacistFACILITY
AdministratorFACILITY, GOVT_ORG
Facility AdminFACILITY
AdminFACILITY, 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 classRoleFields
RoleBaseSpecshared baseid, name, description, is_system, is_archived, contexts
RoleCreateSpecwrite · create & updatebase fields + permissions: list[PermissionEnum]
RoleReadSpecread · detail/listbase fields + permissions: list[PermissionSpec]
RoleReadMinimalSpecread · minimal/embeddedbase fields only
PermissionSpecnested (read)name, description, slug (SlugType), context

Field shapes & bindings

FieldSpec typeNotes
idUUID4 | NoneThe role's external_id on read (set in perform_extra_serialization); ignored on write
namestr | NoneRequired and non-blank on create; case-insensitive uniqueness enforced
descriptionstr | NoneOptional
is_systembool | None (default False)Must be falsy on write — True is rejected
is_archivedbool | None (default False)
contextslist[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.slugSlugTypestr, 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.

  • On create, name must be present and non-blank ("Role name cannot be empty").
  • name must be unique case-insensitively (name__iexact), excluding the current row on update. A duplicate raises "Role with this name already exists".
  • Updating a role with is_system=True raises "Cannot update system roles".
  • is_system=True in the payload raises "Cannot create system roles".
  • permissions must be non-empty, otherwise "At least one permission must be assigned to the role".
  • permissions is de-duplicated (list(set(...))).

Server-side serialization behaviour

  • Write (RoleCreateSpec.perform_extra_deserialization): the validated permissions list is attached to the instance as obj.permissions, a transient attribute — permissions is in __exclude__ and isn't a model column. The view/service layer materializes it into RolePermission rows.
  • Read detail (RoleReadSpec.perform_extra_serialization): sets id = obj.external_id and fills permissions from obj.get_permissions_for_role(), the cached {name, slug, context, description} list for every active (non-temp_deleted) grant.
  • Read minimal (RoleReadMinimalSpec.perform_extra_serialization): sets id = obj.external_id and nothing else; permissions is omitted.

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.

MixinResolvesContext filter
PermissionsMixinbase — adds permissions: list[str]
PatientPermissionsMixinroles the user holds on a patientpermission__context in ("PATIENT", "FACILITY")
EncounterPermissionsMixinroles the user holds on an encounterpermission__context in ("ENCOUNTER", "PATIENT")
FacilityPermissionsMixinroles 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:

  • get_permission_sk_for_role() → list[str] — the slug of every active permission on the role. Cache key role_permissions_cache:{id}.
  • get_permissions_for_role() → list[dict] — full detail (name, slug, context, description) for every active permission. Cache key role_permissions:{id}. Used by RoleReadSpec.

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

  • Both create and update go through RoleCreateSpec. permissions is required (≥ 1) and de-duplicated; you never set is_system, and True is rejected.
  • Reads use RoleReadSpec (full, with nested PermissionSpec[]) or RoleReadMinimalSpec (no permissions), depending on the endpoint.
  • To change a role's membership, write RolePermission rows rather than editing RoleModel directly. Use the ORM so the cache-invalidation signal fires; raw SQL leaves the Redis cache stale.
  • Other access-control models (organization memberships, patient/encounter associations) reference roles to resolve a user's effective permissions in a given context.