Skip to main content
Version: 3.0

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

ModelPurpose
PermissionModelA single, named permission (action) that can be granted to a user in a context

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

FieldTypeNotes
external_idUUIDFielddefault=uuid4, unique, indexed. Opaque public identifier — never expose the integer pk
created_dateDateTimeFieldauto_now_add; nullable, indexed
modified_dateDateTimeFieldauto_now; nullable, indexed
deletedBooleanFielddefault=False, indexed. Soft-delete flag; the default manager hides deleted=True rows

PermissionModel fields

FieldTypeRequiredDefaultNotes
slugCharField(1024)yesunique, 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
nameCharField(1024)yesDisplay name (e.g. "Can Create Patient"), copied from the registry Permission.name
descriptionTextFieldno""Free text from the registry Permission.description; often empty
contextCharField(1024)yesResource context the permission applies to. No DB choices, but the sync command only writes a PermissionContext value (from Permission.context.value), listed below
temp_deletedBooleanFieldnoFalseStaging 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):

ValueMeaning
GENERICNot scoped to a specific resource type
FACILITYScoped to a facility
PATIENTScoped to a patient
QUESTIONNAIREScoped to a questionnaire
ORGANIZATIONScoped to a (govt/role) organization
FACILITY_ORGANIZATIONScoped to a facility organization
ENCOUNTERScoped 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.

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

FieldTypeMaps to PermissionModelNotes
namestrnameDisplay name
descriptionstrdescriptionFree text
contextPermissionContextcontext (.value)One of the enum values above
roleslist[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:

  • get_permissions() → dict[slug, Permission] — the full declared registry (cached).
  • get_enum() → Enum — a dynamically built str enum of all permission slugs, used by RoleCreateSpec to constrain the permissions write field to known slugs.

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 classRoleFileNotes
PermissionSpecread · list + detailrole/spec.pyThe only permission-facing spec. Served read-only by PermissionViewSet
PermissionsMixinread augmentationpermissions.pyAdds 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:

FieldTypeNotes
namestrFrom PermissionModel.name
descriptionstrFrom PermissionModel.description
slugSlugTypestr, 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
contextstrOne of the PermissionContext values above
idUUID4Added 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:

MixinOutput fieldsResolution
PatientPermissionsMixinpermissionsRoles on the patient; permission slugs where context in ["PATIENT", "FACILITY"]
EncounterPermissionsMixinpermissionsRoles on the encounter; permission slugs where context in ["ENCOUNTER", "PATIENT"]
FacilityPermissionsMixinpermissions, root_org_permissions, child_org_permissionsUnion 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

  • Permissions are platform-maintained reference data: declared in the registry, synced into this table by sync_permissions_roles, read-only over the API. PermissionViewSet exposes list + retrieve only.
  • Reference a permission by slug — it is stable, unique, and the API lookup_field. Treat external_id as the opaque public ID; never expose the integer pk.
  • You can't grant a permission directly to a user. A role collects permissions (via RolePermission), and a permission association binds that role to a resource — organization, patient, encounter, and so on. Effective access is the union of active permissions across a user's role bindings.
  • context has no DB choices but is always a PermissionContext enum string. The mixins filter resource permissions by context, so the value matters.
  • When you write a role, PermissionController.get_enum() constrains the permissions field to known slugs; an unknown slug fails validation. See Role.