Skip to content

User Lab Assignment

Dave Lawrence edited this page Jul 30, 2026 · 2 revisions

Classification records are created under a lab.

Lab membership is not a direct user↔lab table — a user belongs to a lab by being in a Django group whose name matches the lab's group_name field. Lab.valid_labs_qs(user) is literally "labs whose group_name is one of this user's group names".

This was primarily done for being able to assign users to labs via OIDC/Keycloak (see oidc_auth/backend.py), where the identity provider sends group membership.

Lab groups are

<institution>/<lab>

Rather than pre-define the groups, if we define the Labs via the admin tool, the groups will be automatically created. If you create a Lab with a "group_name" field of sa_pathology/familial_cancer_frome, saving it auto-creates (if they don't already exist):

  • sa_pathology — the parent institution group
  • sa_pathology/familial_cancer_frome — the lab group

then repeat for the other labs.

Now to assign people to a lab, assign them to the most specific relevant group, and each group below. e.g. if they're a lab member for Familial Cancer assign them to

  • sa_pathology/familial_cancer_frome and
  • sa_pathology

Note that the OIDC login automatically assigns to groups in the hierarchy, but for regular Django login you need to make sure you manually set it.

Via Keycloak / OIDC

Keycloak sends lab membership as groups under a top-level /associations/ tree. oidc_auth/backend.py strips the associations prefix and adds each level of nesting as its own Django group, so /associations/sa_pathology/familial_cancer_frome produces:

  • sa_pathology/familial_cancer_frome
  • sa_pathology

Groups are created on demand (we trust the OIDC connector, since it can already make admins), and groups the user no longer has are removed on each login. auto_set_default_lab() runs at the end of login, so the default lab is kept valid automatically. See Keycloak Setup.

Lab heads

Lab heads are not a group — there is a LabHead model (lab + user, unique together), managed via Django admin. It is what Lab.can_write() and Organization.can_write() check, and what lab.lab_users uses to label a member's role as "head" vs "user".

Default lab

UserSettings.default_lab is a FK used as the default when a user belongs to more than one lab. UserSettings.auto_set_default_lab() maintains it: if the current value is no longer valid for the user (they've lost the matching group) it is cleared, and if unset it is populated with the user's first valid lab. A valid existing value is left alone.

Code Examples

# requires "default_lab" be set if they have multiple labs. Raises ValueError if it can't decide
user_settings.get_lab()

# Same, but returns (lab, error_message) instead of raising
UserSettings.get_lab_and_error(user)

# Active users of a lab, each wrapped in a LabUser carrying .user and .role ('head' or 'user')
for lab_user in lab.lab_users:
    print(lab_user.user, lab_user.role)

# Just the active Users as a queryset
lab.active_users

# Return labs for a user
for lab in Lab.valid_labs_qs(user):
    pass

# Permission checks
lab.is_member(user)
lab.can_write(user)      # superuser or LabHead
lab.check_can_write(user)  # raises PermissionDenied

See also

Clone this wiki locally