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:
- Model:
care/security/models/permission.py - Spec:
care/emr/resources/role/spec.py(PermissionSpec) - Computed-permission mixins:
care/emr/resources/permissions.py - Registry:
care/security/permissions/base.py,care/security/permissions/constants.py
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 Care 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:
get_permissions() → dict[slug, Permission]— the full declared registry (cached).get_enum() → Enum— a dynamically builtstrenum of all permission slugs, used byRoleCreateSpecto constrain thepermissionswrite 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:
- Mark every
PermissionModeltemp_deleted=True. - For each declared permission, upsert by
slug— settingname,description,context(fromPermission.context.value) and clearingtemp_deleted. - Hard-delete any row still
temp_deleted=True(no longer declared). - Upsert system roles, then rebuild
RolePermissionrows from eachPermission.roleslist, 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
- Permissions are platform-maintained reference data: declared in the registry, synced into this table by
sync_permissions_roles, read-only over the API.PermissionViewSetexposes list + retrieve only. - Reference a permission by
slug— it is stable,unique, and the APIlookup_field. Treatexternal_idas the opaque public ID; never expose the integerpk. - 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. contexthas no DBchoicesbut is always aPermissionContextenum string. The mixins filter resource permissions by context, so the value matters.- When you write a role,
PermissionController.get_enum()constrains thepermissionsfield to known slugs; an unknown slug fails validation. See Role.
Related
- Reference: Role
- Reference: Permission association
- Reference: User
- Reference: Patient
- Reference: Encounter
- Reference: Facility
- Reference: Base models & conventions
- Source: permission.py on GitHub
- Source: role/spec.py on GitHub (
PermissionSpec) - Source: resources/permissions.py on GitHub