pkg-auth 3.1.0__tar.gz → 3.2.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.
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/PKG-INFO +3 -3
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/pyproject.toml +3 -3
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/__init__.py +1 -1
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/__init__.py +5 -1
- pkg_auth-3.2.0/src/pkg_auth/authorization/application/use_cases/resolve_delegated_auth_context.py +169 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/domain/entities.py +7 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/domain/exceptions.py +36 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/auth_context_middleware.py +13 -1
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/__init__.py +4 -1
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/auth_context_dep.py +46 -6
- pkg_auth-3.2.0/src/pkg_auth/integrations/fastapi/current_user_dep.py +78 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/strawberry/auth.py +14 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/PKG-INFO +3 -3
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/SOURCES.txt +2 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/requires.txt +2 -2
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/README.md +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/setup.cfg +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/cli.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/client.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/env.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/helpers.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/provision_client.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/admin/settings.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/adapters/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/adapters/keycloak/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/adapters/keycloak/jwt_decoder.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/application/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/application/use_cases/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/application/use_cases/authenticate.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/domain/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/domain/entities.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/domain/exceptions.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/domain/ports.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/domain/value_objects.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/cache/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/cache/decorators.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/cache/memory.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/cache/protocol.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/cache/redis.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/apps.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/mixins.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/models.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/membership.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/organization.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/role.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/service.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/repositories/user.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/base.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/mixins.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/models.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/_helpers.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/check_permission.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/create_organization.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/create_role.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/delete_membership.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/delete_organization.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/delete_role.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/list_user_organizations.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/provision_default_services.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/register_permission_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/resolve_auth_context.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/set_organization_service.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/sync_permission_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/sync_service_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/update_organization.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/update_role.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/upsert_membership.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/cli/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/cli/sync_catalog.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/cli/sync_services.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/config.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/domain/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/domain/ports.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/domain/value_objects.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/platform.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/apps.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/decorators.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/install.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/middleware.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/auth_factory.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/decorators.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/errors.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/fastapi/identity_dep.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/strawberry/__init__.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/strawberry/permissions.py +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/dependency_links.txt +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/entry_points.txt +0 -0
- {pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pkg-auth
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Clean-architecture auth core for multiple Python frameworks
|
|
5
5
|
Author-email: Fritill <info@fritill.ae>
|
|
6
6
|
License: MIT
|
|
@@ -19,7 +19,7 @@ Requires-Dist: types-requests<3.0.0.0,>=2.32.0.0; extra == "dev"
|
|
|
19
19
|
Requires-Dist: types-PyJWT<2.0.0,>=1.7.0; extra == "dev"
|
|
20
20
|
Requires-Dist: testcontainers[postgres,redis]>=4.0; extra == "dev"
|
|
21
21
|
Provides-Extra: acl-sqlalchemy
|
|
22
|
-
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "acl-sqlalchemy"
|
|
22
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0; extra == "acl-sqlalchemy"
|
|
23
23
|
Requires-Dist: asyncpg>=0.29; extra == "acl-sqlalchemy"
|
|
24
24
|
Requires-Dist: alembic>=1.13; extra == "acl-sqlalchemy"
|
|
25
25
|
Provides-Extra: acl-django
|
|
@@ -41,7 +41,7 @@ Requires-Dist: starlette>=0.37; extra == "all"
|
|
|
41
41
|
Requires-Dist: django>=4.2; extra == "all"
|
|
42
42
|
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
|
|
43
43
|
Requires-Dist: strawberry-graphql>=0.255; extra == "all"
|
|
44
|
-
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "all"
|
|
44
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0; extra == "all"
|
|
45
45
|
Requires-Dist: asyncpg>=0.29; extra == "all"
|
|
46
46
|
Requires-Dist: alembic>=1.13; extra == "all"
|
|
47
47
|
Requires-Dist: redis>=5.0; extra == "all"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pkg-auth"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.2.0"
|
|
8
8
|
description = "Clean-architecture auth core for multiple Python frameworks"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -33,7 +33,7 @@ dev = [
|
|
|
33
33
|
|
|
34
34
|
# ACL storage backends (v1.0)
|
|
35
35
|
acl-sqlalchemy = [
|
|
36
|
-
"sqlalchemy>=2.0,<3.0",
|
|
36
|
+
"sqlalchemy[asyncio]>=2.0,<3.0",
|
|
37
37
|
"asyncpg>=0.29",
|
|
38
38
|
"alembic>=1.13",
|
|
39
39
|
]
|
|
@@ -67,7 +67,7 @@ all = [
|
|
|
67
67
|
"django>=4.2",
|
|
68
68
|
"psycopg[binary]>=3.1",
|
|
69
69
|
"strawberry-graphql>=0.255",
|
|
70
|
-
"sqlalchemy>=2.0,<3.0",
|
|
70
|
+
"sqlalchemy[asyncio]>=2.0,<3.0",
|
|
71
71
|
"asyncpg>=0.29",
|
|
72
72
|
"alembic>=1.13",
|
|
73
73
|
"redis>=5.0",
|
|
@@ -8,7 +8,7 @@ Public API:
|
|
|
8
8
|
MembershipRepository, PermissionCatalogRepository
|
|
9
9
|
Exceptions: AuthorizationError, NotAMember, MissingPermission,
|
|
10
10
|
UnknownOrganization, UnknownUser, UnknownRole,
|
|
11
|
-
UserNotProvisioned
|
|
11
|
+
UserNotProvisioned, DelegationRefused, DelegationRefusal
|
|
12
12
|
|
|
13
13
|
The application layer (use cases) is added in M3; SQLAlchemy / Django ORM
|
|
14
14
|
adapters in M4 / M6; cache layer in M5; framework integrations in M7-M9.
|
|
@@ -34,6 +34,8 @@ from .domain.entities import (
|
|
|
34
34
|
from .platform import is_platform_context
|
|
35
35
|
from .domain.exceptions import (
|
|
36
36
|
AuthorizationError,
|
|
37
|
+
DelegationRefusal,
|
|
38
|
+
DelegationRefused,
|
|
37
39
|
MissingPermission,
|
|
38
40
|
NotAMember,
|
|
39
41
|
PermissionVisibilityConflict,
|
|
@@ -118,4 +120,6 @@ __all__ = [
|
|
|
118
120
|
"ServiceNotEnabled",
|
|
119
121
|
"PermissionVisibilityConflict",
|
|
120
122
|
"UserNotProvisioned",
|
|
123
|
+
"DelegationRefused",
|
|
124
|
+
"DelegationRefusal",
|
|
121
125
|
]
|
pkg_auth-3.2.0/src/pkg_auth/authorization/application/use_cases/resolve_delegated_auth_context.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Resolve a delegated (on-behalf-of) :class:`AuthContext` (``auth:AD-1``)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from ...domain.entities import AuthContext, Organization, User
|
|
11
|
+
from ...domain.exceptions import (
|
|
12
|
+
DelegationRefusal,
|
|
13
|
+
DelegationRefused,
|
|
14
|
+
NotAMember,
|
|
15
|
+
)
|
|
16
|
+
from ...domain.ports import OrganizationRepository, UserRepository
|
|
17
|
+
from ...domain.value_objects import OrgId
|
|
18
|
+
from .resolve_auth_context import ResolveAuthContextUseCase
|
|
19
|
+
|
|
20
|
+
Delegators = Mapping[str, frozenset[OrgId]]
|
|
21
|
+
|
|
22
|
+
ON_BEHALF_OF_HEADER = "X-On-Behalf-Of"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def find_organization(
|
|
26
|
+
repo: OrganizationRepository, raw: str
|
|
27
|
+
) -> Organization | None:
|
|
28
|
+
"""Look up an org from a raw ``X-Organization-Id`` value.
|
|
29
|
+
|
|
30
|
+
Tries ``raw`` as a UUID first and falls back to a slug lookup. The one
|
|
31
|
+
lookup rule shared by the FastAPI dependency and the delegated use case,
|
|
32
|
+
so the two cannot drift.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
org_id = OrgId(UUID(raw))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return await repo.get_by_slug(raw)
|
|
38
|
+
return await repo.get(org_id)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _to_org_id(raw: Any, sub: str) -> OrgId:
|
|
42
|
+
if isinstance(raw, OrgId):
|
|
43
|
+
return raw
|
|
44
|
+
if isinstance(raw, UUID):
|
|
45
|
+
return OrgId(raw)
|
|
46
|
+
if isinstance(raw, str):
|
|
47
|
+
try:
|
|
48
|
+
return OrgId(UUID(raw))
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"delegators[{sub!r}]: organization {raw!r} is not a UUID"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_delegators(
|
|
57
|
+
raw: Mapping[str, Iterable[Any]] | None,
|
|
58
|
+
) -> MappingProxyType[str, frozenset[OrgId]]:
|
|
59
|
+
"""Normalize a ``delegators`` config into an immutable allowlist.
|
|
60
|
+
|
|
61
|
+
Keys are Keycloak service-account ``sub`` strings, kept exactly as
|
|
62
|
+
given. Values are iterables of org ids (``OrgId``, ``UUID`` or UUID
|
|
63
|
+
strings). ``None`` or an empty mapping yields an empty map, so nobody
|
|
64
|
+
delegates (fail closed); a ``sub`` with no orgs is dropped, so it is
|
|
65
|
+
refused as not a delegator before any lookup.
|
|
66
|
+
|
|
67
|
+
Raises :class:`ValueError` when ``raw`` is a bare ``str`` or not a
|
|
68
|
+
mapping, a ``sub`` is not a non-blank, unpadded ``str``, a value is a bare
|
|
69
|
+
``str`` or not iterable, or an org is not a UUID.
|
|
70
|
+
"""
|
|
71
|
+
if raw is None:
|
|
72
|
+
return MappingProxyType({})
|
|
73
|
+
if isinstance(raw, (str, bytes)) or not isinstance(raw, Mapping):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"delegators must be a mapping of sub -> org ids, got {type(raw).__name__}"
|
|
76
|
+
)
|
|
77
|
+
normalized: dict[str, frozenset[OrgId]] = {}
|
|
78
|
+
for sub, orgs in raw.items():
|
|
79
|
+
if not isinstance(sub, str) or not sub.strip() or sub != sub.strip():
|
|
80
|
+
raise ValueError(f"delegators: blank, padded or non-string sub {sub!r}")
|
|
81
|
+
if isinstance(orgs, (str, bytes)):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"delegators[{sub!r}] must be a collection of org ids, not a bare string"
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
items = list(orgs)
|
|
87
|
+
except TypeError as exc:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"delegators[{sub!r}] must be a collection of org ids"
|
|
90
|
+
) from exc
|
|
91
|
+
org_ids = frozenset(_to_org_id(o, sub) for o in items)
|
|
92
|
+
if org_ids: # an agent with no orgs is not a delegator at all
|
|
93
|
+
normalized[sub] = org_ids
|
|
94
|
+
return MappingProxyType(normalized)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(slots=True)
|
|
98
|
+
class ResolveDelegatedAuthContextUseCase:
|
|
99
|
+
"""Decide a delegated request and return the target's :class:`AuthContext`.
|
|
100
|
+
|
|
101
|
+
The single framework-free place that holds ``delegators`` (``auth:AD-1``
|
|
102
|
+
Placement). Integrations pass the raw ``X-On-Behalf-Of`` values and the
|
|
103
|
+
raw ``X-Organization-Id`` value; they never see the allowlist.
|
|
104
|
+
|
|
105
|
+
Checks run in ``auth:AD-1``'s order (rows 3–9), first match wins, and
|
|
106
|
+
every refusal is a :class:`DelegationRefused`. The target is looked up
|
|
107
|
+
read-only through ``UserRepository.get_by_keycloak_sub`` — delegation
|
|
108
|
+
never creates a user. On success the result is the target's own
|
|
109
|
+
context from ``resolve_use_case`` (same service guard, same cache
|
|
110
|
+
entry) with ``acted_by`` attached afterwards; the caller's perms are
|
|
111
|
+
never used.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
resolve_use_case: ResolveAuthContextUseCase
|
|
115
|
+
user_repo: UserRepository
|
|
116
|
+
organization_repo: OrganizationRepository
|
|
117
|
+
delegators: Delegators | None = field(default=None)
|
|
118
|
+
|
|
119
|
+
def __post_init__(self) -> None:
|
|
120
|
+
self.delegators = normalize_delegators(self.delegators)
|
|
121
|
+
|
|
122
|
+
async def execute(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
caller: User,
|
|
126
|
+
on_behalf_of: Sequence[str],
|
|
127
|
+
organization: str,
|
|
128
|
+
) -> AuthContext:
|
|
129
|
+
if isinstance(on_behalf_of, (str, bytes)):
|
|
130
|
+
raise TypeError("on_behalf_of must be the header's list of values")
|
|
131
|
+
if len(on_behalf_of) == 0:
|
|
132
|
+
raise ValueError("on_behalf_of is empty; only call when the header is present")
|
|
133
|
+
|
|
134
|
+
# Row 3: repeated header.
|
|
135
|
+
if len(on_behalf_of) > 1:
|
|
136
|
+
raise DelegationRefused(DelegationRefusal.REPEATED)
|
|
137
|
+
|
|
138
|
+
# Row 4: caller not a delegator — before any lookup.
|
|
139
|
+
assert self.delegators is not None
|
|
140
|
+
allowed_orgs = self.delegators.get(caller.keycloak_sub)
|
|
141
|
+
if allowed_orgs is None:
|
|
142
|
+
raise DelegationRefused(DelegationRefusal.NOT_A_DELEGATOR)
|
|
143
|
+
|
|
144
|
+
# Row 5: empty value.
|
|
145
|
+
target_sub = on_behalf_of[0]
|
|
146
|
+
if not target_sub.strip():
|
|
147
|
+
raise DelegationRefused(DelegationRefusal.EMPTY_TARGET)
|
|
148
|
+
|
|
149
|
+
# Row 6: unknown org (UUID first, then slug).
|
|
150
|
+
org = await find_organization(self.organization_repo, organization)
|
|
151
|
+
if org is None:
|
|
152
|
+
raise DelegationRefused(DelegationRefusal.UNKNOWN_ORGANIZATION)
|
|
153
|
+
|
|
154
|
+
# Row 7: org not on this agent's list (no platform-org bypass).
|
|
155
|
+
if org.id not in allowed_orgs:
|
|
156
|
+
raise DelegationRefused(DelegationRefusal.ORGANIZATION_NOT_ALLOWED)
|
|
157
|
+
|
|
158
|
+
# Row 8: target not provisioned — read-only lookup, never upsert.
|
|
159
|
+
target = await self.user_repo.get_by_keycloak_sub(target_sub)
|
|
160
|
+
if target is None:
|
|
161
|
+
raise DelegationRefused(DelegationRefusal.TARGET_NOT_PROVISIONED)
|
|
162
|
+
|
|
163
|
+
# Row 9: target not a member.
|
|
164
|
+
try:
|
|
165
|
+
ctx = await self.resolve_use_case.execute(target.id, org.id)
|
|
166
|
+
except NotAMember as exc:
|
|
167
|
+
raise DelegationRefused(DelegationRefusal.TARGET_NOT_A_MEMBER) from exc
|
|
168
|
+
|
|
169
|
+
return replace(ctx, acted_by=caller.id)
|
|
@@ -163,12 +163,19 @@ class AuthContext:
|
|
|
163
163
|
services cache their platform org's id at startup and call
|
|
164
164
|
:func:`pkg_auth.authorization.is_platform_context` to compare against
|
|
165
165
|
``self.organization_id``. See the package docs for the pattern.
|
|
166
|
+
|
|
167
|
+
``acted_by`` is **attribution only**. On a delegated (on-behalf-of)
|
|
168
|
+
request ``user_id`` is the target user — the acting principal for
|
|
169
|
+
authorization and ownership — and ``acted_by`` is the caller's ACL id;
|
|
170
|
+
it is ``None`` otherwise. It never grants or removes a permission, and
|
|
171
|
+
the cache serializer deliberately excludes it (``auth:AD-1``).
|
|
166
172
|
"""
|
|
167
173
|
|
|
168
174
|
user_id: UserId
|
|
169
175
|
organization_id: OrgId
|
|
170
176
|
role_names: frozenset[str]
|
|
171
177
|
perms: frozenset[str]
|
|
178
|
+
acted_by: UserId | None = None
|
|
172
179
|
|
|
173
180
|
def has(self, perm: str) -> bool:
|
|
174
181
|
"""Return ``True`` if any of the user's roles grant ``perm``."""
|
|
@@ -7,6 +7,8 @@ each to the right HTTP status.
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class AuthorizationError(Exception):
|
|
12
14
|
"""Base for all authorization (ACL) failures."""
|
|
@@ -66,3 +68,37 @@ class ServiceNotSaaSAvailable(AuthorizationError):
|
|
|
66
68
|
|
|
67
69
|
class ServiceNotEnabled(AuthorizationError):
|
|
68
70
|
"""The organization does not have the required service enabled."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DelegationRefusal(str, Enum):
|
|
74
|
+
"""Why a delegated (on-behalf-of) request was refused.
|
|
75
|
+
|
|
76
|
+
One member per row 3–9 of ``auth:AD-1``'s order of checks. Integration
|
|
77
|
+
layers map ``REPEATED`` and ``EMPTY_TARGET`` to 400,
|
|
78
|
+
``UNKNOWN_ORGANIZATION`` to 404, and every other reason to 403.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
REPEATED = "repeated"
|
|
82
|
+
NOT_A_DELEGATOR = "not_a_delegator"
|
|
83
|
+
EMPTY_TARGET = "empty_target"
|
|
84
|
+
UNKNOWN_ORGANIZATION = "unknown_organization"
|
|
85
|
+
ORGANIZATION_NOT_ALLOWED = "organization_not_allowed"
|
|
86
|
+
TARGET_NOT_PROVISIONED = "target_not_provisioned"
|
|
87
|
+
TARGET_NOT_A_MEMBER = "target_not_a_member"
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return self.value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DelegationRefused(AuthorizationError):
|
|
94
|
+
"""A delegated request was refused; ``reason`` says which check failed.
|
|
95
|
+
|
|
96
|
+
The only error :class:`ResolveDelegatedAuthContextUseCase` raises for a
|
|
97
|
+
refusal — never :class:`NotAMember` or :class:`UserNotProvisioned`, so
|
|
98
|
+
no integration can mistake a refused delegation for the caller's own
|
|
99
|
+
failure and fall back to the caller's context.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, reason: DelegationRefusal, detail: str | None = None) -> None:
|
|
103
|
+
self.reason = reason
|
|
104
|
+
super().__init__(detail or f"delegation refused: {reason.value}")
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/integrations/django/auth_context_middleware.py
RENAMED
|
@@ -13,6 +13,9 @@ from ...authorization import (
|
|
|
13
13
|
UnknownOrganization,
|
|
14
14
|
UserNotProvisioned,
|
|
15
15
|
)
|
|
16
|
+
from ...authorization.application.use_cases.resolve_delegated_auth_context import (
|
|
17
|
+
ON_BEHALF_OF_HEADER,
|
|
18
|
+
)
|
|
16
19
|
from ...authorization.domain.entities import User
|
|
17
20
|
from .install import get_registry
|
|
18
21
|
|
|
@@ -23,6 +26,9 @@ class AuthContextMiddleware:
|
|
|
23
26
|
Requires ``IdentityMiddleware`` to run first so ``request.identity``
|
|
24
27
|
is populated. Behavior:
|
|
25
28
|
|
|
29
|
+
- ``X-On-Behalf-Of`` present (any value, with or without the org
|
|
30
|
+
header or an identity) → 403; this middleware cannot delegate
|
|
31
|
+
(``auth:AD-1``)
|
|
26
32
|
- no header → ``request.auth_context = None`` (route decides)
|
|
27
33
|
- identity is None and header present → 401
|
|
28
34
|
- org not found → 404
|
|
@@ -53,8 +59,14 @@ class AuthContextMiddleware:
|
|
|
53
59
|
)
|
|
54
60
|
|
|
55
61
|
async def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
56
|
-
registry = get_registry()
|
|
57
62
|
request.auth_context = None # type: ignore[attr-defined]
|
|
63
|
+
if request.headers.get(ON_BEHALF_OF_HEADER) is not None:
|
|
64
|
+
return JsonResponse(
|
|
65
|
+
{"detail": f"{ON_BEHALF_OF_HEADER} is not accepted here"},
|
|
66
|
+
status=403,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
registry = get_registry()
|
|
58
70
|
|
|
59
71
|
raw = request.headers.get(registry.header_name)
|
|
60
72
|
if raw is None:
|
|
@@ -10,7 +10,8 @@ except ImportError as exc: # pragma: no cover
|
|
|
10
10
|
) from exc
|
|
11
11
|
|
|
12
12
|
from .auth_factory import Authentication, create_authentication
|
|
13
|
-
from .auth_context_dep import make_get_auth_context
|
|
13
|
+
from .auth_context_dep import ON_BEHALF_OF_HEADER, make_get_auth_context
|
|
14
|
+
from .current_user_dep import make_get_current_user
|
|
14
15
|
from .decorators import require_permission
|
|
15
16
|
from .errors import install_exception_handlers
|
|
16
17
|
from .identity_dep import bearer_scheme, extract_token_from_request
|
|
@@ -19,6 +20,8 @@ __all__ = [
|
|
|
19
20
|
"Authentication",
|
|
20
21
|
"create_authentication",
|
|
21
22
|
"make_get_auth_context",
|
|
23
|
+
"make_get_current_user",
|
|
24
|
+
"ON_BEHALF_OF_HEADER",
|
|
22
25
|
"require_permission",
|
|
23
26
|
"install_exception_handlers",
|
|
24
27
|
"bearer_scheme",
|
|
@@ -31,21 +31,26 @@ pattern.
|
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
33
|
from typing import Awaitable, Callable
|
|
34
|
-
from uuid import UUID
|
|
35
34
|
|
|
36
35
|
from fastapi import Depends, HTTPException, Request, status
|
|
37
36
|
|
|
38
37
|
from ...authentication import IdentityContext
|
|
39
38
|
from ...authorization import (
|
|
40
39
|
AuthContext,
|
|
40
|
+
DelegationRefusal,
|
|
41
|
+
DelegationRefused,
|
|
41
42
|
NotAMember,
|
|
42
|
-
OrgId,
|
|
43
43
|
UnknownOrganization,
|
|
44
44
|
UserNotProvisioned,
|
|
45
45
|
)
|
|
46
46
|
from ...authorization.application.use_cases.resolve_auth_context import (
|
|
47
47
|
ResolveAuthContextUseCase,
|
|
48
48
|
)
|
|
49
|
+
from ...authorization.application.use_cases.resolve_delegated_auth_context import (
|
|
50
|
+
ON_BEHALF_OF_HEADER,
|
|
51
|
+
ResolveDelegatedAuthContextUseCase,
|
|
52
|
+
find_organization,
|
|
53
|
+
)
|
|
49
54
|
from ...authorization.application.use_cases.resolve_user_from_jwt import (
|
|
50
55
|
ResolveUserFromJwtUseCase,
|
|
51
56
|
)
|
|
@@ -57,6 +62,13 @@ from ...authorization.domain.ports import OrganizationRepository
|
|
|
57
62
|
|
|
58
63
|
DEFAULT_HEADER_NAME = "X-Organization-Id"
|
|
59
64
|
|
|
65
|
+
# Unlisted reasons fall through to 403, so a new reason fails closed.
|
|
66
|
+
_REFUSAL_STATUS: dict[DelegationRefusal, int] = {
|
|
67
|
+
DelegationRefusal.REPEATED: status.HTTP_400_BAD_REQUEST,
|
|
68
|
+
DelegationRefusal.EMPTY_TARGET: status.HTTP_400_BAD_REQUEST,
|
|
69
|
+
DelegationRefusal.UNKNOWN_ORGANIZATION: status.HTTP_404_NOT_FOUND,
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
|
|
61
73
|
def make_get_auth_context(
|
|
62
74
|
*,
|
|
@@ -66,6 +78,7 @@ def make_get_auth_context(
|
|
|
66
78
|
sync_user_use_case: SyncUserFromJwtUseCase | None = None,
|
|
67
79
|
resolve_user_use_case: ResolveUserFromJwtUseCase | None = None,
|
|
68
80
|
header_name: str = DEFAULT_HEADER_NAME,
|
|
81
|
+
delegation: ResolveDelegatedAuthContextUseCase | None = None,
|
|
69
82
|
) -> Callable[..., Awaitable[tuple[IdentityContext, AuthContext]]]:
|
|
70
83
|
"""Build a FastAPI dependency returning ``(IdentityContext, AuthContext)``.
|
|
71
84
|
|
|
@@ -80,11 +93,21 @@ def make_get_auth_context(
|
|
|
80
93
|
UUID first, falling back to a slug lookup.
|
|
81
94
|
4. Resolves the user's auth context for that organization.
|
|
82
95
|
|
|
96
|
+
If the request carries ``X-On-Behalf-Of`` (checked only after steps 1
|
|
97
|
+
and 2, ``auth:AD-1``), steps 3–4 are replaced by ``delegation``: the
|
|
98
|
+
result is ``(caller's IdentityContext, target's AuthContext)`` with
|
|
99
|
+
``acted_by`` set to the caller's ACL id. Without ``delegation`` the
|
|
100
|
+
header is refused with 403. A refusal never falls back to the
|
|
101
|
+
caller's own context.
|
|
102
|
+
|
|
83
103
|
Errors map to:
|
|
84
104
|
- missing header → 400
|
|
85
105
|
- unknown organization → 404
|
|
86
106
|
- user not a member → 403
|
|
87
107
|
- user not provisioned (Mode B) → 403
|
|
108
|
+
- ``X-On-Behalf-Of`` without ``delegation`` → 403
|
|
109
|
+
- delegation refused: repeated header or empty value → 400,
|
|
110
|
+
unknown organization → 404, any other reason → 403
|
|
88
111
|
"""
|
|
89
112
|
if (sync_user_use_case is None) == (resolve_user_use_case is None):
|
|
90
113
|
raise ValueError(
|
|
@@ -122,10 +145,27 @@ def make_get_auth_context(
|
|
|
122
145
|
detail=str(exc),
|
|
123
146
|
) from exc
|
|
124
147
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
on_behalf_of = request.headers.getlist(ON_BEHALF_OF_HEADER)
|
|
149
|
+
if on_behalf_of:
|
|
150
|
+
if delegation is None:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
153
|
+
detail=f"{ON_BEHALF_OF_HEADER} is not accepted here",
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
auth_ctx = await delegation.execute(
|
|
157
|
+
caller=user, on_behalf_of=on_behalf_of, organization=raw
|
|
158
|
+
)
|
|
159
|
+
except DelegationRefused as exc:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=_REFUSAL_STATUS.get(
|
|
162
|
+
exc.reason, status.HTTP_403_FORBIDDEN
|
|
163
|
+
),
|
|
164
|
+
detail=str(exc),
|
|
165
|
+
) from exc
|
|
166
|
+
return identity, auth_ctx
|
|
167
|
+
|
|
168
|
+
org = await find_organization(organization_repo, raw)
|
|
129
169
|
if org is None:
|
|
130
170
|
raise HTTPException(
|
|
131
171
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""``make_get_current_user`` factory — the org-less ACL user dependency.
|
|
2
|
+
|
|
3
|
+
Resolves the caller's ACL :class:`User` from the token alone, with no
|
|
4
|
+
``X-Organization-Id``. It cannot delegate, so it refuses ``X-On-Behalf-Of``
|
|
5
|
+
with 403 before any user work (``auth:AD-1`` Coverage).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Awaitable, Callable
|
|
10
|
+
|
|
11
|
+
from fastapi import Depends, HTTPException, Request, status
|
|
12
|
+
|
|
13
|
+
from ...authentication import IdentityContext
|
|
14
|
+
from ...authorization import UserNotProvisioned
|
|
15
|
+
from ...authorization.application.use_cases.resolve_delegated_auth_context import (
|
|
16
|
+
ON_BEHALF_OF_HEADER,
|
|
17
|
+
)
|
|
18
|
+
from ...authorization.application.use_cases.resolve_user_from_jwt import (
|
|
19
|
+
ResolveUserFromJwtUseCase,
|
|
20
|
+
)
|
|
21
|
+
from ...authorization.application.use_cases.sync_user_from_jwt import (
|
|
22
|
+
SyncUserFromJwtUseCase,
|
|
23
|
+
)
|
|
24
|
+
from ...authorization.domain.entities import User
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _refuse_on_behalf_of(request: Request) -> None:
|
|
28
|
+
# Declared before ``get_identity`` so FastAPI resolves it first: the
|
|
29
|
+
# header is refused even without a valid token.
|
|
30
|
+
if request.headers.getlist(ON_BEHALF_OF_HEADER):
|
|
31
|
+
raise HTTPException(
|
|
32
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
33
|
+
detail=f"{ON_BEHALF_OF_HEADER} is not accepted here",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_get_current_user(
|
|
38
|
+
*,
|
|
39
|
+
get_identity: Callable[..., Awaitable[IdentityContext]],
|
|
40
|
+
sync_user_use_case: SyncUserFromJwtUseCase | None = None,
|
|
41
|
+
resolve_user_use_case: ResolveUserFromJwtUseCase | None = None,
|
|
42
|
+
) -> Callable[..., Awaitable[User]]:
|
|
43
|
+
"""Build a FastAPI dependency returning the caller's ACL ``User``.
|
|
44
|
+
|
|
45
|
+
Exactly one of ``sync_user_use_case`` (Mode A — upserts the caller)
|
|
46
|
+
or ``resolve_user_use_case`` (Mode B — read-through) must be supplied.
|
|
47
|
+
|
|
48
|
+
Errors map to:
|
|
49
|
+
- ``X-On-Behalf-Of`` present (any value, any token) → 403
|
|
50
|
+
- no or invalid token → 401 (from ``get_identity``)
|
|
51
|
+
- user not provisioned (Mode B) → 403
|
|
52
|
+
"""
|
|
53
|
+
if (sync_user_use_case is None) == (resolve_user_use_case is None):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"make_get_current_user: pass exactly one of "
|
|
56
|
+
"sync_user_use_case (Mode A) or resolve_user_use_case (Mode B)."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def dependency(
|
|
60
|
+
_: None = Depends(_refuse_on_behalf_of),
|
|
61
|
+
identity: IdentityContext = Depends(get_identity),
|
|
62
|
+
) -> User:
|
|
63
|
+
try:
|
|
64
|
+
if sync_user_use_case is not None:
|
|
65
|
+
return await sync_user_use_case.execute(
|
|
66
|
+
sub=identity.subject_str,
|
|
67
|
+
email=identity.email_str or "",
|
|
68
|
+
full_name=identity.full_name,
|
|
69
|
+
)
|
|
70
|
+
assert resolve_user_use_case is not None
|
|
71
|
+
return await resolve_user_use_case.execute(sub=identity.subject_str)
|
|
72
|
+
except UserNotProvisioned as exc:
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
75
|
+
detail=str(exc),
|
|
76
|
+
) from exc
|
|
77
|
+
|
|
78
|
+
return dependency
|
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from typing import Awaitable, Callable
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
|
+
from starlette.exceptions import HTTPException
|
|
8
9
|
from starlette.requests import Request
|
|
9
10
|
|
|
10
11
|
from ...authentication import (
|
|
@@ -23,6 +24,9 @@ from ...authorization import (
|
|
|
23
24
|
from ...authorization.application.use_cases.resolve_auth_context import (
|
|
24
25
|
ResolveAuthContextUseCase,
|
|
25
26
|
)
|
|
27
|
+
from ...authorization.application.use_cases.resolve_delegated_auth_context import (
|
|
28
|
+
ON_BEHALF_OF_HEADER,
|
|
29
|
+
)
|
|
26
30
|
from ...authorization.application.use_cases.resolve_user_from_jwt import (
|
|
27
31
|
ResolveUserFromJwtUseCase,
|
|
28
32
|
)
|
|
@@ -80,6 +84,10 @@ def make_context_getter(
|
|
|
80
84
|
rather than raising. Permission classes (``IsAuthenticated``,
|
|
81
85
|
``RequirePermission``) are responsible for rejecting under-privileged
|
|
82
86
|
queries.
|
|
87
|
+
|
|
88
|
+
The one non-permissive case is ``X-On-Behalf-Of``: this integration
|
|
89
|
+
cannot delegate, so a request carrying it (any value, any token
|
|
90
|
+
state) is refused with HTTP 403 before any user work (``auth:AD-1``).
|
|
83
91
|
"""
|
|
84
92
|
if (sync_user_use_case is None) == (resolve_user_use_case is None):
|
|
85
93
|
raise ValueError(
|
|
@@ -88,6 +96,12 @@ def make_context_getter(
|
|
|
88
96
|
)
|
|
89
97
|
|
|
90
98
|
async def _context_getter(request: Request) -> StrawberryContext:
|
|
99
|
+
if request.headers.getlist(ON_BEHALF_OF_HEADER):
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=403,
|
|
102
|
+
detail=f"{ON_BEHALF_OF_HEADER} is not accepted here",
|
|
103
|
+
)
|
|
104
|
+
|
|
91
105
|
ctx = StrawberryContext(request=request)
|
|
92
106
|
|
|
93
107
|
token = _extract_token(request, cookie_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pkg-auth
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Clean-architecture auth core for multiple Python frameworks
|
|
5
5
|
Author-email: Fritill <info@fritill.ae>
|
|
6
6
|
License: MIT
|
|
@@ -19,7 +19,7 @@ Requires-Dist: types-requests<3.0.0.0,>=2.32.0.0; extra == "dev"
|
|
|
19
19
|
Requires-Dist: types-PyJWT<2.0.0,>=1.7.0; extra == "dev"
|
|
20
20
|
Requires-Dist: testcontainers[postgres,redis]>=4.0; extra == "dev"
|
|
21
21
|
Provides-Extra: acl-sqlalchemy
|
|
22
|
-
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "acl-sqlalchemy"
|
|
22
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0; extra == "acl-sqlalchemy"
|
|
23
23
|
Requires-Dist: asyncpg>=0.29; extra == "acl-sqlalchemy"
|
|
24
24
|
Requires-Dist: alembic>=1.13; extra == "acl-sqlalchemy"
|
|
25
25
|
Provides-Extra: acl-django
|
|
@@ -41,7 +41,7 @@ Requires-Dist: starlette>=0.37; extra == "all"
|
|
|
41
41
|
Requires-Dist: django>=4.2; extra == "all"
|
|
42
42
|
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
|
|
43
43
|
Requires-Dist: strawberry-graphql>=0.255; extra == "all"
|
|
44
|
-
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "all"
|
|
44
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0; extra == "all"
|
|
45
45
|
Requires-Dist: asyncpg>=0.29; extra == "all"
|
|
46
46
|
Requires-Dist: alembic>=1.13; extra == "all"
|
|
47
47
|
Requires-Dist: redis>=5.0; extra == "all"
|
|
@@ -79,6 +79,7 @@ src/pkg_auth/authorization/application/use_cases/list_user_organizations.py
|
|
|
79
79
|
src/pkg_auth/authorization/application/use_cases/provision_default_services.py
|
|
80
80
|
src/pkg_auth/authorization/application/use_cases/register_permission_catalog.py
|
|
81
81
|
src/pkg_auth/authorization/application/use_cases/resolve_auth_context.py
|
|
82
|
+
src/pkg_auth/authorization/application/use_cases/resolve_delegated_auth_context.py
|
|
82
83
|
src/pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py
|
|
83
84
|
src/pkg_auth/authorization/application/use_cases/set_organization_service.py
|
|
84
85
|
src/pkg_auth/authorization/application/use_cases/sync_permission_catalog.py
|
|
@@ -105,6 +106,7 @@ src/pkg_auth/integrations/django/middleware.py
|
|
|
105
106
|
src/pkg_auth/integrations/fastapi/__init__.py
|
|
106
107
|
src/pkg_auth/integrations/fastapi/auth_context_dep.py
|
|
107
108
|
src/pkg_auth/integrations/fastapi/auth_factory.py
|
|
109
|
+
src/pkg_auth/integrations/fastapi/current_user_dep.py
|
|
108
110
|
src/pkg_auth/integrations/fastapi/decorators.py
|
|
109
111
|
src/pkg_auth/integrations/fastapi/errors.py
|
|
110
112
|
src/pkg_auth/integrations/fastapi/identity_dep.py
|
|
@@ -7,7 +7,7 @@ django>=4.2
|
|
|
7
7
|
psycopg[binary]>=3.1
|
|
8
8
|
|
|
9
9
|
[acl-sqlalchemy]
|
|
10
|
-
sqlalchemy<3.0,>=2.0
|
|
10
|
+
sqlalchemy[asyncio]<3.0,>=2.0
|
|
11
11
|
asyncpg>=0.29
|
|
12
12
|
alembic>=1.13
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ starlette>=0.37
|
|
|
18
18
|
django>=4.2
|
|
19
19
|
psycopg[binary]>=3.1
|
|
20
20
|
strawberry-graphql>=0.255
|
|
21
|
-
sqlalchemy<3.0,>=2.0
|
|
21
|
+
sqlalchemy[asyncio]<3.0,>=2.0
|
|
22
22
|
asyncpg>=0.29
|
|
23
23
|
alembic>=1.13
|
|
24
24
|
redis>=5.0
|
|
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
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/adapters/keycloak/jwt_decoder.py
RENAMED
|
File without changes
|
|
File without changes
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/application/use_cases/__init__.py
RENAMED
|
File without changes
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authentication/application/use_cases/authenticate.py
RENAMED
|
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
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/django_orm/__init__.py
RENAMED
|
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
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/adapters/sqlalchemy/__init__.py
RENAMED
|
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
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/__init__.py
RENAMED
|
File without changes
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/create_role.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/delete_role.py
RENAMED
|
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
|
{pkg_auth-3.1.0 → pkg_auth-3.2.0}/src/pkg_auth/authorization/application/use_cases/update_role.py
RENAMED
|
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
|