Skip to main content
Version: 3.1

Permission Association

RoleAssociation grants a RoleModel to a User inside a specific context — an organization or facility. A role is a named collection of permissions; the association scopes that collection to one user in one context. A user can hold many roles across different contexts, one association per grant.

The Django model (care/security/models/permission_association.py) is the storage layer: the user/role foreign keys, the generic context/context_id pair, and expiry. It has no resource spec of its own — it is a platform-internal authorization join, not a client-writable EMR resource. Two sets of Pydantic resource specs surround it: those that govern the role it points at (care/emr/resources/role/spec.py), and the permission-resolution mixins (care/emr/resources/permissions.py) that read these rows to compute a user's effective permissions in a context.

Source:

Models​

ModelPurpose
RoleAssociationGrants a RoleModel to a User within a named context, with optional expiry

RoleAssociation extends BaseModel — the lightweight Care base providing external_id (UUID), created_date, modified_date, and soft-delete via deleted (the overridden delete() sets deleted=True instead of removing the row). See Base model.

It does not extend EMRBaseModel, so there are no created_by / updated_by audit fields, no history/meta JSON, and no external_id-routed EMR API. There is no RoleAssociationCreateSpec/...ReadSpec; rows are created and removed through Care's access-control flows, not through a Pydantic serializer.

RoleAssociation fields​

Subject and role​

FieldTypeRequiredDefaultNotes
userFK → Useryes—Subject receiving the role. on_delete=CASCADE, null=False, blank=False — deleting the user removes the association
roleFK → RoleModelyes—Role (set of permissions) being granted. on_delete=CASCADE, null=False, blank=False. Read/write through the role specs — see Role

Context​

The context identifies where the role applies. Storing it as a type/id pair rather than a typed foreign key lets one table scope roles to any kind of context — organization, facility, and so on. The pair has no DB-level referential integrity, so nothing stops context_id from pointing at a row that no longer exists.

FieldTypeRequiredDefaultNotes
contextCharField(1024)yes—Context type — the name of the entity the role is scoped to. Free-form string at the model layer, not bound to an enum
context_idBigIntegerFieldyes—Integer primary key of the context entity, not a UUID/external_id

Lifecycle​

FieldTypeRequiredDefaultNotes
expiryDateTimeFieldnonullnull=True, blank=True. A single timestamp after which the grant is meant to lapse — not a PeriodSpec/start–end range. Stored but not enforced by the model

The granted role resolves to permissions through the models below. None belong to RoleAssociation, but every effective-permission lookup walks through them.

RoleModel​

The granted role (Role). A named, flat set of permissions; is_system roles are platform-seeded and cannot be edited through the API.

name → CharField(1024) # unique among non-deleted rows
description → TextField (default "")
is_system → BooleanField (default False)
is_archived → BooleanField (default False)
temp_deleted→ BooleanField (default False)
contexts → ArrayField[CharField(24)] (default []) # bound to RoleContext in specs

RolePermission​

Join table linking one RoleModel to one PermissionModel. A role accumulates permissions across many RolePermission rows; lookups exclude temp_deleted=True.

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

PermissionModel​

The atomic permission a role grants (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)

Enum / value tables​

RoleAssociation stores plain strings and integers, but these enums constrain the role it points at and the permissions that role resolves to.

RoleContext values​

Each element of the granted role's contexts array is validated against this enum (care/security/roles/role.py). These are the organizational boundary types a role can apply to — distinct from PermissionContext. The free-form RoleAssociation.context column names an entity of one of these boundary types.

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 takes one of these values (care/security/permissions/constants.py). When computing effective permissions from RoleAssociation rows, the permission-resolution mixins (below) filter a user's grants by these contexts.

Value
GENERIC
FACILITY
PATIENT
QUESTIONNAIRE
ORGANIZATION
FACILITY_ORGANIZATION
ENCOUNTER

PermissionEnum (role write field)​

When a role is written through RoleCreateSpec, its permissions field is typed against PermissionController.get_enum() — a str enum built at runtime from every registered permission name across all permission handlers (for example can_create_patient, can_write_patient, can_list_patients, can_view_clinical_data). The set is deployment-dependent: internal handlers plus any plugin-registered ones, not a fixed list.

Built-in (is_system) roles​

Seeded by RoleController (care/security/roles/role.py). Associations often bind a user to one of these. They cannot 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)​

RoleAssociation has no dedicated ...CreateSpec/...UpdateSpec/...ListSpec/...RetrieveSpec — it is not exposed as an EMR resource. Its Pydantic surface is twofold: the specs that define the role an association grants, and the mixins that consume associations to expose a user's effective permissions on another resource. All extend EMRResource (care/emr/resources/base.py), which provides serialize (DB → Pydantic via perform_extra_serialization) and de_serialize (Pydantic → DB via perform_extra_deserialization).

Specs for the granted role​

Spec classRoleFields
RoleBaseSpecshared base (__exclude__ = ["permissions"])id, name, description, is_system, is_archived, contexts
RoleCreateSpecwrite · create & updatebase fields + permissions: list[PermissionEnum] (≥ 1, de-duplicated)
RoleReadSpecread · detail/listbase fields + permissions: list[PermissionSpec]
RoleReadMinimalSpecread · minimal/embeddedbase fields only
PermissionSpecnested (read)name, description, slug (SlugType), context

RoleBaseSpec.contexts is list[RoleContext] — bound to the RoleContext enum at the API layer even though the model column is an open string array. PermissionSpec.slug is a SlugType: str, length 5–50, pattern ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$. Core role validation (RoleCreateSpec.validate_role, mode="after") requires a non-blank unique name (name__iexact), rejects is_system=True, rejects updating a system role, and requires ≥ 1 permission. See Role for the full spec breakdown.

Permission-resolution mixins (care/emr/resources/permissions.py)​

Other resource specs (patient, encounter, facility) inherit these mixins. When serialize runs with an authenticated user, perform_extra_user_serialization walks the user's RoleAssociation rows for that object, resolves the granted roles to permissions, and writes them into mapping["permissions"]. This is the read path that turns stored associations into a permission list on the wire.

MixinResolves (from the user's associations)Context filterExtra fields
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-orgsnone (root) / child set excludes can_update_facilityroot_org_permissions, child_org_permissions

Permission slugs are resolved via RolePermission.objects.filter(role_id__in=roles, …).values_list("permission__slug", flat=True), so only active (non-temp_deleted) grants on the resolved roles count.

Methods & save behaviour​

RoleAssociation defines no model methods, validators, or save()/delete() overrides of its own. It inherits soft-delete from BaseModel: calling delete() sets deleted=True and persists with save(update_fields=["deleted"]), and the default manager filters out soft-deleted rows.

A source TODO flags a planned composite index on user, context, and context_id that does not yet exist; lookups by those fields are not index-backed today.

expiry is a stored timestamp only — the model does not enforce it. Expiry-based revocation falls to the authorization layer that reads these associations.

RoleAssociation has no perform_extra_serialization / perform_extra_deserialization, since it has no spec. The serialization hooks that matter live on the role specs and the permission mixins above.

API integration notes​

  • RoleAssociation is a platform-maintained authorization record, not a client-writable EMR resource. It is created and removed through Care's access-control flows, not through FHIR, a Pydantic spec, or direct REST writes.
  • The context / context_id pair is a generic (non-FK) reference. context is a free-form string naming an entity in one of the RoleContext boundary types, and context_id is that entity's integer PK, not its external_id UUID. Pair the right type string with the matching PK; nothing at the database level enforces it.
  • A user may have many RoleAssociation rows, one per (role, context) grant. The permissions mixins compute effective permissions in a given context by unioning the active permission slugs of all resolved roles, filtered by PermissionContext.
  • expiry is advisory at the model layer; do not assume the row is automatically deactivated once it passes.
  • The granted role is written and read through the role specs (RoleCreateSpec / RoleReadSpec), and each role's contexts is bound to the RoleContext enum rather than an open string at the API layer.