Skip to content

Commit 1fa28d1

Browse files
authored
Merge pull request #6803 from atsareg/dev-ce-tokens-4
[8.0] Pilot submission with tokens
2 parents 9dd6315 + 84c6a76 commit 1fa28d1

File tree

24 files changed

+353
-220
lines changed

24 files changed

+353
-220
lines changed

dirac.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ Registry
187187
VOMSName = lhcb
188188

189189
# Registered identity provider associated with VO
190-
IdP = CheckIn
190+
IdProvider = CheckIn
191191

192192
# Section to describe all the VOMS servers that can be used with the given VOMS VO
193193
VOMSServers

src/DIRAC/ConfigurationSystem/Client/Helpers/Registry.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,23 @@ def findDefaultGroupForDN(dn):
163163
return findDefaultGroupForUser(result["Value"])
164164

165165

166-
def findDefaultGroupForUser(userName):
166+
def findDefaultGroupForUser(username):
167167
"""Get default group for user
168168
169-
:param str userName: user name
169+
:param str username: user name
170170
171171
:return: S_OK(str)/S_ERROR()
172172
"""
173-
defGroups = getUserOption(userName, "DefaultGroup", [])
173+
defGroups = getUserOption(username, "DefaultGroup", [])
174174
defGroups += gConfig.getValue(f"{gBaseRegistrySection}/DefaultGroup", ["user"])
175-
result = getGroupsForUser(userName)
175+
result = getGroupsForUser(username)
176176
if not result["OK"]:
177177
return result
178178
userGroups = result["Value"]
179179
for group in defGroups:
180180
if group in userGroups:
181181
return S_OK(group)
182-
return S_OK(userGroups[0]) if userGroups else S_ERROR(f"User {userName} has no groups")
182+
return S_OK(userGroups[0]) if userGroups else S_ERROR(f"User {username} has no groups")
183183

184184

185185
def getAllUsers():
@@ -349,16 +349,16 @@ def hostHasProperties(hostName, propList):
349349
return __matchProps(propList, getPropertiesForHost(hostName))
350350

351351

352-
def getUserOption(userName, optName, defaultValue=""):
352+
def getUserOption(username, optName, defaultValue=""):
353353
"""Get user option
354354
355-
:param str userName: user name
355+
:param str username: user name
356356
:param str optName: option name
357357
:param defaultValue: default value
358358
359359
:return: defaultValue or str
360360
"""
361-
return gConfig.getValue(f"{gBaseRegistrySection}/Users/{userName}/{optName}", defaultValue)
361+
return gConfig.getValue(f"{gBaseRegistrySection}/Users/{username}/{optName}", defaultValue)
362362

363363

364364
def getGroupOption(groupName, optName, defaultValue=""):
@@ -430,7 +430,7 @@ def getIdPForGroup(group):
430430
431431
:return: str
432432
"""
433-
return getVOOption(getVOForGroup(group), "IdP")
433+
return getVOOption(getVOForGroup(group), "IdProvider")
434434

435435

436436
def getDefaultVOMSAttribute():

src/DIRAC/ConfigurationSystem/Client/Helpers/Resources.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,17 @@ def getQueue(site, ce, queue):
228228
return S_OK(resultDict)
229229

230230

231-
def getQueues(siteList=None, ceList=None, ceTypeList=None, community=None):
232-
"""Get CE/queue options according to the specified selection"""
231+
def getQueues(siteList=None, ceList=None, ceTypeList=None, community=None, tags=None):
232+
"""Get CE/queue options according to the specified selection
233+
234+
:param str list siteList: sites to be selected
235+
:param str list ceList: CEs to be selected
236+
:param str list ceTypeList: CE types to be selected
237+
:param str community: selected VO
238+
:param str list tags: tags required for selection
239+
240+
:return: S_OK/S_ERROR with Value - dictionary of selected Site/CE/Queue parameters
241+
"""
233242

234243
result = gConfig.getSections("/Resources/Sites")
235244
if not result["OK"]:
@@ -259,6 +268,7 @@ def getQueues(siteList=None, ceList=None, ceTypeList=None, community=None):
259268
continue
260269
ces = result["Value"]
261270
for ce in ces:
271+
ceTags = gConfig.getValue(f"/Resources/Sites/{grid}/{site}/CEs/{ce}/Tag", [])
262272
if ceTypeList:
263273
ceType = gConfig.getValue(f"/Resources/Sites/{grid}/{site}/CEs/{ce}/CEType", "")
264274
if not ceType or ceType not in ceTypeList:
@@ -283,6 +293,11 @@ def getQueues(siteList=None, ceList=None, ceTypeList=None, community=None):
283293
comList = gConfig.getValue(f"/Resources/Sites/{grid}/{site}/CEs/{ce}/Queues/{queue}/VO", [])
284294
if comList and community.lower() not in [cl.lower() for cl in comList]:
285295
continue
296+
if tags:
297+
queueTags = gConfig.getValue(f"/Resources/Sites/{grid}/{site}/CEs/{ce}/Queues/{queue}/Tag", [])
298+
queueTags = set(ceTags + queueTags)
299+
if not queueTags or not set(tags).issubset(queueTags):
300+
continue
286301
resultDict.setdefault(site, {})
287302
resultDict[site].setdefault(ce, ceOptionsDict)
288303
resultDict[site][ce].setdefault("Queues", {})

src/DIRAC/Core/Tornado/Server/private/BaseRequestHandler.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from functools import partial
1515

1616
import jwt
17-
import tornado
1817
from tornado.web import RequestHandler, HTTPError
1918
from tornado.ioloop import IOLoop
2019

src/DIRAC/Core/Utilities/Grid.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from DIRAC.Core.Utilities.Subprocess import systemCall, shellCall
1212

1313

14-
def executeGridCommand(proxy, cmd, gridEnvScript=None):
14+
def executeGridCommand(proxy, cmd, gridEnvScript=None, gridEnvDict=None):
1515
"""
1616
Execute cmd tuple after sourcing GridEnv
1717
"""
@@ -53,6 +53,9 @@ def executeGridCommand(proxy, cmd, gridEnvScript=None):
5353
return ret
5454
gridEnv["X509_USER_PROXY"] = ret["Value"]
5555

56+
if gridEnvDict:
57+
gridEnv.update(gridEnvDict)
58+
5659
result = systemCall(120, cmd, env=gridEnv)
5760
return result
5861

src/DIRAC/FrameworkSystem/API/AuthHandler.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,12 @@ def post_token(self):
420420
return self.server.create_token_response(self.request)
421421

422422
def __researchDIRACGroup(self, extSession, chooseScope, state):
423-
"""Research DIRAC groups for authorized user
423+
"""Look for DIRAC groups of a user already authorized by an Identity Provider
424424
425425
:param dict extSession: ended authorized external IdP session
426426
427427
:return: -- will return (None, response) to provide error or group selector
428-
will return (grant_user, request) to contionue authorization with choosed group
428+
will return (grant_user, request) to continue authorization with chosen group
429429
"""
430430
# Base DIRAC client auth session
431431
firstRequest = createOAuth2Request(extSession["firstRequest"])
@@ -435,9 +435,13 @@ def __researchDIRACGroup(self, extSession, chooseScope, state):
435435
username = extSession["authed"]["username"]
436436
# Requested arguments in first request
437437
provider = firstRequest.provider
438-
self.log.debug(f"Next groups has been found for {username}:", ", ".join(firstRequest.groups))
438+
self.log.debug("The following groups found", f"for {username}: {', '.join(firstRequest.groups)}")
439439

440-
# Researche Group
440+
# If group is already defined in the first request, just return it as it was already validated
441+
if firstRequest.groups:
442+
return extSession["authed"], firstRequest
443+
444+
# Look for DIRAC groups valid for the user
441445
result = getGroupsForUser(username)
442446
if not result["OK"]:
443447
return None, self.server.handle_response(
@@ -460,16 +464,12 @@ def __researchDIRACGroup(self, extSession, chooseScope, state):
460464

461465
self.log.debug(f"The state of {username} user groups has been checked:", pprint.pformat(validGroups))
462466

463-
# If group already defined in first request, just return it
464-
if firstRequest.groups:
465-
return extSession["authed"], firstRequest
466-
467467
# If not and we found only one valid group, apply this group
468468
if len(validGroups) == 1:
469469
firstRequest.addScopes([f"g:{validGroups[0]}"])
470470
return extSession["authed"], firstRequest
471471

472-
# Else give user chanse to choose group in browser
472+
# Else give user a chance to choose a group in the browser
473473
with dom.div(cls="row mt-5 justify-content-md-center align-items-center") as tag:
474474
for group in sorted(validGroups):
475475
vo, gr = group.split("_")

src/DIRAC/FrameworkSystem/Client/TokenManagerClient.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from DIRAC.Core.Base.Client import Client, createClient
88
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
99
from DIRAC.Resources.IdProvider.IdProviderFactory import IdProviderFactory
10+
from DIRAC.FrameworkSystem.private.authorization.utils.Tokens import OAuth2Token
1011

1112
gTokensSync = ThreadSafe.Synchronizer()
1213

@@ -33,7 +34,7 @@ def getToken(
3334
identityProvider: str = None,
3435
requiredTimeLeft: int = 0,
3536
):
36-
"""Get an access token for a user/group.
37+
"""Get an access token for a user/group keeping the local cache
3738
3839
:param username: user name
3940
:param userGroup: group name
@@ -55,9 +56,9 @@ def getToken(
5556
return result
5657
idpObj = result["Value"]
5758

58-
if userGroup and (result := idpObj.getGroupScopes(userGroup))["OK"]:
59+
if userGroup and (result := idpObj.getGroupScopes(userGroup)):
5960
# What scope correspond to the requested group?
60-
scope = list(set((scope or []) + result["Value"]))
61+
scope = list(set((scope or []) + result))
6162

6263
# Set the scope
6364
idpObj.scope = " ".join(scope)
@@ -70,21 +71,20 @@ def getToken(
7071
# Let's check if the access token is fresh
7172
if not token.is_expired(requiredTimeLeft):
7273
return S_OK(token)
73-
# It seems that it is no longer valid for us, but whether there is a refresh token?
74-
if token.get("refresh_token"):
75-
# Okay, so we can try to refresh tokens
76-
if (result := idpObj.refreshToken(token["refresh_token"]))["OK"]:
77-
# caching new tokens
78-
self.__tokensCache.add(
79-
cacheKey,
80-
token.get_claim("exp", "refresh_token") or self.DEFAULT_RT_EXPIRATION_TIME,
81-
result["Value"],
82-
)
83-
return result
84-
self.log.verbose(f"Failed to get token on client's side: {result['Message']}")
85-
# Let's try to revoke broken token
86-
idpObj.revokeToken(token["refresh_token"])
87-
88-
return self.executeRPC(
74+
75+
result = self.executeRPC(
8976
username, userGroup, scope, audience, identityProvider, requiredTimeLeft, call="getToken"
9077
)
78+
79+
if result["OK"]:
80+
token = OAuth2Token(dict(result["Value"]))
81+
self.__tokensCache.add(
82+
cacheKey,
83+
token.get_claim("exp", "refresh_token") or self.DEFAULT_RT_EXPIRATION_TIME,
84+
token,
85+
)
86+
87+
return result
88+
89+
90+
gTokenManager = TokenManagerClient()

src/DIRAC/FrameworkSystem/DB/TokenDB.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
class Token(Model, OAuth2TokenMixin):
24-
"""This class describe token fields"""
24+
"""This class describes token fields"""
2525

2626
__tablename__ = "Token"
2727
__table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8"}
@@ -30,8 +30,8 @@ class Token(Model, OAuth2TokenMixin):
3030
# https://stackoverflow.com/questions/1827063/mysql-error-key-specification-without-a-key-length
3131
id = Column(Integer, autoincrement=True, primary_key=True) # Unique token ID
3232
kid = Column(String(255)) # Unique secret key ID for token encryption
33-
user_id = Column(String(255)) # User identificator that registred in an identity provider, token owner
34-
provider = Column(String(255)) # Provider name registred in DIRAC
33+
user_id = Column(String(255)) # User identifier registered in an identity provider, token owner
34+
provider = Column(String(255)) # Provider name registered in DIRAC
3535
expires_at = Column(Integer, nullable=False, default=0) # When the access token is expired
3636
access_token = Column(Text, nullable=False)
3737
refresh_token = Column(Text, nullable=False)
@@ -101,7 +101,7 @@ def updateToken(self, token: dict, userID: str, provider: str, rt_expired_in: in
101101
:return: S_OK(list)/S_ERROR() -- return old tokens that should be revoked.
102102
"""
103103
if not token["refresh_token"]:
104-
return S_ERROR("Cannot store absent refresh token.")
104+
return S_ERROR("Cannot store token without a refresh token.")
105105

106106
# Let's collect the necessary attributes of the token
107107
token["user_id"] = userID

src/DIRAC/FrameworkSystem/Service/TokenManagerHandler.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def export_getToken(
190190
* LimitedDelegation <- permits downloading only limited tokens
191191
* PrivateLimitedDelegation <- permits downloading only limited tokens for one self
192192
193-
:paarm username: user name
193+
:param username: user name
194194
:param userGroup: user group
195195
:param scope: requested scope
196196
:param audience: requested audience
@@ -210,38 +210,24 @@ def export_getToken(
210210
return result
211211
idpObj = result["Value"]
212212

213-
if userGroup and (result := idpObj.getGroupScopes(userGroup))["OK"]:
213+
if userGroup and (result := idpObj.getGroupScopes(userGroup)):
214214
# What scope correspond to the requested group?
215-
scope = list(set((scope or []) + result["Value"]))
215+
scope = list(set((scope or []) + result))
216216

217217
# Set the scope
218218
idpObj.scope = " ".join(scope)
219219

220-
# Let's check if there are corresponding tokens in the cache
220+
# Let's check if there is a corresponding token in the cache
221221
cacheKey = (username, idpObj.scope, audience, identityProvider)
222222
if self.__tokensCache.exists(cacheKey, requiredTimeLeft):
223223
# Well we have a fresh record containing a Token object
224224
token = self.__tokensCache.get(cacheKey)
225225
# Let's check if the access token is fresh
226226
if not token.is_expired(requiredTimeLeft):
227227
return S_OK(token)
228-
# It seems that it is no longer valid for us, but whether there is a refresh token?
229-
if token.get("refresh_token"):
230-
# Okay, so we can try to refresh tokens
231-
if (result := idpObj.refreshToken(token["refresh_token"]))["OK"]:
232-
# caching new tokens
233-
self.__tokensCache.add(
234-
cacheKey,
235-
result["Value"].get_claim("exp", "refresh_token") or self.DEFAULT_RT_EXPIRATION_TIME,
236-
result["Value"],
237-
)
238-
return result
239-
self.log.verbose(f"Failed to get token with cached tokens: {result['Message']}")
240-
# Let's try to revoke broken token
241-
idpObj.revokeToken(token["refresh_token"])
242228

243229
err = []
244-
# The cache did not help, so let's make an exchange token
230+
# No luck so far, let's refresh the token stored in the database
245231
result = Registry.getDNForUsername(username)
246232
if not result["OK"]:
247233
return result
@@ -256,8 +242,8 @@ def export_getToken(
256242
idpObj.token = result["Value"]
257243
result = self.__checkProperties(dn, userGroup)
258244
if result["OK"]:
259-
# exchange token with requested scope
260-
result = idpObj.exchangeToken()
245+
# refresh token with requested scope
246+
result = idpObj.refreshToken()
261247
if result["OK"]:
262248
# caching new tokens
263249
self.__tokensCache.add(
@@ -266,8 +252,8 @@ def export_getToken(
266252
result["Value"],
267253
)
268254
return result
269-
# Not find any token associated with the found user ID
270-
err.append(result.get("Message", f"No token found for {uid}."))
255+
# Did not find any token associated with the found user ID
256+
err.append(result.get("Message", f"No token found for {uid}"))
271257
# Collect all errors when trying to get a token, or if no user ID is registered
272258
return S_ERROR("; ".join(err or [f"No user ID found for {username}"]))
273259

0 commit comments

Comments
 (0)