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.
- {auth-2.5.1 → auth-2.5.2}/PKG-INFO +5 -2
- {auth-2.5.1 → auth-2.5.2}/README.md +4 -1
- {auth-2.5.1 → auth-2.5.2}/auth/__init__.py +36 -3
- {auth-2.5.1 → auth-2.5.2}/auth/database.py +25 -4
- {auth-2.5.1 → auth-2.5.2}/auth/docs_page.py +6 -1
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/PKG-INFO +5 -2
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/SOURCES.txt +1 -0
- {auth-2.5.1 → auth-2.5.2}/pyproject.toml +1 -1
- auth-2.5.2/tests/test_database_sslmode.py +47 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_strict_users.py +45 -0
- {auth-2.5.1 → auth-2.5.2}/LICENSE +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/api_keys.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/audit.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/circuit_breaker.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/client.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/cmd/__init__.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/cmd/server.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/config.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/core/REST/__init__.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/core/REST/client.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/core/__init__.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/core/models/__init__.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/decorators.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/encryption.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/logging_config.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/main.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/models/sql.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/response_format.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/routes.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/sanitizer.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/server.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/services/service.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/validation.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth/workflow_checker.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/dependency_links.txt +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/entry_points.txt +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/requires.txt +0 -0
- {auth-2.5.1 → auth-2.5.2}/auth.egg-info/top_level.txt +0 -0
- {auth-2.5.1 → auth-2.5.2}/setup.cfg +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_api_keys.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_api_keys_encryption.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_audit_integrity.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_client.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_client_rest.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_cmd_server.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_config.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_correctness_hardening.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_docs_page.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_encryption.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_encryption_integration.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_flask.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_key_rotation.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_log_redaction.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_phase_a_hardening.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_reencryption.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_routes_errors.py +0 -0
- {auth-2.5.1 → auth-2.5.2}/tests/test_server.py +0 -0
- {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.
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
if
|
|
91
|
-
connect_args["sslmode"] =
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|