-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (VOMS2IAM): add options to sync from IAM
- Loading branch information
Showing
6 changed files
with
270 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,12 +25,15 @@ | |
corresponding options defined in the ``/Registry/VO/<VO_name>`` configuration section. | ||
""" | ||
|
||
|
||
from DIRAC import S_OK, gConfig, S_ERROR | ||
from DIRAC.Core.Base.AgentModule import AgentModule | ||
from DIRAC.Core.Utilities.Proxy import executeWithUserProxy | ||
from DIRAC.ConfigurationSystem.Client.Helpers.Registry import getVOOption, getUserOption | ||
from DIRAC.ConfigurationSystem.Client.VOMS2CSSynchronizer import VOMS2CSSynchronizer | ||
from DIRAC.FrameworkSystem.Client.NotificationClient import NotificationClient | ||
from DIRAC.FrameworkSystem.Client.TokenManagerClient import gTokenManager | ||
from DIRAC.Resources.Catalog.FileCatalog import FileCatalog | ||
|
||
|
||
|
@@ -50,6 +53,8 @@ def __init__(self, *args, **kwargs): | |
self.autoLiftSuspendedStatus = True | ||
self.mailFrom = "[email protected]" | ||
self.syncPluginName = None | ||
self.compareWithIAM = False | ||
self.useIAM = False | ||
|
||
def initialize(self): | ||
"""Initialize the default parameters""" | ||
|
@@ -63,6 +68,8 @@ def initialize(self): | |
self.autoLiftSuspendedStatus = self.am_getOption("AutoLiftSuspendedStatus", self.autoLiftSuspendedStatus) | ||
self.makeFCEntry = self.am_getOption("MakeHomeDirectory", self.makeFCEntry) | ||
self.syncPluginName = self.am_getOption("SyncPluginName", self.syncPluginName) | ||
self.compareWithIAM = self.am_getOption("CompareWithIAM", self.compareWithIAM) | ||
self.useIAM = self.am_getOption("UseIAM", self.useIAM) | ||
|
||
self.detailedReport = self.am_getOption("DetailedReport", self.detailedReport) | ||
self.mailFrom = self.am_getOption("MailFrom", self.mailFrom) | ||
|
@@ -95,13 +102,31 @@ def execute(self): | |
autoLiftSuspendedStatus = getVOOption(vo, "AutoLiftSuspendedStatus", self.autoLiftSuspendedStatus) | ||
syncPluginName = getVOOption(vo, "SyncPluginName", self.syncPluginName) | ||
|
||
compareWithIAM = getVOOption(vo, "CompareWithIAM", self.compareWithIAM) | ||
useIAM = getVOOption(vo, "UseIAM", self.useIAM) | ||
|
||
accessToken = None | ||
if compareWithIAM or useIAM: | ||
res = gTokenManager.getToken( | ||
userGroup=voAdminGroup, | ||
requiredTimeLeft=3600, | ||
scope=["scim:read"], | ||
) | ||
if not res["OK"]: | ||
return res | ||
|
||
accessToken = res["Value"]["access_token"] | ||
|
||
vomsSync = VOMS2CSSynchronizer( | ||
vo, | ||
autoAddUsers=autoAddUsers, | ||
autoModifyUsers=autoModifyUsers, | ||
autoDeleteUsers=autoDeleteUsers, | ||
autoLiftSuspendedStatus=autoLiftSuspendedStatus, | ||
syncPluginName=syncPluginName, | ||
compareWithIAM=compareWithIAM, | ||
useIAM=useIAM, | ||
accessToken=accessToken, | ||
) | ||
|
||
result = self.__syncCSWithVOMS( # pylint: disable=unexpected-keyword-arg | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
""" IAMService class encapsulates connection to the IAM service for a given VO | ||
""" | ||
|
||
import requests | ||
|
||
from DIRAC import gConfig, gLogger, S_OK, S_ERROR | ||
from DIRAC.Core.Utilities import DErrno | ||
from DIRAC.Core.Security.Locations import getProxyLocation, getCAsLocation | ||
from DIRAC.Core.Utilities.Decorators import deprecated | ||
from DIRAC.ConfigurationSystem.Client.Helpers.Registry import getVOOption | ||
from DIRAC.ConfigurationSystem.Client.Helpers.CSGlobals import getVO | ||
|
||
|
||
def convert_dn(inStr): | ||
"""Convert a string separated DN into the slash one, like | ||
CN=Christophe Haen,CN=705305,CN=chaen,OU=Users,OU=Organic Units,DC=cern,DC=ch | ||
/DC=ch/DC=cern/OU=Organic Units/OU=Users/CN=chaen/CN=705305/CN=Christophe Haen | ||
""" | ||
return "/" + "/".join(inStr.split(",")[::-1]) | ||
|
||
|
||
class IAMService: | ||
def __init__(self, access_token, vo=None): | ||
"""c'tor | ||
:param str vo: name of the virtual organization (community) | ||
:param str access_token: the token used to talk to IAM, with the scim:read property | ||
""" | ||
|
||
if not access_token: | ||
raise ValueError("access_token not set") | ||
|
||
if vo is None: | ||
vo = getVO() | ||
if not vo: | ||
raise Exception("No VO name given") | ||
|
||
self.vo = vo | ||
|
||
self.iam_url = None | ||
|
||
id_provider = gConfig.getValue(f"/Registry/VO/{self.vo}/IdProvider") | ||
if not id_provider: | ||
raise ValueError(f"/Registry/VO/{self.vo}/IdProvider not found") | ||
result = gConfig.getOptionsDict(f"/Resources/IdProviders/{id_provider}") | ||
if result["OK"]: | ||
self.iam_url = result["Value"]["issuer"] | ||
gLogger.verbose("Using IAM server", self.iam_url) | ||
else: | ||
raise ValueError(f"/Resources/IdProviders/{id_provider}") | ||
|
||
self.userDict = None | ||
self.access_token = access_token | ||
|
||
def _getIamUserDump(self): | ||
"""List the users from IAM""" | ||
|
||
headers = {"Authorization": f"Bearer {self.access_token}"} | ||
iam_list_url = f"{self.iam_url}/scim/Users" | ||
iam_users = [] | ||
startIndex = 1 | ||
totalResults = 1000 # total number of users | ||
itemsPerPage = 10 | ||
while startIndex <= totalResults: | ||
resp = requests.get(iam_list_url, headers=headers, params={"startIndex": startIndex}) | ||
resp.raise_for_status() | ||
data = resp.json() | ||
# These 2 should never change, but just to be sure... | ||
totalResults = data["totalResults"] | ||
itemsPerPage = data["itemsPerPage"] | ||
|
||
startIndex += itemsPerPage | ||
iam_users.extend(data["Resources"]) | ||
return iam_users | ||
|
||
@staticmethod | ||
def convert_iam_to_voms(iam_output): | ||
"""Convert an IAM entry into the voms style, i.e. DN based""" | ||
converted_output = {} | ||
|
||
for cert in iam_output["urn:indigo-dc:scim:schemas:IndigoUser"]["certificates"]: | ||
cert_dict = {} | ||
dn = convert_dn(cert["subjectDn"]) | ||
ca = convert_dn(cert["issuerDn"]) | ||
|
||
cert_dict["CA"] = ca | ||
cert_dict["nickname"] = iam_output["userName"] | ||
# This is not correct, we take the overall status instead of the certificate one | ||
# however there are no known case of cert suspended while the user isn't | ||
cert_dict["certSuspended"] = not iam_output["active"] | ||
# There are still bugs in IAM regarding the active status vs voms suspended | ||
|
||
cert_dict["suspended"] = not iam_output["active"] | ||
# The mail may be different, in particular for robot accounts | ||
cert_dict["mail"] = iam_output["emails"][0]["value"].lower() | ||
|
||
# https://github.com/indigo-iam/voms-importer/blob/main/vomsimporter.py | ||
roles = [] | ||
|
||
for role in iam_output["groups"]: | ||
role_name = role["display"] | ||
if "/" in role_name: | ||
role_name = role_name.replace("/", "/Role=") | ||
roles.append(f"/{role_name}") | ||
|
||
cert_dict["Roles"] = roles | ||
converted_output[dn] = cert_dict | ||
return converted_output | ||
|
||
def getUsers(self): | ||
self.iam_users_raw = self._getIamUserDump() | ||
users = {} | ||
errors = 0 | ||
for user in self.iam_users_raw: | ||
try: | ||
users.update(self.convert_iam_to_voms(user)) | ||
except Exception as e: | ||
errors += 1 | ||
print(f"Could not convert {user['name']} {e!r} ") | ||
print(f"There were in total {errors} errors") | ||
self.userDict = dict(users) | ||
return S_OK(users) |
Oops, something went wrong.