pkg-auth 3.0.0__py3-none-any.whl

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 (110) hide show
  1. pkg_auth/__init__.py +15 -0
  2. pkg_auth/admin/__init__.py +35 -0
  3. pkg_auth/admin/cli.py +87 -0
  4. pkg_auth/admin/client.py +401 -0
  5. pkg_auth/admin/env.py +74 -0
  6. pkg_auth/admin/helpers.py +113 -0
  7. pkg_auth/admin/provision_client.py +86 -0
  8. pkg_auth/admin/settings.py +33 -0
  9. pkg_auth/authentication/__init__.py +33 -0
  10. pkg_auth/authentication/adapters/__init__.py +1 -0
  11. pkg_auth/authentication/adapters/keycloak/__init__.py +6 -0
  12. pkg_auth/authentication/adapters/keycloak/jwt_decoder.py +105 -0
  13. pkg_auth/authentication/application/__init__.py +1 -0
  14. pkg_auth/authentication/application/use_cases/__init__.py +1 -0
  15. pkg_auth/authentication/application/use_cases/authenticate.py +91 -0
  16. pkg_auth/authentication/domain/__init__.py +1 -0
  17. pkg_auth/authentication/domain/entities.py +50 -0
  18. pkg_auth/authentication/domain/exceptions.py +18 -0
  19. pkg_auth/authentication/domain/ports.py +26 -0
  20. pkg_auth/authentication/domain/value_objects.py +42 -0
  21. pkg_auth/authorization/__init__.py +117 -0
  22. pkg_auth/authorization/adapters/__init__.py +1 -0
  23. pkg_auth/authorization/adapters/cache/__init__.py +32 -0
  24. pkg_auth/authorization/adapters/cache/decorators.py +181 -0
  25. pkg_auth/authorization/adapters/cache/memory.py +61 -0
  26. pkg_auth/authorization/adapters/cache/protocol.py +36 -0
  27. pkg_auth/authorization/adapters/cache/redis.py +60 -0
  28. pkg_auth/authorization/adapters/django_orm/__init__.py +37 -0
  29. pkg_auth/authorization/adapters/django_orm/apps.py +24 -0
  30. pkg_auth/authorization/adapters/django_orm/mixins.py +142 -0
  31. pkg_auth/authorization/adapters/django_orm/models.py +226 -0
  32. pkg_auth/authorization/adapters/django_orm/repositories/__init__.py +20 -0
  33. pkg_auth/authorization/adapters/django_orm/repositories/membership.py +118 -0
  34. pkg_auth/authorization/adapters/django_orm/repositories/organization.py +73 -0
  35. pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py +71 -0
  36. pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py +102 -0
  37. pkg_auth/authorization/adapters/django_orm/repositories/role.py +120 -0
  38. pkg_auth/authorization/adapters/django_orm/repositories/service.py +60 -0
  39. pkg_auth/authorization/adapters/django_orm/repositories/user.py +77 -0
  40. pkg_auth/authorization/adapters/sqlalchemy/__init__.py +90 -0
  41. pkg_auth/authorization/adapters/sqlalchemy/base.py +55 -0
  42. pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py +1 -0
  43. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py +293 -0
  44. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py +39 -0
  45. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py +65 -0
  46. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py +52 -0
  47. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py +116 -0
  48. pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py +1 -0
  49. pkg_auth/authorization/adapters/sqlalchemy/mixins.py +187 -0
  50. pkg_auth/authorization/adapters/sqlalchemy/models.py +268 -0
  51. pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py +16 -0
  52. pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py +146 -0
  53. pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py +97 -0
  54. pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py +106 -0
  55. pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py +127 -0
  56. pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py +171 -0
  57. pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py +93 -0
  58. pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py +74 -0
  59. pkg_auth/authorization/application/__init__.py +1 -0
  60. pkg_auth/authorization/application/use_cases/__init__.py +1 -0
  61. pkg_auth/authorization/application/use_cases/_helpers.py +82 -0
  62. pkg_auth/authorization/application/use_cases/check_permission.py +21 -0
  63. pkg_auth/authorization/application/use_cases/create_organization.py +41 -0
  64. pkg_auth/authorization/application/use_cases/create_role.py +69 -0
  65. pkg_auth/authorization/application/use_cases/delete_membership.py +21 -0
  66. pkg_auth/authorization/application/use_cases/delete_organization.py +21 -0
  67. pkg_auth/authorization/application/use_cases/delete_role.py +23 -0
  68. pkg_auth/authorization/application/use_cases/list_user_organizations.py +21 -0
  69. pkg_auth/authorization/application/use_cases/provision_default_services.py +38 -0
  70. pkg_auth/authorization/application/use_cases/register_permission_catalog.py +122 -0
  71. pkg_auth/authorization/application/use_cases/resolve_auth_context.py +70 -0
  72. pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py +34 -0
  73. pkg_auth/authorization/application/use_cases/set_organization_service.py +50 -0
  74. pkg_auth/authorization/application/use_cases/sync_permission_catalog.py +86 -0
  75. pkg_auth/authorization/application/use_cases/sync_service_catalog.py +91 -0
  76. pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py +32 -0
  77. pkg_auth/authorization/application/use_cases/update_organization.py +31 -0
  78. pkg_auth/authorization/application/use_cases/update_role.py +61 -0
  79. pkg_auth/authorization/application/use_cases/upsert_membership.py +54 -0
  80. pkg_auth/authorization/cli/__init__.py +1 -0
  81. pkg_auth/authorization/cli/sync_catalog.py +180 -0
  82. pkg_auth/authorization/cli/sync_services.py +151 -0
  83. pkg_auth/authorization/config.py +21 -0
  84. pkg_auth/authorization/domain/__init__.py +1 -0
  85. pkg_auth/authorization/domain/entities.py +192 -0
  86. pkg_auth/authorization/domain/exceptions.py +68 -0
  87. pkg_auth/authorization/domain/ports.py +217 -0
  88. pkg_auth/authorization/domain/value_objects.py +208 -0
  89. pkg_auth/authorization/platform.py +47 -0
  90. pkg_auth/integrations/__init__.py +0 -0
  91. pkg_auth/integrations/django/__init__.py +32 -0
  92. pkg_auth/integrations/django/apps.py +10 -0
  93. pkg_auth/integrations/django/auth_context_middleware.py +105 -0
  94. pkg_auth/integrations/django/decorators.py +74 -0
  95. pkg_auth/integrations/django/install.py +136 -0
  96. pkg_auth/integrations/django/middleware.py +63 -0
  97. pkg_auth/integrations/fastapi/__init__.py +26 -0
  98. pkg_auth/integrations/fastapi/auth_context_dep.py +150 -0
  99. pkg_auth/integrations/fastapi/auth_factory.py +84 -0
  100. pkg_auth/integrations/fastapi/decorators.py +55 -0
  101. pkg_auth/integrations/fastapi/errors.py +72 -0
  102. pkg_auth/integrations/fastapi/identity_dep.py +41 -0
  103. pkg_auth/integrations/strawberry/__init__.py +20 -0
  104. pkg_auth/integrations/strawberry/auth.py +137 -0
  105. pkg_auth/integrations/strawberry/permissions.py +56 -0
  106. pkg_auth-3.0.0.dist-info/METADATA +147 -0
  107. pkg_auth-3.0.0.dist-info/RECORD +110 -0
  108. pkg_auth-3.0.0.dist-info/WHEEL +5 -0
  109. pkg_auth-3.0.0.dist-info/entry_points.txt +4 -0
  110. pkg_auth-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,41 @@
1
+ """Token extraction helpers and identity dependency for FastAPI."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ from fastapi import HTTPException, Request, status
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+
9
+ DEFAULT_COOKIE_NAME = "access_token"
10
+
11
+ bearer_scheme = HTTPBearer(auto_error=False)
12
+
13
+
14
+ def extract_token_from_request(
15
+ request: Request,
16
+ credentials: Optional[HTTPAuthorizationCredentials] = None,
17
+ cookie_name: str = DEFAULT_COOKIE_NAME,
18
+ ) -> str:
19
+ """Extract a bearer token from the Authorization header or a cookie.
20
+
21
+ Raises ``HTTPException(401)`` if no token is found.
22
+ """
23
+ if credentials is not None:
24
+ token = (credentials.credentials or "").strip()
25
+ if token:
26
+ return token
27
+
28
+ auth_header = request.headers.get("Authorization")
29
+ if auth_header and auth_header.startswith("Bearer "):
30
+ token = auth_header.removeprefix("Bearer ").strip()
31
+ if token:
32
+ return token
33
+
34
+ cookie_token = request.cookies.get(cookie_name)
35
+ if cookie_token:
36
+ return cookie_token
37
+
38
+ raise HTTPException(
39
+ status_code=status.HTTP_401_UNAUTHORIZED,
40
+ detail="Not authenticated",
41
+ )
@@ -0,0 +1,20 @@
1
+ """Strawberry GraphQL integration for pkg_auth (identity + ACL)."""
2
+ from __future__ import annotations
3
+
4
+ try:
5
+ import strawberry # noqa: F401
6
+ except ImportError as exc: # pragma: no cover
7
+ raise ImportError(
8
+ "pkg_auth.integrations.strawberry requires strawberry-graphql. "
9
+ "Install with: pip install pkg-auth[strawberry]"
10
+ ) from exc
11
+
12
+ from .auth import StrawberryContext, make_context_getter
13
+ from .permissions import IsAuthenticated, RequirePermission
14
+
15
+ __all__ = [
16
+ "StrawberryContext",
17
+ "make_context_getter",
18
+ "IsAuthenticated",
19
+ "RequirePermission",
20
+ ]
@@ -0,0 +1,137 @@
1
+ """Strawberry context getter producing ``(IdentityContext, AuthContext)``."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Awaitable, Callable
6
+ from uuid import UUID
7
+
8
+ from starlette.requests import Request
9
+
10
+ from ...authentication import (
11
+ AuthenticateTokenUseCase,
12
+ AuthenticationError,
13
+ IdentityContext,
14
+ InvalidTokenError,
15
+ TokenExpiredError,
16
+ )
17
+ from ...authorization import (
18
+ AuthContext,
19
+ NotAMember,
20
+ OrgId,
21
+ UserNotProvisioned,
22
+ )
23
+ from ...authorization.application.use_cases.resolve_auth_context import (
24
+ ResolveAuthContextUseCase,
25
+ )
26
+ from ...authorization.application.use_cases.resolve_user_from_jwt import (
27
+ ResolveUserFromJwtUseCase,
28
+ )
29
+ from ...authorization.application.use_cases.sync_user_from_jwt import (
30
+ SyncUserFromJwtUseCase,
31
+ )
32
+ from ...authorization.domain.entities import User
33
+ from ...authorization.domain.ports import OrganizationRepository
34
+
35
+ DEFAULT_HEADER_NAME = "X-Organization-Id"
36
+ DEFAULT_COOKIE_NAME = "access_token"
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class StrawberryContext:
41
+ """Context object exposed to every Strawberry resolver via ``info.context``.
42
+
43
+ Carries the request, the validated identity (``None`` for anonymous
44
+ queries), and the per-organization authorization context (``None``
45
+ if no ``X-Organization-Id`` header was provided).
46
+ """
47
+
48
+ request: Request
49
+ identity: IdentityContext | None = None
50
+ auth_context: AuthContext | None = None
51
+ extra: dict[str, object] = field(default_factory=dict)
52
+
53
+
54
+ def _extract_token(request: Request, cookie_name: str) -> str | None:
55
+ auth_header = request.headers.get("Authorization")
56
+ if auth_header and auth_header.startswith("Bearer "):
57
+ token = auth_header.removeprefix("Bearer ").strip()
58
+ if token:
59
+ return token
60
+ return request.cookies.get(cookie_name)
61
+
62
+
63
+ def make_context_getter(
64
+ *,
65
+ authenticate_use_case: AuthenticateTokenUseCase,
66
+ resolve_use_case: ResolveAuthContextUseCase,
67
+ organization_repo: OrganizationRepository,
68
+ sync_user_use_case: SyncUserFromJwtUseCase | None = None,
69
+ resolve_user_use_case: ResolveUserFromJwtUseCase | None = None,
70
+ header_name: str = DEFAULT_HEADER_NAME,
71
+ cookie_name: str = DEFAULT_COOKIE_NAME,
72
+ ) -> Callable[[Request], Awaitable[StrawberryContext]]:
73
+ """Build an async ``context_getter`` for ``strawberry.fastapi.GraphQLRouter``.
74
+
75
+ Exactly one of ``sync_user_use_case`` (Mode A — source-of-truth) or
76
+ ``resolve_user_use_case`` (Mode B — consumer) must be supplied.
77
+
78
+ The returned function is permissive: token errors, missing headers,
79
+ and ``UserNotProvisioned`` degrade the context fields to ``None``
80
+ rather than raising. Permission classes (``IsAuthenticated``,
81
+ ``RequirePermission``) are responsible for rejecting under-privileged
82
+ queries.
83
+ """
84
+ if (sync_user_use_case is None) == (resolve_user_use_case is None):
85
+ raise ValueError(
86
+ "make_context_getter: pass exactly one of "
87
+ "sync_user_use_case (Mode A) or resolve_user_use_case (Mode B)."
88
+ )
89
+
90
+ async def _context_getter(request: Request) -> StrawberryContext:
91
+ ctx = StrawberryContext(request=request)
92
+
93
+ token = _extract_token(request, cookie_name)
94
+ if token is not None:
95
+ try:
96
+ ctx.identity = authenticate_use_case.execute(token)
97
+ except (TokenExpiredError, InvalidTokenError, AuthenticationError):
98
+ ctx.identity = None
99
+
100
+ if ctx.identity is None:
101
+ return ctx
102
+
103
+ raw = request.headers.get(header_name)
104
+ if raw is None:
105
+ return ctx
106
+
107
+ user: User
108
+ try:
109
+ if sync_user_use_case is not None:
110
+ user = await sync_user_use_case.execute(
111
+ sub=ctx.identity.subject_str,
112
+ email=ctx.identity.email_str or "",
113
+ full_name=ctx.identity.full_name,
114
+ )
115
+ else:
116
+ assert resolve_user_use_case is not None
117
+ user = await resolve_user_use_case.execute(
118
+ sub=ctx.identity.subject_str,
119
+ )
120
+ except UserNotProvisioned:
121
+ return ctx
122
+
123
+ try:
124
+ org = await organization_repo.get(OrgId(UUID(raw)))
125
+ except ValueError:
126
+ org = await organization_repo.get_by_slug(raw)
127
+ if org is None:
128
+ return ctx
129
+
130
+ try:
131
+ ctx.auth_context = await resolve_use_case.execute(user.id, org.id)
132
+ except NotAMember:
133
+ ctx.auth_context = None
134
+
135
+ return ctx
136
+
137
+ return _context_getter
@@ -0,0 +1,56 @@
1
+ """Strawberry permission classes built on ``StrawberryContext``."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from strawberry.permission import BasePermission
7
+ from strawberry.types import Info
8
+
9
+ from .auth import StrawberryContext
10
+
11
+
12
+ class IsAuthenticated(BasePermission):
13
+ """Permission class: require a valid identity (any organization or none)."""
14
+
15
+ message = "Authentication required"
16
+
17
+ async def has_permission(
18
+ self, source: Any, info: Info, **kwargs: Any
19
+ ) -> bool:
20
+ ctx: StrawberryContext = info.context
21
+ return ctx.identity is not None
22
+
23
+
24
+ class RequirePermission(BasePermission):
25
+ """Permission class: require a specific perm in the active org context.
26
+
27
+ Usage::
28
+
29
+ @strawberry.type
30
+ class Query:
31
+ @strawberry.field(
32
+ permission_classes=[RequirePermission("course:view")],
33
+ )
34
+ async def course(self, id: strawberry.ID) -> Course: ...
35
+
36
+ Returns ``False`` (and a meaningful ``message``) when:
37
+ - the request has no identity → "Authentication required"
38
+ - the request has identity but no auth_context → "Missing X-Organization-Id"
39
+ - the role does not grant ``perm`` → "Permission denied: <perm>"
40
+ """
41
+
42
+ def __init__(self, perm: str) -> None:
43
+ self.perm = perm
44
+ self.message = f"Permission denied: {perm}"
45
+
46
+ async def has_permission(
47
+ self, source: Any, info: Info, **kwargs: Any
48
+ ) -> bool:
49
+ ctx: StrawberryContext = info.context
50
+ if ctx.identity is None:
51
+ self.message = "Authentication required"
52
+ return False
53
+ if ctx.auth_context is None:
54
+ self.message = "Missing X-Organization-Id header"
55
+ return False
56
+ return ctx.auth_context.has(self.perm)
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: pkg-auth
3
+ Version: 3.0.0
4
+ Summary: Clean-architecture auth core for multiple Python frameworks
5
+ Author-email: Fritill <info@fritill.ae>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fritill-team/fri_pkg_auth
8
+ Project-URL: Repository, https://github.com/fritill-team/fri_pkg_auth
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: pyjwt[crypto]>=2.10.1
13
+ Requires-Dist: requests>=2.32.3
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest<9.0.0,>=8.0.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio<1.0.0,>=0.23.0; extra == "dev"
17
+ Requires-Dist: mypy<2.0.0,>=1.11.0; extra == "dev"
18
+ Requires-Dist: types-requests<3.0.0.0,>=2.32.0.0; extra == "dev"
19
+ Requires-Dist: types-PyJWT<2.0.0,>=1.7.0; extra == "dev"
20
+ Requires-Dist: testcontainers[postgres,redis]>=4.0; extra == "dev"
21
+ Provides-Extra: acl-sqlalchemy
22
+ Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "acl-sqlalchemy"
23
+ Requires-Dist: asyncpg>=0.29; extra == "acl-sqlalchemy"
24
+ Requires-Dist: alembic>=1.13; extra == "acl-sqlalchemy"
25
+ Provides-Extra: acl-django
26
+ Requires-Dist: django>=4.2; extra == "acl-django"
27
+ Requires-Dist: psycopg[binary]>=3.1; extra == "acl-django"
28
+ Provides-Extra: cache-redis
29
+ Requires-Dist: redis>=5.0; extra == "cache-redis"
30
+ Provides-Extra: fastapi
31
+ Requires-Dist: fastapi>=0.115; extra == "fastapi"
32
+ Requires-Dist: starlette>=0.37; extra == "fastapi"
33
+ Provides-Extra: django
34
+ Requires-Dist: django>=4.2; extra == "django"
35
+ Provides-Extra: strawberry
36
+ Requires-Dist: strawberry-graphql>=0.255; extra == "strawberry"
37
+ Provides-Extra: all
38
+ Requires-Dist: django>=6.0; extra == "all"
39
+ Requires-Dist: fastapi>=0.115; extra == "all"
40
+ Requires-Dist: starlette>=0.37; extra == "all"
41
+ Requires-Dist: django>=4.2; extra == "all"
42
+ Requires-Dist: psycopg[binary]>=3.1; extra == "all"
43
+ Requires-Dist: strawberry-graphql>=0.255; extra == "all"
44
+ Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "all"
45
+ Requires-Dist: asyncpg>=0.29; extra == "all"
46
+ Requires-Dist: alembic>=1.13; extra == "all"
47
+ Requires-Dist: redis>=5.0; extra == "all"
48
+
49
+ # pkg-auth
50
+
51
+ Clean-architecture **identity + ACL** for multi-framework Python services. Handles JWT authentication (via Keycloak) and database-backed authorization (users, organizations, roles, permissions, memberships) in a single package with first-class support for **FastAPI**, **Django**, and **Strawberry GraphQL**.
52
+
53
+ > **v1.0 is a breaking change from v0.x.** The old claim-based authorization model (`AccessContext`, `AccessRights`, `require_permissions`) is replaced by a real ACL database. See [`docs/MIGRATION_v1.md`](docs/MIGRATION_v1.md) for the upgrade guide.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ # Core (identity only — no DB deps)
59
+ pip install pkg-auth
60
+
61
+ # With ACL + FastAPI (most common for itqadem services)
62
+ pip install pkg-auth[acl-sqlalchemy,fastapi]
63
+
64
+ # With ACL + Django
65
+ pip install pkg-auth[acl-django,django]
66
+
67
+ # With optional Redis cache
68
+ pip install pkg-auth[cache-redis]
69
+ ```
70
+
71
+ ## Quickstart (FastAPI)
72
+
73
+ ```python
74
+ from fastapi import Depends, FastAPI
75
+ from pkg_auth.authentication import IdentityContext
76
+ from pkg_auth.authorization import AuthContext
77
+ from pkg_auth.integrations.fastapi import (
78
+ create_authentication,
79
+ make_get_auth_context,
80
+ require_permission,
81
+ )
82
+
83
+ # --- Wire authentication + authorization ---
84
+
85
+ auth = create_authentication(
86
+ keycloak_base_url="https://auth.example.com",
87
+ realm="itqadem",
88
+ audience="courses-service",
89
+ )
90
+
91
+ # Mode B (consumer — the common case): pass resolve_user_use_case.
92
+ # Mode A (source-of-truth): pass sync_user_use_case instead. Exactly
93
+ # one of the two is required; passing both raises ValueError.
94
+ get_auth_context = make_get_auth_context(
95
+ get_identity=auth.get_identity,
96
+ resolve_user_use_case=resolve_user, # or: sync_user_use_case=sync_user (Mode A)
97
+ resolve_use_case=resolve,
98
+ organization_repo=org_repo,
99
+ )
100
+
101
+ app = FastAPI()
102
+
103
+ # --- Use in routes ---
104
+
105
+ @app.get("/courses/{id}")
106
+ async def get_course(
107
+ id: str,
108
+ bundle: tuple[IdentityContext, AuthContext] = Depends(
109
+ require_permission("course:view", get_auth_context=get_auth_context)
110
+ ),
111
+ ):
112
+ identity, auth_ctx = bundle
113
+ return {"course_id": id, "role": str(auth_ctx.role_name)}
114
+ ```
115
+
116
+ See [`examples/itqadem_courses_app`](examples/itqadem_courses_app) for a complete working example.
117
+
118
+ ## Architecture
119
+
120
+ ```
121
+ pkg_auth/
122
+ authentication/ JWT validation → IdentityContext (identity only)
123
+ authorization/ Full ACL (users, orgs, roles, perms, memberships)
124
+ domain/ Pure entities, ports (Protocol), exceptions
125
+ application/use_cases/ Business logic (13 use cases)
126
+ adapters/
127
+ sqlalchemy/ Canonical schema + Alembic migration + repos
128
+ django_orm/ Mirror models (managed=False) + repos
129
+ cache/ InMemoryTTLCache / RedisCache + decorator
130
+ integrations/
131
+ fastapi/ Deps + require_permission + exception handlers
132
+ django/ Middleware + decorators
133
+ strawberry/ Context getter + permission classes
134
+ admin/ Keycloak admin client (user provisioning)
135
+ ```
136
+
137
+ **Layering rules**: domain has zero external imports; application imports only domain; adapters import their framework; integrations import everything.
138
+
139
+ ## Documentation
140
+
141
+ - [Authorization model](docs/Authorization.md) — schema, permission catalog, roles, memberships
142
+ - [Caching](docs/Caching.md) — InMemoryTTLCache, RedisCache, invalidation contract
143
+ - [FastAPI Integration](docs/FastAPI.md)
144
+ - [Django Integration](docs/Django.md)
145
+ - [Strawberry Integration](docs/Strawberry.md)
146
+ - [Keycloak Admin](docs/Keycloak-Admin.md)
147
+ - [Migration from v0.x](docs/MIGRATION_v1.md)
@@ -0,0 +1,110 @@
1
+ pkg_auth/__init__.py,sha256=Ncdh3MFgxrBDZ_mm0tvRwEtd6NCRdGUrdroUYZ1Rojg,501
2
+ pkg_auth/admin/__init__.py,sha256=3rhV9qexqMKgBmtlz5EeTsjh4Big1M0n0gTMbO8i56M,1053
3
+ pkg_auth/admin/cli.py,sha256=Zqa9arZSDWOgnnC5gIyUOZpsebaplwkZdJOlybm3HUI,2654
4
+ pkg_auth/admin/client.py,sha256=Zv-T0Z9mtD-OKi0KlWqe4rWHMXUIo7rE3OpxjPgUnCM,15640
5
+ pkg_auth/admin/env.py,sha256=y5ojrPwdbsTIpa8TKE1PNT_qbMCIUjbjjIDGwgOJEhI,2456
6
+ pkg_auth/admin/helpers.py,sha256=Zqq01HZChHcQsaDsWOIznqLRnC2rDoxKzabahonT0dE,3659
7
+ pkg_auth/admin/provision_client.py,sha256=tHqTXNCwD3Q-VqdUgEB6hbyxGYs8EEH15rtGtaAyk3g,2874
8
+ pkg_auth/admin/settings.py,sha256=qFQGl12zgt0O5wfAhXlRJ-uWxFelm-AR7keXRQnkJbE,907
9
+ pkg_auth/authentication/__init__.py,sha256=tC11N5chlpilu9keLJCPqciHhJswMEsfpfOsSNAoeCE,1003
10
+ pkg_auth/authentication/adapters/__init__.py,sha256=zT2tSG-CunLGX5-B_nH1VwUbiah78dXP4jjUmotstyE,46
11
+ pkg_auth/authentication/adapters/keycloak/__init__.py,sha256=jT6EGZ2hJvIubi_lDZP383gbV7-mJwRu4tgjE7e51Kw,144
12
+ pkg_auth/authentication/adapters/keycloak/jwt_decoder.py,sha256=ntsA6Rl5RkQxJaFn8uS53qsEKD-tP784wTE9pMIpSOc,3278
13
+ pkg_auth/authentication/application/__init__.py,sha256=Hds19u1CgNfI00uL9fkYmWF2O71IjIcy4VamMnbSjQ4,52
14
+ pkg_auth/authentication/application/use_cases/__init__.py,sha256=MHUxj-iDbSDzjFHgUgy6PAV6fy2-GJjyeWqGYuZRAX4,32
15
+ pkg_auth/authentication/application/use_cases/authenticate.py,sha256=ahXhfwpdCDNrAMNLYRdJPoGYRYkFdnYG_bKX3FYK9mE,3262
16
+ pkg_auth/authentication/domain/__init__.py,sha256=Lv-UUicaWH5_wTUk3Wq2rHy2ndWSSv6CPkk-kORfk4w,80
17
+ pkg_auth/authentication/domain/entities.py,sha256=hXsoN7kmrEXrDb8DOCkv0HGnHfPwedydc6Y7xtBsmBQ,1650
18
+ pkg_auth/authentication/domain/exceptions.py,sha256=u6Fd6pQfSU3ese0BIs7v6DsyZT7D0xnm24rXgxfJ2ak,486
19
+ pkg_auth/authentication/domain/ports.py,sha256=G3a2Et9QYkSW3muSgflb1hdJDHO6OV4dO9RmlVL_i7Y,785
20
+ pkg_auth/authentication/domain/value_objects.py,sha256=aiMEoD8n8Ij1FBL8txXqYzfcvHctlI4ONUIPBchnBxc,936
21
+ pkg_auth/authorization/__init__.py,sha256=Q79hWDxqH9yjupKzZBchJ9ONR99Pk9m6TyMkixhamFs,2941
22
+ pkg_auth/authorization/config.py,sha256=AGEYfwh19cVZ64yQXTL-_FBzfG2RCngRhCOH-QArKcM,662
23
+ pkg_auth/authorization/platform.py,sha256=_emk0vDEGjBtKlbHs0TLokLj4HLqMSHlVZ2zztkP-ek,2034
24
+ pkg_auth/authorization/adapters/__init__.py,sha256=5HHRX7dlZqqgtoJ0UoMqIbE9ayESEvb0DpdbczzDLWo,77
25
+ pkg_auth/authorization/adapters/cache/__init__.py,sha256=8T8HA1albF1m_aMKtb6_ueXgW37KAM2vBsb1StRdJ3w,966
26
+ pkg_auth/authorization/adapters/cache/decorators.py,sha256=9a1H9G9o6cWA5YwfGcZAKrMCreG01zS8IFsNzwYMHPA,6115
27
+ pkg_auth/authorization/adapters/cache/memory.py,sha256=L6JjCJlN-JVwpGmzt2a3Y536HuH0ylkgIYfE23u-vRE,2060
28
+ pkg_auth/authorization/adapters/cache/protocol.py,sha256=qgBMuBiIuLEfgW6JL9QyafuZlWfYVOyv5n7nl5ry7gs,1159
29
+ pkg_auth/authorization/adapters/cache/redis.py,sha256=OmKKmR5q0nLAh0GklFCqz8_NZ8OwIqjU928GNT3gCL8,1879
30
+ pkg_auth/authorization/adapters/django_orm/__init__.py,sha256=K0yp8LPavqds8wIcn3XpHr5BOocCrirsLKCUC05gd04,1419
31
+ pkg_auth/authorization/adapters/django_orm/apps.py,sha256=POt9oMuHd6ZhyzwOuFTsVOjaOq4DNeLsZrzoSV8Of2o,990
32
+ pkg_auth/authorization/adapters/django_orm/mixins.py,sha256=OVxrD38eQlgjOWoZ8GOCgUPHuJB460cg9Q_kAqLWfx8,4810
33
+ pkg_auth/authorization/adapters/django_orm/models.py,sha256=tPfu7UXEMuGueQsEiruMivX8ZPCW9E7Xahjpy1VEU94,6633
34
+ pkg_auth/authorization/adapters/django_orm/repositories/__init__.py,sha256=StguAKeqZXOuVDhmzm6dfeN7wp8swz74O6Kd7idz-F8,720
35
+ pkg_auth/authorization/adapters/django_orm/repositories/membership.py,sha256=r0dJ9Jk56dFtvNmJz-41qgXsoeS8Y5-fDIwXirGwnrg,3839
36
+ pkg_auth/authorization/adapters/django_orm/repositories/organization.py,sha256=9MLUugCqsae4spQpPQ-cQ-brmKv99DlDC0aTNQgfZRo,2631
37
+ pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py,sha256=N-OLsHVGyy8_Fx92cGAheqnGFHjeecMTN096zpJrujM,2376
38
+ pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py,sha256=Tl6u-jKVVXrbODFBsLo-nYGbsJBVg_rqL5dn6miPo-E,3231
39
+ pkg_auth/authorization/adapters/django_orm/repositories/role.py,sha256=pXdsKG004M_26g-FXMeAoM_c5pEKPOaQeJQxaF-6FEY,3896
40
+ pkg_auth/authorization/adapters/django_orm/repositories/service.py,sha256=EfIxJAEvvnlbTfvq4ZLrT_DgK5MDv7-gBa9L_x69fQI,2101
41
+ pkg_auth/authorization/adapters/django_orm/repositories/user.py,sha256=mSw3TU3cIfzvp8devH4zaDZxwjXitCBfV6wG0h5rGUQ,2467
42
+ pkg_auth/authorization/adapters/sqlalchemy/__init__.py,sha256=YrYsBwOCp-XxTlwmd_wsD43DAybvklUXGbJQhrk86C4,2621
43
+ pkg_auth/authorization/adapters/sqlalchemy/base.py,sha256=e3-OeqT_lSNMFzsJMhcfusKjahUVEfCjZL-m0VWAdi8,1988
44
+ pkg_auth/authorization/adapters/sqlalchemy/mixins.py,sha256=CSf9Uo2POfQq72nprHlsSUAe4mW-E7kw8EZxQAe4AWc,6324
45
+ pkg_auth/authorization/adapters/sqlalchemy/models.py,sha256=5ywCymfzzL6qhzFDRLvY5w9IYK3tKcApy8oHQm-Jjbk,8003
46
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py,sha256=vPLhCk568lV86IasOo4rEbHaXETwTN8gxYA1dhVLNwg,45
47
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py,sha256=f3xZGEqRLdtwzW1V83MQAa2JSL7AJ8-ezml5dXlA0WE,10029
48
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py,sha256=ykew9uG3vQvxMyH6TTTnuS6hnySMbWeJ_h9rIvYpVRU,1056
49
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py,sha256=s2J8X-RIHQM8wSATmclBl0aHUv9bMceSVbabt9hJF1Q,1874
50
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py,sha256=HojPzi79eDc0NMTqOrBIzxBl8wVr_BOX4jVFFodWOpk,1578
51
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py,sha256=CSDVMKpAYIRtsikPNQow6APQtbx_LWbGs2YSQ3zVsuw,3440
52
+ pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py,sha256=Lc0QV0ti8nqP6Z0nCsWRDmK5TEWaeJRz9DR88VbcyY4,73
53
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py,sha256=duKdKkcbZn2gIqOy5vYiXQuuSVUtgcN_6tswSpMMVuM,571
54
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py,sha256=LgXpvvw099VioEFJQSpgYgKVk5vOQCcaeM44VPtI5Po,5143
55
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py,sha256=Gw5WUuuWoa3XMN7TVhd1aTVMshT-T5fFWwx2R3ZHtD4,3556
56
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py,sha256=huNY2Q9Jg0Dy1tBO7cSUNWTflHXuOV4VVuVil7d4j4w,3731
57
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py,sha256=ECTdXKaWvuK7VGQgS8Ms8VXehzotpue9aLf5a-XXRgw,4489
58
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py,sha256=jxS6DCBfLo4jtf65Y3YI81_0Ex_3ob3YAFZzougofW4,5925
59
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py,sha256=vP5fSoEC1eT26ZwsjcUkd2CFlReDj9pIp_Va5VrnTz4,3534
60
+ pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py,sha256=04tS9cdKAgj4MEoOBuxYFepNd1NMtSCEsbmAmrlKmes,2430
61
+ pkg_auth/authorization/application/__init__.py,sha256=clRG0-N0xMg7AONQR4_sMGHFnqjPYefzO0qezvcDaZ0,51
62
+ pkg_auth/authorization/application/use_cases/__init__.py,sha256=0Hh6G-ojeIPJ5coQRp-16vhAOlkofUPflzEX6xDPZBw,31
63
+ pkg_auth/authorization/application/use_cases/_helpers.py,sha256=e0NehT0LNEXx7wc2re4wWQjW9EqRe3j5ho6M8fCXTHI,2786
64
+ pkg_auth/authorization/application/use_cases/check_permission.py,sha256=GrFA7UgIeUFZfolptcEAaIlO74xkvl1IKMXNTWrNucU,699
65
+ pkg_auth/authorization/application/use_cases/create_organization.py,sha256=GBGnDe8g4T0ZIQqaaO4aRrC9ZARx_pis5P_4kOet3-0,1570
66
+ pkg_auth/authorization/application/use_cases/create_role.py,sha256=Y5X-KNQCDibb2mTXAj3U4rNQE0vf-p_ILeQVE5mgmvk,2333
67
+ pkg_auth/authorization/application/use_cases/delete_membership.py,sha256=_m_XXCT7k3bfrZj49zbJVLztwU880HmdiN2iOBdUQhw,613
68
+ pkg_auth/authorization/application/use_cases/delete_organization.py,sha256=SVWqCdUWOJ6-bX135gB_aqMpCeZL-NhFvxRY81-KwU8,621
69
+ pkg_auth/authorization/application/use_cases/delete_role.py,sha256=aWdeE19dkqorvqEMR3JnmG76cb-VyI2FQd7xTXstFFQ,715
70
+ pkg_auth/authorization/application/use_cases/list_user_organizations.py,sha256=ycfsKh5Iof7RbmYS81EyU1IyUsH9SlFWGwHgVVS3lOI,626
71
+ pkg_auth/authorization/application/use_cases/provision_default_services.py,sha256=o27wgHvouvJ7_y6nuZnZ2ik3fQ1t410hKijWZJPfU1g,1369
72
+ pkg_auth/authorization/application/use_cases/register_permission_catalog.py,sha256=V7woKRAyKfHWV_TkfwL_G5rYtT6_AaI-adVR12dhwQU,4369
73
+ pkg_auth/authorization/application/use_cases/resolve_auth_context.py,sha256=rPRnZaV7lKo5YABiv-MC419PdR6U740qN3P_3bfADY0,2841
74
+ pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py,sha256=kjBr0Qwwy0ObLcoH6CsBuYXoGh8Ae3a9CULqJ64AhUo,1242
75
+ pkg_auth/authorization/application/use_cases/set_organization_service.py,sha256=XQeb-TAIoIK80bMuS6EW5pBLl45z2CIbR5Yhg_nkvqk,1725
76
+ pkg_auth/authorization/application/use_cases/sync_permission_catalog.py,sha256=wNSaBxPgB9TIaoW1eB9xXZkptF_dV28BjvNEc6m7Oaw,2789
77
+ pkg_auth/authorization/application/use_cases/sync_service_catalog.py,sha256=Sqr--6wpDCo-joFvMz7Hubm3b15ceyjas743pcEslmw,2893
78
+ pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py,sha256=eGy78u3PGyFmT3FbQxZETyqmn2-uVSmcwi1frZq010w,871
79
+ pkg_auth/authorization/application/use_cases/update_organization.py,sha256=N2yYeW2GgVdg2mNbvhP4m6rHocmYupyU8_d4w9Z3_QI,967
80
+ pkg_auth/authorization/application/use_cases/update_role.py,sha256=KbDQRo2NrOceOR5ikqGn1qlE1goHVaJbjwj17Ut0X-c,2087
81
+ pkg_auth/authorization/application/use_cases/upsert_membership.py,sha256=HkCXI0nEy1INsex5wou_6JvWNey6sDv7Hr6-nCqPLII,1559
82
+ pkg_auth/authorization/cli/__init__.py,sha256=yegbFIXOk-DLPjGQJuQtTfL_PTmC2TZglAnsLQZ1b78,53
83
+ pkg_auth/authorization/cli/sync_catalog.py,sha256=JJvJ907aSe41VLAEgP2Cup1dkZ6QBS0r3rYxAyjWCRc,5820
84
+ pkg_auth/authorization/cli/sync_services.py,sha256=HG60WlruD5aiXt3QSLmHQG0Cyts-1HmVYvVI_UIEaQI,4964
85
+ pkg_auth/authorization/domain/__init__.py,sha256=iCXr9ZV7LqKZr0pNcyiggcPPGr7T_-Eg4UY0qnywQlo,79
86
+ pkg_auth/authorization/domain/entities.py,sha256=ImjlwLaxnz0ptWj0zWe1c20pKf8n3JLk5m8Gj6r07pQ,6249
87
+ pkg_auth/authorization/domain/exceptions.py,sha256=abJucoInylc5EGyInYsLrIbp90UHg24NliJvdv90PXo,2145
88
+ pkg_auth/authorization/domain/ports.py,sha256=tNt1j7gQ7wL4UtoBsfbgKl2C78no7qvwbPFwwLFlYlw,7327
89
+ pkg_auth/authorization/domain/value_objects.py,sha256=HSDUCticFuLl-gIA2xtLAR4LSO6uKfTGLsnaR0bkI_0,6693
90
+ pkg_auth/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
+ pkg_auth/integrations/django/__init__.py,sha256=ViunP2eC1KUCbYlsagKpfpcOJEKg99SCwyDdVoQGfe4,1093
92
+ pkg_auth/integrations/django/apps.py,sha256=vNXF6vqstShEpXtDTA4HMGK4FdYOiO5gNx65nPgH_7Q,292
93
+ pkg_auth/integrations/django/auth_context_middleware.py,sha256=iHXfUtATAUmECC6a2Pua6rjvqs81Ke5m9pZprD5v_4s,3863
94
+ pkg_auth/integrations/django/decorators.py,sha256=LU7OcVzQrezzDS6sGIqXZTzhFJOepQVGeibAUT1n5hU,2320
95
+ pkg_auth/integrations/django/install.py,sha256=DpSwSZ40DwNe6rAHYl0JCK2Q-u2ZlrAIvyLXIBS0cf4,5076
96
+ pkg_auth/integrations/django/middleware.py,sha256=tUNRnFkHxsvHkqtgr4hH17OUOTX0uBhGsJ0PiexIERQ,2268
97
+ pkg_auth/integrations/fastapi/__init__.py,sha256=4SBK9gZdFh9ZXhu1dy7swvqB7-G6VAxPblHQ5rnQqs4,814
98
+ pkg_auth/integrations/fastapi/auth_context_dep.py,sha256=T3384N7Ex5fNyBUWCf_o9Sv6_NcXz-eJV2x9BBBMWnU,5733
99
+ pkg_auth/integrations/fastapi/auth_factory.py,sha256=G8enX_hisGItbLBkSd9dMXp4cTxazuEzHIy5E_IG0Hc,2804
100
+ pkg_auth/integrations/fastapi/decorators.py,sha256=vgME0__QrZwWtFTsyve6vL3NxMp9pGANxN4OEobIHeo,1653
101
+ pkg_auth/integrations/fastapi/errors.py,sha256=mhcpBYrkmrwuaHoiRvBIiA73Eps9EbV1em1E8e7kXTw,2383
102
+ pkg_auth/integrations/fastapi/identity_dep.py,sha256=9pHY9GEIT6jo0XEtbP8FsP-_0LHZ5YZaEbxJj9JDh-A,1218
103
+ pkg_auth/integrations/strawberry/__init__.py,sha256=3nOveRw8rOGQsiCaKvWO4Vtfxt6Ft23QuqJN_6HHzEQ,593
104
+ pkg_auth/integrations/strawberry/auth.py,sha256=DE3wPKa1i1sfyIJUzMg2fnyKQnWIBu76290y0jkX2Z4,4642
105
+ pkg_auth/integrations/strawberry/permissions.py,sha256=MmD8_RuQz7MUk1j_bmVBWY2XL4J6uuWSZRaR0ztGvnI,1806
106
+ pkg_auth-3.0.0.dist-info/METADATA,sha256=eYMYwFJNjLqJ9tjR2_qGuePfJufHTzfLs4SxPYo95p4,5699
107
+ pkg_auth-3.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
108
+ pkg_auth-3.0.0.dist-info/entry_points.txt,sha256=cbN-pxVE5BuYAb5LVfurD84I9S1gBe9_Zdd2jGnvtyY,205
109
+ pkg_auth-3.0.0.dist-info/top_level.txt,sha256=xEdLnVj1Gbyv7q5dJSiWASWs-soG3RKfSuP2HHdcUNY,9
110
+ pkg_auth-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ keycloak-init-client = pkg_auth.admin.cli:main
3
+ pkg-auth-sync-catalog = pkg_auth.authorization.cli.sync_catalog:main
4
+ pkg-auth-sync-services = pkg_auth.authorization.cli.sync_services:main
@@ -0,0 +1 @@
1
+ pkg_auth