Skip to main content
Version: 3.0

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 → UseryesSubject receiving the role. on_delete=CASCADE, null=False, blank=False — deleting the user removes the association
roleFK → RoleModelyesRole (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)yesContext type — the name of the entity the role is scoped to. Free-form string at the model layer, not bound to an enum
context_idBigIntegerFieldyesInteger 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.