auth 2.0.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.
- auth-2.4.0/PKG-INFO +157 -0
- auth-2.4.0/README.md +100 -0
- {auth-2.0.0 → auth-2.4.0}/auth/__init__.py +6 -1
- auth-2.4.0/auth/api_keys.py +50 -0
- {auth-2.0.0 → auth-2.4.0}/auth/audit.py +103 -33
- auth-2.4.0/auth/client.py +595 -0
- {auth-2.0.0 → auth-2.4.0}/auth/config.py +70 -2
- auth-2.4.0/auth/decorators.py +123 -0
- auth-2.4.0/auth/docs_page.py +565 -0
- {auth-2.0.0 → auth-2.4.0}/auth/main.py +9 -3
- {auth-2.0.0 → auth-2.4.0}/auth/models/sql.py +74 -0
- {auth-2.0.0 → auth-2.4.0}/auth/response_format.py +0 -37
- {auth-2.0.0 → auth-2.4.0}/auth/routes.py +251 -32
- auth-2.4.0/auth/services/service.py +836 -0
- {auth-2.0.0 → auth-2.4.0}/auth/validation.py +17 -0
- auth-2.4.0/auth.egg-info/PKG-INFO +157 -0
- {auth-2.0.0 → auth-2.4.0}/auth.egg-info/SOURCES.txt +11 -10
- auth-2.4.0/auth.egg-info/requires.txt +31 -0
- {auth-2.0.0 → auth-2.4.0}/pyproject.toml +30 -18
- auth-2.4.0/tests/test_api_keys.py +353 -0
- auth-2.4.0/tests/test_api_keys_encryption.py +91 -0
- auth-2.4.0/tests/test_audit_integrity.py +139 -0
- auth-2.4.0/tests/test_client.py +317 -0
- auth-2.4.0/tests/test_config.py +79 -0
- auth-2.4.0/tests/test_correctness_hardening.py +133 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_docs_page.py +59 -1
- auth-2.4.0/tests/test_encryption_integration.py +102 -0
- auth-2.4.0/tests/test_key_rotation.py +277 -0
- auth-2.4.0/tests/test_log_redaction.py +55 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_migrations.py +9 -2
- {auth-2.0.0 → auth-2.4.0}/tests/test_routes_errors.py +20 -3
- {auth-2.0.0 → auth-2.4.0}/tests/test_service_lifecycle.py +63 -7
- auth-2.0.0/PKG-INFO +0 -257
- auth-2.0.0/README.rst +0 -201
- auth-2.0.0/auth/client.py +0 -385
- auth-2.0.0/auth/dal/authorization_sqlite.py +0 -180
- auth-2.0.0/auth/decorators.py +0 -64
- auth-2.0.0/auth/docs_page.py +0 -287
- auth-2.0.0/auth/jwt_auth.py +0 -135
- auth-2.0.0/auth/models/sqlite.py +0 -395
- auth-2.0.0/auth/services/rest_service.py +0 -141
- auth-2.0.0/auth/services/service.py +0 -485
- auth-2.0.0/auth.egg-info/PKG-INFO +0 -257
- auth-2.0.0/auth.egg-info/requires.txt +0 -30
- auth-2.0.0/tests/test_auth_sqlite.py +0 -266
- auth-2.0.0/tests/test_authorization_sqlite.py +0 -749
- auth-2.0.0/tests/test_client.py +0 -127
- auth-2.0.0/tests/test_db_sqlite.py +0 -509
- auth-2.0.0/tests/test_service_rest.py +0 -221
- {auth-2.0.0 → auth-2.4.0}/LICENSE +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/circuit_breaker.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/cmd/__init__.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/cmd/server.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/core/REST/__init__.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/core/REST/client.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/core/__init__.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/core/models/__init__.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/database.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/encryption.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/logging_config.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/sanitizer.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/server.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth/workflow_checker.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth.egg-info/dependency_links.txt +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth.egg-info/entry_points.txt +0 -0
- {auth-2.0.0 → auth-2.4.0}/auth.egg-info/top_level.txt +0 -0
- {auth-2.0.0 → auth-2.4.0}/setup.cfg +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_client_rest.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_cmd_server.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_encryption.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_flask.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_phase_a_hardening.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_reencryption.py +0 -0
- {auth-2.0.0 → auth-2.4.0}/tests/test_server.py +0 -0
auth-2.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auth
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Summary: Authorization for humans
|
|
5
|
+
Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: authorization,role,auth,groups,membership,ensure,ldap
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: flask<4,>=2.0.0
|
|
29
|
+
Requires-Dist: flask-cors<7,>=3.0.0
|
|
30
|
+
Requires-Dist: sqlalchemy<3,>=2.0.0
|
|
31
|
+
Requires-Dist: waitress<4,>=2.0.0
|
|
32
|
+
Requires-Dist: cryptography<48,>=3.0.0
|
|
33
|
+
Requires-Dist: APScheduler<4,>=3.0.0
|
|
34
|
+
Requires-Dist: psycopg[binary]<4,>=3.0.0
|
|
35
|
+
Requires-Dist: pydantic<3,>=2.0.0
|
|
36
|
+
Requires-Dist: pydantic-settings<3,>=2.0.0
|
|
37
|
+
Requires-Dist: requests<3,>=2.25.0
|
|
38
|
+
Requires-Dist: bleach<7,>=5.0.0
|
|
39
|
+
Requires-Dist: python-json-logger<4,>=3.1.0
|
|
40
|
+
Provides-Extra: ratelimit
|
|
41
|
+
Requires-Dist: flask-limiter>=3.0.0; extra == "ratelimit"
|
|
42
|
+
Provides-Extra: migrations
|
|
43
|
+
Requires-Dist: migretti>=0.10.0; extra == "migrations"
|
|
44
|
+
Requires-Dist: alembic>=1.13.0; extra == "migrations"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
48
|
+
Requires-Dist: alembic>=1.13.0; extra == "dev"
|
|
49
|
+
Requires-Dist: ruff>=0.0.260; extra == "dev"
|
|
50
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
51
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
52
|
+
Requires-Dist: types-waitress; extra == "dev"
|
|
53
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
54
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
55
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# auth — RBAC authorization service
|
|
59
|
+
|
|
60
|
+
[](https://github.com/ourway/auth/actions/workflows/ci.yml)
|
|
61
|
+
[]()
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
Role-based access control over HTTP. `auth` answers one question — **may user X
|
|
65
|
+
do Y** — so your services don't reinvent roles and permissions.
|
|
66
|
+
|
|
67
|
+
It is **authorization, not authentication**: it does not log anyone in, store
|
|
68
|
+
passwords, or issue tokens. It trusts that the caller already knows *who* the
|
|
69
|
+
user is, and decides *what they may do*. Model:
|
|
70
|
+
`user → (member of) → role → (holds) → permission`.
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
Your **client key is any UUID4** — it is also your private, isolated namespace.
|
|
75
|
+
Generate one, keep it secret, and reuse it for every call. A role must exist
|
|
76
|
+
before you add members or permissions to it.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
|
|
80
|
+
BASE=https://auth.rodmena.app
|
|
81
|
+
|
|
82
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/role/engineers
|
|
83
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/permission/engineers/deploy
|
|
84
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/membership/alice/engineers
|
|
85
|
+
curl -H "Authorization: Bearer $KEY" $BASE/api/has_permission/alice/deploy
|
|
86
|
+
# -> {"success": true, "data": {"has_permission": true}, ...}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
With the Python client (`pip install auth`):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from auth import Client
|
|
93
|
+
|
|
94
|
+
with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
95
|
+
c.create_role("engineers")
|
|
96
|
+
c.add_permission("engineers", "deploy")
|
|
97
|
+
c.add_membership("alice", "engineers")
|
|
98
|
+
c.user_has_permission("alice", "deploy") # -> {... "has_permission": true}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Things to know before you write code
|
|
102
|
+
|
|
103
|
+
- **Writes can return HTTP 200 with `{"result": false}`** (e.g. adding to a
|
|
104
|
+
missing role). Check the `result`/`data` field, not just the status code.
|
|
105
|
+
- **Two response shapes** — bare `{"result": ...}` and wrapped
|
|
106
|
+
`{"success", "data", ...}`. The API reference says which per endpoint.
|
|
107
|
+
- **Errors below 2xx are HTML**, not JSON. Branch on the status code first.
|
|
108
|
+
- **Python client: check `success` before reading `data`.** On transport
|
|
109
|
+
failure the client does not raise by default — it returns
|
|
110
|
+
`{"error", "success": False, "transport_error": True, "data": {...}}` where
|
|
111
|
+
`data` only echoes your inputs and does NOT contain the answer field
|
|
112
|
+
(`has_permission`, `count`, ...). Reading `data` blindly turns an outage into
|
|
113
|
+
a false "no". Pass `Client(..., raise_on_error=True)` to get an
|
|
114
|
+
`AuthTransportError` exception instead of the error dict.
|
|
115
|
+
- **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
|
|
116
|
+
key out of source control, logs, and URLs — it is the only thing protecting
|
|
117
|
+
your data. Rotate it with `POST /api/keys/rotate` if it leaks.
|
|
118
|
+
- **Per-user API keys** (2.4.0): `/api/apikeys/user/<user>` (create/list),
|
|
119
|
+
`/api/apikeys/user/<user>/<key_id>` (revoke), `/api/apikeys/validate`. auth
|
|
120
|
+
mints `rak_...` secrets for *your users*, shows each exactly once, stores only
|
|
121
|
+
a hash, and validates them inside your namespace — an identity UI fronts the
|
|
122
|
+
lifecycle, backends validate then use the RBAC checks. Client methods:
|
|
123
|
+
`create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
|
|
124
|
+
|
|
125
|
+
## Documentation
|
|
126
|
+
|
|
127
|
+
| Doc | What's in it |
|
|
128
|
+
|---|---|
|
|
129
|
+
| **Live API reference** — [`/docs`](https://auth.rodmena.app/docs) · [`/llms.txt`](https://auth.rodmena.app/llms.txt) | Every endpoint and exact response shape, served by the app (agent-friendly). |
|
|
130
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design, components, request lifecycle, data model, permission-check and key-rotation flows, diagrams. |
|
|
131
|
+
| [SECURITY.md](SECURITY.md) | Security model (tenant isolation, encryption, audit, rotation), threat notes, reporting. |
|
|
132
|
+
| [MIGRATIONS.md](MIGRATIONS.md) | Schema creation vs migrations, upgrade/rollback runbook. |
|
|
133
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Local setup, tests (sqlite + postgres), lint/type-check, CI. |
|
|
134
|
+
| [docs/](docs/) | Full Sphinx docs (concepts, configuration, encryption, deployment, REST & Python usage). |
|
|
135
|
+
|
|
136
|
+
## When to use — and not
|
|
137
|
+
|
|
138
|
+
**Use it** for RBAC: named roles, permissions, group membership, and boolean
|
|
139
|
+
"can user X do Y" gates for a service, CLI, or workflow engine.
|
|
140
|
+
|
|
141
|
+
**Not** for authentication (login/passwords/sessions/OAuth/JWT), fine-grained /
|
|
142
|
+
attribute-based rules (owner-of-*this*-record, time-of-day, row-level tenancy —
|
|
143
|
+
reach for an ABAC/policy engine), or air-gapped hot loops where a network hop per
|
|
144
|
+
check is too costly (cache, or use the library in-process).
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python3.11 -m venv .venv && . .venv/bin/activate
|
|
150
|
+
pip install -e ".[dev,ratelimit,migrations]"
|
|
151
|
+
make check # ruff + mypy
|
|
152
|
+
make test # sqlite suite
|
|
153
|
+
make test-postgres # postgres integration (Docker), encryption on
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow. Licensed under the
|
|
157
|
+
[MIT License](LICENSE).
|
auth-2.4.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# auth — RBAC authorization service
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ourway/auth/actions/workflows/ci.yml)
|
|
4
|
+
[]()
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Role-based access control over HTTP. `auth` answers one question — **may user X
|
|
8
|
+
do Y** — so your services don't reinvent roles and permissions.
|
|
9
|
+
|
|
10
|
+
It is **authorization, not authentication**: it does not log anyone in, store
|
|
11
|
+
passwords, or issue tokens. It trusts that the caller already knows *who* the
|
|
12
|
+
user is, and decides *what they may do*. Model:
|
|
13
|
+
`user → (member of) → role → (holds) → permission`.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Your **client key is any UUID4** — it is also your private, isolated namespace.
|
|
18
|
+
Generate one, keep it secret, and reuse it for every call. A role must exist
|
|
19
|
+
before you add members or permissions to it.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
|
|
23
|
+
BASE=https://auth.rodmena.app
|
|
24
|
+
|
|
25
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/role/engineers
|
|
26
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/permission/engineers/deploy
|
|
27
|
+
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/membership/alice/engineers
|
|
28
|
+
curl -H "Authorization: Bearer $KEY" $BASE/api/has_permission/alice/deploy
|
|
29
|
+
# -> {"success": true, "data": {"has_permission": true}, ...}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With the Python client (`pip install auth`):
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from auth import Client
|
|
36
|
+
|
|
37
|
+
with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
|
|
38
|
+
c.create_role("engineers")
|
|
39
|
+
c.add_permission("engineers", "deploy")
|
|
40
|
+
c.add_membership("alice", "engineers")
|
|
41
|
+
c.user_has_permission("alice", "deploy") # -> {... "has_permission": true}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Things to know before you write code
|
|
45
|
+
|
|
46
|
+
- **Writes can return HTTP 200 with `{"result": false}`** (e.g. adding to a
|
|
47
|
+
missing role). Check the `result`/`data` field, not just the status code.
|
|
48
|
+
- **Two response shapes** — bare `{"result": ...}` and wrapped
|
|
49
|
+
`{"success", "data", ...}`. The API reference says which per endpoint.
|
|
50
|
+
- **Errors below 2xx are HTML**, not JSON. Branch on the status code first.
|
|
51
|
+
- **Python client: check `success` before reading `data`.** On transport
|
|
52
|
+
failure the client does not raise by default — it returns
|
|
53
|
+
`{"error", "success": False, "transport_error": True, "data": {...}}` where
|
|
54
|
+
`data` only echoes your inputs and does NOT contain the answer field
|
|
55
|
+
(`has_permission`, `count`, ...). Reading `data` blindly turns an outage into
|
|
56
|
+
a false "no". Pass `Client(..., raise_on_error=True)` to get an
|
|
57
|
+
`AuthTransportError` exception instead of the error dict.
|
|
58
|
+
- **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
|
|
59
|
+
key out of source control, logs, and URLs — it is the only thing protecting
|
|
60
|
+
your data. Rotate it with `POST /api/keys/rotate` if it leaks.
|
|
61
|
+
- **Per-user API keys** (2.4.0): `/api/apikeys/user/<user>` (create/list),
|
|
62
|
+
`/api/apikeys/user/<user>/<key_id>` (revoke), `/api/apikeys/validate`. auth
|
|
63
|
+
mints `rak_...` secrets for *your users*, shows each exactly once, stores only
|
|
64
|
+
a hash, and validates them inside your namespace — an identity UI fronts the
|
|
65
|
+
lifecycle, backends validate then use the RBAC checks. Client methods:
|
|
66
|
+
`create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
| Doc | What's in it |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **Live API reference** — [`/docs`](https://auth.rodmena.app/docs) · [`/llms.txt`](https://auth.rodmena.app/llms.txt) | Every endpoint and exact response shape, served by the app (agent-friendly). |
|
|
73
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design, components, request lifecycle, data model, permission-check and key-rotation flows, diagrams. |
|
|
74
|
+
| [SECURITY.md](SECURITY.md) | Security model (tenant isolation, encryption, audit, rotation), threat notes, reporting. |
|
|
75
|
+
| [MIGRATIONS.md](MIGRATIONS.md) | Schema creation vs migrations, upgrade/rollback runbook. |
|
|
76
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Local setup, tests (sqlite + postgres), lint/type-check, CI. |
|
|
77
|
+
| [docs/](docs/) | Full Sphinx docs (concepts, configuration, encryption, deployment, REST & Python usage). |
|
|
78
|
+
|
|
79
|
+
## When to use — and not
|
|
80
|
+
|
|
81
|
+
**Use it** for RBAC: named roles, permissions, group membership, and boolean
|
|
82
|
+
"can user X do Y" gates for a service, CLI, or workflow engine.
|
|
83
|
+
|
|
84
|
+
**Not** for authentication (login/passwords/sessions/OAuth/JWT), fine-grained /
|
|
85
|
+
attribute-based rules (owner-of-*this*-record, time-of-day, row-level tenancy —
|
|
86
|
+
reach for an ABAC/policy engine), or air-gapped hot loops where a network hop per
|
|
87
|
+
check is too costly (cache, or use the library in-process).
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3.11 -m venv .venv && . .venv/bin/activate
|
|
93
|
+
pip install -e ".[dev,ratelimit,migrations]"
|
|
94
|
+
make check # ruff + mypy
|
|
95
|
+
make test # sqlite suite
|
|
96
|
+
make test-postgres # postgres integration (Docker), encryption on
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow. Licensed under the
|
|
100
|
+
[MIT License](LICENSE).
|
|
@@ -3,7 +3,11 @@ __author__ = "Farshid Ashouri"
|
|
|
3
3
|
from importlib.metadata import PackageNotFoundError, version
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
-
from auth.client import
|
|
6
|
+
from auth.client import ( # Import the new client
|
|
7
|
+
AuthTransportError,
|
|
8
|
+
Client,
|
|
9
|
+
EnhancedAuthClient,
|
|
10
|
+
)
|
|
7
11
|
from auth.database import SessionLocal
|
|
8
12
|
from auth.services.service import AuthorizationService
|
|
9
13
|
|
|
@@ -94,6 +98,7 @@ class Authorization:
|
|
|
94
98
|
# Export the new client for users who want enhanced features
|
|
95
99
|
__all__ = [
|
|
96
100
|
"Authorization",
|
|
101
|
+
"AuthTransportError",
|
|
97
102
|
"Client",
|
|
98
103
|
"EnhancedAuthClient",
|
|
99
104
|
"SessionLocal",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Per-user API key generation and hashing (SPEC 0004).
|
|
2
|
+
|
|
3
|
+
Secrets are ``rak_`` + 43 base62 characters carrying the full 256 bits of
|
|
4
|
+
``secrets.token_bytes(32)``. Only the SHA-256 hex digest is stored; the raw
|
|
5
|
+
secret is returned once at creation and never persisted, logged, or audited.
|
|
6
|
+
The digest deliberately excludes the tenant and any server pepper: at this
|
|
7
|
+
entropy an offline attack on a leaked hash is not a threat, and neither a
|
|
8
|
+
client-key rotation nor a pepper change may invalidate issued keys.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import re
|
|
13
|
+
import secrets
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
|
|
17
|
+
API_KEY_PREFIX = "rak_"
|
|
18
|
+
API_KEY_PATTERN = re.compile(r"^rak_[0-9A-Za-z]{43}$")
|
|
19
|
+
|
|
20
|
+
# "rak_" + first 8 payload chars — safe to store and display in listings.
|
|
21
|
+
KEY_PREFIX_LEN = 12
|
|
22
|
+
|
|
23
|
+
_PAYLOAD_LEN = 43
|
|
24
|
+
_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _base62(data: bytes) -> str:
|
|
28
|
+
number = int.from_bytes(data, "big")
|
|
29
|
+
chars = []
|
|
30
|
+
while number:
|
|
31
|
+
number, rem = divmod(number, 62)
|
|
32
|
+
chars.append(_ALPHABET[rem])
|
|
33
|
+
return "".join(reversed(chars)) or _ALPHABET[0]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_api_key() -> Tuple[str, str, str, str]:
|
|
37
|
+
"""Mint a fresh key: returns ``(secret, key_id, key_hash, key_prefix)``.
|
|
38
|
+
|
|
39
|
+
``secret`` is shown to the caller exactly once; ``key_id`` is the public
|
|
40
|
+
UUID4 handle used in revoke paths; ``key_hash``/``key_prefix`` are what
|
|
41
|
+
gets stored.
|
|
42
|
+
"""
|
|
43
|
+
payload = _base62(secrets.token_bytes(32)).rjust(_PAYLOAD_LEN, _ALPHABET[0])
|
|
44
|
+
secret = API_KEY_PREFIX + payload
|
|
45
|
+
return secret, str(uuid.uuid4()), hash_api_key(secret), secret[:KEY_PREFIX_LEN]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hash_api_key(secret: str) -> str:
|
|
49
|
+
"""SHA-256 hex digest of the full secret string (equality-queryable)."""
|
|
50
|
+
return hashlib.sha256(secret.encode()).hexdigest()
|
|
@@ -37,6 +37,11 @@ class AuditAction(Enum):
|
|
|
37
37
|
LIST_PERMISSIONS = "LIST_PERMISSIONS"
|
|
38
38
|
LIST_MEMBERSHIPS = "LIST_MEMBERSHIPS"
|
|
39
39
|
USER_PERMISSIONS = "USER_PERMISSIONS"
|
|
40
|
+
ROTATE_KEY = "ROTATE_KEY"
|
|
41
|
+
CREATE_API_KEY = "CREATE_API_KEY"
|
|
42
|
+
LIST_API_KEYS = "LIST_API_KEYS"
|
|
43
|
+
REVOKE_API_KEY = "REVOKE_API_KEY"
|
|
44
|
+
VALIDATE_API_KEY = "VALIDATE_API_KEY"
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
class AuditLog(Base):
|
|
@@ -111,7 +116,65 @@ def client_fingerprint(token: Optional[str]) -> str:
|
|
|
111
116
|
return "fpr_" + digest[:32]
|
|
112
117
|
|
|
113
118
|
|
|
114
|
-
def
|
|
119
|
+
def _build_audit_entry(
|
|
120
|
+
client_id: str,
|
|
121
|
+
user: Optional[str],
|
|
122
|
+
action: AuditAction,
|
|
123
|
+
resource: Optional[str],
|
|
124
|
+
details: Optional[Dict[str, Any]],
|
|
125
|
+
ip_address: Optional[str],
|
|
126
|
+
user_agent: Optional[str],
|
|
127
|
+
success: bool,
|
|
128
|
+
) -> "AuditLog":
|
|
129
|
+
# The managed user is a human identifier (often an email) — store a
|
|
130
|
+
# non-reversible fingerprint, never plaintext, matching how the client key is
|
|
131
|
+
# handled. Auditors correlate by fingerprint and can confirm a known user by
|
|
132
|
+
# computing its fingerprint. Role/permission/workflow names (the `resource`)
|
|
133
|
+
# are application identifiers, not PII, and stay readable — except the caller
|
|
134
|
+
# is responsible for fingerprinting any user embedded in `resource`.
|
|
135
|
+
user_fp = client_fingerprint(user) if user else None
|
|
136
|
+
return AuditLog(
|
|
137
|
+
client_id=_fit(client_id, "client_id"),
|
|
138
|
+
user=_fit(user_fp, "user"),
|
|
139
|
+
action=_fit(action.value, "action"),
|
|
140
|
+
resource=_fit(resource, "resource"),
|
|
141
|
+
details=json.dumps(details) if details else None,
|
|
142
|
+
ip_address=_fit(ip_address, "ip_address"),
|
|
143
|
+
user_agent=_fit(user_agent, "user_agent"),
|
|
144
|
+
success=1 if success else 0,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _emit_structured_log(
|
|
149
|
+
client_id: str,
|
|
150
|
+
user: Optional[str],
|
|
151
|
+
action: AuditAction,
|
|
152
|
+
resource: Optional[str],
|
|
153
|
+
details: Optional[Dict[str, Any]],
|
|
154
|
+
ip_address: Optional[str],
|
|
155
|
+
success: bool,
|
|
156
|
+
) -> None:
|
|
157
|
+
# The DB row is the system of record. The log STREAM (journald / SIEM /
|
|
158
|
+
# shipping) is more widely exposed, so it carries no PII: no raw user and no
|
|
159
|
+
# resource string (which may embed a user). Only the non-reversible client
|
|
160
|
+
# fingerprint, the action, and the outcome.
|
|
161
|
+
log_msg: Dict[str, Any] = {
|
|
162
|
+
"type": "audit",
|
|
163
|
+
"client_id": client_id,
|
|
164
|
+
"action": action.value,
|
|
165
|
+
"success": success,
|
|
166
|
+
"timestamp": _utcnow().isoformat(),
|
|
167
|
+
}
|
|
168
|
+
if details:
|
|
169
|
+
log_msg["details"] = details
|
|
170
|
+
if ip_address:
|
|
171
|
+
log_msg["ip"] = ip_address
|
|
172
|
+
audit_logger.info(json.dumps(log_msg))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def record_audit(
|
|
176
|
+
session,
|
|
177
|
+
*,
|
|
115
178
|
client_id: str,
|
|
116
179
|
user: Optional[str],
|
|
117
180
|
action: AuditAction,
|
|
@@ -121,45 +184,52 @@ def log_audit_event(
|
|
|
121
184
|
user_agent: Optional[str] = None,
|
|
122
185
|
success: bool = True,
|
|
123
186
|
) -> None:
|
|
187
|
+
"""Add an audit row to an EXISTING session so it commits atomically with the
|
|
188
|
+
caller's transaction (the mutation and its audit land together, or not at
|
|
189
|
+
all). The caller is responsible for committing.
|
|
190
|
+
|
|
191
|
+
This does NOT swallow errors: a failure to stage the audit row must fail the
|
|
192
|
+
surrounding request (fail-closed), never leave a committed mutation
|
|
193
|
+
unaudited.
|
|
124
194
|
"""
|
|
125
|
-
|
|
195
|
+
session.add(
|
|
196
|
+
_build_audit_entry(
|
|
197
|
+
client_id, user, action, resource, details, ip_address, user_agent, success
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
_emit_structured_log(client_id, user, action, resource, details, ip_address, success)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def log_audit_event(
|
|
204
|
+
client_id: str,
|
|
205
|
+
user: Optional[str],
|
|
206
|
+
action: AuditAction,
|
|
207
|
+
resource: Optional[str] = None,
|
|
208
|
+
details: Optional[Dict[str, Any]] = None,
|
|
209
|
+
ip_address: Optional[str] = None,
|
|
210
|
+
user_agent: Optional[str] = None,
|
|
211
|
+
success: bool = True,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Write an audit event on its OWN committed session.
|
|
214
|
+
|
|
215
|
+
For contexts with no request transaction to join: in-process/library
|
|
216
|
+
callers, and the *failure* path of the request decorator (where the request
|
|
217
|
+
transaction is being rolled back and must not carry the audit row). This one
|
|
218
|
+
is best-effort — a failure is logged, not raised, so it cannot mask the
|
|
219
|
+
original error it is recording.
|
|
126
220
|
"""
|
|
127
221
|
session = SessionLocal()
|
|
128
222
|
try:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
action=_fit(action.value, "action"),
|
|
134
|
-
resource=_fit(resource, "resource"),
|
|
135
|
-
details=json.dumps(details) if details else None,
|
|
136
|
-
ip_address=_fit(ip_address, "ip_address"),
|
|
137
|
-
user_agent=_fit(user_agent, "user_agent"),
|
|
138
|
-
success=1 if success else 0,
|
|
223
|
+
session.add(
|
|
224
|
+
_build_audit_entry(
|
|
225
|
+
client_id, user, action, resource, details, ip_address, user_agent, success
|
|
226
|
+
)
|
|
139
227
|
)
|
|
140
|
-
|
|
141
|
-
session.add(audit_entry)
|
|
142
228
|
session.commit()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"type": "audit",
|
|
147
|
-
"client_id": client_id,
|
|
148
|
-
"user": user,
|
|
149
|
-
"action": action.value,
|
|
150
|
-
"resource": resource,
|
|
151
|
-
"success": success,
|
|
152
|
-
"timestamp": _utcnow().isoformat(),
|
|
153
|
-
}
|
|
154
|
-
if details:
|
|
155
|
-
log_msg["details"] = details
|
|
156
|
-
if ip_address:
|
|
157
|
-
log_msg["ip"] = ip_address
|
|
158
|
-
|
|
159
|
-
audit_logger.info(json.dumps(log_msg))
|
|
229
|
+
_emit_structured_log(
|
|
230
|
+
client_id, user, action, resource, details, ip_address, success
|
|
231
|
+
)
|
|
160
232
|
except Exception:
|
|
161
|
-
# If audit logging fails, we don't want to break the main operation
|
|
162
|
-
# But log the failure for monitoring
|
|
163
233
|
audit_logger.error(
|
|
164
234
|
f"Failed to log audit event: client_id={client_id}, action={action.value}"
|
|
165
235
|
)
|