Skip to content

Commit 2d5e13c

Browse files
committed
fix(Core): Improve IAMService user to CS conversion
- Handle nested groups - Filter groups by vo name - Filter groups by having voms.role label - Add tests
1 parent c4e89ab commit 2d5e13c

3 files changed

Lines changed: 1011 additions & 35 deletions

File tree

src/DIRAC/Core/Security/IAMService.py

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,37 +53,52 @@ def __init__(self, access_token, vo=None, forceNickname=False):
5353
self.userDict = None
5454
self.access_token = access_token
5555
self.iam_users_raw = []
56+
self.iam_groups_raw = []
57+
58+
def _getIamPagedResources(self, url):
59+
"""Get all items from IAM that are served on a paged endpoint"""
60+
all_items = []
61+
headers = {"Authorization": f"Bearer {self.access_token}"}
62+
startIndex = 1
63+
# These are just initial values, they are updated
64+
# while we loop to their actual values
65+
totalResults = 1000 # total number of users
66+
itemsPerPage = 10
67+
while startIndex <= totalResults:
68+
resp = requests.get(url, headers=headers, params={"startIndex": startIndex})
69+
resp.raise_for_status()
70+
data = resp.json()
71+
# These 2 should never change while looping
72+
# but you may have a new user appearing
73+
# while looping
74+
totalResults = data["totalResults"]
75+
itemsPerPage = data["itemsPerPage"]
76+
77+
startIndex += itemsPerPage
78+
all_items.extend(data["Resources"])
79+
return all_items
5680

5781
def _getIamUserDump(self):
5882
"""List the users from IAM"""
59-
6083
if not self.iam_users_raw:
61-
headers = {"Authorization": f"Bearer {self.access_token}"}
62-
iam_list_url = f"{self.iam_url}/scim/Users"
63-
startIndex = 1
64-
# These are just initial values, they are updated
65-
# while we loop to their actual values
66-
totalResults = 1000 # total number of users
67-
itemsPerPage = 10
68-
while startIndex <= totalResults:
69-
resp = requests.get(iam_list_url, headers=headers, params={"startIndex": startIndex})
70-
resp.raise_for_status()
71-
data = resp.json()
72-
# These 2 should never change while looping
73-
# but you may have a new user appearing
74-
# while looping
75-
totalResults = data["totalResults"]
76-
itemsPerPage = data["itemsPerPage"]
77-
78-
startIndex += itemsPerPage
79-
self.iam_users_raw.extend(data["Resources"])
84+
iam_users_url = f"{self.iam_url}/scim/Users"
85+
self.iam_users_raw = self._getIamPagedResources(iam_users_url)
8086
return self.iam_users_raw
8187

82-
def convert_iam_to_voms(self, iam_output):
88+
def _getIamGroupDump(self):
89+
"""List the groups from IAM"""
90+
if not self.iam_groups_raw:
91+
iam_group_url = f"{self.iam_url}/scim/Groups"
92+
self.iam_groups_raw = self._getIamPagedResources(iam_group_url)
93+
return self.iam_groups_raw
94+
95+
def convert_iam_to_voms(self, iam_user, iam_voms_groups):
8396
"""Convert an IAM entry into the voms style, i.e. DN based"""
8497
converted_output = {}
8598

86-
for cert in iam_output["urn:indigo-dc:scim:schemas:IndigoUser"]["certificates"]:
99+
certificates = iam_user["urn:indigo-dc:scim:schemas:IndigoUser"].get("certificates", [])
100+
101+
for cert in certificates:
87102
cert_dict = {}
88103
dn = convert_dn(cert["subjectDn"])
89104
ca = convert_dn(cert["issuerDn"])
@@ -96,46 +111,83 @@ def convert_iam_to_voms(self, iam_output):
96111
try:
97112
cert_dict["nickname"] = [
98113
attr["value"]
99-
for attr in iam_output["urn:indigo-dc:scim:schemas:IndigoUser"]["attributes"]
114+
for attr in iam_user["urn:indigo-dc:scim:schemas:IndigoUser"]["attributes"]
100115
if attr["name"] == "nickname"
101116
][0]
102117
except (KeyError, IndexError):
103118
if not self.forceNickname:
104-
cert_dict["nickname"] = iam_output["userName"]
119+
cert_dict["nickname"] = iam_user["userName"]
105120

106121
# This is not correct, we take the overall status instead of the certificate one
107122
# however there are no known case of cert suspended while the user isn't
108-
cert_dict["certSuspended"] = not iam_output["active"]
123+
cert_dict["certSuspended"] = not iam_user["active"]
109124
# There are still bugs in IAM regarding the active status vs voms suspended
110125

111-
cert_dict["suspended"] = not iam_output["active"]
126+
cert_dict["suspended"] = not iam_user["active"]
112127
# The mail may be different, in particular for robot accounts
113-
cert_dict["mail"] = iam_output["emails"][0]["value"].lower()
128+
cert_dict["mail"] = iam_user["emails"][0]["value"].lower()
114129

115130
# https://github.com/indigo-iam/voms-importer/blob/main/vomsimporter.py
116131
roles = []
117132

118-
for role in iam_output["groups"]:
119-
role_name = role["display"]
120-
if "/" in role_name:
121-
role_name = role_name.replace("/", "/Role=")
122-
roles.append(f"/{role_name}")
133+
for group in iam_user.get("groups", []):
134+
# ignore non-voms-role groups
135+
if group["value"] not in iam_voms_groups:
136+
continue
137+
138+
group_name = group["display"]
139+
140+
# filter also by selected vo
141+
if self.vo is not None and group_name.partition("/")[0] != self.vo:
142+
continue
143+
144+
role_name = IAMService._group_name_to_role_string(group_name)
145+
roles.append(role_name)
123146

124147
cert_dict["Roles"] = roles
125148
converted_output[dn] = cert_dict
126149
return converted_output
127150

151+
@staticmethod
152+
def _group_name_to_role_string(group_name):
153+
parts = group_name.split("/")
154+
# last part is the role name, need to add Role=
155+
parts[-1] = f"Role={parts[-1]}"
156+
return "/" + "/".join(parts)
157+
158+
@staticmethod
159+
def _is_voms_role(group):
160+
# labels is returned also with value None, so we cannot simply do get("labels", [])
161+
labels = group.get("urn:indigo-dc:scim:schemas:IndigoGroup", {}).get("labels")
162+
if labels is None:
163+
return False
164+
165+
for label in labels:
166+
if label["name"] == "voms.role":
167+
return True
168+
169+
return False
170+
171+
@staticmethod
172+
def _filter_voms_groups(groups):
173+
return [g for g in groups if IAMService._is_voms_role(g)]
174+
128175
def getUsers(self):
129176
"""Extract users from IAM user dump.
130177
131178
:return: dictionary of: "Users": user dictionary keyed by the user DN, "Errors": list of error messages
132179
"""
133-
self.iam_users_raw = self._getIamUserDump()
180+
iam_users_raw = self._getIamUserDump()
181+
all_groups = self._getIamGroupDump()
182+
183+
voms_groups = self._filter_voms_groups(all_groups)
184+
groups_by_id = {g["id"] for g in voms_groups}
185+
134186
users = {}
135187
errors = []
136-
for user in self.iam_users_raw:
188+
for user in iam_users_raw:
137189
try:
138-
users.update(self.convert_iam_to_voms(user))
190+
users.update(self.convert_iam_to_voms(user, groups_by_id))
139191
except Exception as e:
140192
errors.append(f"{user['name']} {e!r}")
141193
self.log.error("Could not convert", f"{user['name']} {e!r}")

0 commit comments

Comments
 (0)