Skip to content

Commit 4577d07

Browse files
authored
Merge pull request #8491 from maxnoe/voms2cs-nested-groups
Improve IAMService user to CS conversion
2 parents 07ce121 + 159b51e commit 4577d07

File tree

3 files changed

+1012
-35
lines changed

3 files changed

+1012
-35
lines changed

src/DIRAC/Core/Security/IAMService.py

Lines changed: 90 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"]["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,86 @@ 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)
146+
147+
if len(roles) == 0:
148+
raise ValueError("User must have at least one voms role")
123149

124150
cert_dict["Roles"] = roles
125151
converted_output[dn] = cert_dict
126152
return converted_output
127153

154+
@staticmethod
155+
def _group_name_to_role_string(group_name):
156+
parts = group_name.split("/")
157+
# last part is the role name, need to add Role=
158+
parts[-1] = f"Role={parts[-1]}"
159+
return "/" + "/".join(parts)
160+
161+
@staticmethod
162+
def _is_voms_role(group):
163+
# labels is returned also with value None, so we cannot simply do get("labels", [])
164+
labels = group.get("urn:indigo-dc:scim:schemas:IndigoGroup", {}).get("labels")
165+
if labels is None:
166+
return False
167+
168+
for label in labels:
169+
if label["name"] == "voms.role":
170+
return True
171+
172+
return False
173+
174+
@staticmethod
175+
def _filter_voms_groups(groups):
176+
return [g for g in groups if IAMService._is_voms_role(g)]
177+
128178
def getUsers(self):
129179
"""Extract users from IAM user dump.
130180
131181
:return: dictionary of: "Users": user dictionary keyed by the user DN, "Errors": list of error messages
132182
"""
133-
self.iam_users_raw = self._getIamUserDump()
183+
iam_users_raw = self._getIamUserDump()
184+
all_groups = self._getIamGroupDump()
185+
186+
voms_groups = self._filter_voms_groups(all_groups)
187+
groups_by_id = {g["id"] for g in voms_groups}
188+
134189
users = {}
135190
errors = []
136-
for user in self.iam_users_raw:
191+
for user in iam_users_raw:
137192
try:
138-
users.update(self.convert_iam_to_voms(user))
193+
users.update(self.convert_iam_to_voms(user, groups_by_id))
139194
except Exception as e:
140195
errors.append(f"{user['name']} {e!r}")
141196
self.log.error("Could not convert", f"{user['name']} {e!r}")

0 commit comments

Comments
 (0)