Skip to content

Commit 8724ef1

Browse files
committed
feat: Include DiracX token in proxy PEM files
1 parent 64f2d36 commit 8724ef1

File tree

11 files changed

+167
-61
lines changed

11 files changed

+167
-61
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ console_scripts =
160160
# FrameworkSystem
161161
dirac-login = DIRAC.FrameworkSystem.scripts.dirac_login:main
162162
dirac-logout = DIRAC.FrameworkSystem.scripts.dirac_logout:main
163+
dirac-diracx-whoami = DIRAC.FrameworkSystem.scripts.dirac_diracx_whoami:main
163164
dirac-admin-get-CAs = DIRAC.FrameworkSystem.scripts.dirac_admin_get_CAs:main [server]
164165
dirac-admin-get-proxy = DIRAC.FrameworkSystem.scripts.dirac_admin_get_proxy:main [admin]
165166
dirac-admin-proxy-upload = DIRAC.FrameworkSystem.scripts.dirac_admin_proxy_upload:main [admin]

src/DIRAC/Core/Security/DiracX.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
__all__ = (
4+
"DiracXClient",
5+
"diracxTokenFromPEM",
6+
)
7+
8+
import base64
9+
import json
10+
import re
11+
import textwrap
12+
from contextlib import contextmanager
13+
from pathlib import Path
14+
from tempfile import NamedTemporaryFile
15+
from typing import Any
16+
17+
from diracx.client import DiracClient as _DiracClient
18+
from diracx.core.models import TokenResponse
19+
from diracx.core.preferences import DiracxPreferences
20+
from diracx.core.utils import serialize_credentials
21+
22+
from DIRAC import gConfig
23+
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
24+
from DIRAC.Core.Security.Locations import getDefaultProxyLocation
25+
from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue, returnValueOrRaise
26+
27+
28+
PEM_BEGIN = "-----BEGIN DIRACX-----"
29+
PEM_END = "-----END DIRACX-----"
30+
RE_DIRACX_PEM = re.compile(rf"{PEM_BEGIN}\n(.*)\n{PEM_END}", re.MULTILINE | re.DOTALL)
31+
32+
33+
@convertToReturnValue
34+
def addProxyToPEM(pemPath, group):
35+
from DIRAC.Core.Base.Client import Client
36+
37+
vo = Registry.getVOMSVOForGroup(group)
38+
disabledVOs = gConfig.getValue("/DiracX/DisabledVOs", [])
39+
if vo and vo not in disabledVOs:
40+
token_content = returnValueOrRaise(
41+
Client(url="Framework/ProxyManager", proxyLocation=pemPath).exchangeProxyForToken()
42+
)
43+
44+
diracxUrl = gConfig.getValue("/DiracX/URL")
45+
if not diracxUrl:
46+
return S_ERROR("Missing mandatory /DiracX/URL configuration")
47+
48+
token = TokenResponse(
49+
access_token=token_content["access_token"],
50+
expires_in=token_content["expires_in"],
51+
token_type=token_content.get("token_type"),
52+
refresh_token=token_content.get("refresh_token"),
53+
)
54+
55+
token_pem = f"{PEM_BEGIN}\n"
56+
data = base64.b64encode(serialize_credentials(token).encode("utf-8")).decode()
57+
token_pem += textwrap.fill(data, width=64)
58+
token_pem += f"\n{PEM_END}\n"
59+
60+
with open(pemPath, "a") as f:
61+
f.write(token_pem)
62+
63+
64+
def diracxTokenFromPEM(pemPath) -> dict[str, Any] | None:
65+
"""Extract the DiracX token from the proxy PEM file"""
66+
pem = Path(pemPath).read_text()
67+
if match := RE_DIRACX_PEM.search(pem):
68+
match = match.group(1)
69+
return json.loads(base64.b64decode(match).decode("utf-8"))
70+
71+
72+
@contextmanager
73+
def DiracXClient() -> _DiracClient:
74+
"""Get a DiracX client instance with the current user's credentials"""
75+
diracxUrl = gConfig.getValue("/DiracX/URL")
76+
if not diracxUrl:
77+
raise ValueError("Missing mandatory /DiracX/URL configuration")
78+
79+
proxyLocation = getDefaultProxyLocation()
80+
diracxToken = diracxTokenFromPEM(proxyLocation)
81+
82+
with NamedTemporaryFile(mode="wt") as token_file:
83+
token_file.write(json.dumps(diracxToken))
84+
token_file.flush()
85+
token_file.seek(0)
86+
87+
pref = DiracxPreferences(url=diracxUrl, credentials_path=token_file.name)
88+
with _DiracClient(diracx_preferences=pref) as api:
89+
yield api

src/DIRAC/Core/Security/ProxyInfo.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error
99
from DIRAC.Core.Security.VOMS import VOMS
1010
from DIRAC.Core.Security import Locations
11+
from DIRAC.Core.Security.DiracX import diracxTokenFromPEM
1112

1213
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
1314

@@ -25,6 +26,7 @@ def getProxyInfo(proxy=False, disableVOMS=False):
2526
* 'validDN' : Valid DN in DIRAC
2627
* 'validGroup' : Valid Group in DIRAC
2728
* 'secondsLeft' : Seconds left
29+
* 'hasDiracxToken'
2830
* values that can be there
2931
* 'path' : path to the file,
3032
* 'group' : DIRAC group
@@ -67,6 +69,11 @@ def getProxyInfo(proxy=False, disableVOMS=False):
6769
infoDict["VOMS"] = retVal["Value"]
6870
else:
6971
infoDict["VOMSError"] = retVal["Message"].strip()
72+
73+
infoDict["hasDiracxToken"] = False
74+
if proxyLocation:
75+
infoDict["hasDiracxToken"] = bool(diracxTokenFromPEM(proxyLocation))
76+
7077
return S_OK(infoDict)
7178

7279

@@ -94,6 +101,7 @@ def formatProxyInfoAsString(infoDict):
94101
"subproxyUser",
95102
("secondsLeft", "timeleft"),
96103
("group", "DIRAC group"),
104+
("hasDiracxToken", "DiracX"),
97105
"rfc",
98106
"path",
99107
"username",

src/DIRAC/Core/Tornado/Client/private/TornadoBaseClient.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,10 +511,12 @@ def _request(self, retry=0, outputFile=None, **kwargs):
511511
# getting certificate
512512
# Do we use the server certificate ?
513513
if self.kwargs[self.KW_USE_CERTIFICATES]:
514+
# TODO: Does this code path need to work with DiracX?
514515
auth = {"cert": Locations.getHostCertificateAndKeyLocation()}
515516

516517
# Use access token?
517518
elif self.__useAccessToken:
519+
# TODO: Remove this code path?
518520
from DIRAC.FrameworkSystem.private.authorization.utils.Tokens import (
519521
getLocalTokenDict,
520522
writeTokenDictToTokenFile,
@@ -543,13 +545,13 @@ def _request(self, retry=0, outputFile=None, **kwargs):
543545

544546
auth = {"headers": {"Authorization": f"Bearer {token['access_token']}"}}
545547
elif self.kwargs.get(self.KW_PROXY_STRING):
548+
# TODO: This code path cannot work with DiracX
546549
tmpHandle, cert = tempfile.mkstemp()
547550
fp = os.fdopen(tmpHandle, "w")
548551
fp.write(self.kwargs[self.KW_PROXY_STRING])
549552
fp.close()
550-
551-
# CHRIS 04.02.21
552-
# TODO: add proxyLocation check ?
553+
elif self.kwargs.get(self.KW_PROXY_LOCATION):
554+
auth = {"cert": self.kwargs[self.KW_PROXY_LOCATION]}
553555
else:
554556
auth = {"cert": Locations.getProxyLocation()}
555557
if not auth["cert"]:

src/DIRAC/FrameworkSystem/Client/ProxyManagerClient.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
1111
from DIRAC.Core.Utilities import ThreadSafe, DIRACSingleton
1212
from DIRAC.Core.Utilities.DictCache import DictCache
13+
from DIRAC.Core.Security.DiracX import addProxyToPEM
1314
from DIRAC.Core.Security.ProxyFile import multiProxyArgument, deleteMultiProxy
1415
from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error
1516
from DIRAC.Core.Security.X509Request import X509Request # pylint: disable=import-error
@@ -547,6 +548,10 @@ def dumpProxyToFile(self, chain, destinationFile=None, requiredTimeLeft=600):
547548
if not retVal["OK"]:
548549
return retVal
549550
filename = retVal["Value"]
551+
if not (result := chain.getDIRACGroup())["OK"]:
552+
return result
553+
if not (result := addProxyToPEM(filename, result["Value"]))["OK"]:
554+
return result
550555
self.__filesCache.add(cHash, chain.getRemainingSecs()["Value"], filename)
551556
return S_OK(filename)
552557

@@ -655,7 +660,14 @@ def renewProxy(self, proxyToBeRenewed=None, minLifeTime=3600, newProxyLifeTime=4
655660
chain = retVal["Value"]
656661

657662
if not proxyToRenewDict["tempFile"]:
658-
return chain.dumpAllToFile(proxyToRenewDict["file"])
663+
filename = proxyToRenewDict["file"]
664+
if not (result := chain.dumpAllToFile(filename))["OK"]:
665+
return result
666+
if not (result := chain.getDIRACGroup())["OK"]:
667+
return result
668+
if not (result := addProxyToPEM(filename, result["Value"]))["OK"]:
669+
return result
670+
return S_OK(filename)
659671

660672
return S_OK(chain)
661673

src/DIRAC/FrameworkSystem/scripts/dirac_admin_get_proxy.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import DIRAC
1616
from DIRAC import gLogger, S_OK, S_ERROR
1717
from DIRAC.Core.Base.Script import Script
18+
from DIRAC.Core.Security.DiracX import addProxyToPEM
1819
from DIRAC.FrameworkSystem.Client.ProxyManagerClient import gProxyManager
1920
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
2021

@@ -159,6 +160,10 @@ def main():
159160
if not result["OK"]:
160161
gLogger.notice(f"Proxy file cannot be written to {params.proxyPath}: {result['Message']}")
161162
DIRAC.exit(2)
163+
if not (result := chain.getDIRACGroup())["OK"]:
164+
return result
165+
if not (result := addProxyToPEM(params.proxyPath, result["Value"]))["OK"]:
166+
return result
162167
gLogger.notice(f"Proxy downloaded to {params.proxyPath}")
163168
DIRAC.exit(0)
164169

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Query DiracX for information about the current user
2+
3+
This is a stripped down version of the "dirac whoami" script from DiracX.
4+
It primarily exists as a method of validating the current user's credentials are functional.
5+
"""
6+
import json
7+
8+
from DIRAC.Core.Base.Script import Script
9+
from DIRAC.Core.Security.DiracX import DiracXClient
10+
11+
12+
@Script()
13+
def main():
14+
Script.parseCommandLine()
15+
16+
with DiracXClient() as api:
17+
user_info = api.auth.userinfo()
18+
print(json.dumps(user_info.as_dict(), indent=2))
19+
20+
21+
if __name__ == "__main__":
22+
main()

src/DIRAC/FrameworkSystem/scripts/dirac_login.py

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from DIRAC import gConfig, gLogger, S_OK, S_ERROR
2626
from DIRAC.Core.Security.Locations import getDefaultProxyLocation, getCertificateAndKeyLocation
2727
from DIRAC.Core.Security.VOMS import VOMS
28+
from DIRAC.Core.Security.DiracX import addProxyToPEM
2829
from DIRAC.Core.Security.ProxyFile import writeToProxyFile
2930
from DIRAC.Core.Security.ProxyInfo import getProxyInfo, formatProxyInfoAsString
3031
from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error
@@ -314,32 +315,8 @@ def loginWithCertificate(self):
314315
return res
315316

316317
# Get a token for use with diracx
317-
vo = getVOMSVOForGroup(self.group)
318-
disabledVOs = gConfig.getValue("/DiracX/DisabledVOs", [])
319-
if vo not in disabledVOs:
320-
from diracx.core.utils import write_credentials # pylint: disable=import-error
321-
from diracx.core.models import TokenResponse # pylint: disable=import-error
322-
from diracx.core.preferences import DiracxPreferences # pylint: disable=import-error
323-
324-
res = Client(url="Framework/ProxyManager").exchangeProxyForToken()
325-
if not res["OK"]:
326-
return res
327-
token_content = res["Value"]
328-
329-
diracxUrl = gConfig.getValue("/DiracX/URL")
330-
if not diracxUrl:
331-
return S_ERROR("Missing mandatory /DiracX/URL configuration")
332-
333-
preferences = DiracxPreferences(url=diracxUrl)
334-
write_credentials(
335-
TokenResponse(
336-
access_token=token_content["access_token"],
337-
expires_in=token_content["expires_in"],
338-
token_type=token_content.get("token_type"),
339-
refresh_token=token_content.get("refresh_token"),
340-
),
341-
location=preferences.credentials_path,
342-
)
318+
if not (result := addProxyToPEM(self.outputFile, self.group))["OK"]:
319+
return result
343320

344321
return S_OK()
345322

src/DIRAC/FrameworkSystem/scripts/dirac_proxy_info.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def main():
7777
from DIRAC.Core.Security import VOMS
7878
from DIRAC.FrameworkSystem.Client.ProxyManagerClient import gProxyManager
7979
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
80+
from DIRAC.Core.Security.DiracX import DiracXClient
8081

8182
if params.csEnabled:
8283
retVal = Script.enableCS()
@@ -151,6 +152,12 @@ def invalidProxy(msg):
151152
invalidProxy(f"Cannot determine life time of VOMS attributes: {result['Message']}")
152153
if int(result["Value"].strip()) == 0:
153154
invalidProxy("VOMS attributes are expired")
155+
# Ensure the proxy is working with DiracX
156+
try:
157+
with DiracXClient() as api:
158+
api.auth.userinfo()
159+
except Exception as e:
160+
invalidProxy(f"Failed to access DiracX: {e}")
154161

155162
sys.exit(0)
156163

src/DIRAC/FrameworkSystem/scripts/dirac_proxy_init.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
from DIRAC.Core.Base.Script import Script
1919
from DIRAC.FrameworkSystem.Client import ProxyGeneration, ProxyUpload
2020
from DIRAC.Core.Security import X509Chain, ProxyInfo, VOMS
21-
from DIRAC.Core.Security.Locations import getCAsLocation
21+
from DIRAC.Core.Security.DiracX import addProxyToPEM
22+
from DIRAC.Core.Security.Locations import getCAsLocation, getDefaultProxyLocation
2223
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
2324
from DIRAC.FrameworkSystem.Client.BundleDeliveryClient import BundleDeliveryClient
24-
from DIRAC.Core.Base.Client import Client
2525

2626

2727
class Params(ProxyGeneration.CLIParams):
@@ -221,6 +221,11 @@ def doTheMagic(self):
221221
self.checkCAs()
222222
pI.certLifeTimeCheck()
223223
resultProxyWithVOMS = pI.addVOMSExtIfNeeded()
224+
225+
proxyLoc = self.__piParams.proxyLoc or getDefaultProxyLocation()
226+
if not (result := addProxyToPEM(proxyLoc, self.__piParams.diracGroup))["OK"]:
227+
return result
228+
224229
if not resultProxyWithVOMS["OK"]:
225230
if "returning a valid AC for the user" in resultProxyWithVOMS["Message"]:
226231
gLogger.error(resultProxyWithVOMS["Message"])
@@ -238,33 +243,6 @@ def doTheMagic(self):
238243
if self.__piParams.strict:
239244
return resultProxyUpload
240245

241-
vo = Registry.getVOMSVOForGroup(self.__piParams.diracGroup)
242-
disabledVOs = gConfig.getValue("/DiracX/DisabledVOs", [])
243-
if vo and vo not in disabledVOs:
244-
from diracx.core.utils import write_credentials # pylint: disable=import-error
245-
from diracx.core.models import TokenResponse # pylint: disable=import-error
246-
from diracx.core.preferences import DiracxPreferences # pylint: disable=import-error
247-
248-
res = Client(url="Framework/ProxyManager").exchangeProxyForToken()
249-
if not res["OK"]:
250-
return res
251-
252-
diracxUrl = gConfig.getValue("/DiracX/URL")
253-
if not diracxUrl:
254-
return S_ERROR("Missing mandatory /DiracX/URL configuration")
255-
256-
token_content = res["Value"]
257-
preferences = DiracxPreferences(url=diracxUrl)
258-
write_credentials(
259-
TokenResponse(
260-
access_token=token_content["access_token"],
261-
expires_in=token_content["expires_in"],
262-
token_type=token_content.get("token_type"),
263-
refresh_token=token_content.get("refresh_token"),
264-
),
265-
location=preferences.credentials_path,
266-
)
267-
268246
return S_OK()
269247

270248

0 commit comments

Comments
 (0)