Coverage for domain/rbac.py: 100%

15 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 14:06 +0000

1"""RBAC catalog + pure permission check, ported from ``arive-spec/src/domain/rbac.ts``. 

2 

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""" 

7 

8from __future__ import annotations 

9 

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) 

15 

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) 

22 

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 

34 

35_ADMIN = ["Owner", "Company Admin", "Associate Admin", "Branch Admin"] 

36_ALL_STAFF = list(ROLES) 

37 

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} 

94 

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} 

101 

102 

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 } 

108 

109 

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 )