auth 2.3.1__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.
Files changed (59) hide show
  1. {auth-2.3.1 → auth-2.4.0}/PKG-INFO +15 -1
  2. {auth-2.3.1 → auth-2.4.0}/README.md +13 -0
  3. {auth-2.3.1 → auth-2.4.0}/auth/__init__.py +6 -1
  4. auth-2.4.0/auth/api_keys.py +50 -0
  5. {auth-2.3.1 → auth-2.4.0}/auth/audit.py +4 -0
  6. auth-2.4.0/auth/client.py +595 -0
  7. {auth-2.3.1 → auth-2.4.0}/auth/docs_page.py +44 -5
  8. {auth-2.3.1 → auth-2.4.0}/auth/models/sql.py +74 -0
  9. {auth-2.3.1 → auth-2.4.0}/auth/routes.py +140 -1
  10. {auth-2.3.1 → auth-2.4.0}/auth/services/service.py +194 -25
  11. {auth-2.3.1 → auth-2.4.0}/auth/validation.py +17 -0
  12. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/PKG-INFO +15 -1
  13. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/SOURCES.txt +3 -0
  14. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/requires.txt +1 -0
  15. {auth-2.3.1 → auth-2.4.0}/pyproject.toml +5 -3
  16. auth-2.4.0/tests/test_api_keys.py +353 -0
  17. auth-2.4.0/tests/test_api_keys_encryption.py +91 -0
  18. auth-2.4.0/tests/test_client.py +317 -0
  19. {auth-2.3.1 → auth-2.4.0}/tests/test_docs_page.py +11 -0
  20. {auth-2.3.1 → auth-2.4.0}/tests/test_key_rotation.py +68 -3
  21. {auth-2.3.1 → auth-2.4.0}/tests/test_migrations.py +9 -2
  22. auth-2.3.1/auth/client.py +0 -408
  23. auth-2.3.1/tests/test_client.py +0 -157
  24. {auth-2.3.1 → auth-2.4.0}/LICENSE +0 -0
  25. {auth-2.3.1 → auth-2.4.0}/auth/circuit_breaker.py +0 -0
  26. {auth-2.3.1 → auth-2.4.0}/auth/cmd/__init__.py +0 -0
  27. {auth-2.3.1 → auth-2.4.0}/auth/cmd/server.py +0 -0
  28. {auth-2.3.1 → auth-2.4.0}/auth/config.py +0 -0
  29. {auth-2.3.1 → auth-2.4.0}/auth/core/REST/__init__.py +0 -0
  30. {auth-2.3.1 → auth-2.4.0}/auth/core/REST/client.py +0 -0
  31. {auth-2.3.1 → auth-2.4.0}/auth/core/__init__.py +0 -0
  32. {auth-2.3.1 → auth-2.4.0}/auth/core/models/__init__.py +0 -0
  33. {auth-2.3.1 → auth-2.4.0}/auth/database.py +0 -0
  34. {auth-2.3.1 → auth-2.4.0}/auth/decorators.py +0 -0
  35. {auth-2.3.1 → auth-2.4.0}/auth/encryption.py +0 -0
  36. {auth-2.3.1 → auth-2.4.0}/auth/logging_config.py +0 -0
  37. {auth-2.3.1 → auth-2.4.0}/auth/main.py +0 -0
  38. {auth-2.3.1 → auth-2.4.0}/auth/response_format.py +0 -0
  39. {auth-2.3.1 → auth-2.4.0}/auth/sanitizer.py +0 -0
  40. {auth-2.3.1 → auth-2.4.0}/auth/server.py +0 -0
  41. {auth-2.3.1 → auth-2.4.0}/auth/workflow_checker.py +0 -0
  42. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/dependency_links.txt +0 -0
  43. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/entry_points.txt +0 -0
  44. {auth-2.3.1 → auth-2.4.0}/auth.egg-info/top_level.txt +0 -0
  45. {auth-2.3.1 → auth-2.4.0}/setup.cfg +0 -0
  46. {auth-2.3.1 → auth-2.4.0}/tests/test_audit_integrity.py +0 -0
  47. {auth-2.3.1 → auth-2.4.0}/tests/test_client_rest.py +0 -0
  48. {auth-2.3.1 → auth-2.4.0}/tests/test_cmd_server.py +0 -0
  49. {auth-2.3.1 → auth-2.4.0}/tests/test_config.py +0 -0
  50. {auth-2.3.1 → auth-2.4.0}/tests/test_correctness_hardening.py +0 -0
  51. {auth-2.3.1 → auth-2.4.0}/tests/test_encryption.py +0 -0
  52. {auth-2.3.1 → auth-2.4.0}/tests/test_encryption_integration.py +0 -0
  53. {auth-2.3.1 → auth-2.4.0}/tests/test_flask.py +0 -0
  54. {auth-2.3.1 → auth-2.4.0}/tests/test_log_redaction.py +0 -0
  55. {auth-2.3.1 → auth-2.4.0}/tests/test_phase_a_hardening.py +0 -0
  56. {auth-2.3.1 → auth-2.4.0}/tests/test_reencryption.py +0 -0
  57. {auth-2.3.1 → auth-2.4.0}/tests/test_routes_errors.py +0 -0
  58. {auth-2.3.1 → auth-2.4.0}/tests/test_server.py +0 -0
  59. {auth-2.3.1 → auth-2.4.0}/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.0
4
4
  Summary: Authorization for humans
5
5
  Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ 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: migretti>=0.10.0; extra == "migrations"
43
44
  Requires-Dist: alembic>=1.13.0; extra == "migrations"
44
45
  Provides-Extra: dev
45
46
  Requires-Dist: pytest>=6.0; extra == "dev"
@@ -104,9 +105,22 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
104
105
  - **Two response shapes** — bare `{"result": ...}` and wrapped
105
106
  `{"success", "data", ...}`. The API reference says which per endpoint.
106
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.
107
115
  - **Reuse one key.** A new key is a new empty namespace, not an error. Keep the
108
116
  key out of source control, logs, and URLs — it is the only thing protecting
109
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`.
110
124
 
111
125
  ## Documentation
112
126
 
@@ -48,9 +48,22 @@ 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`.
54
67
 
55
68
  ## Documentation
56
69
 
@@ -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):