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