Coverage for domain/rbac.py: 100%
15 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
1"""RBAC catalog + pure permission check, ported from ``arive-spec/src/domain/rbac.ts``.
3The role/permission matrix lives here as pure data (the seed source and the parity
4reference). Runtime effective-permission resolution is DB-backed in
5``apps/accounts/auth.py``; this module is the source of truth the seeder loads.
6"""
8from __future__ import annotations
10ROLES: tuple[str, ...] = (
11 "Owner", "Company Admin", "Associate Admin", "Branch Admin", "Loan Officer",
12 "Loan Officer Assistant", "Processor", "Assistant Processor", "Contract Processor Admin",
13 "Funder", "Disclosure Desk", "Payroll", "Lock Desk", "Closer", "Post Closer",
14)
16# Coarse CRUD permissions added for API gating (the oracle gates edits by scenario;
17# these give the loan endpoints a simple permission to declare).
18CRUD_PERMISSIONS: tuple[str, ...] = (
19 "LOAN_VIEW", "LOAN_CREATE", "LOAN_EDIT", "CONTACT_VIEW", "CONTACT_EDIT",
20 "LEAD_VIEW", "LEAD_EDIT", "DOCUMENT_CLASSIFICATION_VIEW", "DOCUMENT_CLASSIFICATION_EDIT",
21)
23ORACLE_PERMISSIONS: tuple[str, ...] = (
24 "ALLOW_EDIT_ON_READ_ONLY_LOAN", "COC_READ_ONLY", "EDIT_LOAN_BROKER_CHECK_RECEIVED",
25 "EDIT_LOAN_FINALISED", "EDIT_LOAN_ADVERSE", "EDIT_LOAN_FUNDED", "MODIFY_LOAN_PROCESSOR",
26 "REVIEW_LOAN_PROCESSOR", "ALLOW_DOC_CENTER_MOD_READ_ONLY", "EDIT_LEAD_DETAILS",
27 "ALLOW_LOAN_ARCHIVE", "FINALIZE_DOCUMENTS", "MANAGE_MCR_STATE_REPORTS",
28 "ALLOW_EDIT_READ_ONLY", "PRIVATE_FOLDER", "MANAGE_USER_ACCOUNT", "EDIT_PREAPPROVAL_LETTER",
29 "ROCKET_SSO", "EDIT_ORG_POS_APP_CONFIG", "EDIT_DOCMAGIC_CREDENTIALS",
30 "EDIT_CREDIT_CREDENTIALS", "EDIT_AUS_CREDENTIALS", "EDIT_USER_POS_APP_CONFIG",
31 "SHOW_BUSINESS_CONTACTS", "VIEW_ALL_PIPELINE", "EDIT_USER_LICENSES",
32)
33PERMISSIONS: tuple[str, ...] = CRUD_PERMISSIONS + ORACLE_PERMISSIONS
35_ADMIN = ["Owner", "Company Admin", "Associate Admin", "Branch Admin"]
36_ALL_STAFF = list(ROLES)
38# Which roles hold each permission (allowedRoles, spec §8.3), plus coarse CRUD.
39ROLE_PERMISSIONS: dict[str, list[str]] = {
40 "LOAN_VIEW": _ALL_STAFF,
41 "LOAN_CREATE": [*_ADMIN, "Loan Officer"],
42 "LOAN_EDIT": [
43 *_ADMIN, "Loan Officer", "Loan Officer Assistant", "Processor", "Assistant Processor",
44 ],
45 # No oracle/spec reference defines these; mirrors LOAN_VIEW/LOAN_EDIT's already-validated
46 # split (add-contacts-api design.md D1).
47 "CONTACT_VIEW": _ALL_STAFF,
48 "CONTACT_EDIT": [
49 *_ADMIN, "Loan Officer", "Loan Officer Assistant", "Processor", "Assistant Processor",
50 ],
51 # Same reasoning as CONTACT_VIEW/CONTACT_EDIT above (add-leads-api design.md D4) —
52 # no oracle/spec reference defines these; EDIT_LEAD_DETAILS is a different concern
53 # (lead-form config, not lead CRUD).
54 "LEAD_VIEW": _ALL_STAFF,
55 "LEAD_EDIT": [
56 *_ADMIN, "Loan Officer", "Loan Officer Assistant", "Processor", "Assistant Processor",
57 ],
58 # Unlike CONTACT_EDIT/LEAD_EDIT, _EDIT is admin-only: adding a classification
59 # changes a shared dropdown for every other staff member, not routine
60 # individual-staff work (add-document-classification-db-config design.md).
61 "DOCUMENT_CLASSIFICATION_VIEW": _ALL_STAFF,
62 "DOCUMENT_CLASSIFICATION_EDIT": _ADMIN,
63 "ALLOW_EDIT_ON_READ_ONLY_LOAN": [
64 *_ADMIN, "Loan Officer", "Loan Officer Assistant", "Processor", "Assistant Processor",
65 ],
66 "COC_READ_ONLY": [
67 "Associate Admin", "Branch Admin", "Loan Officer", "Loan Officer Assistant", "Processor",
68 ],
69 "EDIT_LOAN_BROKER_CHECK_RECEIVED": _ADMIN,
70 "EDIT_LOAN_FINALISED": _ADMIN,
71 "EDIT_LOAN_ADVERSE": [*_ADMIN, "Loan Officer", "Processor"],
72 "EDIT_LOAN_FUNDED": [*_ADMIN, "Processor"],
73 "MODIFY_LOAN_PROCESSOR": _ADMIN,
74 "REVIEW_LOAN_PROCESSOR": _ADMIN,
75 "ALLOW_DOC_CENTER_MOD_READ_ONLY": [*_ADMIN, "Loan Officer", "Processor"],
76 "EDIT_LEAD_DETAILS": [*_ADMIN, "Loan Officer"],
77 "ALLOW_LOAN_ARCHIVE": _ADMIN,
78 "FINALIZE_DOCUMENTS": [*_ADMIN, "Disclosure Desk", "Closer"],
79 "MANAGE_MCR_STATE_REPORTS": [*_ADMIN, "Payroll"],
80 "ALLOW_EDIT_READ_ONLY": _ADMIN,
81 "PRIVATE_FOLDER": [*_ADMIN, "Funder", "Disclosure Desk", "Lock Desk"],
82 "MANAGE_USER_ACCOUNT": _ADMIN,
83 "EDIT_PREAPPROVAL_LETTER": [*_ADMIN, "Loan Officer"],
84 "ROCKET_SSO": _ADMIN,
85 "EDIT_ORG_POS_APP_CONFIG": _ADMIN,
86 "EDIT_DOCMAGIC_CREDENTIALS": _ADMIN,
87 "EDIT_CREDIT_CREDENTIALS": _ADMIN,
88 "EDIT_AUS_CREDENTIALS": _ADMIN,
89 "EDIT_USER_POS_APP_CONFIG": ["Loan Officer"],
90 "SHOW_BUSINESS_CONTACTS": [], # org-wide toggle; empty = per-loan only
91 "VIEW_ALL_PIPELINE": [*_ADMIN, "Processor", "Funder"],
92 "EDIT_USER_LICENSES": _ADMIN,
93}
95# Per-permission metadata (spec §8.3).
96_NO_BRANCH_OVERRIDE = {"MANAGE_USER_ACCOUNT", "EDIT_USER_LICENSES", "MANAGE_MCR_STATE_REPORTS"}
97_ADMIN_ROLE_CHECK = {
98 "MANAGE_USER_ACCOUNT", "EDIT_USER_LICENSES", "MODIFY_LOAN_PROCESSOR",
99 "REVIEW_LOAN_PROCESSOR", "ALLOW_LOAN_ARCHIVE",
100}
103def permission_meta(code: str) -> dict:
104 return {
105 "branch_override": code not in _NO_BRANCH_OVERRIDE,
106 "admin_role_check_reqd": code in _ADMIN_ROLE_CHECK,
107 }
110def has_permission(
111 role_bindings: list[tuple[str, str | None]],
112 permission: str,
113 org_unit_id: str | None = None,
114) -> bool:
115 """Pure check: role_bindings is a list of (role, org_unit_id); ``None``/"HQ" org = org-wide."""
116 allowed = ROLE_PERMISSIONS.get(permission, [])
117 return any(
118 role in allowed and (org_unit_id is None or bound_org in (org_unit_id, None, "HQ"))
119 for role, bound_org in role_bindings
120 )