auth 2.3.1__tar.gz → 2.4.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.
Files changed (59) hide show
  1. {auth-2.3.1 → auth-2.4.1}/PKG-INFO +21 -3
  2. {auth-2.3.1 → auth-2.4.1}/README.md +18 -0
  3. {auth-2.3.1 → auth-2.4.1}/auth/__init__.py +6 -1
  4. auth-2.4.1/auth/api_keys.py +50 -0
  5. {auth-2.3.1 → auth-2.4.1}/auth/audit.py +4 -0
  6. auth-2.4.1/auth/client.py +595 -0
  7. {auth-2.3.1 → auth-2.4.1}/auth/docs_page.py +54 -5
  8. {auth-2.3.1 → auth-2.4.1}/auth/models/sql.py +74 -0
  9. {auth-2.3.1 → auth-2.4.1}/auth/routes.py +140 -1
  10. {auth-2.3.1 → auth-2.4.1}/auth/services/service.py +194 -25
  11. {auth-2.3.1 → auth-2.4.1}/auth/validation.py +17 -0
  12. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/PKG-INFO +21 -3
  13. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/SOURCES.txt +3 -1
  14. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/requires.txt +2 -2
  15. {auth-2.3.1 → auth-2.4.1}/pyproject.toml +5 -5
  16. auth-2.4.1/tests/test_api_keys.py +353 -0
  17. auth-2.4.1/tests/test_api_keys_encryption.py +89 -0
  18. auth-2.4.1/tests/test_client.py +317 -0
  19. {auth-2.3.1 → auth-2.4.1}/tests/test_docs_page.py +11 -0
  20. {auth-2.3.1 → auth-2.4.1}/tests/test_key_rotation.py +68 -3
  21. auth-2.3.1/auth/client.py +0 -408
  22. auth-2.3.1/tests/test_client.py +0 -157
  23. auth-2.3.1/tests/test_migrations.py +0 -38
  24. {auth-2.3.1 → auth-2.4.1}/LICENSE +0 -0
  25. {auth-2.3.1 → auth-2.4.1}/auth/circuit_breaker.py +0 -0
  26. {auth-2.3.1 → auth-2.4.1}/auth/cmd/__init__.py +0 -0
  27. {auth-2.3.1 → auth-2.4.1}/auth/cmd/server.py +0 -0
  28. {auth-2.3.1 → auth-2.4.1}/auth/config.py +0 -0
  29. {auth-2.3.1 → auth-2.4.1}/auth/core/REST/__init__.py +0 -0
  30. {auth-2.3.1 → auth-2.4.1}/auth/core/REST/client.py +0 -0
  31. {auth-2.3.1 → auth-2.4.1}/auth/core/__init__.py +0 -0
  32. {auth-2.3.1 → auth-2.4.1}/auth/core/models/__init__.py +0 -0
  33. {auth-2.3.1 → auth-2.4.1}/auth/database.py +0 -0
  34. {auth-2.3.1 → auth-2.4.1}/auth/decorators.py +0 -0
  35. {auth-2.3.1 → auth-2.4.1}/auth/encryption.py +0 -0
  36. {auth-2.3.1 → auth-2.4.1}/auth/logging_config.py +0 -0
  37. {auth-2.3.1 → auth-2.4.1}/auth/main.py +0 -0
  38. {auth-2.3.1 → auth-2.4.1}/auth/response_format.py +0 -0
  39. {auth-2.3.1 → auth-2.4.1}/auth/sanitizer.py +0 -0
  40. {auth-2.3.1 → auth-2.4.1}/auth/server.py +0 -0
  41. {auth-2.3.1 → auth-2.4.1}/auth/workflow_checker.py +0 -0
  42. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/dependency_links.txt +0 -0
  43. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/entry_points.txt +0 -0
  44. {auth-2.3.1 → auth-2.4.1}/auth.egg-info/top_level.txt +0 -0
  45. {auth-2.3.1 → auth-2.4.1}/setup.cfg +0 -0
  46. {auth-2.3.1 → auth-2.4.1}/tests/test_audit_integrity.py +0 -0
  47. {auth-2.3.1 → auth-2.4.1}/tests/test_client_rest.py +0 -0
  48. {auth-2.3.1 → auth-2.4.1}/tests/test_cmd_server.py +0 -0
  49. {auth-2.3.1 → auth-2.4.1}/tests/test_config.py +0 -0
  50. {auth-2.3.1 → auth-2.4.1}/tests/test_correctness_hardening.py +0 -0
  51. {auth-2.3.1 → auth-2.4.1}/tests/test_encryption.py +0 -0
  52. {auth-2.3.1 → auth-2.4.1}/tests/test_encryption_integration.py +0 -0
  53. {auth-2.3.1 → auth-2.4.1}/tests/test_flask.py +0 -0
  54. {auth-2.3.1 → auth-2.4.1}/tests/test_log_redaction.py +0 -0
  55. {auth-2.3.1 → auth-2.4.1}/tests/test_phase_a_hardening.py +0 -0
  56. {auth-2.3.1 → auth-2.4.1}/tests/test_reencryption.py +0 -0
  57. {auth-2.3.1 → auth-2.4.1}/tests/test_routes_errors.py +0 -0
  58. {auth-2.3.1 → auth-2.4.1}/tests/test_server.py +0 -0
  59. {auth-2.3.1 → auth-2.4.1}/tests/test_service_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth
3
- Version: 2.3.1
3
+ Version: 2.4.1
4
4
  Summary: Authorization for humans
5
5
  Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
6
6
  License-Expression: MIT
@@ -40,11 +40,11 @@ Requires-Dist: python-json-logger<4,>=3.1.0
40
40
  Provides-Extra: ratelimit
41
41
  Requires-Dist: flask-limiter>=3.0.0; extra == "ratelimit"
42
42
  Provides-Extra: migrations
43
- Requires-Dist: alembic>=1.13.0; extra == "migrations"
43
+ Requires-Dist: migretti>=0.10.0; extra == "migrations"
44
44
  Provides-Extra: dev
45
45
  Requires-Dist: pytest>=6.0; extra == "dev"
46
46
  Requires-Dist: pytest-cov>=2.0; extra == "dev"
47
- Requires-Dist: alembic>=1.13.0; extra == "dev"
47
+ Requires-Dist: migretti>=0.10.0; extra == "dev"
48
48
  Requires-Dist: ruff>=0.0.260; extra == "dev"
49
49
  Requires-Dist: mypy>=1.0; extra == "dev"
50
50
  Requires-Dist: types-requests; extra == "dev"
@@ -104,9 +104,27 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
104
104
  - **Two response shapes** — bare `{"result": ...}` and wrapped
105
105
  `{"success", "data", ...}`. The API reference says which per endpoint.
106
106
  - **Errors below 2xx are HTML**, not JSON. Branch on the status code first.
107
+ - **Python client: check `success` before reading `data`.** On transport
108
+ failure the client does not raise by default — it returns
109
+ `{"error", "success": False, "transport_error": True, "data": {...}}` where
110
+ `data` only echoes your inputs and does NOT contain the answer field
111
+ (`has_permission`, `count`, ...). Reading `data` blindly turns an outage into
112
+ a false "no". Pass `Client(..., raise_on_error=True)` to get an
113
+ `AuthTransportError` exception instead of the error dict.
107
114
  - **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
108
115
  key out of source control, logs, and URLs — it is the only thing protecting
109
116
  your data. Rotate it with `POST /api/keys/rotate` if it leaks.
117
+ - **Per-user API keys** (2.4.0): `/api/apikeys/user/<user>` (create/list),
118
+ `/api/apikeys/user/<user>/<key_id>` (revoke), `/api/apikeys/validate`. auth
119
+ mints `rak_...` secrets for *your users*, shows each exactly once, stores only
120
+ a hash, and validates them inside your namespace — an identity UI fronts the
121
+ lifecycle, backends validate then use the RBAC checks. Client methods:
122
+ `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
123
+ - **DEPRECATED — bare user strings.** Asserting a `<user>` that no validated API
124
+ key backs is scheduled for decommission: 2.5.0 adds opt-in per-tenant strict
125
+ mode, **3.0.0 makes key-backed users mandatory** (each phase ships only after
126
+ every consuming platform confirms). Migrate backends to
127
+ `validate → user → has_permission` now. Details: [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md).
110
128
 
111
129
  ## Documentation
112
130
 
@@ -48,9 +48,27 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
48
48
  - **Two response shapes** — bare `{"result": ...}` and wrapped
49
49
  `{"success", "data", ...}`. The API reference says which per endpoint.
50
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.
51
58
  - **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
52
59
  key out of source control, logs, and URLs — it is the only thing protecting
53
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
+ - **DEPRECATED — bare user strings.** Asserting a `<user>` that no validated API
68
+ key backs is scheduled for decommission: 2.5.0 adds opt-in per-tenant strict
69
+ mode, **3.0.0 makes key-backed users mandatory** (each phase ships only after
70
+ every consuming platform confirms). Migrate backends to
71
+ `validate → user → has_permission` now. Details: [docs/DEPRECATIONS.md](docs/DEPRECATIONS.md).
54
72
 
55
73
  ## Documentation
56
74
 
@@ -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 Client, EnhancedAuthClient # Import the new client
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()
@@ -38,6 +38,10 @@ class AuditAction(Enum):
38
38
  LIST_MEMBERSHIPS = "LIST_MEMBERSHIPS"
39
39
  USER_PERMISSIONS = "USER_PERMISSIONS"
40
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"
41
45
 
42
46
 
43
47
  class AuditLog(Base):