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:
- Model:
care/security/models/permission_association.py - Role spec (the granted role):
care/emr/resources/role/spec.py - Permission-resolution mixins:
care/emr/resources/permissions.py - Role definitions /
RoleContext:care/security/roles/role.py - Permission contexts:
care/security/permissions/constants.py
Models
| Model | Purpose |
|---|---|
RoleAssociation | Grants 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
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
user | FK → User | yes | — | Subject receiving the role. on_delete=CASCADE, null=False, blank=False — deleting the user removes the association |
role | FK → RoleModel | yes | — | 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
context | CharField(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_id | BigIntegerField | yes | — | Integer primary key of the context entity, not a UUID/external_id |
Lifecycle
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
expiry | DateTimeField | no | null | null=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 |
Related models
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.
| Value | Meaning |
|---|---|
FACILITY | Role applies within a facility |
GOVT_ORG | Role applies within a government organization |
ROLE_ORG | Role 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.
| Role | Contexts |
|---|---|
Doctor | FACILITY, GOVT_ORG |
Nurse | FACILITY, GOVT_ORG |
Staff | FACILITY, GOVT_ORG |
Volunteer | FACILITY, GOVT_ORG |
Pharmacist | FACILITY |
Administrator | FACILITY, GOVT_ORG |
Facility Admin | FACILITY |
Admin | FACILITY, 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 class | Role | Fields |
|---|---|---|
RoleBaseSpec | shared base (__exclude__ = ["permissions"]) | id, name, description, is_system, is_archived, contexts |
RoleCreateSpec | write · create & update | base fields + permissions: list[PermissionEnum] (≥ 1, de-duplicated) |
RoleReadSpec | read · detail/list | base fields + permissions: list[PermissionSpec] |
RoleReadMinimalSpec | read · minimal/embedded | base fields only |
PermissionSpec | nested (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.
| Mixin | Resolves (from the user's associations) | Context filter | Extra fields |
|---|---|---|---|
PermissionsMixin | base — adds permissions: list[str] | — | — |
PatientPermissionsMixin | roles the user holds on a patient | permission__context in ("PATIENT", "FACILITY") | — |
EncounterPermissionsMixin | roles the user holds on an encounter | permission__context in ("ENCOUNTER", "PATIENT") | — |
FacilityPermissionsMixin | roles on facility root + sub-orgs | none (root) / child set excludes can_update_facility | root_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
RoleAssociationis 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_idpair is a generic (non-FK) reference.contextis a free-form string naming an entity in one of theRoleContextboundary types, andcontext_idis that entity's integer PK, not itsexternal_idUUID. Pair the right type string with the matching PK; nothing at the database level enforces it. - A user may have many
RoleAssociationrows, one per (role, context) grant. Thepermissionsmixins compute effective permissions in a given context by unioning the active permission slugs of all resolved roles, filtered byPermissionContext. expiryis advisory at the model layer; do not assume the row is automatically deactivated once it passes.- The granted
roleis written and read through the role specs (RoleCreateSpec/RoleReadSpec), and each role'scontextsis bound to theRoleContextenum rather than an open string at the API layer.
Related
- Reference: Role
- Reference: Permission
- Reference: User
- Reference: Organization
- Reference: Base model
- Source: permission_association.py on GitHub
- Source: role/spec.py on GitHub
- Source: permissions.py on GitHub