pkg-auth 3.0.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.0.0/PKG-INFO +147 -0
- pkg_auth-3.0.0/README.md +99 -0
- pkg_auth-3.0.0/pyproject.toml +133 -0
- pkg_auth-3.0.0/setup.cfg +4 -0
- pkg_auth-3.0.0/src/pkg_auth/__init__.py +15 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/__init__.py +35 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/cli.py +87 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/client.py +401 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/env.py +74 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/helpers.py +113 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/provision_client.py +86 -0
- pkg_auth-3.0.0/src/pkg_auth/admin/settings.py +33 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/__init__.py +33 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/keycloak/__init__.py +6 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/keycloak/jwt_decoder.py +105 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/application/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/application/use_cases/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/application/use_cases/authenticate.py +91 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/domain/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/domain/entities.py +50 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/domain/exceptions.py +18 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/domain/ports.py +26 -0
- pkg_auth-3.0.0/src/pkg_auth/authentication/domain/value_objects.py +42 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/__init__.py +117 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/__init__.py +32 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/decorators.py +181 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/memory.py +61 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/protocol.py +36 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/redis.py +60 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/__init__.py +37 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/apps.py +24 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/mixins.py +142 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/models.py +226 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/__init__.py +20 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/membership.py +118 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/organization.py +73 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py +71 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py +102 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/role.py +120 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/service.py +60 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/user.py +77 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/__init__.py +90 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/base.py +55 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py +293 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py +39 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py +65 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py +52 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py +116 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/mixins.py +187 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/models.py +268 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py +16 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py +146 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py +97 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py +106 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py +127 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py +171 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py +93 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py +74 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/_helpers.py +82 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/check_permission.py +21 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/create_organization.py +41 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/create_role.py +69 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_membership.py +21 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_organization.py +21 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_role.py +23 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/list_user_organizations.py +21 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/provision_default_services.py +38 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/register_permission_catalog.py +122 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/resolve_auth_context.py +70 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py +34 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/set_organization_service.py +50 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_permission_catalog.py +86 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_service_catalog.py +91 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py +32 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/update_organization.py +31 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/update_role.py +61 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/upsert_membership.py +54 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/cli/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/cli/sync_catalog.py +180 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/cli/sync_services.py +151 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/config.py +21 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/domain/__init__.py +1 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/domain/entities.py +192 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/domain/exceptions.py +68 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/domain/ports.py +217 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/domain/value_objects.py +208 -0
- pkg_auth-3.0.0/src/pkg_auth/authorization/platform.py +47 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/__init__.py +0 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/__init__.py +32 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/apps.py +10 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/auth_context_middleware.py +105 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/decorators.py +74 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/install.py +136 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/django/middleware.py +63 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/__init__.py +26 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/auth_context_dep.py +150 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/auth_factory.py +84 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/decorators.py +55 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/errors.py +72 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/identity_dep.py +41 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/__init__.py +20 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/auth.py +137 -0
- pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/permissions.py +56 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/PKG-INFO +147 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/SOURCES.txt +113 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/dependency_links.txt +1 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/entry_points.txt +4 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/requires.txt +45 -0
- pkg_auth-3.0.0/src/pkg_auth.egg-info/top_level.txt +1 -0
pkg_auth-3.0.0/PKG-INFO
ADDED
|
@@ -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)
|
pkg_auth-3.0.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pkg-auth
|
|
2
|
+
|
|
3
|
+
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**.
|
|
4
|
+
|
|
5
|
+
> **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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Core (identity only — no DB deps)
|
|
11
|
+
pip install pkg-auth
|
|
12
|
+
|
|
13
|
+
# With ACL + FastAPI (most common for itqadem services)
|
|
14
|
+
pip install pkg-auth[acl-sqlalchemy,fastapi]
|
|
15
|
+
|
|
16
|
+
# With ACL + Django
|
|
17
|
+
pip install pkg-auth[acl-django,django]
|
|
18
|
+
|
|
19
|
+
# With optional Redis cache
|
|
20
|
+
pip install pkg-auth[cache-redis]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart (FastAPI)
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from fastapi import Depends, FastAPI
|
|
27
|
+
from pkg_auth.authentication import IdentityContext
|
|
28
|
+
from pkg_auth.authorization import AuthContext
|
|
29
|
+
from pkg_auth.integrations.fastapi import (
|
|
30
|
+
create_authentication,
|
|
31
|
+
make_get_auth_context,
|
|
32
|
+
require_permission,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# --- Wire authentication + authorization ---
|
|
36
|
+
|
|
37
|
+
auth = create_authentication(
|
|
38
|
+
keycloak_base_url="https://auth.example.com",
|
|
39
|
+
realm="itqadem",
|
|
40
|
+
audience="courses-service",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Mode B (consumer — the common case): pass resolve_user_use_case.
|
|
44
|
+
# Mode A (source-of-truth): pass sync_user_use_case instead. Exactly
|
|
45
|
+
# one of the two is required; passing both raises ValueError.
|
|
46
|
+
get_auth_context = make_get_auth_context(
|
|
47
|
+
get_identity=auth.get_identity,
|
|
48
|
+
resolve_user_use_case=resolve_user, # or: sync_user_use_case=sync_user (Mode A)
|
|
49
|
+
resolve_use_case=resolve,
|
|
50
|
+
organization_repo=org_repo,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
app = FastAPI()
|
|
54
|
+
|
|
55
|
+
# --- Use in routes ---
|
|
56
|
+
|
|
57
|
+
@app.get("/courses/{id}")
|
|
58
|
+
async def get_course(
|
|
59
|
+
id: str,
|
|
60
|
+
bundle: tuple[IdentityContext, AuthContext] = Depends(
|
|
61
|
+
require_permission("course:view", get_auth_context=get_auth_context)
|
|
62
|
+
),
|
|
63
|
+
):
|
|
64
|
+
identity, auth_ctx = bundle
|
|
65
|
+
return {"course_id": id, "role": str(auth_ctx.role_name)}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See [`examples/itqadem_courses_app`](examples/itqadem_courses_app) for a complete working example.
|
|
69
|
+
|
|
70
|
+
## Architecture
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pkg_auth/
|
|
74
|
+
authentication/ JWT validation → IdentityContext (identity only)
|
|
75
|
+
authorization/ Full ACL (users, orgs, roles, perms, memberships)
|
|
76
|
+
domain/ Pure entities, ports (Protocol), exceptions
|
|
77
|
+
application/use_cases/ Business logic (13 use cases)
|
|
78
|
+
adapters/
|
|
79
|
+
sqlalchemy/ Canonical schema + Alembic migration + repos
|
|
80
|
+
django_orm/ Mirror models (managed=False) + repos
|
|
81
|
+
cache/ InMemoryTTLCache / RedisCache + decorator
|
|
82
|
+
integrations/
|
|
83
|
+
fastapi/ Deps + require_permission + exception handlers
|
|
84
|
+
django/ Middleware + decorators
|
|
85
|
+
strawberry/ Context getter + permission classes
|
|
86
|
+
admin/ Keycloak admin client (user provisioning)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Layering rules**: domain has zero external imports; application imports only domain; adapters import their framework; integrations import everything.
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
- [Authorization model](docs/Authorization.md) — schema, permission catalog, roles, memberships
|
|
94
|
+
- [Caching](docs/Caching.md) — InMemoryTTLCache, RedisCache, invalidation contract
|
|
95
|
+
- [FastAPI Integration](docs/FastAPI.md)
|
|
96
|
+
- [Django Integration](docs/Django.md)
|
|
97
|
+
- [Strawberry Integration](docs/Strawberry.md)
|
|
98
|
+
- [Keycloak Admin](docs/Keycloak-Admin.md)
|
|
99
|
+
- [Migration from v0.x](docs/MIGRATION_v1.md)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pkg-auth"
|
|
7
|
+
version = "3.0.0"
|
|
8
|
+
description = "Clean-architecture auth core for multiple Python frameworks"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Fritill", email = "info@fritill.ae" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Runtime dependencies
|
|
18
|
+
dependencies = [
|
|
19
|
+
"httpx>=0.28.1",
|
|
20
|
+
"pyjwt[crypto]>=2.10.1",
|
|
21
|
+
"requests>=2.32.3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0.0,<9.0.0",
|
|
27
|
+
"pytest-asyncio>=0.23.0,<1.0.0",
|
|
28
|
+
"mypy>=1.11.0,<2.0.0",
|
|
29
|
+
"types-requests>=2.32.0.0,<3.0.0.0",
|
|
30
|
+
"types-PyJWT>=1.7.0,<2.0.0",
|
|
31
|
+
"testcontainers[postgres,redis]>=4.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# ACL storage backends (v1.0)
|
|
35
|
+
acl-sqlalchemy = [
|
|
36
|
+
"sqlalchemy>=2.0,<3.0",
|
|
37
|
+
"asyncpg>=0.29",
|
|
38
|
+
"alembic>=1.13",
|
|
39
|
+
]
|
|
40
|
+
acl-django = [
|
|
41
|
+
"django>=4.2",
|
|
42
|
+
"psycopg[binary]>=3.1",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Cache backends (v1.0)
|
|
46
|
+
cache-redis = [
|
|
47
|
+
"redis>=5.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Framework integrations
|
|
51
|
+
fastapi = [
|
|
52
|
+
"fastapi>=0.115",
|
|
53
|
+
"starlette>=0.37",
|
|
54
|
+
]
|
|
55
|
+
django = [
|
|
56
|
+
"django>=4.2",
|
|
57
|
+
]
|
|
58
|
+
strawberry = [
|
|
59
|
+
"strawberry-graphql>=0.255",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Convenience meta-extra
|
|
63
|
+
all = [
|
|
64
|
+
"django>=6.0",
|
|
65
|
+
"fastapi>=0.115",
|
|
66
|
+
"starlette>=0.37",
|
|
67
|
+
"django>=4.2",
|
|
68
|
+
"psycopg[binary]>=3.1",
|
|
69
|
+
"strawberry-graphql>=0.255",
|
|
70
|
+
"sqlalchemy>=2.0,<3.0",
|
|
71
|
+
"asyncpg>=0.29",
|
|
72
|
+
"alembic>=1.13",
|
|
73
|
+
"redis>=5.0",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
[project.urls]
|
|
78
|
+
Homepage = "https://github.com/fritill-team/fri_pkg_auth"
|
|
79
|
+
Repository = "https://github.com/fritill-team/fri_pkg_auth"
|
|
80
|
+
|
|
81
|
+
[project.scripts]
|
|
82
|
+
keycloak-init-client = "pkg_auth.admin.cli:main"
|
|
83
|
+
pkg-auth-sync-catalog = "pkg_auth.authorization.cli.sync_catalog:main"
|
|
84
|
+
pkg-auth-sync-services = "pkg_auth.authorization.cli.sync_services:main"
|
|
85
|
+
|
|
86
|
+
[tool.setuptools.packages.find]
|
|
87
|
+
where = ["src"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
[tool.mypy]
|
|
91
|
+
python_version = "3.10"
|
|
92
|
+
ignore_missing_imports = true
|
|
93
|
+
|
|
94
|
+
[[tool.mypy.overrides]]
|
|
95
|
+
module = "pkg_auth.authentication.*"
|
|
96
|
+
strict = true
|
|
97
|
+
disallow_any_generics = true
|
|
98
|
+
disallow_untyped_defs = true
|
|
99
|
+
disallow_incomplete_defs = true
|
|
100
|
+
no_implicit_optional = true
|
|
101
|
+
warn_redundant_casts = true
|
|
102
|
+
warn_unused_ignores = true
|
|
103
|
+
warn_return_any = true
|
|
104
|
+
no_implicit_reexport = true
|
|
105
|
+
strict_equality = true
|
|
106
|
+
|
|
107
|
+
[[tool.mypy.overrides]]
|
|
108
|
+
module = "pkg_auth.authorization.*"
|
|
109
|
+
strict = true
|
|
110
|
+
disallow_any_generics = true
|
|
111
|
+
disallow_untyped_defs = true
|
|
112
|
+
disallow_incomplete_defs = true
|
|
113
|
+
no_implicit_optional = true
|
|
114
|
+
warn_redundant_casts = true
|
|
115
|
+
warn_unused_ignores = true
|
|
116
|
+
warn_return_any = true
|
|
117
|
+
no_implicit_reexport = true
|
|
118
|
+
strict_equality = true
|
|
119
|
+
|
|
120
|
+
[[tool.mypy.overrides]]
|
|
121
|
+
module = "pkg_auth.integrations.*"
|
|
122
|
+
strict = true
|
|
123
|
+
# Slightly relaxed: framework decorators (Django/Strawberry) sometimes need Any
|
|
124
|
+
disallow_any_expr = false
|
|
125
|
+
warn_return_any = false
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
[tool.pytest.ini_options]
|
|
129
|
+
asyncio_mode = "auto"
|
|
130
|
+
addopts = "-m 'not integration'"
|
|
131
|
+
markers = [
|
|
132
|
+
"integration: tests requiring real Postgres or Redis (testcontainers)",
|
|
133
|
+
]
|
pkg_auth-3.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""pkg_auth: clean-architecture identity + ACL for Python services.
|
|
2
|
+
|
|
3
|
+
Importing this top-level package gives you only the version. Reach for
|
|
4
|
+
specific surfaces via the sub-packages:
|
|
5
|
+
|
|
6
|
+
from pkg_auth.authentication import IdentityContext, AuthenticateTokenUseCase
|
|
7
|
+
from pkg_auth.authentication.adapters.keycloak import JWTTokenDecoder
|
|
8
|
+
|
|
9
|
+
Authorization (ACL) and framework integrations (FastAPI, Django,
|
|
10
|
+
Strawberry) are available via sub-packages.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "2.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkg_auth.admin.keycloak
|
|
3
|
+
|
|
4
|
+
Async Keycloak admin utilities:
|
|
5
|
+
|
|
6
|
+
- KCAdminSettings: configuration for Keycloak admin connection.
|
|
7
|
+
- KeycloakAdminClient: minimal async admin client (httpx-based).
|
|
8
|
+
- provision_keycloak_client: high-level async helper to:
|
|
9
|
+
* ensure API client exists (bearer-only)
|
|
10
|
+
* ensure client roles match your permission list
|
|
11
|
+
* ensure audience + roles mappers on frontend clients
|
|
12
|
+
- settings_from_env / ensure_keycloak_client_from_env:
|
|
13
|
+
convenience wrappers for env-driven CLI / initContainers.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .client import KeycloakAdminClient
|
|
19
|
+
from .env import settings_from_env, ensure_keycloak_client_from_env
|
|
20
|
+
from .helpers import (
|
|
21
|
+
_ensure_api_client,
|
|
22
|
+
_ensure_roles,
|
|
23
|
+
_ensure_frontend_mappers,
|
|
24
|
+
_remove_frontend_mappers,
|
|
25
|
+
)
|
|
26
|
+
from .provision_client import provision_keycloak_client
|
|
27
|
+
from .settings import KCAdminSettings
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"KCAdminSettings",
|
|
31
|
+
"KeycloakAdminClient",
|
|
32
|
+
"settings_from_env",
|
|
33
|
+
"ensure_keycloak_client_from_env",
|
|
34
|
+
"provision_keycloak_client"
|
|
35
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# src/pkg_auth/keycloak_admin/__main__.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Sequence
|
|
10
|
+
|
|
11
|
+
from .env import settings_from_env
|
|
12
|
+
from .provision_client import provision_keycloak_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Provision Keycloak API client, roles and audience mappers",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--client-id",
|
|
22
|
+
help="Override API clientId (default: {APP_NAME|SERVICE_NAME}-api)",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--permissions",
|
|
26
|
+
"-P",
|
|
27
|
+
nargs="*",
|
|
28
|
+
help="Explicit list of permission/role names "
|
|
29
|
+
"(if omitted, your caller can pass them programmatically).",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--frontend-client-ids",
|
|
33
|
+
"-F",
|
|
34
|
+
nargs="*",
|
|
35
|
+
help="Frontend clientIds to grant audience and client-roles mappers to "
|
|
36
|
+
"(defaults from env KEYCLOAK_FRONTEND_CLIENT_IDS).",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--remove-frontend-client-ids",
|
|
40
|
+
"-R",
|
|
41
|
+
nargs="*",
|
|
42
|
+
help="Frontend clientIds to remove audience + roles mappers from "
|
|
43
|
+
"(effective only with --strict-audience).",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--strict-roles",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Reconcile roles strictly: create missing and delete extra roles.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--strict-audience",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Reconcile audience + roles mappers strictly and remove mappers "
|
|
54
|
+
"from --remove-frontend-client-ids.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return parser.parse_args(args=argv)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _run(args: argparse.Namespace) -> dict[str, Any]:
|
|
61
|
+
settings = settings_from_env()
|
|
62
|
+
return await provision_keycloak_client(
|
|
63
|
+
settings=settings,
|
|
64
|
+
client_id=args.client_id,
|
|
65
|
+
permissions=list(args.permissions or []),
|
|
66
|
+
frontend_client_ids=list(args.frontend_client_ids or []),
|
|
67
|
+
remove_frontend_client_ids=list(args.remove_frontend_client_ids or []),
|
|
68
|
+
strict_roles=bool(args.strict_roles),
|
|
69
|
+
strict_audience=bool(args.strict_audience),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
74
|
+
args = _parse_args(argv)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
summary = asyncio.run(_run(args))
|
|
78
|
+
json.dump({"ok": True, **summary}, sys.stdout, indent=2)
|
|
79
|
+
sys.stdout.write("\n")
|
|
80
|
+
except Exception as exc: # noqa: BLE001
|
|
81
|
+
json.dump({"ok": False, "error": str(exc)}, sys.stdout, indent=2)
|
|
82
|
+
sys.stdout.write("\n")
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|