Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

References/Permission Association

From OHC Network Wiki
Revision as of 04:13, 5 July 2026 by Admin (talk | contribs) (OHC identity seed)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
referenceaccess-governanceCARE 3.0+

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

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
role FK → RoleModel yes

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_id BigIntegerField yes

Lifecycle

Field Type Required Default Notes
expiry DateTimeField no

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 (References/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 (References/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 References/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

  • 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.

Pages that link here: