References/Permission
More languages
More actions
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: [https://github.com/ohcnetwork/care/blob/develop/care/security/models/permission.py
care/security/models/permission.py] - Spec: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/role/spec.py
care/emr/resources/role/spec.py] (PermissionSpec) - Computed-permission mixins: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/permissions.py
care/emr/resources/permissions.py] - Registry: [https://github.com/ohcnetwork/care/blob/develop/care/security/permissions/base.py
care/security/permissions/base.py], [https://github.com/ohcnetwork/care/blob/develop/care/security/permissions/constants.pycare/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 | ||
name |
CharField(1024) |
yes | ||
description |
TextField |
no | ||
context |
CharField(1024) |
yes | ||
temp_deleted |
BooleanField |
no |
context values (PermissionContext)
context is a free-form CharField in the database, but the registry only writes one of these enum values ([https://github.com/ohcnetwork/care/blob/develop/care/security/permissions/constants.py 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 ([https://github.com/ohcnetwork/care/blob/develop/care/security/permissions/constants.py constants.py]):
| Field | Type | Maps to PermissionModel
|
Notes |
|---|---|---|---|
name |
str |
name
| |
description |
str |
description
| |
context |
PermissionContext |
context (.value)
| |
roles |
list[Role] |
— (drives RolePermission)
|
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 ([https://github.com/ohcnetwork/care/blob/develop/care/security/permissions/base.py 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 [https://github.com/ohcnetwork/care/blob/develop/care/security/management/commands/sync_permissions_roles.py 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 ([https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/base.py 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 References/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 [https://github.com/ohcnetwork/care/blob/develop/care/emr/utils/slug_type.py slug_type.py]
|
context |
str |
One of the PermissionContext values above
|
id |
UUID4 |
Added by serialize as external_id (inherited base behaviour)
|
Served by [https://github.com/ohcnetwork/care/blob/develop/care/security/api/viewsets/permissions.py 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)
[https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/permissions.py 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 References/Role.
Related
Pages that link here:
- Reference: References/Base models & conventions