auth 2.5.1__tar.gz → 2.5.2__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 (58) hide show
  1. {auth-2.5.1 → auth-2.5.2}/PKG-INFO +5 -2
  2. {auth-2.5.1 → auth-2.5.2}/README.md +4 -1
  3. {auth-2.5.1 → auth-2.5.2}/auth/__init__.py +36 -3
  4. {auth-2.5.1 → auth-2.5.2}/auth/database.py +25 -4
  5. {auth-2.5.1 → auth-2.5.2}/auth/docs_page.py +6 -1
  6. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/PKG-INFO +5 -2
  7. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/SOURCES.txt +1 -0
  8. {auth-2.5.1 → auth-2.5.2}/pyproject.toml +1 -1
  9. auth-2.5.2/tests/test_database_sslmode.py +47 -0
  10. {auth-2.5.1 → auth-2.5.2}/tests/test_strict_users.py +45 -0
  11. {auth-2.5.1 → auth-2.5.2}/LICENSE +0 -0
  12. {auth-2.5.1 → auth-2.5.2}/auth/api_keys.py +0 -0
  13. {auth-2.5.1 → auth-2.5.2}/auth/audit.py +0 -0
  14. {auth-2.5.1 → auth-2.5.2}/auth/circuit_breaker.py +0 -0
  15. {auth-2.5.1 → auth-2.5.2}/auth/client.py +0 -0
  16. {auth-2.5.1 → auth-2.5.2}/auth/cmd/__init__.py +0 -0
  17. {auth-2.5.1 → auth-2.5.2}/auth/cmd/server.py +0 -0
  18. {auth-2.5.1 → auth-2.5.2}/auth/config.py +0 -0
  19. {auth-2.5.1 → auth-2.5.2}/auth/core/REST/__init__.py +0 -0
  20. {auth-2.5.1 → auth-2.5.2}/auth/core/REST/client.py +0 -0
  21. {auth-2.5.1 → auth-2.5.2}/auth/core/__init__.py +0 -0
  22. {auth-2.5.1 → auth-2.5.2}/auth/core/models/__init__.py +0 -0
  23. {auth-2.5.1 → auth-2.5.2}/auth/decorators.py +0 -0
  24. {auth-2.5.1 → auth-2.5.2}/auth/encryption.py +0 -0
  25. {auth-2.5.1 → auth-2.5.2}/auth/logging_config.py +0 -0
  26. {auth-2.5.1 → auth-2.5.2}/auth/main.py +0 -0
  27. {auth-2.5.1 → auth-2.5.2}/auth/models/sql.py +0 -0
  28. {auth-2.5.1 → auth-2.5.2}/auth/response_format.py +0 -0
  29. {auth-2.5.1 → auth-2.5.2}/auth/routes.py +0 -0
  30. {auth-2.5.1 → auth-2.5.2}/auth/sanitizer.py +0 -0
  31. {auth-2.5.1 → auth-2.5.2}/auth/server.py +0 -0
  32. {auth-2.5.1 → auth-2.5.2}/auth/services/service.py +0 -0
  33. {auth-2.5.1 → auth-2.5.2}/auth/validation.py +0 -0
  34. {auth-2.5.1 → auth-2.5.2}/auth/workflow_checker.py +0 -0
  35. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/dependency_links.txt +0 -0
  36. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/entry_points.txt +0 -0
  37. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/requires.txt +0 -0
  38. {auth-2.5.1 → auth-2.5.2}/auth.egg-info/top_level.txt +0 -0
  39. {auth-2.5.1 → auth-2.5.2}/setup.cfg +0 -0
  40. {auth-2.5.1 → auth-2.5.2}/tests/test_api_keys.py +0 -0
  41. {auth-2.5.1 → auth-2.5.2}/tests/test_api_keys_encryption.py +0 -0
  42. {auth-2.5.1 → auth-2.5.2}/tests/test_audit_integrity.py +0 -0
  43. {auth-2.5.1 → auth-2.5.2}/tests/test_client.py +0 -0
  44. {auth-2.5.1 → auth-2.5.2}/tests/test_client_rest.py +0 -0
  45. {auth-2.5.1 → auth-2.5.2}/tests/test_cmd_server.py +0 -0
  46. {auth-2.5.1 → auth-2.5.2}/tests/test_config.py +0 -0
  47. {auth-2.5.1 → auth-2.5.2}/tests/test_correctness_hardening.py +0 -0
  48. {auth-2.5.1 → auth-2.5.2}/tests/test_docs_page.py +0 -0
  49. {auth-2.5.1 → auth-2.5.2}/tests/test_encryption.py +0 -0
  50. {auth-2.5.1 → auth-2.5.2}/tests/test_encryption_integration.py +0 -0
  51. {auth-2.5.1 → auth-2.5.2}/tests/test_flask.py +0 -0
  52. {auth-2.5.1 → auth-2.5.2}/tests/test_key_rotation.py +0 -0
  53. {auth-2.5.1 → auth-2.5.2}/tests/test_log_redaction.py +0 -0
  54. {auth-2.5.1 → auth-2.5.2}/tests/test_phase_a_hardening.py +0 -0
  55. {auth-2.5.1 → auth-2.5.2}/tests/test_reencryption.py +0 -0
  56. {auth-2.5.1 → auth-2.5.2}/tests/test_routes_errors.py +0 -0
  57. {auth-2.5.1 → auth-2.5.2}/tests/test_server.py +0 -0
  58. {auth-2.5.1 → auth-2.5.2}/tests/test_service_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth
3
- Version: 2.5.1
3
+ Version: 2.5.2
4
4
  Summary: Authorization for humans
5
5
  Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
6
6
  License-Expression: MIT
@@ -119,7 +119,10 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
119
119
  mints `rak_...` secrets for *your users*, shows each exactly once, stores only
120
120
  a hash, and validates them inside your namespace — an identity UI fronts the
121
121
  lifecycle, backends validate then use the RBAC checks. Client methods:
122
- `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
122
+ `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`,
123
+ `check_api_key_permission` (validate + permission in one round trip), plus
124
+ `get_settings`/`set_strict_users`. All of these also exist on the in-process
125
+ `Authorization` wrapper for embedded consumers.
123
126
  - **DEPRECATED — bare user strings.** Asserting a `<user>` that no validated API
124
127
  key backs is scheduled for decommission. The opt-in phase is live (2.5.0):
125
128
  `PUT /api/settings {"strict_users": true}` makes keyless users answer
@@ -63,7 +63,10 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
63
63
  mints `rak_...` secrets for *your users*, shows each exactly once, stores only
64
64
  a hash, and validates them inside your namespace — an identity UI fronts the
65
65
  lifecycle, backends validate then use the RBAC checks. Client methods:
66
- `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
66
+ `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`,
67
+ `check_api_key_permission` (validate + permission in one round trip), plus
68
+ `get_settings`/`set_strict_users`. All of these also exist on the in-process
69
+ `Authorization` wrapper for embedded consumers.
67
70
  - **DEPRECATED — bare user strings.** Asserting a `<user>` that no validated API
68
71
  key backs is scheduled for decommission. The opt-in phase is live (2.5.0):
69
72
  `PUT /api/settings {"strict_users": true}` makes keyless users answer
@@ -38,12 +38,21 @@ api = app
38
38
  class Authorization:
39
39
  """Compatibility wrapper for old Authorization interface"""
40
40
 
41
- def __init__(self, client: str, db_session=None):
41
+ def __init__(
42
+ self,
43
+ client: str,
44
+ db_session=None,
45
+ strict_users: Optional[bool] = None,
46
+ ):
42
47
  self.client = client
43
48
  # Use provided session or create a new one
44
49
  self.db = db_session if db_session else SessionLocal()
45
- # Use legacy mode (no client validation) for backward compatibility
46
- self.service = AuthorizationService(self.db, client, validate_client=False)
50
+ # Use legacy mode (no client validation) for backward compatibility.
51
+ # strict_users: None reads the tenant's stored setting (identical
52
+ # semantics to the REST layer); an explicit bool pins it per instance.
53
+ self.service = AuthorizationService(
54
+ self.db, client, validate_client=False, strict_users=strict_users
55
+ )
47
56
 
48
57
  @property
49
58
  def roles(self):
@@ -94,6 +103,30 @@ class Authorization:
94
103
  def which_users_can(self, name: str):
95
104
  return self.service.which_users_can(name)
96
105
 
106
+ # Tenant settings & strict user identity (SPEC 0010) — embedded consumers
107
+ # get the same semantics the REST layer serves.
108
+ def get_settings(self):
109
+ return self.service.get_settings()
110
+
111
+ def set_strict_users(self, enabled: bool):
112
+ return self.service.set_strict_users(enabled)
113
+
114
+ # Per-user API keys (SPEC 0004)
115
+ def create_api_key(self, user: str, label: Optional[str] = None):
116
+ return self.service.create_api_key(user, label)
117
+
118
+ def list_api_keys(self, user: str):
119
+ return self.service.list_api_keys(user)
120
+
121
+ def revoke_api_key(self, user: str, key_id: str):
122
+ return self.service.revoke_api_key(user, key_id)
123
+
124
+ def validate_api_key(self, api_key: str):
125
+ return self.service.validate_api_key(api_key)
126
+
127
+ def check_api_key_permission(self, api_key: str, permission: str):
128
+ return self.service.check_api_key_permission(api_key, permission)
129
+
97
130
 
98
131
  # Export the new client for users who want enhanced features
99
132
  __all__ = [
@@ -3,18 +3,39 @@
3
3
  SQLAlchemy database session management with enterprise-grade connection pooling
4
4
  """
5
5
  import logging
6
+ import os
6
7
  import sqlite3
7
8
  import threading
8
9
  from contextlib import contextmanager
9
10
  from typing import Generator, Optional
10
11
 
11
12
  from sqlalchemy import create_engine, event, pool
12
- from sqlalchemy.engine import Engine
13
+ from sqlalchemy.engine import Engine, make_url
13
14
  from sqlalchemy.orm import Session, sessionmaker
14
15
  from sqlalchemy.pool import Pool
15
16
 
16
17
  from auth.config import DatabaseType, get_settings
17
18
 
19
+
20
+ def _forced_sslmode(database_url: str) -> Optional[str]:
21
+ """The sslmode to force via connect_args, or None to leave it alone.
22
+
23
+ Secure-by-default SSL for remote hosts — but never overriding an explicit
24
+ caller choice. connect_args beat URL conninfo in psycopg, so forcing
25
+ sslmode here would silently discard a ``?sslmode=...`` the caller wrote
26
+ (highway report, agent-mail thr-7745c815fd0a425cabac). Precedence:
27
+ URL sslmode param > PGSSLMODE env > require-for-remote. The host is
28
+ decided by component comparison, not URL substring — a URL carrying
29
+ ``?fallback_application_name=localhost`` must not skip SSL.
30
+ """
31
+ url = make_url(database_url)
32
+ if "sslmode" in url.query or os.environ.get("PGSSLMODE"):
33
+ return None
34
+ host = (url.host or "").lower()
35
+ if host in ("localhost", "127.0.0.1", "::1"):
36
+ return None
37
+ return "require"
38
+
18
39
  logger = logging.getLogger(__name__)
19
40
 
20
41
 
@@ -86,9 +107,9 @@ class DatabaseEngine(metaclass=SingletonMeta):
86
107
  "options": "-c statement_timeout=30000", # 30 seconds
87
108
  }
88
109
 
89
- # Enable SSL for remote connections
90
- if "localhost" not in database_url and "127.0.0.1" not in database_url:
91
- connect_args["sslmode"] = "require"
110
+ forced = _forced_sslmode(database_url)
111
+ if forced:
112
+ connect_args["sslmode"] = forced
92
113
 
93
114
  return create_engine(
94
115
  database_url,
@@ -155,7 +155,12 @@ adds for key-less subjects answer **409** `{{"result": false, "reason":
155
155
  success; check `result` on writes regardless). Create the key first, then grant
156
156
  roles: key creation committing before the grant is a contract, with no eventual
157
157
  consistency in between. Strict mode never blocks key issuance or any
158
- delete/revoke path. 3.0.0 makes strict identity the default — the audited
158
+ delete/revoke path. The `reason` field is **stable contract** (only ever
159
+ present on strict blocks). A strict block on read decisions is an **HTTP
160
+ 200** — transport-failure handling (retries, breakers, fallbacks) will not
161
+ fire on it; tenants that deliberately hold `strict_users: false` should assert
162
+ that value in their deploy/health checks so an unexpected flip alarms instead
163
+ of silently zeroing entitlements. 3.0.0 makes strict identity the default — the audited
159
164
  per-tenant opt-out survives for platforms that authenticate their own callers —
160
165
  and ships only after every consuming platform has confirmed readiness. Migrate
161
166
  now: issue keys to your users, derive the user from `/api/apikeys/validate` —
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth
3
- Version: 2.5.1
3
+ Version: 2.5.2
4
4
  Summary: Authorization for humans
5
5
  Author-email: Farshid Ashouri <farsheed.ashouri@gmail.com>
6
6
  License-Expression: MIT
@@ -119,7 +119,10 @@ with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
119
119
  mints `rak_...` secrets for *your users*, shows each exactly once, stores only
120
120
  a hash, and validates them inside your namespace — an identity UI fronts the
121
121
  lifecycle, backends validate then use the RBAC checks. Client methods:
122
- `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`.
122
+ `create_api_key`, `list_api_keys`, `revoke_api_key`, `validate_api_key`,
123
+ `check_api_key_permission` (validate + permission in one round trip), plus
124
+ `get_settings`/`set_strict_users`. All of these also exist on the in-process
125
+ `Authorization` wrapper for embedded consumers.
123
126
  - **DEPRECATED — bare user strings.** Asserting a `<user>` that no validated API
124
127
  key backs is scheduled for decommission. The opt-in phase is live (2.5.0):
125
128
  `PUT /api/settings {"strict_users": true}` makes keyless users answer
@@ -41,6 +41,7 @@ tests/test_client_rest.py
41
41
  tests/test_cmd_server.py
42
42
  tests/test_config.py
43
43
  tests/test_correctness_hardening.py
44
+ tests/test_database_sslmode.py
44
45
  tests/test_docs_page.py
45
46
  tests/test_encryption.py
46
47
  tests/test_encryption_integration.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "auth"
7
- version = "2.5.1"
7
+ version = "2.5.2"
8
8
  description = "Authorization for humans"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,47 @@
1
+ """sslmode precedence for the PostgreSQL engine factory (SPEC 0011, highway
2
+ report thr-7745c815fd0a425cabac).
3
+
4
+ Contract: explicit URL sslmode wins, then PGSSLMODE, then secure-by-default
5
+ ``require`` for genuinely remote hosts — decided by host component, never by
6
+ URL substring.
7
+ """
8
+
9
+ import pytest
10
+
11
+ from auth.database import _forced_sslmode
12
+
13
+ REMOTE = "postgresql+psycopg://app:pw@db:5432/x"
14
+
15
+
16
+ @pytest.fixture(autouse=True)
17
+ def _no_pgsslmode(monkeypatch):
18
+ monkeypatch.delenv("PGSSLMODE", raising=False)
19
+
20
+
21
+ def test_remote_host_defaults_to_require():
22
+ # Known-positive first: the default CAN fire.
23
+ assert _forced_sslmode(REMOTE) == "require"
24
+ assert _forced_sslmode("postgresql://u@10.0.0.5/db") == "require"
25
+
26
+
27
+ def test_explicit_url_sslmode_is_never_overridden():
28
+ assert _forced_sslmode(REMOTE + "?sslmode=disable") is None
29
+ assert _forced_sslmode(REMOTE + "?sslmode=verify-full") is None
30
+
31
+
32
+ def test_pgsslmode_env_is_respected(monkeypatch):
33
+ monkeypatch.setenv("PGSSLMODE", "disable")
34
+ assert _forced_sslmode(REMOTE) is None
35
+
36
+
37
+ def test_local_hosts_get_no_forced_sslmode():
38
+ assert _forced_sslmode("postgresql://u@localhost/db") is None
39
+ assert _forced_sslmode("postgresql://u@127.0.0.1:5432/db") is None
40
+
41
+
42
+ def test_host_is_compared_as_component_not_substring():
43
+ # The old substring test skipped SSL for this remote URL; it must not.
44
+ tricky = "postgresql://app@db:5432/x?fallback_application_name=localhost"
45
+ assert _forced_sslmode(tricky) == "require"
46
+ # And a host merely CONTAINING 'localhost' is remote.
47
+ assert _forced_sslmode("postgresql://u@localhost.attacker.net/db") == "require"
@@ -301,6 +301,51 @@ def test_settings_update_is_audited(client):
301
301
  session.close()
302
302
 
303
303
 
304
+ def test_authorization_wrapper_embedded_strict_flow():
305
+ """Embedded consumers (Authorization wrapper) get REST-identical strict
306
+ semantics: same setting row, same gates, same release-on-key-issuance."""
307
+ import uuid as _uuid
308
+
309
+ from auth import Authorization
310
+ from auth.database import SessionLocal
311
+ from auth.main import create_app
312
+
313
+ create_app()
314
+ tenant = str(_uuid.uuid4())
315
+ db = SessionLocal()
316
+ try:
317
+ wrapper = Authorization(client=tenant, db_session=db)
318
+ assert wrapper.add_role("ops") is True
319
+ assert wrapper.add_permission("ops", "deploy") is True
320
+ assert wrapper.add_membership("erin", "ops") is True
321
+ assert wrapper.user_has_permission("erin", "deploy") is True
322
+
323
+ assert wrapper.get_settings() == {"strict_users": False}
324
+ wrapper.set_strict_users(True)
325
+ # Fresh wrapper (no per-instance cache) sees the flip and blocks.
326
+ strict = Authorization(client=tenant, db_session=db)
327
+ assert strict.user_has_permission("erin", "deploy") is False
328
+
329
+ created = strict.create_api_key("erin", label="svc erin")
330
+ assert created is not None and created["api_key"].startswith("rak_")
331
+ assert strict.user_has_permission("erin", "deploy") is True
332
+ checked = strict.check_api_key_permission(created["api_key"], "deploy")
333
+ assert checked["valid"] is True and checked["has_permission"] is True
334
+ assert strict.list_api_keys("erin")[0]["label"] == "svc erin"
335
+ assert (
336
+ strict.revoke_api_key("erin", created["key_id"]) == "revoked"
337
+ )
338
+ assert strict.validate_api_key(created["api_key"]) == {
339
+ "valid": False,
340
+ "reason": "revoked",
341
+ }
342
+ # Explicit per-instance override pins behavior regardless of the row.
343
+ pinned_off = Authorization(client=tenant, db_session=db, strict_users=False)
344
+ assert pinned_off.user_has_permission("erin", "deploy") is True
345
+ finally:
346
+ db.close()
347
+
348
+
304
349
  def test_in_process_service_override_param():
305
350
  """Library callers pin strict behavior explicitly, DB row ignored."""
306
351
  import uuid as _uuid
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