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.
Files changed (115) hide show
  1. pkg_auth-3.0.0/PKG-INFO +147 -0
  2. pkg_auth-3.0.0/README.md +99 -0
  3. pkg_auth-3.0.0/pyproject.toml +133 -0
  4. pkg_auth-3.0.0/setup.cfg +4 -0
  5. pkg_auth-3.0.0/src/pkg_auth/__init__.py +15 -0
  6. pkg_auth-3.0.0/src/pkg_auth/admin/__init__.py +35 -0
  7. pkg_auth-3.0.0/src/pkg_auth/admin/cli.py +87 -0
  8. pkg_auth-3.0.0/src/pkg_auth/admin/client.py +401 -0
  9. pkg_auth-3.0.0/src/pkg_auth/admin/env.py +74 -0
  10. pkg_auth-3.0.0/src/pkg_auth/admin/helpers.py +113 -0
  11. pkg_auth-3.0.0/src/pkg_auth/admin/provision_client.py +86 -0
  12. pkg_auth-3.0.0/src/pkg_auth/admin/settings.py +33 -0
  13. pkg_auth-3.0.0/src/pkg_auth/authentication/__init__.py +33 -0
  14. pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/__init__.py +1 -0
  15. pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/keycloak/__init__.py +6 -0
  16. pkg_auth-3.0.0/src/pkg_auth/authentication/adapters/keycloak/jwt_decoder.py +105 -0
  17. pkg_auth-3.0.0/src/pkg_auth/authentication/application/__init__.py +1 -0
  18. pkg_auth-3.0.0/src/pkg_auth/authentication/application/use_cases/__init__.py +1 -0
  19. pkg_auth-3.0.0/src/pkg_auth/authentication/application/use_cases/authenticate.py +91 -0
  20. pkg_auth-3.0.0/src/pkg_auth/authentication/domain/__init__.py +1 -0
  21. pkg_auth-3.0.0/src/pkg_auth/authentication/domain/entities.py +50 -0
  22. pkg_auth-3.0.0/src/pkg_auth/authentication/domain/exceptions.py +18 -0
  23. pkg_auth-3.0.0/src/pkg_auth/authentication/domain/ports.py +26 -0
  24. pkg_auth-3.0.0/src/pkg_auth/authentication/domain/value_objects.py +42 -0
  25. pkg_auth-3.0.0/src/pkg_auth/authorization/__init__.py +117 -0
  26. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/__init__.py +1 -0
  27. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/__init__.py +32 -0
  28. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/decorators.py +181 -0
  29. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/memory.py +61 -0
  30. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/protocol.py +36 -0
  31. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/cache/redis.py +60 -0
  32. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/__init__.py +37 -0
  33. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/apps.py +24 -0
  34. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/mixins.py +142 -0
  35. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/models.py +226 -0
  36. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/__init__.py +20 -0
  37. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/membership.py +118 -0
  38. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/organization.py +73 -0
  39. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py +71 -0
  40. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py +102 -0
  41. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/role.py +120 -0
  42. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/service.py +60 -0
  43. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/django_orm/repositories/user.py +77 -0
  44. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/__init__.py +90 -0
  45. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/base.py +55 -0
  46. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py +1 -0
  47. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py +293 -0
  48. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py +39 -0
  49. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py +65 -0
  50. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py +52 -0
  51. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py +116 -0
  52. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py +1 -0
  53. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/mixins.py +187 -0
  54. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/models.py +268 -0
  55. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py +16 -0
  56. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py +146 -0
  57. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py +97 -0
  58. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py +106 -0
  59. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py +127 -0
  60. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py +171 -0
  61. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py +93 -0
  62. pkg_auth-3.0.0/src/pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py +74 -0
  63. pkg_auth-3.0.0/src/pkg_auth/authorization/application/__init__.py +1 -0
  64. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/__init__.py +1 -0
  65. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/_helpers.py +82 -0
  66. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/check_permission.py +21 -0
  67. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/create_organization.py +41 -0
  68. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/create_role.py +69 -0
  69. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_membership.py +21 -0
  70. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_organization.py +21 -0
  71. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/delete_role.py +23 -0
  72. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/list_user_organizations.py +21 -0
  73. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/provision_default_services.py +38 -0
  74. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/register_permission_catalog.py +122 -0
  75. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/resolve_auth_context.py +70 -0
  76. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py +34 -0
  77. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/set_organization_service.py +50 -0
  78. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_permission_catalog.py +86 -0
  79. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_service_catalog.py +91 -0
  80. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py +32 -0
  81. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/update_organization.py +31 -0
  82. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/update_role.py +61 -0
  83. pkg_auth-3.0.0/src/pkg_auth/authorization/application/use_cases/upsert_membership.py +54 -0
  84. pkg_auth-3.0.0/src/pkg_auth/authorization/cli/__init__.py +1 -0
  85. pkg_auth-3.0.0/src/pkg_auth/authorization/cli/sync_catalog.py +180 -0
  86. pkg_auth-3.0.0/src/pkg_auth/authorization/cli/sync_services.py +151 -0
  87. pkg_auth-3.0.0/src/pkg_auth/authorization/config.py +21 -0
  88. pkg_auth-3.0.0/src/pkg_auth/authorization/domain/__init__.py +1 -0
  89. pkg_auth-3.0.0/src/pkg_auth/authorization/domain/entities.py +192 -0
  90. pkg_auth-3.0.0/src/pkg_auth/authorization/domain/exceptions.py +68 -0
  91. pkg_auth-3.0.0/src/pkg_auth/authorization/domain/ports.py +217 -0
  92. pkg_auth-3.0.0/src/pkg_auth/authorization/domain/value_objects.py +208 -0
  93. pkg_auth-3.0.0/src/pkg_auth/authorization/platform.py +47 -0
  94. pkg_auth-3.0.0/src/pkg_auth/integrations/__init__.py +0 -0
  95. pkg_auth-3.0.0/src/pkg_auth/integrations/django/__init__.py +32 -0
  96. pkg_auth-3.0.0/src/pkg_auth/integrations/django/apps.py +10 -0
  97. pkg_auth-3.0.0/src/pkg_auth/integrations/django/auth_context_middleware.py +105 -0
  98. pkg_auth-3.0.0/src/pkg_auth/integrations/django/decorators.py +74 -0
  99. pkg_auth-3.0.0/src/pkg_auth/integrations/django/install.py +136 -0
  100. pkg_auth-3.0.0/src/pkg_auth/integrations/django/middleware.py +63 -0
  101. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/__init__.py +26 -0
  102. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/auth_context_dep.py +150 -0
  103. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/auth_factory.py +84 -0
  104. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/decorators.py +55 -0
  105. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/errors.py +72 -0
  106. pkg_auth-3.0.0/src/pkg_auth/integrations/fastapi/identity_dep.py +41 -0
  107. pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/__init__.py +20 -0
  108. pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/auth.py +137 -0
  109. pkg_auth-3.0.0/src/pkg_auth/integrations/strawberry/permissions.py +56 -0
  110. pkg_auth-3.0.0/src/pkg_auth.egg-info/PKG-INFO +147 -0
  111. pkg_auth-3.0.0/src/pkg_auth.egg-info/SOURCES.txt +113 -0
  112. pkg_auth-3.0.0/src/pkg_auth.egg-info/dependency_links.txt +1 -0
  113. pkg_auth-3.0.0/src/pkg_auth.egg-info/entry_points.txt +4 -0
  114. pkg_auth-3.0.0/src/pkg_auth.egg-info/requires.txt +45 -0
  115. pkg_auth-3.0.0/src/pkg_auth.egg-info/top_level.txt +1 -0
@@ -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,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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()