Skip to content

Commit e240729

Browse files
authored
Merge pull request #7261 from chrisburr/token-in-pem
[9.0] Include DiracX token in proxy PEM files
2 parents 64f2d36 + cfe2fc4 commit e240729

File tree

21 files changed

+228
-85
lines changed

21 files changed

+228
-85
lines changed

integration_tests.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,13 @@ def create(
146146
flags: Optional[list[str]] = typer.Argument(None),
147147
editable: Optional[bool] = None,
148148
extra_module: Optional[list[str]] = None,
149+
diracx_dist_dir: Optional[str] = None,
149150
release_var: Optional[str] = None,
150151
run_server_tests: bool = True,
151152
run_client_tests: bool = True,
152153
):
153154
"""Start a local instance of the integration tests"""
154-
prepare_environment(flags, editable, extra_module, release_var)
155+
prepare_environment(flags, editable, extra_module, diracx_dist_dir, release_var)
155156
install_server()
156157
install_client()
157158
exit_code = 0
@@ -191,6 +192,7 @@ def prepare_environment(
191192
flags: Optional[list[str]] = typer.Argument(None),
192193
editable: Optional[bool] = None,
193194
extra_module: Optional[list[str]] = None,
195+
diracx_dist_dir: Optional[str] = None,
194196
release_var: Optional[str] = None,
195197
):
196198
"""Prepare the local environment for installing DIRAC."""
@@ -227,7 +229,7 @@ def prepare_environment(
227229
extra_services = list(chain(*[config["extra-services"] for config in module_configs.values()]))
228230

229231
typer.secho("Running docker-compose to create containers", fg=c.GREEN)
230-
with _gen_docker_compose(modules) as docker_compose_fn:
232+
with _gen_docker_compose(modules, diracx_dist_dir=diracx_dist_dir) as docker_compose_fn:
231233
subprocess.run(
232234
["docker-compose", "-f", docker_compose_fn, "up", "-d", "dirac-server", "dirac-client"] + extra_services,
233235
check=True,
@@ -322,7 +324,7 @@ def prepare_environment(
322324
typer.secho("Running docker-compose to create DiracX containers", fg=c.GREEN)
323325
typer.secho(f"Will leave a folder behind: {docker_compose_fn_final}", fg=c.YELLOW)
324326

325-
with _gen_docker_compose(modules) as docker_compose_fn:
327+
with _gen_docker_compose(modules, diracx_dist_dir=diracx_dist_dir) as docker_compose_fn:
326328
# We cannot use the temporary directory created in the context manager because
327329
# we don't stay in the contect manager (Popen)
328330
# So we need something that outlives it.
@@ -545,7 +547,7 @@ class TestExit(typer.Exit):
545547

546548

547549
@contextmanager
548-
def _gen_docker_compose(modules):
550+
def _gen_docker_compose(modules, *, diracx_dist_dir=None):
549551
# Load the docker-compose configuration and mount the necessary volumes
550552
input_fn = Path(__file__).parent / "tests/CI/docker-compose.yml"
551553
docker_compose = yaml.safe_load(input_fn.read_text())
@@ -560,10 +562,12 @@ def _gen_docker_compose(modules):
560562
docker_compose["services"]["diracx-wait-for-db"]["volumes"].extend(volumes[:])
561563

562564
module_configs = _load_module_configs(modules)
563-
if "diracx" in module_configs:
564-
docker_compose["services"]["diracx"]["volumes"].append(
565-
f"{modules['diracx']}/src/diracx:{module_configs['diracx']['install-location']}"
566-
)
565+
if diracx_dist_dir is not None:
566+
for container_name in ["dirac-client", "dirac-server", "diracx-init-cs", "diracx-wait-for-db", "diracx"]:
567+
docker_compose["services"][container_name]["volumes"].append(f"{diracx_dist_dir}:/diracx_sources")
568+
docker_compose["services"][container_name].setdefault("environment", []).append(
569+
"DIRACX_CUSTOM_SOURCE_PREFIXES=/diracx_sources"
570+
)
567571

568572
# Add any extension services
569573
for module_name, module_configs in module_configs.items():

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ install_requires =
2929
cachetools
3030
certifi
3131
diraccfg
32+
diracx-client
33+
diracx-core
3234
db12
3335
fts3
3436
gfal2-python
@@ -160,6 +162,7 @@ console_scripts =
160162
# FrameworkSystem
161163
dirac-login = DIRAC.FrameworkSystem.scripts.dirac_login:main
162164
dirac-logout = DIRAC.FrameworkSystem.scripts.dirac_logout:main
165+
dirac-diracx-whoami = DIRAC.FrameworkSystem.scripts.dirac_diracx_whoami:main
163166
dirac-admin-get-CAs = DIRAC.FrameworkSystem.scripts.dirac_admin_get_CAs:main [server]
164167
dirac-admin-get-proxy = DIRAC.FrameworkSystem.scripts.dirac_admin_get_proxy:main [admin]
165168
dirac-admin-proxy-upload = DIRAC.FrameworkSystem.scripts.dirac_admin_proxy_upload:main [admin]

src/DIRAC/Core/Security/DiracX.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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, S_ERROR
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 addTokenToPEM(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+
token = TokenResponse(
45+
access_token=token_content["access_token"],
46+
expires_in=token_content["expires_in"],
47+
token_type=token_content.get("token_type"),
48+
refresh_token=token_content.get("refresh_token"),
49+
)
50+
51+
token_pem = f"{PEM_BEGIN}\n"
52+
data = base64.b64encode(serialize_credentials(token).encode("utf-8")).decode()
53+
token_pem += textwrap.fill(data, width=64)
54+
token_pem += f"\n{PEM_END}\n"
55+
56+
with open(pemPath, "a") as f:
57+
f.write(token_pem)
58+
59+
60+
def diracxTokenFromPEM(pemPath) -> dict[str, Any] | None:
61+
"""Extract the DiracX token from the proxy PEM file"""
62+
pem = Path(pemPath).read_text()
63+
if match := RE_DIRACX_PEM.search(pem):
64+
match = match.group(1)
65+
return json.loads(base64.b64decode(match).decode("utf-8"))
66+
67+
68+
@contextmanager
69+
def DiracXClient() -> _DiracClient:
70+
"""Get a DiracX client instance with the current user's credentials"""
71+
diracxUrl = gConfig.getValue("/DiracX/URL")
72+
if not diracxUrl:
73+
raise ValueError("Missing mandatory /DiracX/URL configuration")
74+
75+
proxyLocation = getDefaultProxyLocation()
76+
diracxToken = diracxTokenFromPEM(proxyLocation)
77+
78+
with NamedTemporaryFile(mode="wt") as token_file:
79+
token_file.write(json.dumps(diracxToken))
80+
token_file.flush()
81+
token_file.seek(0)
82+
83+
pref = DiracxPreferences(url=diracxUrl, credentials_path=token_file.name)
84+
with _DiracClient(diracx_preferences=pref) as api:
85+
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: make this code path work with DiracX for Agents and possibly webapp ?
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 addTokenToPEM
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 := addTokenToPEM(filename, result["Value"]))["OK"]: # pylint: disable=unsubscriptable-object
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 := addTokenToPEM(filename, result["Value"]))["OK"]: # pylint: disable=unsubscriptable-object
669+
return result
670+
return S_OK(filename)
659671

660672
return S_OK(chain)
661673

src/DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from DIRAC.Core.Security import Properties
1313
from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader
1414
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
15-
15+
from DIRAC.FrameworkSystem.Utilities.diracx import get_token
1616

1717
DEFAULT_MAIL_FROM = "[email protected]"
1818

@@ -412,7 +412,6 @@ def export_getVOMSProxyWithToken(self, userDN, userGroup, requestPem, requiredLi
412412
@convertToReturnValue
413413
def export_exchangeProxyForToken(self):
414414
"""Exchange a proxy for an equivalent token to be used with diracx"""
415-
from DIRAC.FrameworkSystem.Utilities.diracx import get_token
416415

417416
credDict = self.getRemoteCredentials()
418417
return get_token(

src/DIRAC/FrameworkSystem/Utilities/diracx.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# pylint: disable=import-error
21
import requests
32

43
from cachetools import TTLCache, cached

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 addTokenToPEM
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 := addTokenToPEM(params.proxyPath, result["Value"]))["OK"]: # pylint: disable=unsubscriptable-object
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()

0 commit comments

Comments
 (0)