cmsdials 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmsdials-2.3.0 → cmsdials-2.4.0}/PKG-INFO +15 -12
- {cmsdials-2.3.0 → cmsdials-2.4.0}/README.md +14 -11
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/_version.py +3 -3
- cmsdials-2.4.0/cmsdials/auth/bearer.py +62 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/auth/client.py +18 -0
- cmsdials-2.4.0/cmsdials/auth/client_credentials.py +48 -0
- cmsdials-2.3.0/cmsdials/auth/bearer.py → cmsdials-2.4.0/cmsdials/auth/device.py +6 -3
- cmsdials-2.4.0/cmsdials/auth/secret_key.py +68 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/PKG-INFO +15 -12
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/SOURCES.txt +2 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/scm_file_list.json +2 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/scm_version.json +2 -2
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/env.py +3 -2
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_auth_client.py +19 -10
- cmsdials-2.4.0/tests/integration/utils.py +32 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/unit/test_auth.py +101 -11
- cmsdials-2.3.0/cmsdials/auth/secret_key.py +0 -65
- cmsdials-2.3.0/tests/integration/utils.py +0 -23
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.coveragerc +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.editorconfig +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.gitignore +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.gitlab-ci.yml +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.mailmap +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.pre-commit-config.yaml +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/.tool-versions +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/LICENSE +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/auth/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/auth/_base.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/auth/exceptions.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/auth/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/dataset_index/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/dataset_index/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/dataset_index/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/file_index/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/file_index/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/file_index/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h1d/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h1d/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h1d/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h2d/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h2d/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/h2d/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/lumisection/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/lumisection/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/lumisection/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/mes/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/mes/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/mes/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_bad_lumisection/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_bad_lumisection/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_bad_lumisection/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_models_index/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_models_index/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/ml_models_index/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/oms_proxy/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/oms_proxy/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/oms_proxy/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/run/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/run/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/run/models.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/workspace/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/clients/workspace/client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/cmsdials.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/filters.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/utils/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/utils/_json.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/utils/api_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/utils/base_model.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials/utils/logger.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/dependency_links.txt +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/requires.txt +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/cmsdials.egg-info/top_level.txt +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/mypy.ini +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/pyproject.toml +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/pytest.ini +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/ruff.toml +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/setup.cfg +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_file_index_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_h1d_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_h2d_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_lumisection_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_mes_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_oms_proxy_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_resume.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/integration/test_run_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/unit/__init__.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tests/unit/test_base_api_client.py +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/tox.ini +0 -0
- {cmsdials-2.3.0 → cmsdials-2.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmsdials
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The Python api client interface to DIALS service
|
|
5
5
|
Author-email: Gabriel Moreira <gabriel.moreira@cern.ch>
|
|
6
6
|
Project-URL: Repository, https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py
|
|
@@ -53,21 +53,21 @@ Credentials are issued by CERN's Keycloak directly, dials-py never sends your pa
|
|
|
53
53
|
### Generating credentials with client secret
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
|
-
from cmsdials.auth.
|
|
56
|
+
from cmsdials.auth.client_credentials import Credentials
|
|
57
57
|
|
|
58
|
-
creds = Credentials(client_id="my-application",
|
|
58
|
+
creds = Credentials(client_id="my-application", client_secret=".....")
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
The token is renewed automatically before it expires.
|
|
62
62
|
|
|
63
|
-
To get API access:
|
|
63
|
+
To get API access (see [API access for applications](https://cms-ppd.docs.cern.ch/ts/software/DIALS/operations/api_access.html) for the details):
|
|
64
64
|
|
|
65
65
|
1. Register an application in the [CERN Application Portal](https://application-portal.web.cern.ch) and enable "My application will need to get tokens using its own client ID and secret".
|
|
66
66
|
2. Ask the DIALS team to grant API access to the application (it is added to the DIALS API access group).
|
|
67
67
|
3. Use the application's client id and secret as shown above.
|
|
68
68
|
|
|
69
69
|
> [!WARNING]
|
|
70
|
-
> `Credentials(token
|
|
70
|
+
> `cmsdials.auth.secret_key.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Replace `Credentials(client_id=..., token=...)` with `client_credentials.Credentials(client_id=..., client_secret=...)`. Without `client_id`, it authenticates with the `X-CLIENT-SECRET` header, which DIALS will stop accepting.
|
|
71
71
|
|
|
72
72
|
### Generating credentials with device
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ To get API access:
|
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
77
|
from cmsdials.auth.client import AuthClient
|
|
78
|
-
from cmsdials.auth.
|
|
78
|
+
from cmsdials.auth.device import Credentials
|
|
79
79
|
|
|
80
80
|
auth = AuthClient()
|
|
81
81
|
token = auth.device_auth_flow()
|
|
@@ -87,15 +87,18 @@ creds = Credentials.from_authclient_token(token)
|
|
|
87
87
|
Credentials are always cached once you authenticate at least one time, calling this method without having a cached credential file will automatically trigger the AuthClient device flow.
|
|
88
88
|
|
|
89
89
|
```python
|
|
90
|
-
from cmsdials.auth.
|
|
90
|
+
from cmsdials.auth.device import Credentials
|
|
91
91
|
|
|
92
92
|
creds = Credentials.from_creds_file()
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
> [!WARNING]
|
|
96
|
+
> `cmsdials.auth.bearer.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Import `Credentials` from `cmsdials.auth.device` instead, it takes the same arguments.
|
|
97
|
+
|
|
95
98
|
### Basic Example
|
|
96
99
|
|
|
97
100
|
```python
|
|
98
|
-
from cmsdials.auth.
|
|
101
|
+
from cmsdials.auth.device import Credentials
|
|
99
102
|
from cmsdials import Dials
|
|
100
103
|
from cmsdials.filters import LumisectionHistogram1DFilters
|
|
101
104
|
|
|
@@ -260,7 +263,7 @@ When listing and endpoint for a long time, you may loose connection or the serve
|
|
|
260
263
|
|
|
261
264
|
```python
|
|
262
265
|
from cmsdials.auth.client import AuthClient
|
|
263
|
-
from cmsdials.auth.
|
|
266
|
+
from cmsdials.auth.device import Credentials
|
|
264
267
|
from cmsdials import Dials
|
|
265
268
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
266
269
|
|
|
@@ -319,7 +322,7 @@ The `AuthClient` issues tokens for the production DIALS applications by default.
|
|
|
319
322
|
|
|
320
323
|
```python
|
|
321
324
|
from cmsdials.auth.client import AuthClient
|
|
322
|
-
from cmsdials.auth.
|
|
325
|
+
from cmsdials.auth.device import Credentials
|
|
323
326
|
from cmsdials import Dials
|
|
324
327
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
325
328
|
|
|
@@ -338,9 +341,9 @@ dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy
|
|
|
338
341
|
When authenticating with a client secret, pass the same `AuthClient` so the token is issued for that instance:
|
|
339
342
|
|
|
340
343
|
```python
|
|
341
|
-
from cmsdials.auth.
|
|
344
|
+
from cmsdials.auth.client_credentials import Credentials as ClientCredentials
|
|
342
345
|
|
|
343
|
-
creds =
|
|
346
|
+
creds = ClientCredentials(client_id="my-application", client_secret=".....", client=auth)
|
|
344
347
|
dials = Dials(creds, base_url=DIALS_URL)
|
|
345
348
|
```
|
|
346
349
|
|
|
@@ -30,21 +30,21 @@ Credentials are issued by CERN's Keycloak directly, dials-py never sends your pa
|
|
|
30
30
|
### Generating credentials with client secret
|
|
31
31
|
|
|
32
32
|
```python
|
|
33
|
-
from cmsdials.auth.
|
|
33
|
+
from cmsdials.auth.client_credentials import Credentials
|
|
34
34
|
|
|
35
|
-
creds = Credentials(client_id="my-application",
|
|
35
|
+
creds = Credentials(client_id="my-application", client_secret=".....")
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
The token is renewed automatically before it expires.
|
|
39
39
|
|
|
40
|
-
To get API access:
|
|
40
|
+
To get API access (see [API access for applications](https://cms-ppd.docs.cern.ch/ts/software/DIALS/operations/api_access.html) for the details):
|
|
41
41
|
|
|
42
42
|
1. Register an application in the [CERN Application Portal](https://application-portal.web.cern.ch) and enable "My application will need to get tokens using its own client ID and secret".
|
|
43
43
|
2. Ask the DIALS team to grant API access to the application (it is added to the DIALS API access group).
|
|
44
44
|
3. Use the application's client id and secret as shown above.
|
|
45
45
|
|
|
46
46
|
> [!WARNING]
|
|
47
|
-
> `Credentials(token
|
|
47
|
+
> `cmsdials.auth.secret_key.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Replace `Credentials(client_id=..., token=...)` with `client_credentials.Credentials(client_id=..., client_secret=...)`. Without `client_id`, it authenticates with the `X-CLIENT-SECRET` header, which DIALS will stop accepting.
|
|
48
48
|
|
|
49
49
|
### Generating credentials with device
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ To get API access:
|
|
|
52
52
|
|
|
53
53
|
```python
|
|
54
54
|
from cmsdials.auth.client import AuthClient
|
|
55
|
-
from cmsdials.auth.
|
|
55
|
+
from cmsdials.auth.device import Credentials
|
|
56
56
|
|
|
57
57
|
auth = AuthClient()
|
|
58
58
|
token = auth.device_auth_flow()
|
|
@@ -64,15 +64,18 @@ creds = Credentials.from_authclient_token(token)
|
|
|
64
64
|
Credentials are always cached once you authenticate at least one time, calling this method without having a cached credential file will automatically trigger the AuthClient device flow.
|
|
65
65
|
|
|
66
66
|
```python
|
|
67
|
-
from cmsdials.auth.
|
|
67
|
+
from cmsdials.auth.device import Credentials
|
|
68
68
|
|
|
69
69
|
creds = Credentials.from_creds_file()
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
> [!WARNING]
|
|
73
|
+
> `cmsdials.auth.bearer.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Import `Credentials` from `cmsdials.auth.device` instead, it takes the same arguments.
|
|
74
|
+
|
|
72
75
|
### Basic Example
|
|
73
76
|
|
|
74
77
|
```python
|
|
75
|
-
from cmsdials.auth.
|
|
78
|
+
from cmsdials.auth.device import Credentials
|
|
76
79
|
from cmsdials import Dials
|
|
77
80
|
from cmsdials.filters import LumisectionHistogram1DFilters
|
|
78
81
|
|
|
@@ -237,7 +240,7 @@ When listing and endpoint for a long time, you may loose connection or the serve
|
|
|
237
240
|
|
|
238
241
|
```python
|
|
239
242
|
from cmsdials.auth.client import AuthClient
|
|
240
|
-
from cmsdials.auth.
|
|
243
|
+
from cmsdials.auth.device import Credentials
|
|
241
244
|
from cmsdials import Dials
|
|
242
245
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
243
246
|
|
|
@@ -296,7 +299,7 @@ The `AuthClient` issues tokens for the production DIALS applications by default.
|
|
|
296
299
|
|
|
297
300
|
```python
|
|
298
301
|
from cmsdials.auth.client import AuthClient
|
|
299
|
-
from cmsdials.auth.
|
|
302
|
+
from cmsdials.auth.device import Credentials
|
|
300
303
|
from cmsdials import Dials
|
|
301
304
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
302
305
|
|
|
@@ -315,9 +318,9 @@ dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy
|
|
|
315
318
|
When authenticating with a client secret, pass the same `AuthClient` so the token is issued for that instance:
|
|
316
319
|
|
|
317
320
|
```python
|
|
318
|
-
from cmsdials.auth.
|
|
321
|
+
from cmsdials.auth.client_credentials import Credentials as ClientCredentials
|
|
319
322
|
|
|
320
|
-
creds =
|
|
323
|
+
creds = ClientCredentials(client_id="my-application", client_secret=".....", client=auth)
|
|
321
324
|
dials = Dials(creds, base_url=DIALS_URL)
|
|
322
325
|
```
|
|
323
326
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g9cd07880c'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .client import AuthClient
|
|
6
|
+
from .device import Credentials as DeviceCredentials
|
|
7
|
+
from .models import Token
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DEPRECATION = (
|
|
11
|
+
"cmsdials.auth.bearer.Credentials is deprecated and will be removed in "
|
|
12
|
+
"dials-py 3.0.0, use cmsdials.auth.device.Credentials instead."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Credentials(DeviceCredentials):
|
|
17
|
+
"""
|
|
18
|
+
Deprecated, use `cmsdials.auth.device.Credentials`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
token: str,
|
|
24
|
+
expires_at: datetime,
|
|
25
|
+
expires_in: int,
|
|
26
|
+
refresh_expires_at: datetime,
|
|
27
|
+
refresh_expires_in: int,
|
|
28
|
+
refresh_token: str,
|
|
29
|
+
token_type: str,
|
|
30
|
+
cache_dir: Optional[str] = None,
|
|
31
|
+
client: Optional[AuthClient] = None,
|
|
32
|
+
):
|
|
33
|
+
warnings.warn(_DEPRECATION, DeprecationWarning, stacklevel=2)
|
|
34
|
+
super().__init__(
|
|
35
|
+
token=token,
|
|
36
|
+
expires_at=expires_at,
|
|
37
|
+
expires_in=expires_in,
|
|
38
|
+
refresh_expires_at=refresh_expires_at,
|
|
39
|
+
refresh_expires_in=refresh_expires_in,
|
|
40
|
+
refresh_token=refresh_token,
|
|
41
|
+
token_type=token_type,
|
|
42
|
+
cache_dir=cache_dir,
|
|
43
|
+
client=client,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def from_authclient_token(
|
|
48
|
+
token: Token,
|
|
49
|
+
cache_dir: Optional[str] = None,
|
|
50
|
+
client: Optional[AuthClient] = None,
|
|
51
|
+
) -> DeviceCredentials:
|
|
52
|
+
warnings.warn(_DEPRECATION, DeprecationWarning, stacklevel=2)
|
|
53
|
+
return DeviceCredentials.from_authclient_token(
|
|
54
|
+
token, cache_dir, client
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def from_creds_file(
|
|
59
|
+
cache_dir: Optional[str] = None, client: Optional[AuthClient] = None
|
|
60
|
+
) -> DeviceCredentials:
|
|
61
|
+
warnings.warn(_DEPRECATION, DeprecationWarning, stacklevel=2)
|
|
62
|
+
return DeviceCredentials.from_creds_file(cache_dir, client)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import warnings
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
@@ -47,6 +48,23 @@ class AuthClient(BaseAPIClient):
|
|
|
47
48
|
audience: Optional[str] = None,
|
|
48
49
|
**kwargs,
|
|
49
50
|
) -> None:
|
|
51
|
+
# Leftovers from releases that logged in through DIALS: tokens are now
|
|
52
|
+
# issued by Keycloak, and the instance is selected by client_id and
|
|
53
|
+
# audience, so these would silently target production.
|
|
54
|
+
ignored = [
|
|
55
|
+
name
|
|
56
|
+
for name in ("base_url", "route", "version")
|
|
57
|
+
if kwargs.get(name) is not None
|
|
58
|
+
]
|
|
59
|
+
if args or ignored:
|
|
60
|
+
warnings.warn(
|
|
61
|
+
"AuthClient ignores base_url, route and version, which will "
|
|
62
|
+
"raise TypeError in dials-py 3.0.0. Tokens are issued by CERN "
|
|
63
|
+
"Keycloak: to target another DIALS instance, pass its "
|
|
64
|
+
"client_id and audience instead.",
|
|
65
|
+
DeprecationWarning,
|
|
66
|
+
stacklevel=2,
|
|
67
|
+
)
|
|
50
68
|
super().__init__(*args, **kwargs)
|
|
51
69
|
keycloak_url = keycloak_url or self.PRODUCTION_KEYCLOAK_URL
|
|
52
70
|
if not keycloak_url.endswith("/"):
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ._base import BaseCredentials
|
|
4
|
+
from .client import AuthClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Credentials(BaseCredentials):
|
|
8
|
+
"""
|
|
9
|
+
Client credentials.
|
|
10
|
+
These credentials use an application's client id and secret to provide
|
|
11
|
+
authorization to automation scripts.
|
|
12
|
+
|
|
13
|
+
An api-access token is issued by CERN's Keycloak and sent as a Bearer
|
|
14
|
+
token, and a new one is issued before it expires. The application must
|
|
15
|
+
have been granted API access in DIALS.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
client_id: str,
|
|
21
|
+
client_secret: str,
|
|
22
|
+
client: Optional[AuthClient] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
if not client_id:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"client_id must be the non-empty client id of the application"
|
|
27
|
+
)
|
|
28
|
+
if not client_secret:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"client_secret must be the non-empty client secret of the application"
|
|
31
|
+
)
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.client_id = client_id
|
|
34
|
+
self.client_secret = client_secret
|
|
35
|
+
self.client = client or AuthClient()
|
|
36
|
+
self.refresh()
|
|
37
|
+
|
|
38
|
+
def refresh(self) -> None:
|
|
39
|
+
api_token = self.client.api_access_token(
|
|
40
|
+
self.client_id, self.client_secret
|
|
41
|
+
)
|
|
42
|
+
self.token = api_token.access_token
|
|
43
|
+
self.token_type = api_token.token_type
|
|
44
|
+
self.expires_in = api_token.expires_in
|
|
45
|
+
self.expires_at = api_token.expires_at
|
|
46
|
+
|
|
47
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
48
|
+
headers["Authorization"] = f"{self.token_type} {self.token}"
|
|
@@ -19,8 +19,11 @@ DEFAULT_CACHE_FILENAME = "creds"
|
|
|
19
19
|
|
|
20
20
|
class Credentials(BaseCredentials):
|
|
21
21
|
"""
|
|
22
|
-
|
|
23
|
-
These
|
|
22
|
+
Device credentials.
|
|
23
|
+
These credentials authenticate a person with the device authorization
|
|
24
|
+
grant on CERN's Keycloak, and renew the token with its refresh token.
|
|
25
|
+
Tokens are cached in a file, and the device flow is triggered again
|
|
26
|
+
when they can no longer be renewed.
|
|
24
27
|
"""
|
|
25
28
|
|
|
26
29
|
def __init__(
|
|
@@ -170,6 +173,6 @@ class Credentials(BaseCredentials):
|
|
|
170
173
|
|
|
171
174
|
def apply(self, headers: dict[str, str]) -> None:
|
|
172
175
|
"""
|
|
173
|
-
Apply the
|
|
176
|
+
Apply the access token to the Authorization header.
|
|
174
177
|
"""
|
|
175
178
|
headers["Authorization"] = f"{self.token_type} {self.token}"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ._base import BaseCredentials
|
|
5
|
+
from .client import AuthClient
|
|
6
|
+
from .client_credentials import Credentials as ClientCredentials
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Credentials(ClientCredentials):
|
|
10
|
+
"""
|
|
11
|
+
Deprecated, use `cmsdials.auth.client_credentials.Credentials`.
|
|
12
|
+
|
|
13
|
+
With `client_id`, it behaves as client credentials, with `token` as the
|
|
14
|
+
client secret.
|
|
15
|
+
|
|
16
|
+
Without `client_id`, the secret is sent in the X-CLIENT-SECRET header.
|
|
17
|
+
This will stop working once DIALS removes the header.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
token: Optional[str],
|
|
23
|
+
client_id: Optional[str] = None,
|
|
24
|
+
client: Optional[AuthClient] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
if not token:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
"token must be a non-empty Client secret key string"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
replacement = (
|
|
32
|
+
"use cmsdials.auth.client_credentials.Credentials("
|
|
33
|
+
"client_id=..., client_secret=...) instead."
|
|
34
|
+
)
|
|
35
|
+
self.legacy_header = client_id is None
|
|
36
|
+
if client_id is None:
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"Credentials without client_id authenticate with the "
|
|
39
|
+
"X-CLIENT-SECRET header, which DIALS will stop accepting, "
|
|
40
|
+
+ replacement,
|
|
41
|
+
DeprecationWarning,
|
|
42
|
+
stacklevel=2,
|
|
43
|
+
)
|
|
44
|
+
BaseCredentials.__init__(self)
|
|
45
|
+
self.client_secret = token
|
|
46
|
+
self.token = token
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
warnings.warn(
|
|
50
|
+
"cmsdials.auth.secret_key.Credentials is deprecated and will be "
|
|
51
|
+
"removed in dials-py 3.0.0, " + replacement,
|
|
52
|
+
DeprecationWarning,
|
|
53
|
+
stacklevel=2,
|
|
54
|
+
)
|
|
55
|
+
super().__init__(
|
|
56
|
+
client_id=client_id, client_secret=token, client=client
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def refresh(self) -> None:
|
|
60
|
+
if self.legacy_header:
|
|
61
|
+
return
|
|
62
|
+
super().refresh()
|
|
63
|
+
|
|
64
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
65
|
+
if self.legacy_header:
|
|
66
|
+
headers["X-CLIENT-SECRET"] = self.token
|
|
67
|
+
else:
|
|
68
|
+
super().apply(headers)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmsdials
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The Python api client interface to DIALS service
|
|
5
5
|
Author-email: Gabriel Moreira <gabriel.moreira@cern.ch>
|
|
6
6
|
Project-URL: Repository, https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py
|
|
@@ -53,21 +53,21 @@ Credentials are issued by CERN's Keycloak directly, dials-py never sends your pa
|
|
|
53
53
|
### Generating credentials with client secret
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
|
-
from cmsdials.auth.
|
|
56
|
+
from cmsdials.auth.client_credentials import Credentials
|
|
57
57
|
|
|
58
|
-
creds = Credentials(client_id="my-application",
|
|
58
|
+
creds = Credentials(client_id="my-application", client_secret=".....")
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
The token is renewed automatically before it expires.
|
|
62
62
|
|
|
63
|
-
To get API access:
|
|
63
|
+
To get API access (see [API access for applications](https://cms-ppd.docs.cern.ch/ts/software/DIALS/operations/api_access.html) for the details):
|
|
64
64
|
|
|
65
65
|
1. Register an application in the [CERN Application Portal](https://application-portal.web.cern.ch) and enable "My application will need to get tokens using its own client ID and secret".
|
|
66
66
|
2. Ask the DIALS team to grant API access to the application (it is added to the DIALS API access group).
|
|
67
67
|
3. Use the application's client id and secret as shown above.
|
|
68
68
|
|
|
69
69
|
> [!WARNING]
|
|
70
|
-
> `Credentials(token
|
|
70
|
+
> `cmsdials.auth.secret_key.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Replace `Credentials(client_id=..., token=...)` with `client_credentials.Credentials(client_id=..., client_secret=...)`. Without `client_id`, it authenticates with the `X-CLIENT-SECRET` header, which DIALS will stop accepting.
|
|
71
71
|
|
|
72
72
|
### Generating credentials with device
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ To get API access:
|
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
77
|
from cmsdials.auth.client import AuthClient
|
|
78
|
-
from cmsdials.auth.
|
|
78
|
+
from cmsdials.auth.device import Credentials
|
|
79
79
|
|
|
80
80
|
auth = AuthClient()
|
|
81
81
|
token = auth.device_auth_flow()
|
|
@@ -87,15 +87,18 @@ creds = Credentials.from_authclient_token(token)
|
|
|
87
87
|
Credentials are always cached once you authenticate at least one time, calling this method without having a cached credential file will automatically trigger the AuthClient device flow.
|
|
88
88
|
|
|
89
89
|
```python
|
|
90
|
-
from cmsdials.auth.
|
|
90
|
+
from cmsdials.auth.device import Credentials
|
|
91
91
|
|
|
92
92
|
creds = Credentials.from_creds_file()
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
> [!WARNING]
|
|
96
|
+
> `cmsdials.auth.bearer.Credentials` is deprecated and emits a `DeprecationWarning`: it will be removed in dials-py 3.0.0. Import `Credentials` from `cmsdials.auth.device` instead, it takes the same arguments.
|
|
97
|
+
|
|
95
98
|
### Basic Example
|
|
96
99
|
|
|
97
100
|
```python
|
|
98
|
-
from cmsdials.auth.
|
|
101
|
+
from cmsdials.auth.device import Credentials
|
|
99
102
|
from cmsdials import Dials
|
|
100
103
|
from cmsdials.filters import LumisectionHistogram1DFilters
|
|
101
104
|
|
|
@@ -260,7 +263,7 @@ When listing and endpoint for a long time, you may loose connection or the serve
|
|
|
260
263
|
|
|
261
264
|
```python
|
|
262
265
|
from cmsdials.auth.client import AuthClient
|
|
263
|
-
from cmsdials.auth.
|
|
266
|
+
from cmsdials.auth.device import Credentials
|
|
264
267
|
from cmsdials import Dials
|
|
265
268
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
266
269
|
|
|
@@ -319,7 +322,7 @@ The `AuthClient` issues tokens for the production DIALS applications by default.
|
|
|
319
322
|
|
|
320
323
|
```python
|
|
321
324
|
from cmsdials.auth.client import AuthClient
|
|
322
|
-
from cmsdials.auth.
|
|
325
|
+
from cmsdials.auth.device import Credentials
|
|
323
326
|
from cmsdials import Dials
|
|
324
327
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
325
328
|
|
|
@@ -338,9 +341,9 @@ dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy
|
|
|
338
341
|
When authenticating with a client secret, pass the same `AuthClient` so the token is issued for that instance:
|
|
339
342
|
|
|
340
343
|
```python
|
|
341
|
-
from cmsdials.auth.
|
|
344
|
+
from cmsdials.auth.client_credentials import Credentials as ClientCredentials
|
|
342
345
|
|
|
343
|
-
creds =
|
|
346
|
+
creds = ClientCredentials(client_id="my-application", client_secret=".....", client=auth)
|
|
344
347
|
dials = Dials(creds, base_url=DIALS_URL)
|
|
345
348
|
```
|
|
346
349
|
|
|
@@ -28,6 +28,8 @@ cmsdials/auth/__init__.py
|
|
|
28
28
|
cmsdials/auth/_base.py
|
|
29
29
|
cmsdials/auth/bearer.py
|
|
30
30
|
cmsdials/auth/client.py
|
|
31
|
+
cmsdials/auth/client_credentials.py
|
|
32
|
+
cmsdials/auth/device.py
|
|
31
33
|
cmsdials/auth/exceptions.py
|
|
32
34
|
cmsdials/auth/models.py
|
|
33
35
|
cmsdials/auth/secret_key.py
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
if not
|
|
4
|
+
_client_secret = os.getenv("CLIENT_SECRET")
|
|
5
|
+
if not _client_secret:
|
|
6
6
|
raise OSError("CLIENT_SECRET environment variable is not set.")
|
|
7
|
+
CLIENT_SECRET: str = _client_secret
|
|
7
8
|
|
|
8
9
|
BASE_URL = os.getenv("BASE_URL", "https://dev-cmsdials-api.web.cern.ch/")
|
|
9
10
|
# Client id of the application owning CLIENT_SECRET. When set, tests authenticate
|
|
@@ -5,7 +5,10 @@ import pytest
|
|
|
5
5
|
from requests.exceptions import HTTPError
|
|
6
6
|
|
|
7
7
|
from cmsdials import Dials
|
|
8
|
-
from cmsdials.auth.
|
|
8
|
+
from cmsdials.auth.client_credentials import (
|
|
9
|
+
Credentials as ClientCredentials,
|
|
10
|
+
)
|
|
11
|
+
from cmsdials.auth.device import Credentials as DeviceCredentials
|
|
9
12
|
from cmsdials.auth.models import Token
|
|
10
13
|
from cmsdials.auth.secret_key import Credentials as SecretKeyCredentials
|
|
11
14
|
|
|
@@ -14,7 +17,7 @@ from .utils import build_auth_client
|
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def __setup_dials_from_creds_and_test(
|
|
17
|
-
creds: Union[SecretKeyCredentials,
|
|
20
|
+
creds: Union[ClientCredentials, SecretKeyCredentials, DeviceCredentials],
|
|
18
21
|
) -> None:
|
|
19
22
|
dials = Dials(creds, workspace=TEST_WORKSPACE, base_url=BASE_URL)
|
|
20
23
|
# just test if we can access the workspace endpoints
|
|
@@ -29,8 +32,11 @@ def test_secret_key_success() -> None:
|
|
|
29
32
|
|
|
30
33
|
@pytest.mark.skipif(CLIENT_ID is None, reason="CLIENT_ID is not set")
|
|
31
34
|
def test_api_access_success() -> None:
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
assert CLIENT_ID is not None
|
|
36
|
+
creds = ClientCredentials(
|
|
37
|
+
client_id=CLIENT_ID,
|
|
38
|
+
client_secret=CLIENT_SECRET,
|
|
39
|
+
client=build_auth_client(),
|
|
34
40
|
)
|
|
35
41
|
headers: dict[str, str] = {}
|
|
36
42
|
creds.before_request(headers)
|
|
@@ -40,9 +46,12 @@ def test_api_access_success() -> None:
|
|
|
40
46
|
|
|
41
47
|
@pytest.mark.skipif(CLIENT_ID is None, reason="CLIENT_ID is not set")
|
|
42
48
|
def test_api_access_fail() -> None:
|
|
49
|
+
assert CLIENT_ID is not None
|
|
43
50
|
with pytest.raises(HTTPError, match=r"401 Client Error: Unauthorized"):
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
ClientCredentials(
|
|
52
|
+
client_id=CLIENT_ID,
|
|
53
|
+
client_secret="123456789",
|
|
54
|
+
client=build_auth_client(),
|
|
46
55
|
)
|
|
47
56
|
|
|
48
57
|
|
|
@@ -57,14 +66,14 @@ def test_secret_key_fail() -> None:
|
|
|
57
66
|
# This cannot be tested automatically since the `device_auth_flow` prints to stdout the url
|
|
58
67
|
# the user needs to access in order to authenticate and pytest only enable "live log call"
|
|
59
68
|
# if you specify the parameter "--log-cli-level=INFO".
|
|
60
|
-
# def
|
|
69
|
+
# def test_device_success() -> None:
|
|
61
70
|
# auth = build_auth_client()
|
|
62
71
|
# token = auth.device_auth_flow()
|
|
63
|
-
# creds =
|
|
72
|
+
# creds = DeviceCredentials.from_authclient_token(token, client=auth)
|
|
64
73
|
# __setup_dials_from_creds_and_test(creds)
|
|
65
74
|
|
|
66
75
|
|
|
67
|
-
def
|
|
76
|
+
def test_device_fail() -> None:
|
|
68
77
|
token = Token(
|
|
69
78
|
access_token="123456789",
|
|
70
79
|
expires_in=600,
|
|
@@ -75,7 +84,7 @@ def test_bearer_fail() -> None:
|
|
|
75
84
|
refresh_expires_at=datetime.now() + timedelta(seconds=12000),
|
|
76
85
|
)
|
|
77
86
|
auth = build_auth_client()
|
|
78
|
-
creds =
|
|
87
|
+
creds = DeviceCredentials.from_authclient_token(token, client=auth)
|
|
79
88
|
with pytest.raises(
|
|
80
89
|
HTTPError, match=r"403 Client Error: Forbidden for url"
|
|
81
90
|
):
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from cmsdials import Dials
|
|
2
|
+
from cmsdials.auth._base import BaseCredentials
|
|
3
|
+
from cmsdials.auth.client import AuthClient
|
|
4
|
+
from cmsdials.auth.client_credentials import Credentials
|
|
5
|
+
from cmsdials.auth.secret_key import Credentials as SecretKeyCredentials
|
|
6
|
+
|
|
7
|
+
from .env import (
|
|
8
|
+
AUTH_AUDIENCE,
|
|
9
|
+
AUTH_CLIENT_ID,
|
|
10
|
+
BASE_URL,
|
|
11
|
+
CLIENT_ID,
|
|
12
|
+
CLIENT_SECRET,
|
|
13
|
+
TEST_WORKSPACE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_auth_client() -> AuthClient:
|
|
18
|
+
return AuthClient(client_id=AUTH_CLIENT_ID, audience=AUTH_AUDIENCE)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_dials_object() -> Dials:
|
|
22
|
+
creds: BaseCredentials
|
|
23
|
+
if CLIENT_ID is None:
|
|
24
|
+
# Deprecated X-CLIENT-SECRET header, until DIALS stops accepting it
|
|
25
|
+
creds = SecretKeyCredentials(token=CLIENT_SECRET)
|
|
26
|
+
else:
|
|
27
|
+
creds = Credentials(
|
|
28
|
+
client_id=CLIENT_ID,
|
|
29
|
+
client_secret=CLIENT_SECRET,
|
|
30
|
+
client=build_auth_client(),
|
|
31
|
+
)
|
|
32
|
+
return Dials(creds, base_url=BASE_URL, workspace=TEST_WORKSPACE)
|
|
@@ -7,6 +7,10 @@ from requests.exceptions import HTTPError
|
|
|
7
7
|
from cmsdials.auth import client as client_module
|
|
8
8
|
from cmsdials.auth.bearer import Credentials as BearerCredentials
|
|
9
9
|
from cmsdials.auth.client import AuthClient
|
|
10
|
+
from cmsdials.auth.client_credentials import (
|
|
11
|
+
Credentials as ClientCredentials,
|
|
12
|
+
)
|
|
13
|
+
from cmsdials.auth.device import Credentials as DeviceCredentials
|
|
10
14
|
from cmsdials.auth.exceptions import PendingAuthorizationError
|
|
11
15
|
from cmsdials.auth.models import ApiToken, Token
|
|
12
16
|
from cmsdials.auth.secret_key import Credentials as SecretKeyCredentials
|
|
@@ -70,6 +74,21 @@ def test_defaults_target_production():
|
|
|
70
74
|
assert auth.audience == "cms-dials-prod-confidential-app"
|
|
71
75
|
|
|
72
76
|
|
|
77
|
+
@pytest.mark.parametrize(
|
|
78
|
+
("args", "kwargs"),
|
|
79
|
+
[
|
|
80
|
+
(("https://dev-cmsdials-api.web.cern.ch/",), {}),
|
|
81
|
+
((), {"base_url": "https://dev-cmsdials-api.web.cern.ch/"}),
|
|
82
|
+
((), {"route": "api/"}),
|
|
83
|
+
((), {"version": "v1/"}),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
def test_auth_client_warns_on_ignored_api_arguments(args, kwargs):
|
|
87
|
+
with pytest.warns(DeprecationWarning, match="client_id and audience"):
|
|
88
|
+
auth = AuthClient(*args, **kwargs)
|
|
89
|
+
assert auth.audience == AuthClient.PRODUCTION_AUDIENCE
|
|
90
|
+
|
|
91
|
+
|
|
73
92
|
def test_api_access_token_requests_dials_audience(keycloak):
|
|
74
93
|
calls, responses = keycloak
|
|
75
94
|
responses.append(FakeResponse(200, API_TOKEN))
|
|
@@ -137,7 +156,7 @@ def test_refresh_token_refreshes_then_exchanges(keycloak):
|
|
|
137
156
|
|
|
138
157
|
|
|
139
158
|
def test_secret_key_without_client_id_is_deprecated():
|
|
140
|
-
with pytest.warns(DeprecationWarning):
|
|
159
|
+
with pytest.warns(DeprecationWarning, match="X-CLIENT-SECRET"):
|
|
141
160
|
creds = SecretKeyCredentials(token="my-secret")
|
|
142
161
|
headers = {}
|
|
143
162
|
creds.before_request(headers)
|
|
@@ -159,10 +178,21 @@ class FakeAuthClient:
|
|
|
159
178
|
)
|
|
160
179
|
|
|
161
180
|
|
|
162
|
-
def
|
|
181
|
+
def test_client_credentials_require_client_id_and_secret():
|
|
182
|
+
with pytest.raises(ValueError, match="client_id"):
|
|
183
|
+
ClientCredentials(
|
|
184
|
+
client_id="", client_secret="my-secret", client=FakeAuthClient()
|
|
185
|
+
)
|
|
186
|
+
with pytest.raises(ValueError, match="client_secret"):
|
|
187
|
+
ClientCredentials(
|
|
188
|
+
client_id="my-app", client_secret="", client=FakeAuthClient()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_client_credentials_send_bearer():
|
|
163
193
|
auth = FakeAuthClient()
|
|
164
|
-
creds =
|
|
165
|
-
|
|
194
|
+
creds = ClientCredentials(
|
|
195
|
+
client_id="my-app", client_secret="my-secret", client=auth
|
|
166
196
|
)
|
|
167
197
|
|
|
168
198
|
headers = {}
|
|
@@ -174,10 +204,10 @@ def test_secret_key_with_client_id_sends_bearer():
|
|
|
174
204
|
assert auth.issued == 1
|
|
175
205
|
|
|
176
206
|
|
|
177
|
-
def
|
|
207
|
+
def test_client_credentials_reissue_expired_token():
|
|
178
208
|
auth = FakeAuthClient()
|
|
179
|
-
creds =
|
|
180
|
-
|
|
209
|
+
creds = ClientCredentials(
|
|
210
|
+
client_id="my-app", client_secret="my-secret", client=auth
|
|
181
211
|
)
|
|
182
212
|
creds.expires_at = datetime.now() - timedelta(seconds=1)
|
|
183
213
|
|
|
@@ -188,6 +218,21 @@ def test_secret_key_reissues_expired_token():
|
|
|
188
218
|
assert auth.issued == 2
|
|
189
219
|
|
|
190
220
|
|
|
221
|
+
def test_secret_key_with_client_id_is_deprecated_alias():
|
|
222
|
+
auth = FakeAuthClient()
|
|
223
|
+
with pytest.warns(DeprecationWarning, match="client_credentials"):
|
|
224
|
+
creds = SecretKeyCredentials(
|
|
225
|
+
token="my-secret", client_id="my-app", client=auth
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
headers = {}
|
|
229
|
+
creds.before_request(headers)
|
|
230
|
+
|
|
231
|
+
assert isinstance(creds, ClientCredentials)
|
|
232
|
+
assert creds.client_secret == "my-secret"
|
|
233
|
+
assert headers == {"Authorization": "Bearer api-access-1"}
|
|
234
|
+
|
|
235
|
+
|
|
191
236
|
class RejectingAuthClient:
|
|
192
237
|
def __init__(self):
|
|
193
238
|
self.device_flows = 0
|
|
@@ -209,10 +254,10 @@ class RejectingAuthClient:
|
|
|
209
254
|
)
|
|
210
255
|
|
|
211
256
|
|
|
212
|
-
def
|
|
257
|
+
def test_device_falls_back_to_device_flow_when_refresh_rejected(tmp_path):
|
|
213
258
|
now = datetime.now()
|
|
214
259
|
auth = RejectingAuthClient()
|
|
215
|
-
creds =
|
|
260
|
+
creds = DeviceCredentials(
|
|
216
261
|
token="old-access",
|
|
217
262
|
expires_at=now - timedelta(seconds=1),
|
|
218
263
|
expires_in=1200,
|
|
@@ -245,13 +290,13 @@ class UnreachableAuthClient(RejectingAuthClient):
|
|
|
245
290
|
(UnreachableAuthClient, requests.exceptions.ConnectionError),
|
|
246
291
|
],
|
|
247
292
|
)
|
|
248
|
-
def
|
|
293
|
+
def test_device_does_not_hide_other_refresh_errors(
|
|
249
294
|
tmp_path, auth_class, error
|
|
250
295
|
):
|
|
251
296
|
now = datetime.now()
|
|
252
297
|
auth = auth_class()
|
|
253
298
|
with pytest.raises(error):
|
|
254
|
-
|
|
299
|
+
DeviceCredentials(
|
|
255
300
|
token="old-access",
|
|
256
301
|
expires_at=now - timedelta(seconds=1),
|
|
257
302
|
expires_in=1200,
|
|
@@ -263,3 +308,48 @@ def test_bearer_does_not_hide_other_refresh_errors(
|
|
|
263
308
|
client=auth,
|
|
264
309
|
)
|
|
265
310
|
assert auth.device_flows == 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def fresh_token():
|
|
314
|
+
now = datetime.now()
|
|
315
|
+
return Token(
|
|
316
|
+
access_token="dials-access",
|
|
317
|
+
expires_in=1200,
|
|
318
|
+
refresh_expires_in=36000,
|
|
319
|
+
refresh_token="public-refresh",
|
|
320
|
+
token_type="Bearer",
|
|
321
|
+
expires_at=now + timedelta(seconds=1200),
|
|
322
|
+
refresh_expires_at=now + timedelta(seconds=36000),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_bearer_is_deprecated_alias(tmp_path):
|
|
327
|
+
token = fresh_token()
|
|
328
|
+
with pytest.warns(DeprecationWarning, match="auth.device"):
|
|
329
|
+
creds = BearerCredentials(
|
|
330
|
+
token=token.access_token,
|
|
331
|
+
expires_at=token.expires_at,
|
|
332
|
+
expires_in=token.expires_in,
|
|
333
|
+
refresh_expires_at=token.refresh_expires_at,
|
|
334
|
+
refresh_expires_in=token.refresh_expires_in,
|
|
335
|
+
refresh_token=token.refresh_token,
|
|
336
|
+
token_type=token.token_type,
|
|
337
|
+
cache_dir=str(tmp_path),
|
|
338
|
+
client=RejectingAuthClient(),
|
|
339
|
+
)
|
|
340
|
+
assert isinstance(creds, DeviceCredentials)
|
|
341
|
+
|
|
342
|
+
with pytest.warns(DeprecationWarning, match="auth.device"):
|
|
343
|
+
creds = BearerCredentials.from_authclient_token(
|
|
344
|
+
token, cache_dir=str(tmp_path), client=RejectingAuthClient()
|
|
345
|
+
)
|
|
346
|
+
assert isinstance(creds, DeviceCredentials)
|
|
347
|
+
|
|
348
|
+
# Reads the credentials cached by the calls above
|
|
349
|
+
with pytest.warns(DeprecationWarning, match="auth.device"):
|
|
350
|
+
creds = BearerCredentials.from_creds_file(
|
|
351
|
+
cache_dir=str(tmp_path), client=RejectingAuthClient()
|
|
352
|
+
)
|
|
353
|
+
headers = {}
|
|
354
|
+
creds.before_request(headers)
|
|
355
|
+
assert headers == {"Authorization": "Bearer dials-access"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import warnings
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from ._base import BaseCredentials
|
|
5
|
-
from .client import AuthClient
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Credentials(BaseCredentials):
|
|
9
|
-
"""
|
|
10
|
-
Client secret key credentials.
|
|
11
|
-
These credentials use an application's client id and secret to provide
|
|
12
|
-
authorization to automation scripts.
|
|
13
|
-
|
|
14
|
-
With `client_id`, an api-access token is issued by CERN's Keycloak and sent
|
|
15
|
-
as a Bearer token, and a new one is issued before it expires. The
|
|
16
|
-
application must have been granted API access in DIALS.
|
|
17
|
-
|
|
18
|
-
Without `client_id`, the secret is sent in the X-CLIENT-SECRET header.
|
|
19
|
-
This is deprecated and will stop working once DIALS removes the header.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
23
|
-
self,
|
|
24
|
-
token: Optional[str],
|
|
25
|
-
client_id: Optional[str] = None,
|
|
26
|
-
client: Optional[AuthClient] = None,
|
|
27
|
-
) -> None:
|
|
28
|
-
if not token:
|
|
29
|
-
raise ValueError(
|
|
30
|
-
"token must be a non-empty Client secret key string"
|
|
31
|
-
)
|
|
32
|
-
super().__init__()
|
|
33
|
-
self.client_id = client_id
|
|
34
|
-
self.client_secret = token
|
|
35
|
-
|
|
36
|
-
if client_id is None:
|
|
37
|
-
warnings.warn(
|
|
38
|
-
"Credentials without client_id authenticate with the "
|
|
39
|
-
"X-CLIENT-SECRET header, which DIALS will stop accepting. "
|
|
40
|
-
"Pass the application's client_id as well.",
|
|
41
|
-
DeprecationWarning,
|
|
42
|
-
stacklevel=2,
|
|
43
|
-
)
|
|
44
|
-
self.token = token
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
self.client = client or AuthClient()
|
|
48
|
-
self.refresh()
|
|
49
|
-
|
|
50
|
-
def refresh(self) -> None:
|
|
51
|
-
if self.client_id is None:
|
|
52
|
-
return
|
|
53
|
-
api_token = self.client.api_access_token(
|
|
54
|
-
self.client_id, self.client_secret
|
|
55
|
-
)
|
|
56
|
-
self.token = api_token.access_token
|
|
57
|
-
self.token_type = api_token.token_type
|
|
58
|
-
self.expires_in = api_token.expires_in
|
|
59
|
-
self.expires_at = api_token.expires_at
|
|
60
|
-
|
|
61
|
-
def apply(self, headers: dict[str, str]) -> None:
|
|
62
|
-
if self.client_id is None:
|
|
63
|
-
headers["X-CLIENT-SECRET"] = self.token
|
|
64
|
-
else:
|
|
65
|
-
headers["Authorization"] = f"{self.token_type} {self.token}"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from cmsdials import Dials
|
|
2
|
-
from cmsdials.auth.client import AuthClient
|
|
3
|
-
from cmsdials.auth.secret_key import Credentials
|
|
4
|
-
|
|
5
|
-
from .env import (
|
|
6
|
-
AUTH_AUDIENCE,
|
|
7
|
-
AUTH_CLIENT_ID,
|
|
8
|
-
BASE_URL,
|
|
9
|
-
CLIENT_ID,
|
|
10
|
-
CLIENT_SECRET,
|
|
11
|
-
TEST_WORKSPACE,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def build_auth_client() -> AuthClient:
|
|
16
|
-
return AuthClient(client_id=AUTH_CLIENT_ID, audience=AUTH_AUDIENCE)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def setup_dials_object() -> Dials:
|
|
20
|
-
creds = Credentials(
|
|
21
|
-
token=CLIENT_SECRET, client_id=CLIENT_ID, client=build_auth_client()
|
|
22
|
-
)
|
|
23
|
-
return Dials(creds, base_url=BASE_URL, workspace=TEST_WORKSPACE)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|