Multi-tenant isolation¶
Status: Placeholder — to be developed. Last reviewed: — Reference structural sibling:
guidelines/i18n/translation-rules.md(single hard rule + workflow + antipatterns style — not component-style).
Scope (when this guideline lands)¶
How practice-scoping works and the rules that prevent data leaks across practices: UserPracticeAccess model, the active-practice context (session? middleware? URL?), required queryset filtering on every view that reads practice-scoped data, how new models declare their practice FK, audit checklist for any view that does not filter by practice.
This is high blast radius — a missed filter exposes another practice's patient data. Of the unwritten guidelines, this is the one with the most "if we don't enforce this, real harm happens" weight.
Out of scope (cross-refs)¶
- Authentication / login flow / view-level access (which views a user can REACH) →
guidelines/security/auth-and-permissions.md(placeholder). This file covers WHICH ROWS a user can see, given they've reached the view. - Data-content sensitivity / logging redaction →
guidelines/security/pii-and-logging.md(placeholder). - Admin practice scoping (the demoted superuser-only admin) →
guidelines/backend/admin.md(placeholder). - Model FK declaration patterns →
guidelines/backend/models.md(placeholder); this file owns the which models need a practice FK policy.
Sources to mine when writing this¶
apps/accounts/models.py— customUser,UserPracticeAccessmodel and its enforcement points.apps/accounts/middleware.py(or wherever the active-practice context lives) — how the request knows the current practice.apps/core/mixins.py—MultiPracticeFilterMixinand any other practice-scoping mixins.- Existing list/detail views — enumerate the queryset-filtering pattern (mixin? base view? manual?).
- Models with a
practiceFK vs models without one — document the rule for which is which. - Any past data-leak incident or near-miss (check
roadmap/done/for security-related items; checkgit log --all --grep="leak\|cross-practice\|isolation").
Starter hard rules to investigate¶
- Every queryset on a practice-scoped model MUST filter by current practice. Period. No exceptions for "internal" views.
- Filtering is mixin-driven, not manual. Use
MultiPracticeFilterMixin(or whatever the canonical mixin is) on every view. Manual.filter(practice=request.user.practice)is a smell — easy to forget. - Every new model declares whether it's practice-scoped. Either has a
practiceFK or has a comment explaining why it's global. - Cross-practice operations (e.g. group-level reports) require explicit
MultiPracticeFilterMixinconfig or an admin-only view. - The active practice is set once per request by middleware, not derived ad-hoc in views.
Decision points to settle¶
- Where the active practice context lives: session? middleware-attached request attribute? URL? Pick one canonical mechanism.
- What counts as practice-scoped: directly (
practiceFK) vs transitively (patient.practicevia FK chain). Define the rule. - Fail-open vs fail-closed: when the active practice is unset (e.g. mid-login), do queries return [] or raise? Pick fail-closed.
- Direct-URL access to a record outside the user's practice scope: 404 (don't reveal existence) or 403 (acknowledge but deny)? Coordinate with
ux/navigation.md. - Multi-practice users (group admins): how is the "active" practice picked when a user has access to several?
Known deviations to look for during writing¶
- Views with
Model.objects.all()orModel.objects.filter(...)without practice scoping where the model has apracticeFK. - Models with a
practiceFK that'snull=Truefor unclear reasons (potential cross-practice records). - Admin queries that return all practices regardless of user.
- API endpoints in
apps/websites/— how do they handle multi-tenant scoping vs the website views' mixin pattern?
If found, file IMMEDIATELY as a backlog item with Area: security | Effort: ? — do NOT wait for the guideline to land. Cross-practice leaks are P0.