Repository navigation
Expand file tree
/
Copy pathrole_groups.py
More file actions
154 lines (126 loc) · 5.87 KB
/
Copy pathrole_groups.py
File metadata and controls
154 lines (126 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""Read the Role Group matrix from the minimum fields spreadsheet (min_field_matrix.xlsx).
Columns S onwards of the first sheet, up to the first empty header, are Role
Groups: row 1 holds the group's name (e.g. S1 = Operator), and the cell where a
group's column crosses an `Entity: <name>` row says which of that entity's
record endpoints the group may call:
C create -> CanCreateDynamicEntityRecord_<entity>
R read -> CanGetDynamicEntityRecord_<entity> (GET list and GET one)
U update -> CanUpdateDynamicEntityRecord_<entity>
D delete -> CanDeleteDynamicEntityRecord_<entity>
Any combination may be written (R, CR, CRUD, ...); empty means no access. Cells
on field rows are ignored: access is per entity, not per field.
Every group also holds MEMBER_ROLES, so any user set up through the groups can
list (GET) the dynamic entity definitions at the space's bank id.
No side effects on import (no login), so offline scripts can use it too.
"""
import re
import pandas as pd
from obp_space import ROLE_BANK_ID
END_MARKER = "END_OF_FILE"
# Column S. The sheet says not to move columns, so the matrix starts here.
FIRST_GROUP_COLUMN = 18
# The header of the column just before the matrix (R). If it isn't there, columns have
# been inserted or removed and the matrix is not where FIRST_GROUP_COLUMN says, so
# nothing is read rather than a wrong column being taken for a group.
COLUMN_BEFORE_GROUPS = "HideFieldFromPublicAccess"
ROLE_PREFIX_BY_LETTER = {
"C": "CanCreateDynamicEntityRecord_",
"R": "CanGetDynamicEntityRecord_",
"U": "CanUpdateDynamicEntityRecord_",
"D": "CanDeleteDynamicEntityRecord_",
}
ACCESS_ORDER = "CRUD"
# The Roles that create_role_groups.py and add_users_to_groups.py need, as
# (role_name, bank_id): the group Roles at the bank id of the space, the user
# lookup and group-member listing at system level (empty bank id).
GROUP_ADMIN_ROLES = [
("CanCreateGroupAtOneBank", ROLE_BANK_ID),
("CanUpdateGroupAtOneBank", ROLE_BANK_ID),
("CanGetGroupsAtOneBank", ROLE_BANK_ID),
("CanAddUserToGroupAtOneBank", ROLE_BANK_ID),
("CanRemoveUserFromGroupAtOneBank", ROLE_BANK_ID),
("CanGetEntitlementsForAnyBank", ""),
("CanGetAnyUser", ""),
("CanDeleteEntitlementAtAnyBank", ""),
]
def column_letter(idx):
"""0-based column index -> Excel letter(s), e.g. 18 -> S."""
letters = ""
idx += 1
while idx:
idx, rem = divmod(idx - 1, 26)
letters = chr(ord("A") + rem) + letters
return letters
def roles_for(entity_name, access):
"""Role names for `access` (e.g. 'CRUD') on one entity, in C R U D order."""
return [ROLE_PREFIX_BY_LETTER[c] + entity_name for c in ACCESS_ORDER if c in access]
def _group_columns(df):
"""[(index, name)] of the group columns: from S, until the first empty header.
pandas names an empty header 'Unnamed: N'."""
columns = []
for idx in range(FIRST_GROUP_COLUMN, len(df.columns)):
header = str(df.columns[idx]).strip()
if not header or header.startswith("Unnamed:"):
break
columns.append((idx, header))
return columns
def parse_role_groups(file_path):
"""Return (groups, errors, warnings).
groups is a list of {"name", "column", "access": {entity: "CRUD"-subset}}, in
sheet order. Each problem is a string starting with its Excel cell. Cells that
are errors are left out of `access`.
"""
df = pd.read_excel(file_path, engine="openpyxl")
errors, warnings = [], []
before = column_letter(FIRST_GROUP_COLUMN - 1)
header_before = str(df.columns[FIRST_GROUP_COLUMN - 1]).strip() if len(df.columns) >= FIRST_GROUP_COLUMN else ""
if header_before != COLUMN_BEFORE_GROUPS:
errors.append(f"{before}1: expected the header {COLUMN_BEFORE_GROUPS!r} just before the Role Group matrix "
f"(which starts at {column_letter(FIRST_GROUP_COLUMN)}), found {header_before!r}. Columns have moved; "
f"update FIRST_GROUP_COLUMN in role_groups.py. No Role Groups were read.")
return [], errors, warnings
columns = _group_columns(df)
groups = [{"name": name, "column": column_letter(idx), "access": {}} for idx, name in columns]
seen_names = {}
for group in groups:
if group["name"] in seen_names:
errors.append(f"{group['column']}1: group {group['name']!r} is already defined at "
f"{seen_names[group['name']]}1")
seen_names.setdefault(group["name"], group["column"])
current = None
for index, row in df.iterrows():
excel_row = index + 2 # header is row 1
col_a = "" if pd.isna(row.iloc[0]) else str(row.iloc[0]).strip()
if col_a == END_MARKER:
break
is_entity_row = col_a.lower().startswith("entity:")
if is_entity_row:
current = col_a[7:].strip()
for (idx, _), group in zip(columns, groups):
value = row.iloc[idx]
if pd.isna(value) or not str(value).strip():
continue
cell = f"{group['column']}{excel_row}"
raw = str(value).strip()
if not is_entity_row:
warnings.append(f"{cell}: {raw!r} for group {group['name']!r} is not on an `Entity:` row "
"(ignored; access is set per entity)")
continue
letters = re.sub(r"\s+", "", raw)
if not re.fullmatch(r"[CRUDcrud]+", letters):
errors.append(f"{cell}: {raw!r} for group {group['name']!r} on {current} "
"may only contain the letters C R U D (ignored)")
continue
if letters != letters.upper():
warnings.append(f"{cell}: {raw!r} for group {group['name']!r} on {current} "
f"should be written {letters.upper()!r}")
letters = letters.upper()
if len(set(letters)) != len(letters):
warnings.append(f"{cell}: {raw!r} for group {group['name']!r} on {current} repeats a letter")
group["access"][current] = "".join(c for c in ACCESS_ORDER if c in letters)
return groups, errors, warnings
# Held by every group, whatever its column ticks: listing the entity definitions.
MEMBER_ROLES = ["CanGetDynamicEntityDefinitions"]
def group_roles(group):
"""All the Role names of one parsed group, entity by entity, then MEMBER_ROLES."""
return [role for entity, access in group["access"].items() for role in roles_for(entity, access)] + MEMBER_ROLES