auth 2.5.2__tar.gz → 3.0.1__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.
- {auth-2.5.2 → auth-3.0.1}/PKG-INFO +37 -16
- {auth-2.5.2 → auth-3.0.1}/README.md +36 -15
- {auth-2.5.2 → auth-3.0.1}/auth/__init__.py +9 -0
- {auth-2.5.2 → auth-3.0.1}/auth/client.py +93 -104
- {auth-2.5.2 → auth-3.0.1}/auth/config.py +58 -42
- {auth-2.5.2 → auth-3.0.1}/auth/database.py +152 -4
- {auth-2.5.2 → auth-3.0.1}/auth/docs_page.py +117 -31
- {auth-2.5.2 → auth-3.0.1}/auth/main.py +17 -5
- {auth-2.5.2 → auth-3.0.1}/auth/services/service.py +9 -1
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/PKG-INFO +37 -16
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/SOURCES.txt +1 -0
- {auth-2.5.2 → auth-3.0.1}/pyproject.toml +1 -1
- {auth-2.5.2 → auth-3.0.1}/tests/test_client.py +51 -50
- auth-3.0.1/tests/test_config.py +211 -0
- auth-3.0.1/tests/test_strict_default.py +139 -0
- auth-2.5.2/tests/test_config.py +0 -79
- {auth-2.5.2 → auth-3.0.1}/LICENSE +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/api_keys.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/audit.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/circuit_breaker.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/cmd/__init__.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/cmd/server.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/core/REST/__init__.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/core/REST/client.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/core/__init__.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/core/models/__init__.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/decorators.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/encryption.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/logging_config.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/models/sql.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/response_format.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/routes.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/sanitizer.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/server.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/validation.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth/workflow_checker.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/dependency_links.txt +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/entry_points.txt +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/requires.txt +0 -0
- {auth-2.5.2 → auth-3.0.1}/auth.egg-info/top_level.txt +0 -0
- {auth-2.5.2 → auth-3.0.1}/setup.cfg +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_api_keys.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_api_keys_encryption.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_audit_integrity.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_client_rest.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_cmd_server.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_correctness_hardening.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_database_sslmode.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_docs_page.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_encryption.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_encryption_integration.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_flask.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_key_rotation.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_log_redaction.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_phase_a_hardening.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_reencryption.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_routes_errors.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_server.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_service_lifecycle.py +0 -0
- {auth-2.5.2 → auth-3.0.1}/tests/test_strict_users.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Authorization for humans
|
|
5
5
|
Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -80,11 +80,32 @@ BASE=https://auth.rodmena.app
|
|
|
80
80
|
|
|
81
81
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/role/engineers
|
|
82
82
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/permission/engineers/deploy
|
|
83
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/apikeys/user/alice
|
|
83
84
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/membership/alice/engineers
|
|
84
85
|
curl -H "Authorization: Bearer $KEY" $BASE/api/has_permission/alice/deploy
|
|
85
86
|
# -> {"success": true, "data": {"has_permission": true}, ...}
|
|
86
87
|
```
|
|
87
88
|
|
|
89
|
+
**Why the `apikeys` call is in there.** Since 3.0.0 a namespace created from a
|
|
90
|
+
brand-new key is **strict**: a user must hold an API key in your namespace
|
|
91
|
+
before it can be given a role. Omit that line and the membership call answers
|
|
92
|
+
`409 {"reason": "user_not_key_backed", "result": false}` — a permanent refusal,
|
|
93
|
+
so retrying never helps. You need not keep the returned secret if you only want
|
|
94
|
+
the identity to exist.
|
|
95
|
+
|
|
96
|
+
If your users can never hold auth API keys (you authenticate them yourself and
|
|
97
|
+
your "users" are opaque ids), opt the namespace out once and skip that step
|
|
98
|
+
forever:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
|
|
102
|
+
-d '{"strict_users": false}' $BASE/api/settings
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The opt-out is audited, per-tenant, and supported indefinitely. Namespaces
|
|
106
|
+
created before 3.0.0 were grandfathered onto it, which is why existing
|
|
107
|
+
integrations saw no change at upgrade.
|
|
108
|
+
|
|
88
109
|
With the Python client (`pip install auth`):
|
|
89
110
|
|
|
90
111
|
```python
|
|
@@ -93,6 +114,7 @@ from auth import Client
|
|
|
93
114
|
with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
94
115
|
c.create_role("engineers")
|
|
95
116
|
c.add_permission("engineers", "deploy")
|
|
117
|
+
c.create_api_key("alice") # strict default since 3.0.0
|
|
96
118
|
c.add_membership("alice", "engineers")
|
|
97
119
|
c.user_has_permission("alice", "deploy") # -> {... "has_permission": true}
|
|
98
120
|
```
|
|
@@ -104,13 +126,11 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
|
104
126
|
- **Two response shapes** — bare `{"result": ...}` and wrapped
|
|
105
127
|
`{"success", "data", ...}`. The API reference says which per endpoint.
|
|
106
128
|
- **Errors below 2xx are HTML**, not JSON. Branch on the status code first.
|
|
107
|
-
- **Python client:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
a false "no". Pass `Client(..., raise_on_error=True)` to get an
|
|
113
|
-
`AuthTransportError` exception instead of the error dict.
|
|
129
|
+
- **Python client: transport failures raise `AuthTransportError`** (3.0.0).
|
|
130
|
+
The 2.x answer-shaped error dict is gone — an outage can never be misread
|
|
131
|
+
as a denial. Catch the exception and map it to your unavailable/503 path,
|
|
132
|
+
never to a permission denial. The old `raise_on_error` constructor argument
|
|
133
|
+
is a deprecated no-op, so 2.x code keeps constructing.
|
|
114
134
|
- **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
|
|
115
135
|
key out of source control, logs, and URLs — it is the only thing protecting
|
|
116
136
|
your data. Rotate it with `POST /api/keys/rotate` if it leaks.
|
|
@@ -123,14 +143,15 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
|
123
143
|
`check_api_key_permission` (validate + permission in one round trip), plus
|
|
124
144
|
`get_settings`/`set_strict_users`. All of these also exist on the in-process
|
|
125
145
|
`Authorization` wrapper for embedded consumers.
|
|
126
|
-
- **
|
|
127
|
-
|
|
128
|
-
`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
- **Strict user identity is the default for NEW tenants (3.0.0).** A tenant
|
|
147
|
+
namespace created after 3.0.0 requires key-backed users for authorization
|
|
148
|
+
decisions (`user_not_key_backed` answers; key-less membership grants answer
|
|
149
|
+
409) — create the user's API key first, then grant roles. Every tenant that
|
|
150
|
+
existed before 3.0.0 was **grandfathered** with an explicit
|
|
151
|
+
`strict_users: false` row (nothing changed for them at upgrade), and the
|
|
152
|
+
audited per-tenant opt-out (`PUT /api/settings {"strict_users": false}`)
|
|
153
|
+
survives indefinitely for validated machine-subject architectures.
|
|
154
|
+
Details: [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md).
|
|
134
155
|
|
|
135
156
|
## Documentation
|
|
136
157
|
|
|
@@ -24,11 +24,32 @@ BASE=https://auth.rodmena.app
|
|
|
24
24
|
|
|
25
25
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/role/engineers
|
|
26
26
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/permission/engineers/deploy
|
|
27
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/apikeys/user/alice
|
|
27
28
|
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/membership/alice/engineers
|
|
28
29
|
curl -H "Authorization: Bearer $KEY" $BASE/api/has_permission/alice/deploy
|
|
29
30
|
# -> {"success": true, "data": {"has_permission": true}, ...}
|
|
30
31
|
```
|
|
31
32
|
|
|
33
|
+
**Why the `apikeys` call is in there.** Since 3.0.0 a namespace created from a
|
|
34
|
+
brand-new key is **strict**: a user must hold an API key in your namespace
|
|
35
|
+
before it can be given a role. Omit that line and the membership call answers
|
|
36
|
+
`409 {"reason": "user_not_key_backed", "result": false}` — a permanent refusal,
|
|
37
|
+
so retrying never helps. You need not keep the returned secret if you only want
|
|
38
|
+
the identity to exist.
|
|
39
|
+
|
|
40
|
+
If your users can never hold auth API keys (you authenticate them yourself and
|
|
41
|
+
your "users" are opaque ids), opt the namespace out once and skip that step
|
|
42
|
+
forever:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
|
|
46
|
+
-d '{"strict_users": false}' $BASE/api/settings
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The opt-out is audited, per-tenant, and supported indefinitely. Namespaces
|
|
50
|
+
created before 3.0.0 were grandfathered onto it, which is why existing
|
|
51
|
+
integrations saw no change at upgrade.
|
|
52
|
+
|
|
32
53
|
With the Python client (`pip install auth`):
|
|
33
54
|
|
|
34
55
|
```python
|
|
@@ -37,6 +58,7 @@ from auth import Client
|
|
|
37
58
|
with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
38
59
|
c.create_role("engineers")
|
|
39
60
|
c.add_permission("engineers", "deploy")
|
|
61
|
+
c.create_api_key("alice") # strict default since 3.0.0
|
|
40
62
|
c.add_membership("alice", "engineers")
|
|
41
63
|
c.user_has_permission("alice", "deploy") # -> {... "has_permission": true}
|
|
42
64
|
```
|
|
@@ -48,13 +70,11 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
|
48
70
|
- **Two response shapes** — bare `{"result": ...}` and wrapped
|
|
49
71
|
`{"success", "data", ...}`. The API reference says which per endpoint.
|
|
50
72
|
- **Errors below 2xx are HTML**, not JSON. Branch on the status code first.
|
|
51
|
-
- **Python client:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
a false "no". Pass `Client(..., raise_on_error=True)` to get an
|
|
57
|
-
`AuthTransportError` exception instead of the error dict.
|
|
73
|
+
- **Python client: transport failures raise `AuthTransportError`** (3.0.0).
|
|
74
|
+
The 2.x answer-shaped error dict is gone — an outage can never be misread
|
|
75
|
+
as a denial. Catch the exception and map it to your unavailable/503 path,
|
|
76
|
+
never to a permission denial. The old `raise_on_error` constructor argument
|
|
77
|
+
is a deprecated no-op, so 2.x code keeps constructing.
|
|
58
78
|
- **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
|
|
59
79
|
key out of source control, logs, and URLs — it is the only thing protecting
|
|
60
80
|
your data. Rotate it with `POST /api/keys/rotate` if it leaks.
|
|
@@ -67,14 +87,15 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
|
67
87
|
`check_api_key_permission` (validate + permission in one round trip), plus
|
|
68
88
|
`get_settings`/`set_strict_users`. All of these also exist on the in-process
|
|
69
89
|
`Authorization` wrapper for embedded consumers.
|
|
70
|
-
- **
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
- **Strict user identity is the default for NEW tenants (3.0.0).** A tenant
|
|
91
|
+
namespace created after 3.0.0 requires key-backed users for authorization
|
|
92
|
+
decisions (`user_not_key_backed` answers; key-less membership grants answer
|
|
93
|
+
409) — create the user's API key first, then grant roles. Every tenant that
|
|
94
|
+
existed before 3.0.0 was **grandfathered** with an explicit
|
|
95
|
+
`strict_users: false` row (nothing changed for them at upgrade), and the
|
|
96
|
+
audited per-tenant opt-out (`PUT /api/settings {"strict_users": false}`)
|
|
97
|
+
survives indefinitely for validated machine-subject architectures.
|
|
98
|
+
Details: [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md).
|
|
78
99
|
|
|
79
100
|
## Documentation
|
|
80
101
|
|
|
@@ -44,6 +44,15 @@ class Authorization:
|
|
|
44
44
|
db_session=None,
|
|
45
45
|
strict_users: Optional[bool] = None,
|
|
46
46
|
):
|
|
47
|
+
# Constructing this wrapper means running auth embedded, against a real
|
|
48
|
+
# database — the one embedded path that never goes through create_app.
|
|
49
|
+
# Weak server secrets are actionable here; on a bare `import auth` by a
|
|
50
|
+
# client-only consumer they are not, which is why they are not emitted
|
|
51
|
+
# at import time (issuedb #20).
|
|
52
|
+
from auth.config import get_settings, warn_on_weak_secrets
|
|
53
|
+
|
|
54
|
+
warn_on_weak_secrets(get_settings())
|
|
55
|
+
|
|
47
56
|
self.client = client
|
|
48
57
|
# Use provided session or create a new one
|
|
49
58
|
self.db = db_session if db_session else SessionLocal()
|
|
@@ -3,7 +3,8 @@ Enhanced client library with connection pooling, retry logic, and circuit breake
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any, Dict, NoReturn, Optional
|
|
7
8
|
from urllib.parse import urljoin
|
|
8
9
|
|
|
9
10
|
import requests
|
|
@@ -21,10 +22,11 @@ _RETRY_METHODS = ["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
|
|
|
21
22
|
class AuthTransportError(Exception):
|
|
22
23
|
"""The client could not get an answer from the auth service.
|
|
23
24
|
|
|
24
|
-
Raised
|
|
25
|
-
|
|
26
|
-
ask"
|
|
27
|
-
|
|
25
|
+
Raised by every client method on transport failure (connection failure,
|
|
26
|
+
exhausted retries, open circuit breaker, non-2xx status) since 3.0.0.
|
|
27
|
+
Distinguishes "we could not ask" from a genuine negative answer such as
|
|
28
|
+
``has_permission: false`` — map it to your unavailable/503 path, never
|
|
29
|
+
to a denial.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
|
|
@@ -70,17 +72,12 @@ class RetryableHTTPAdapter(HTTPAdapter):
|
|
|
70
72
|
class EnhancedAuthClient:
|
|
71
73
|
"""Enhanced client with connection pooling, retry logic, and circuit breaker.
|
|
72
74
|
|
|
73
|
-
Error contract:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
``data`` echoes the inputs and does NOT contain the answer field
|
|
80
|
-
(``has_permission``, ``count``, ...), so reading it without checking
|
|
81
|
-
``success`` turns an outage into a false negative. Either check
|
|
82
|
-
``success`` first, or construct with ``raise_on_error=True`` to make
|
|
83
|
-
every method raise :class:`AuthTransportError` on transport failure.
|
|
75
|
+
Error contract (3.0.0): every method raises :class:`AuthTransportError`
|
|
76
|
+
on transport failure — connection error, exhausted retries, open circuit
|
|
77
|
+
breaker, or a non-2xx status. A transport failure can therefore never
|
|
78
|
+
reach your authorization logic as a value: catch the exception and map it
|
|
79
|
+
to your unavailable/503 path, never to a denial. Success payloads are
|
|
80
|
+
unchanged from 2.x.
|
|
84
81
|
"""
|
|
85
82
|
|
|
86
83
|
def __init__(
|
|
@@ -92,7 +89,7 @@ class EnhancedAuthClient:
|
|
|
92
89
|
pool_maxsize: int = 64,
|
|
93
90
|
timeout: int = 30,
|
|
94
91
|
circuit_breaker_enabled: bool = True,
|
|
95
|
-
raise_on_error: bool =
|
|
92
|
+
raise_on_error: Optional[bool] = None,
|
|
96
93
|
):
|
|
97
94
|
"""
|
|
98
95
|
Initialize the enhanced client
|
|
@@ -107,14 +104,22 @@ class EnhancedAuthClient:
|
|
|
107
104
|
as transport failures under load)
|
|
108
105
|
timeout: Request timeout in seconds
|
|
109
106
|
circuit_breaker_enabled: Whether circuit breaker is enabled
|
|
110
|
-
raise_on_error:
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
raise_on_error: DEPRECATED no-op kept so 2.x constructor calls
|
|
108
|
+
keep working — since 3.0.0 the client ALWAYS raises
|
|
109
|
+
AuthTransportError on transport failure
|
|
110
|
+
"""
|
|
111
|
+
if raise_on_error is not None:
|
|
112
|
+
warnings.warn(
|
|
113
|
+
"raise_on_error is deprecated and ignored: since auth 3.0.0 "
|
|
114
|
+
"the client always raises AuthTransportError on transport "
|
|
115
|
+
"failure.",
|
|
116
|
+
DeprecationWarning,
|
|
117
|
+
stacklevel=2,
|
|
118
|
+
)
|
|
113
119
|
self.api_key = api_key
|
|
114
120
|
self.service_url = service_url
|
|
115
121
|
self.timeout = timeout
|
|
116
122
|
self.circuit_breaker_enabled = circuit_breaker_enabled
|
|
117
|
-
self.raise_on_error = raise_on_error
|
|
118
123
|
|
|
119
124
|
# Create session with connection pooling
|
|
120
125
|
self.session = requests.Session()
|
|
@@ -233,30 +238,22 @@ class EnhancedAuthClient:
|
|
|
233
238
|
|
|
234
239
|
def _transport_failure(
|
|
235
240
|
self, exc: Exception, data: Optional[Dict[str, Any]] = None
|
|
236
|
-
) ->
|
|
237
|
-
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
payload: Dict[str, Any] = {
|
|
247
|
-
"error": str(exc),
|
|
248
|
-
"success": False,
|
|
249
|
-
"transport_error": True,
|
|
250
|
-
}
|
|
251
|
-
if data is not None:
|
|
252
|
-
payload["data"] = data
|
|
253
|
-
return payload
|
|
241
|
+
) -> NoReturn:
|
|
242
|
+
"""Raise :class:`AuthTransportError` for a transport-level failure.
|
|
243
|
+
|
|
244
|
+
Since 3.0.0 this ALWAYS raises — the 2.x error-dict return (an
|
|
245
|
+
answer-shaped payload without the answer field) is gone, so an outage
|
|
246
|
+
can never be misread as a denial. ``data`` is accepted for call-site
|
|
247
|
+
compatibility and intentionally unused: inputs like key material must
|
|
248
|
+
never ride on an exception.
|
|
249
|
+
"""
|
|
250
|
+
raise AuthTransportError(str(exc)) from exc
|
|
254
251
|
|
|
255
252
|
def ping(self) -> Dict[str, Any]:
|
|
256
253
|
"""Health check.
|
|
257
254
|
|
|
258
|
-
Transport failure
|
|
259
|
-
|
|
255
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
256
|
+
your unavailable/503 path, never to a denial.
|
|
260
257
|
"""
|
|
261
258
|
try:
|
|
262
259
|
return self._make_request("GET", self.endpoints["ping"])
|
|
@@ -266,8 +263,8 @@ class EnhancedAuthClient:
|
|
|
266
263
|
def add_membership(self, user: str, group: str) -> Dict[str, Any]:
|
|
267
264
|
"""Add user to a group.
|
|
268
265
|
|
|
269
|
-
Transport failure
|
|
270
|
-
|
|
266
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
267
|
+
your unavailable/503 path, never to a denial.
|
|
271
268
|
"""
|
|
272
269
|
endpoint = self.endpoints["membership"].format(user=user, group=group)
|
|
273
270
|
try:
|
|
@@ -278,8 +275,8 @@ class EnhancedAuthClient:
|
|
|
278
275
|
def remove_membership(self, user: str, group: str) -> Dict[str, Any]:
|
|
279
276
|
"""Remove user from a group.
|
|
280
277
|
|
|
281
|
-
Transport failure
|
|
282
|
-
|
|
278
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
279
|
+
your unavailable/503 path, never to a denial.
|
|
283
280
|
"""
|
|
284
281
|
endpoint = self.endpoints["membership"].format(user=user, group=group)
|
|
285
282
|
try:
|
|
@@ -290,8 +287,8 @@ class EnhancedAuthClient:
|
|
|
290
287
|
def has_membership(self, user: str, group: str) -> Dict[str, Any]:
|
|
291
288
|
"""Check if user is member of a group.
|
|
292
289
|
|
|
293
|
-
Transport failure
|
|
294
|
-
|
|
290
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
291
|
+
your unavailable/503 path, never to a denial.
|
|
295
292
|
"""
|
|
296
293
|
endpoint = self.endpoints["membership"].format(user=user, group=group)
|
|
297
294
|
try:
|
|
@@ -302,8 +299,8 @@ class EnhancedAuthClient:
|
|
|
302
299
|
def add_permission(self, group: str, name: str) -> Dict[str, Any]:
|
|
303
300
|
"""Add permission to a group.
|
|
304
301
|
|
|
305
|
-
Transport failure
|
|
306
|
-
|
|
302
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
303
|
+
your unavailable/503 path, never to a denial.
|
|
307
304
|
"""
|
|
308
305
|
endpoint = self.endpoints["permission"].format(group=group, name=name)
|
|
309
306
|
try:
|
|
@@ -314,8 +311,8 @@ class EnhancedAuthClient:
|
|
|
314
311
|
def remove_permission(self, group: str, name: str) -> Dict[str, Any]:
|
|
315
312
|
"""Remove permission from a group.
|
|
316
313
|
|
|
317
|
-
Transport failure
|
|
318
|
-
|
|
314
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
315
|
+
your unavailable/503 path, never to a denial.
|
|
319
316
|
"""
|
|
320
317
|
endpoint = self.endpoints["permission"].format(group=group, name=name)
|
|
321
318
|
try:
|
|
@@ -326,8 +323,8 @@ class EnhancedAuthClient:
|
|
|
326
323
|
def has_permission(self, group: str, name: str) -> Dict[str, Any]:
|
|
327
324
|
"""Check if group has permission.
|
|
328
325
|
|
|
329
|
-
Transport failure
|
|
330
|
-
|
|
326
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
327
|
+
your unavailable/503 path, never to a denial.
|
|
331
328
|
"""
|
|
332
329
|
endpoint = self.endpoints["permission"].format(group=group, name=name)
|
|
333
330
|
try:
|
|
@@ -338,10 +335,8 @@ class EnhancedAuthClient:
|
|
|
338
335
|
def user_has_permission(self, user: str, name: str) -> Dict[str, Any]:
|
|
339
336
|
"""Check if user has permission.
|
|
340
337
|
|
|
341
|
-
Transport failure
|
|
342
|
-
|
|
343
|
-
before reading ``data`` — a missing ``has_permission`` is an outage,
|
|
344
|
-
not a denial.
|
|
338
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
339
|
+
your unavailable/503 path, never to a denial.
|
|
345
340
|
"""
|
|
346
341
|
endpoint = self.endpoints["has_permission"].format(user=user, name=name)
|
|
347
342
|
try:
|
|
@@ -352,10 +347,8 @@ class EnhancedAuthClient:
|
|
|
352
347
|
def get_user_permissions(self, user: str) -> Dict[str, Any]:
|
|
353
348
|
"""Get all permissions for a user.
|
|
354
349
|
|
|
355
|
-
Transport failure
|
|
356
|
-
|
|
357
|
-
before reading ``data`` — a missing ``count`` is an outage, not an
|
|
358
|
-
unknown user.
|
|
350
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
351
|
+
your unavailable/503 path, never to a denial.
|
|
359
352
|
"""
|
|
360
353
|
endpoint = self.endpoints["user_permissions"].format(user=user)
|
|
361
354
|
try:
|
|
@@ -366,8 +359,8 @@ class EnhancedAuthClient:
|
|
|
366
359
|
def get_role_permissions(self, role: str) -> Dict[str, Any]:
|
|
367
360
|
"""Get all permissions for a role.
|
|
368
361
|
|
|
369
|
-
Transport failure
|
|
370
|
-
|
|
362
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
363
|
+
your unavailable/503 path, never to a denial.
|
|
371
364
|
"""
|
|
372
365
|
endpoint = self.endpoints["role_permissions"].format(role=role)
|
|
373
366
|
try:
|
|
@@ -378,8 +371,8 @@ class EnhancedAuthClient:
|
|
|
378
371
|
def get_user_roles(self, user: str) -> Dict[str, Any]:
|
|
379
372
|
"""Get all roles for a user.
|
|
380
373
|
|
|
381
|
-
Transport failure
|
|
382
|
-
|
|
374
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
375
|
+
your unavailable/503 path, never to a denial.
|
|
383
376
|
"""
|
|
384
377
|
endpoint = self.endpoints["user_roles"].format(user=user)
|
|
385
378
|
try:
|
|
@@ -390,8 +383,8 @@ class EnhancedAuthClient:
|
|
|
390
383
|
def get_role_members(self, role: str) -> Dict[str, Any]:
|
|
391
384
|
"""Get all members of a role.
|
|
392
385
|
|
|
393
|
-
Transport failure
|
|
394
|
-
|
|
386
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
387
|
+
your unavailable/503 path, never to a denial.
|
|
395
388
|
"""
|
|
396
389
|
endpoint = self.endpoints["role_members"].format(role=role)
|
|
397
390
|
try:
|
|
@@ -402,8 +395,8 @@ class EnhancedAuthClient:
|
|
|
402
395
|
def list_roles(self) -> Dict[str, Any]:
|
|
403
396
|
"""List all roles.
|
|
404
397
|
|
|
405
|
-
Transport failure
|
|
406
|
-
|
|
398
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
399
|
+
your unavailable/503 path, never to a denial.
|
|
407
400
|
"""
|
|
408
401
|
try:
|
|
409
402
|
return self._make_request("GET", self.endpoints["roles"])
|
|
@@ -413,8 +406,8 @@ class EnhancedAuthClient:
|
|
|
413
406
|
def which_roles_can(self, name: str) -> Dict[str, Any]:
|
|
414
407
|
"""Get roles that can perform an action.
|
|
415
408
|
|
|
416
|
-
Transport failure
|
|
417
|
-
|
|
409
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
410
|
+
your unavailable/503 path, never to a denial.
|
|
418
411
|
"""
|
|
419
412
|
endpoint = self.endpoints["which_roles_can"].format(name=name)
|
|
420
413
|
try:
|
|
@@ -425,8 +418,8 @@ class EnhancedAuthClient:
|
|
|
425
418
|
def which_users_can(self, name: str) -> Dict[str, Any]:
|
|
426
419
|
"""Get users that can perform an action.
|
|
427
420
|
|
|
428
|
-
Transport failure
|
|
429
|
-
|
|
421
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
422
|
+
your unavailable/503 path, never to a denial.
|
|
430
423
|
"""
|
|
431
424
|
endpoint = self.endpoints["which_users_can"].format(name=name)
|
|
432
425
|
try:
|
|
@@ -437,8 +430,8 @@ class EnhancedAuthClient:
|
|
|
437
430
|
def create_role(self, role: str) -> Dict[str, Any]:
|
|
438
431
|
"""Create a new role.
|
|
439
432
|
|
|
440
|
-
Transport failure
|
|
441
|
-
|
|
433
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
434
|
+
your unavailable/503 path, never to a denial.
|
|
442
435
|
"""
|
|
443
436
|
endpoint = self.endpoints["role"].format(role=role)
|
|
444
437
|
try:
|
|
@@ -449,8 +442,8 @@ class EnhancedAuthClient:
|
|
|
449
442
|
def delete_role(self, role: str) -> Dict[str, Any]:
|
|
450
443
|
"""Delete a role.
|
|
451
444
|
|
|
452
|
-
Transport failure
|
|
453
|
-
|
|
445
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
446
|
+
your unavailable/503 path, never to a denial.
|
|
454
447
|
"""
|
|
455
448
|
endpoint = self.endpoints["role"].format(role=role)
|
|
456
449
|
try:
|
|
@@ -468,8 +461,8 @@ class EnhancedAuthClient:
|
|
|
468
461
|
returned key is the ONLY copy: persist ``data.new_key`` (e.g. to your
|
|
469
462
|
secret store) or you lose access to the namespace.
|
|
470
463
|
|
|
471
|
-
Transport failure
|
|
472
|
-
|
|
464
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
465
|
+
your unavailable/503 path, never to a denial.
|
|
473
466
|
"""
|
|
474
467
|
endpoint = self.endpoints["rotate_key"]
|
|
475
468
|
try:
|
|
@@ -493,8 +486,8 @@ class EnhancedAuthClient:
|
|
|
493
486
|
retry after an ambiguous failure could mint a second key whose secret
|
|
494
487
|
nobody ever saw. On an ambiguous failure, list and revoke instead.
|
|
495
488
|
|
|
496
|
-
Transport failure
|
|
497
|
-
|
|
489
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
490
|
+
your unavailable/503 path, never to a denial.
|
|
498
491
|
"""
|
|
499
492
|
endpoint = self.endpoints["apikeys_user"].format(user=user)
|
|
500
493
|
payload = {"label": label} if label is not None else None
|
|
@@ -506,8 +499,8 @@ class EnhancedAuthClient:
|
|
|
506
499
|
def list_api_keys(self, user: str) -> Dict[str, Any]:
|
|
507
500
|
"""List a user's API keys (metadata only; never the secrets).
|
|
508
501
|
|
|
509
|
-
Transport failure
|
|
510
|
-
|
|
502
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
503
|
+
your unavailable/503 path, never to a denial.
|
|
511
504
|
"""
|
|
512
505
|
endpoint = self.endpoints["apikeys_user"].format(user=user)
|
|
513
506
|
try:
|
|
@@ -518,8 +511,8 @@ class EnhancedAuthClient:
|
|
|
518
511
|
def revoke_api_key(self, user: str, key_id: str) -> Dict[str, Any]:
|
|
519
512
|
"""Revoke one of a user's API keys by its public key_id (idempotent).
|
|
520
513
|
|
|
521
|
-
Transport failure
|
|
522
|
-
|
|
514
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
515
|
+
your unavailable/503 path, never to a denial.
|
|
523
516
|
"""
|
|
524
517
|
endpoint = self.endpoints["apikey_revoke"].format(user=user, key_id=key_id)
|
|
525
518
|
try:
|
|
@@ -530,13 +523,11 @@ class EnhancedAuthClient:
|
|
|
530
523
|
def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
|
531
524
|
"""Validate an API-key secret; answers ``data.valid`` true/false.
|
|
532
525
|
|
|
533
|
-
The secret travels in the JSON body, never a URL
|
|
534
|
-
|
|
526
|
+
The secret travels in the JSON body, never a URL, and never rides
|
|
527
|
+
on an exception.
|
|
535
528
|
|
|
536
|
-
Transport failure
|
|
537
|
-
|
|
538
|
-
before reading ``data`` — a missing ``valid`` is an outage, not an
|
|
539
|
-
invalid key.
|
|
529
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
530
|
+
your unavailable/503 path, never to a denial.
|
|
540
531
|
"""
|
|
541
532
|
try:
|
|
542
533
|
return self._make_request(
|
|
@@ -552,12 +543,10 @@ class EnhancedAuthClient:
|
|
|
552
543
|
|
|
553
544
|
``data.valid`` false → the key failed (reason as in validate_api_key);
|
|
554
545
|
true → ``data.has_permission`` answers for the key's user. The secret
|
|
555
|
-
travels in the JSON body
|
|
546
|
+
travels in the JSON body and never rides on an exception.
|
|
556
547
|
|
|
557
|
-
Transport failure
|
|
558
|
-
|
|
559
|
-
before reading ``data`` — a missing ``valid`` is an outage, not a
|
|
560
|
-
denial.
|
|
548
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
549
|
+
your unavailable/503 path, never to a denial.
|
|
561
550
|
"""
|
|
562
551
|
try:
|
|
563
552
|
return self._make_request(
|
|
@@ -574,8 +563,8 @@ class EnhancedAuthClient:
|
|
|
574
563
|
def get_settings(self) -> Dict[str, Any]:
|
|
575
564
|
"""This tenant's settings (``data.strict_users``).
|
|
576
565
|
|
|
577
|
-
Transport failure
|
|
578
|
-
|
|
566
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
567
|
+
your unavailable/503 path, never to a denial.
|
|
579
568
|
"""
|
|
580
569
|
try:
|
|
581
570
|
return self._make_request("GET", self.endpoints["settings"])
|
|
@@ -589,8 +578,8 @@ class EnhancedAuthClient:
|
|
|
589
578
|
key answer negatively (``reason: user_not_key_backed``) — issue keys
|
|
590
579
|
before flipping this on.
|
|
591
580
|
|
|
592
|
-
Transport failure
|
|
593
|
-
|
|
581
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
582
|
+
your unavailable/503 path, never to a denial.
|
|
594
583
|
"""
|
|
595
584
|
try:
|
|
596
585
|
return self._make_request(
|
|
@@ -603,8 +592,8 @@ class EnhancedAuthClient:
|
|
|
603
592
|
def get_users_for_workflow(self, workflow_name: str) -> Dict[str, Any]:
|
|
604
593
|
"""Get all users who can run a specific workflow.
|
|
605
594
|
|
|
606
|
-
Transport failure
|
|
607
|
-
|
|
595
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
596
|
+
your unavailable/503 path, never to a denial.
|
|
608
597
|
"""
|
|
609
598
|
endpoint = self.endpoints["workflow_users"].format(workflow_name=workflow_name)
|
|
610
599
|
try:
|
|
@@ -617,8 +606,8 @@ class EnhancedAuthClient:
|
|
|
617
606
|
) -> Dict[str, Any]:
|
|
618
607
|
"""Check if a user can run a specific workflow.
|
|
619
608
|
|
|
620
|
-
Transport failure
|
|
621
|
-
|
|
609
|
+
Transport failure raises :class:`AuthTransportError` — map it to
|
|
610
|
+
your unavailable/503 path, never to a denial.
|
|
622
611
|
"""
|
|
623
612
|
endpoint = self.endpoints["workflow_permission"].format(
|
|
624
613
|
user=user, workflow_name=workflow_name
|