cardo-python-utils 0.5.dev30__tar.gz → 0.5.dev32__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.
- cardo_python_utils-0.5.dev32/MANIFEST.in +5 -0
- {cardo_python_utils-0.5.dev30/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev32}/PKG-INFO +3 -1
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32/cardo_python_utils.egg-info}/PKG-INFO +3 -1
- cardo_python_utils-0.5.dev32/cardo_python_utils.egg-info/SOURCES.txt +58 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/requires.txt +2 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/pyproject.toml +3 -1
- cardo_python_utils-0.5.dev32/python_utils/django/README.md +122 -0
- cardo_python_utils-0.5.dev32/python_utils/django/__init__.py +1 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/admin/auth.py +33 -2
- cardo_python_utils-0.5.dev32/python_utils/django/admin/templates/__init__.py +3 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/admin/user_group.py +2 -2
- cardo_python_utils-0.5.dev32/python_utils/django/admin/views.py +63 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/api/utils.py +18 -1
- cardo_python_utils-0.5.dev32/python_utils/django/apps.py +5 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django → cardo_python_utils-0.5.dev32/python_utils/django/auth}/service.py +8 -6
- cardo_python_utils-0.5.dev32/python_utils/django/celery/__init__.py +4 -0
- cardo_python_utils-0.5.dev32/python_utils/django/celery/tenant_aware_task.py +31 -0
- cardo_python_utils-0.5.dev32/python_utils/django/db/routers.py +11 -0
- cardo_python_utils-0.5.dev32/python_utils/django/db/transaction.py +26 -0
- cardo_python_utils-0.5.dev32/python_utils/django/db/utils.py +66 -0
- cardo_python_utils-0.5.dev32/python_utils/django/management/commands/__init__.py +0 -0
- cardo_python_utils-0.5.dev32/python_utils/django/management/commands/migrateall.py +25 -0
- cardo_python_utils-0.5.dev32/python_utils/django/management/commands/tenant_aware_command.py +74 -0
- cardo_python_utils-0.5.dev32/python_utils/django/middleware/__init__.py +6 -0
- cardo_python_utils-0.5.dev32/python_utils/django/middleware/tenant_aware_http_middleware.py +113 -0
- cardo_python_utils-0.5.dev32/python_utils/django/models/__init__.py +0 -0
- cardo_python_utils-0.5.dev32/python_utils/django/oidc_settings.py +91 -0
- cardo_python_utils-0.5.dev32/python_utils/django/redis/__init__.py +4 -0
- cardo_python_utils-0.5.dev32/python_utils/django/redis/key_function.py +5 -0
- cardo_python_utils-0.5.dev32/python_utils/django/settings.py +17 -0
- cardo_python_utils-0.5.dev32/python_utils/django/storage/__init__.py +6 -0
- cardo_python_utils-0.5.dev32/python_utils/django/storage/tenant_aware_storage.py +86 -0
- cardo_python_utils-0.5.dev32/python_utils/django/tenant_context.py +89 -0
- cardo_python_utils-0.5.dev30/MANIFEST.in +0 -4
- cardo_python_utils-0.5.dev30/cardo_python_utils.egg-info/SOURCES.txt +0 -36
- cardo_python_utils-0.5.dev30/python_utils/keycloak/utils.py +0 -51
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/LICENSE +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/README.rst +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/top_level.txt +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/choices.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/data_structures.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/db.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django → cardo_python_utils-0.5.dev32/python_utils/django/admin}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django/admin → cardo_python_utils-0.5.dev32/python_utils/django/admin/templates}/user_groups_changelist.html +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django/admin → cardo_python_utils-0.5.dev32/python_utils/django/api}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/api/drf.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/api/ninja.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django/api → cardo_python_utils-0.5.dev32/python_utils/django/db}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak/django/models → cardo_python_utils-0.5.dev32/python_utils/django/management}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/models/user_group.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/tests/__init__.py +0 -0
- {cardo_python_utils-0.5.dev30/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/tests/conftest.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/django_utils.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/esma_choices.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/exceptions.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/imports.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/math.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/text.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/time.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/python_utils/types_hinting.py +0 -0
- {cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32}/setup.cfg +0 -0
{cardo_python_utils-0.5.dev30/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev32}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cardo-python-utils
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.dev32
|
|
4
4
|
Summary: Python library enhanced with a wide range of functions for different scenarios.
|
|
5
5
|
Author-email: CardoAI <hello@cardoai.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,12 +27,14 @@ License-File: LICENSE
|
|
|
27
27
|
Provides-Extra: django-keycloak
|
|
28
28
|
Requires-Dist: PyJWT>=2.10.1; extra == "django-keycloak"
|
|
29
29
|
Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-keycloak"
|
|
30
|
+
Requires-Dist: requests; extra == "django-keycloak"
|
|
30
31
|
Provides-Extra: django-keycloak-groups
|
|
31
32
|
Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
|
|
32
33
|
Provides-Extra: all
|
|
33
34
|
Requires-Dist: PyJWT>=2.10.1; extra == "all"
|
|
34
35
|
Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "all"
|
|
35
36
|
Requires-Dist: python-keycloak>=5.8.1; extra == "all"
|
|
37
|
+
Requires-Dist: requests; extra == "all"
|
|
36
38
|
Provides-Extra: dev
|
|
37
39
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
40
|
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
{cardo_python_utils-0.5.dev30 → cardo_python_utils-0.5.dev32/cardo_python_utils.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cardo-python-utils
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.dev32
|
|
4
4
|
Summary: Python library enhanced with a wide range of functions for different scenarios.
|
|
5
5
|
Author-email: CardoAI <hello@cardoai.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,12 +27,14 @@ License-File: LICENSE
|
|
|
27
27
|
Provides-Extra: django-keycloak
|
|
28
28
|
Requires-Dist: PyJWT>=2.10.1; extra == "django-keycloak"
|
|
29
29
|
Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-keycloak"
|
|
30
|
+
Requires-Dist: requests; extra == "django-keycloak"
|
|
30
31
|
Provides-Extra: django-keycloak-groups
|
|
31
32
|
Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
|
|
32
33
|
Provides-Extra: all
|
|
33
34
|
Requires-Dist: PyJWT>=2.10.1; extra == "all"
|
|
34
35
|
Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "all"
|
|
35
36
|
Requires-Dist: python-keycloak>=5.8.1; extra == "all"
|
|
37
|
+
Requires-Dist: requests; extra == "all"
|
|
36
38
|
Provides-Extra: dev
|
|
37
39
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
40
|
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.rst
|
|
4
|
+
pyproject.toml
|
|
5
|
+
cardo_python_utils.egg-info/PKG-INFO
|
|
6
|
+
cardo_python_utils.egg-info/SOURCES.txt
|
|
7
|
+
cardo_python_utils.egg-info/dependency_links.txt
|
|
8
|
+
cardo_python_utils.egg-info/requires.txt
|
|
9
|
+
cardo_python_utils.egg-info/top_level.txt
|
|
10
|
+
python_utils/__init__.py
|
|
11
|
+
python_utils/choices.py
|
|
12
|
+
python_utils/data_structures.py
|
|
13
|
+
python_utils/db.py
|
|
14
|
+
python_utils/django_utils.py
|
|
15
|
+
python_utils/esma_choices.py
|
|
16
|
+
python_utils/exceptions.py
|
|
17
|
+
python_utils/imports.py
|
|
18
|
+
python_utils/math.py
|
|
19
|
+
python_utils/text.py
|
|
20
|
+
python_utils/time.py
|
|
21
|
+
python_utils/types_hinting.py
|
|
22
|
+
python_utils/django/README.md
|
|
23
|
+
python_utils/django/__init__.py
|
|
24
|
+
python_utils/django/apps.py
|
|
25
|
+
python_utils/django/oidc_settings.py
|
|
26
|
+
python_utils/django/settings.py
|
|
27
|
+
python_utils/django/tenant_context.py
|
|
28
|
+
python_utils/django/admin/__init__.py
|
|
29
|
+
python_utils/django/admin/auth.py
|
|
30
|
+
python_utils/django/admin/user_group.py
|
|
31
|
+
python_utils/django/admin/views.py
|
|
32
|
+
python_utils/django/admin/templates/__init__.py
|
|
33
|
+
python_utils/django/admin/templates/user_groups_changelist.html
|
|
34
|
+
python_utils/django/api/__init__.py
|
|
35
|
+
python_utils/django/api/drf.py
|
|
36
|
+
python_utils/django/api/ninja.py
|
|
37
|
+
python_utils/django/api/utils.py
|
|
38
|
+
python_utils/django/auth/service.py
|
|
39
|
+
python_utils/django/celery/__init__.py
|
|
40
|
+
python_utils/django/celery/tenant_aware_task.py
|
|
41
|
+
python_utils/django/db/__init__.py
|
|
42
|
+
python_utils/django/db/routers.py
|
|
43
|
+
python_utils/django/db/transaction.py
|
|
44
|
+
python_utils/django/db/utils.py
|
|
45
|
+
python_utils/django/management/__init__.py
|
|
46
|
+
python_utils/django/management/commands/__init__.py
|
|
47
|
+
python_utils/django/management/commands/migrateall.py
|
|
48
|
+
python_utils/django/management/commands/tenant_aware_command.py
|
|
49
|
+
python_utils/django/middleware/__init__.py
|
|
50
|
+
python_utils/django/middleware/tenant_aware_http_middleware.py
|
|
51
|
+
python_utils/django/models/__init__.py
|
|
52
|
+
python_utils/django/models/user_group.py
|
|
53
|
+
python_utils/django/redis/__init__.py
|
|
54
|
+
python_utils/django/redis/key_function.py
|
|
55
|
+
python_utils/django/storage/__init__.py
|
|
56
|
+
python_utils/django/storage/tenant_aware_storage.py
|
|
57
|
+
python_utils/django/tests/__init__.py
|
|
58
|
+
python_utils/django/tests/conftest.py
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
PyJWT>=2.10.1
|
|
4
4
|
mozilla-django-oidc>=4.0.1
|
|
5
5
|
python-keycloak>=5.8.1
|
|
6
|
+
requests
|
|
6
7
|
|
|
7
8
|
[dev]
|
|
8
9
|
pytest>=7.0
|
|
@@ -13,6 +14,7 @@ tox>=3.25
|
|
|
13
14
|
[django-keycloak]
|
|
14
15
|
PyJWT>=2.10.1
|
|
15
16
|
mozilla-django-oidc>=4.0.1
|
|
17
|
+
requests
|
|
16
18
|
|
|
17
19
|
[django-keycloak-groups]
|
|
18
20
|
python-keycloak>=5.8.1
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cardo-python-utils"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.dev32"
|
|
8
8
|
description = "Python library enhanced with a wide range of functions for different scenarios."
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -34,6 +34,7 @@ dependencies = []
|
|
|
34
34
|
django-keycloak = [
|
|
35
35
|
"PyJWT>=2.10.1",
|
|
36
36
|
"mozilla-django-oidc>=4.0.1",
|
|
37
|
+
"requests",
|
|
37
38
|
]
|
|
38
39
|
django-keycloak-groups = [
|
|
39
40
|
"python-keycloak>=5.8.1",
|
|
@@ -42,6 +43,7 @@ all = [
|
|
|
42
43
|
"PyJWT>=2.10.1",
|
|
43
44
|
"mozilla-django-oidc>=4.0.1",
|
|
44
45
|
"python-keycloak>=5.8.1",
|
|
46
|
+
"requests",
|
|
45
47
|
]
|
|
46
48
|
dev = [
|
|
47
49
|
"pytest>=7.0",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
This package provides utilities for facilitating IDP communication and multi-tenancy support.
|
|
2
|
+
|
|
3
|
+
# Usage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Environment variables to set
|
|
7
|
+
|
|
8
|
+
- AWS_STORAGE_TENANT_BUCKET_NAMES
|
|
9
|
+
- A JSON dictionary where each key is the tenant name and the value is the bucket name.
|
|
10
|
+
- DATABASE_CONFIG
|
|
11
|
+
- A dictionary where each key is the tenant name and the value is a dict with the datase config.
|
|
12
|
+
- If multiple 'DATABASE_CONFIG'-prefixed variables are set, they will be merged into a single dictionary.
|
|
13
|
+
- KEYCLOAK_SERVER_URL
|
|
14
|
+
- The URL of the Keycloak deployment
|
|
15
|
+
- KEYCLOAK_CONFIDENTIAL_CLIENT_ID
|
|
16
|
+
- The id of the confidential client of the backend service
|
|
17
|
+
|
|
18
|
+
## settings.py file
|
|
19
|
+
|
|
20
|
+
### For multi-tenancy
|
|
21
|
+
|
|
22
|
+
```python3
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
...
|
|
25
|
+
"python_utils.django",
|
|
26
|
+
...
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
MIDDLEWARE = [
|
|
30
|
+
"python_utils.django.middleware.TenantAwareHttpMiddleware",
|
|
31
|
+
...
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Include the database configuration for each tenant in the DATABASES setting.
|
|
35
|
+
# You can use the get_database_configs() function from python_utils.django.db.utils as a helper.
|
|
36
|
+
DATABASES = {
|
|
37
|
+
'tenant1': { ... },
|
|
38
|
+
'tenant2': { ... },
|
|
39
|
+
...
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# If you want to override the database alias to use for local development (when DEBUG is True).
|
|
43
|
+
# By default, the first database defined in DATABASES is used.
|
|
44
|
+
DEVELOPMENT_TENANT = "development"
|
|
45
|
+
|
|
46
|
+
# This is required to use the tenant context when routing database queries
|
|
47
|
+
DATABASE_ROUTERS = [
|
|
48
|
+
"python_utils.django.db.routers.TenantAwareRouter"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# If using celery, set the task class to TenantAwareTask:
|
|
52
|
+
CELERY_TASK_CLS = "python_utils.django.celery.TenantAwareTask"
|
|
53
|
+
|
|
54
|
+
# If using Redis caching, configure the cache backend as follows:
|
|
55
|
+
CACHES = {
|
|
56
|
+
"default": {
|
|
57
|
+
"BACKEND": "django_redis.cache.RedisCache",
|
|
58
|
+
"LOCATION": REDIS_LOCATION,
|
|
59
|
+
"KEY_FUNCTION": "python_utils.django.redis.make_tenant_aware_key",
|
|
60
|
+
**OPTIONS,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# If using Django Storages with S3, configure the storage backends as follows:
|
|
65
|
+
STORAGES = {
|
|
66
|
+
"default": {
|
|
67
|
+
"BACKEND": "python_utils.django.storage.TenantAwarePrivateS3Storage",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# If you want to exclude certain paths from tenant processing, use TENANT_AWARE_EXCLUDED_PATHS:
|
|
72
|
+
# They are considered as prefixes, so all paths starting with the given strings will be excluded.
|
|
73
|
+
TENANT_AWARE_EXCLUDED_PATHS = ("/some/path",)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### For OIDC auth
|
|
77
|
+
|
|
78
|
+
```python3
|
|
79
|
+
from python_utils.django.admin.templates import TEMPLATE_PATH
|
|
80
|
+
|
|
81
|
+
INSTALLED_APPS.append("mozilla_django_oidc")
|
|
82
|
+
TEMPLATES[0]["DIRS"].append(TEMPLATE_PATH)
|
|
83
|
+
|
|
84
|
+
JWT_AUDIENCE = "myapp"
|
|
85
|
+
JWT_SCOPE_PREFIX = "myapp"
|
|
86
|
+
|
|
87
|
+
# If using DRF
|
|
88
|
+
REST_FRAMEWORK.update(
|
|
89
|
+
{
|
|
90
|
+
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
91
|
+
"DEFAULT_AUTHENTICATION_CLASSES": ("python_utils.django.api.drf.AuthenticationBackend",),
|
|
92
|
+
"DEFAULT_PERMISSION_CLASSES": (
|
|
93
|
+
"rest_framework.permissions.IsAuthenticated",
|
|
94
|
+
"python_utils.django.api.drf.HasScope",
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Admin auth backends
|
|
100
|
+
AUTHENTICATION_BACKENDS = [
|
|
101
|
+
"django.contrib.auth.backends.ModelBackend",
|
|
102
|
+
"python_utils.django.admin.auth.AdminAuthenticationBackend",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# If user groups are used for Row Level Security (RLS)
|
|
106
|
+
KEYCLOAK_USER_GROUP_MODEL = "myapp.UserGroup"
|
|
107
|
+
|
|
108
|
+
KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", f"{JWT_AUDIENCE}_confidential")
|
|
109
|
+
|
|
110
|
+
OIDC_RP_CLIENT_ID = KEYCLOAK_CONFIDENTIAL_CLIENT_ID
|
|
111
|
+
OIDC_RP_CLIENT_SECRET = None
|
|
112
|
+
OIDC_RP_SIGN_ALGO = "RS256"
|
|
113
|
+
OIDC_CREATE_USER = True
|
|
114
|
+
|
|
115
|
+
LOGIN_REDIRECT_URL = "/admin"
|
|
116
|
+
SESSION_COOKIE_AGE = 60 * 30 # 30 minutes
|
|
117
|
+
SESSION_SAVE_EVERY_REQUEST = True # Extend session on each request
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## With django-ninja
|
|
121
|
+
|
|
122
|
+
If using `django-ninja`, apart from the settings configured above, auth utils are provided in the django/api/ninja.py module.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = "python_utils.django.apps.DjangoAppConfig"
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from ..oidc_settings import (
|
|
5
|
+
get_oidc_confidential_client_token,
|
|
6
|
+
get_oidc_op_token_endpoint,
|
|
7
|
+
get_oidc_op_user_endpoint,
|
|
8
|
+
get_oidc_op_jwks_endpoint,
|
|
9
|
+
)
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
class AdminAuthenticationBackend(OIDCAuthenticationBackend):
|
|
13
|
+
"""
|
|
14
|
+
Tenant-aware OIDC authentication backend for Django admin.
|
|
15
|
+
|
|
16
|
+
This backend dynamically resolves OIDC endpoints based on the current
|
|
17
|
+
tenant context, allowing each tenant to authenticate against their
|
|
18
|
+
own Keycloak realm.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def OIDC_OP_TOKEN_ENDPOINT(self):
|
|
23
|
+
"""Dynamically get the token endpoint for the current tenant."""
|
|
24
|
+
|
|
25
|
+
return get_oidc_op_token_endpoint()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def OIDC_OP_USER_ENDPOINT(self):
|
|
29
|
+
"""Dynamically get the userinfo endpoint for the current tenant."""
|
|
30
|
+
|
|
31
|
+
return get_oidc_op_user_endpoint()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def OIDC_OP_JWKS_ENDPOINT(self):
|
|
35
|
+
"""Dynamically get the JWKS endpoint for the current tenant."""
|
|
36
|
+
|
|
37
|
+
return get_oidc_op_jwks_endpoint()
|
|
38
|
+
|
|
8
39
|
def get_token(self, payload):
|
|
9
40
|
# Instead of passing client_id and client_secret,
|
|
10
41
|
# client_assertion with service account token will be used
|
|
11
42
|
payload.pop("client_id", None)
|
|
12
43
|
payload.pop("client_secret", None)
|
|
13
44
|
|
|
14
|
-
return
|
|
45
|
+
return get_oidc_confidential_client_token(**payload)
|
|
15
46
|
|
|
16
47
|
def _get_user_data(self, claims) -> dict:
|
|
17
48
|
client_roles = (
|
|
@@ -5,7 +5,7 @@ from django.db.models import Count
|
|
|
5
5
|
from django.urls import path
|
|
6
6
|
from django.shortcuts import redirect
|
|
7
7
|
|
|
8
|
-
from ..service import KeycloakService
|
|
8
|
+
from ..auth.service import KeycloakService
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class UserGroupAdminMetaclass(forms.MediaDefiningClass):
|
|
@@ -134,7 +134,7 @@ class UserGroupAdminBase(admin.ModelAdmin, metaclass=UserGroupAdminMetaclass):
|
|
|
134
134
|
]
|
|
135
135
|
|
|
136
136
|
@staticmethod
|
|
137
|
-
def sync_groups_with_keycloak(request):
|
|
137
|
+
def sync_groups_with_keycloak(request):
|
|
138
138
|
"""Syncs user groups with Keycloak"""
|
|
139
139
|
|
|
140
140
|
try:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tenant-aware OIDC views for mozilla-django-oidc.
|
|
3
|
+
|
|
4
|
+
These views override the default mozilla-django-oidc views to dynamically
|
|
5
|
+
resolve OIDC endpoints based on the current tenant context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mozilla_django_oidc.views import (
|
|
9
|
+
OIDCAuthenticationCallbackView,
|
|
10
|
+
OIDCAuthenticationRequestView,
|
|
11
|
+
OIDCLogoutView,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..oidc_settings import (
|
|
15
|
+
get_oidc_op_authorization_endpoint,
|
|
16
|
+
get_oidc_op_logout_endpoint,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TenantAwareOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
|
|
21
|
+
"""
|
|
22
|
+
Tenant-aware OIDC authentication request view.
|
|
23
|
+
|
|
24
|
+
Dynamically resolves the authorization endpoint based on the current tenant.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def OIDC_OP_AUTH_ENDPOINT(self):
|
|
29
|
+
"""Dynamically get the authorization endpoint for the current tenant."""
|
|
30
|
+
return get_oidc_op_authorization_endpoint()
|
|
31
|
+
|
|
32
|
+
def get_settings(self, attr, *args):
|
|
33
|
+
if attr == "OIDC_OP_AUTHORIZATION_ENDPOINT":
|
|
34
|
+
return self.OIDC_OP_AUTH_ENDPOINT
|
|
35
|
+
return super().get_settings(attr, *args)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TenantAwareOIDCAuthenticationCallbackView(OIDCAuthenticationCallbackView):
|
|
39
|
+
"""
|
|
40
|
+
Tenant-aware OIDC authentication callback view.
|
|
41
|
+
|
|
42
|
+
Uses the tenant-aware authentication backend.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TenantAwareOIDCLogoutView(OIDCLogoutView):
|
|
49
|
+
"""
|
|
50
|
+
Tenant-aware OIDC logout view.
|
|
51
|
+
|
|
52
|
+
Dynamically resolves the logout endpoint based on the current tenant.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def OIDC_OP_LOGOUT_ENDPOINT(self):
|
|
57
|
+
"""Dynamically get the logout endpoint for the current tenant."""
|
|
58
|
+
return get_oidc_op_logout_endpoint()
|
|
59
|
+
|
|
60
|
+
def get_settings(self, attr, *args):
|
|
61
|
+
if attr == "OIDC_OP_LOGOUT_ENDPOINT":
|
|
62
|
+
return self.OIDC_OP_LOGOUT_ENDPOINT
|
|
63
|
+
return super().get_settings(attr, *args)
|
|
@@ -4,10 +4,26 @@ from django.conf import settings
|
|
|
4
4
|
from django.contrib.auth import get_user_model
|
|
5
5
|
from jwt import decode, PyJWKClient
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from django.oidc_settings import get_oidc_op_jwks_endpoint
|
|
8
|
+
|
|
9
|
+
from ..tenant_context import TenantContext
|
|
10
|
+
|
|
11
|
+
JWKS_CLIENTS = {} # Cache for PyJWKClient instances
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_jwks_client():
|
|
15
|
+
tenant = TenantContext.get()
|
|
16
|
+
|
|
17
|
+
if tenant not in JWKS_CLIENTS:
|
|
18
|
+
jwks_client = PyJWKClient(get_oidc_op_jwks_endpoint())
|
|
19
|
+
JWKS_CLIENTS[tenant] = jwks_client
|
|
20
|
+
|
|
21
|
+
return JWKS_CLIENTS[tenant]
|
|
22
|
+
|
|
8
23
|
|
|
9
24
|
User = get_user_model()
|
|
10
25
|
|
|
26
|
+
|
|
11
27
|
class TokenPayload(TypedDict, total=False):
|
|
12
28
|
exp: int
|
|
13
29
|
iat: int
|
|
@@ -35,6 +51,7 @@ def decode_jwt(token: str, audience: Optional[str] = None) -> TokenPayload:
|
|
|
35
51
|
jwt.exceptions.PyJWKClientError: If there is an error fetching the signing key.
|
|
36
52
|
jwt.exceptions.InvalidTokenError: If the token is invalid or cannot be decoded.
|
|
37
53
|
"""
|
|
54
|
+
jwks_client = get_jwks_client()
|
|
38
55
|
signing_key = jwks_client.get_signing_key_from_jwt(token)
|
|
39
56
|
|
|
40
57
|
if audience is None:
|
|
@@ -5,12 +5,12 @@ from keycloak import KeycloakAdmin
|
|
|
5
5
|
from keycloak import KeycloakOpenIDConnection
|
|
6
6
|
from keycloak.exceptions import KeycloakGetError
|
|
7
7
|
|
|
8
|
-
from ..
|
|
8
|
+
from ..oidc_settings import (
|
|
9
9
|
KEYCLOAK_SERVER_URL,
|
|
10
|
-
KEYCLOAK_REALM,
|
|
11
10
|
KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
|
|
12
|
-
|
|
11
|
+
get_oidc_confidential_client_token,
|
|
13
12
|
)
|
|
13
|
+
from ..tenant_context import TenantContext
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def get_user_group_model():
|
|
@@ -67,12 +67,14 @@ class KeycloakService:
|
|
|
67
67
|
deleted_groups.delete()
|
|
68
68
|
|
|
69
69
|
def _get_keycloak_admin(self):
|
|
70
|
+
tenant = TenantContext.get()
|
|
71
|
+
|
|
70
72
|
keycloak_connection = KeycloakOpenIDConnection(
|
|
71
73
|
server_url=KEYCLOAK_SERVER_URL,
|
|
72
|
-
realm_name=
|
|
73
|
-
user_realm_name=
|
|
74
|
+
realm_name=tenant,
|
|
75
|
+
user_realm_name=tenant,
|
|
74
76
|
client_id=KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
|
|
75
|
-
token=
|
|
77
|
+
token=get_oidc_confidential_client_token(),
|
|
76
78
|
verify=True,
|
|
77
79
|
)
|
|
78
80
|
return KeycloakAdmin(connection=keycloak_connection)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from celery import Task
|
|
2
|
+
|
|
3
|
+
from ..settings import TENANT_KEY
|
|
4
|
+
from ..tenant_context import TenantContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TenantAwareTask(Task):
|
|
8
|
+
#: Enable argument checking.
|
|
9
|
+
#: You can set this to false if you don't want the signature to be
|
|
10
|
+
#: checked when calling the task.
|
|
11
|
+
#: Set to false because we are attaching a tenant to the task
|
|
12
|
+
#: and we don't want to check the signature.
|
|
13
|
+
#: Defaults to :attr:`app.strict_typing <@Celery.strict_typing>`.
|
|
14
|
+
typing = False
|
|
15
|
+
|
|
16
|
+
def __call__(self, *args, **kwargs):
|
|
17
|
+
"""Override the __call__ method to set the tenant name in the thread namespace."""
|
|
18
|
+
|
|
19
|
+
# Celery backend_cleanup doesn't need a tenant and cannot be configured to pass the tenant kwarg
|
|
20
|
+
# because it is dynamically generated at @connect_on_app_finalize by celery itself
|
|
21
|
+
# ref: celery/app/builtins.py def add_backend_cleanup_task
|
|
22
|
+
if self.name != "celery.backend_cleanup":
|
|
23
|
+
tenant = kwargs.pop(TENANT_KEY)
|
|
24
|
+
TenantContext.set(tenant)
|
|
25
|
+
|
|
26
|
+
return self.run(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
def after_return(self, status, retval, task_id, args, kwargs, einfo):
|
|
29
|
+
"""Clear the tenant from the thread namespace after the task has returned."""
|
|
30
|
+
TenantContext.clear()
|
|
31
|
+
super().after_return(status, retval, task_id, args, kwargs, einfo)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from django.db.transaction import Atomic
|
|
2
|
+
|
|
3
|
+
from ..tenant_context import TenantContext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TenantAtomic(Atomic):
|
|
7
|
+
"""
|
|
8
|
+
A transaction that can be used in a multi-tenant application.
|
|
9
|
+
This transaction is bound to the current tenant.
|
|
10
|
+
The default implementation is to use the default database.
|
|
11
|
+
We want to override this by using the tenant database alias instead.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __enter__(self):
|
|
15
|
+
self.using = TenantContext.get()
|
|
16
|
+
super().__enter__()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def tenant_atomic(using=None, savepoint=True, durable=False):
|
|
20
|
+
# Bare decorator: @atomic -- although the first argument is called
|
|
21
|
+
# `using`, it's actually the function being decorated.
|
|
22
|
+
if callable(using):
|
|
23
|
+
return TenantAtomic(using=None, savepoint=savepoint, durable=durable)(using)
|
|
24
|
+
# Decorator: @atomic(...) or context manager: with atomic(...): ...
|
|
25
|
+
else:
|
|
26
|
+
return TenantAtomic(using, savepoint, durable)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from functools import reduce
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import NotRequired, TypedDict
|
|
5
|
+
from django.db import connections
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatabaseConfigData(TypedDict):
|
|
9
|
+
host: str
|
|
10
|
+
name: str
|
|
11
|
+
user: str
|
|
12
|
+
password: str
|
|
13
|
+
port: NotRequired[int]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_connection(tenant: str = None):
|
|
17
|
+
"""
|
|
18
|
+
Get the connection to the database with the given tenant or the default one.
|
|
19
|
+
Default value will be retrieved from TenantContext.get()
|
|
20
|
+
This uses the connections dict from django.db to get the required connection.
|
|
21
|
+
|
|
22
|
+
The default implementation of this function uses the 'default' alias
|
|
23
|
+
if not argument is given. We want to use the TenantContext class to
|
|
24
|
+
determine the correct alias.
|
|
25
|
+
Args:
|
|
26
|
+
tenant: Name of the tenant database alias
|
|
27
|
+
|
|
28
|
+
Returns: The connection to the database
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
from ..tenant_context import TenantContext
|
|
32
|
+
|
|
33
|
+
database_alias = tenant or TenantContext.get()
|
|
34
|
+
connection = connections[database_alias]
|
|
35
|
+
|
|
36
|
+
return connection
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_database_configs() -> dict[str, DatabaseConfigData]:
|
|
40
|
+
"""
|
|
41
|
+
The env variables prefixed with 'DATABASE_CONFIG' should be provided in the following format:
|
|
42
|
+
{
|
|
43
|
+
"tenant1": {
|
|
44
|
+
"host": "",
|
|
45
|
+
"name": "",
|
|
46
|
+
"user": "",
|
|
47
|
+
"password": "",
|
|
48
|
+
"port": 5432 / null
|
|
49
|
+
},
|
|
50
|
+
"tenant2": {
|
|
51
|
+
"host": "",
|
|
52
|
+
"name": "",
|
|
53
|
+
"user": "",
|
|
54
|
+
"password": ""
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
If multiple 'DATABASE_CONFIG'-prefixed variables are set, they will be merged into a single dictionary.
|
|
58
|
+
"""
|
|
59
|
+
configs = [json.loads(v or "{}") for k, v in os.environ.items() if k.startswith("DATABASE_CONFIG")]
|
|
60
|
+
database_configs = reduce(lambda a, b: {**a, **b}, configs, {})
|
|
61
|
+
for tenant, db_config in database_configs.items():
|
|
62
|
+
for key in ["host", "name", "user", "password"]:
|
|
63
|
+
if key not in db_config:
|
|
64
|
+
raise ValueError(f"Missing required database config '{key}' for tenant {tenant}")
|
|
65
|
+
|
|
66
|
+
return database_configs
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django.core.management.commands.migrate import Command as BaseMigrateCommand
|
|
2
|
+
from django.db.models.signals import post_migrate, pre_migrate
|
|
3
|
+
from django.dispatch import receiver
|
|
4
|
+
|
|
5
|
+
from ...settings import TENANT_DATABASES
|
|
6
|
+
from ...tenant_context import TenantContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@receiver(pre_migrate)
|
|
10
|
+
def set_tenant_pre_migrate(sender, **kwargs):
|
|
11
|
+
database_alias = kwargs.get("using")
|
|
12
|
+
TenantContext.set(database_alias)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@receiver(post_migrate)
|
|
16
|
+
def clear_tenant_post_migrate(sender, **kwargs):
|
|
17
|
+
TenantContext.clear()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Command(BaseMigrateCommand):
|
|
21
|
+
def handle(self, *args, **options):
|
|
22
|
+
for tenant in TENANT_DATABASES:
|
|
23
|
+
self.stdout.write(f"Migrating tenant: {tenant}")
|
|
24
|
+
options["database"] = tenant
|
|
25
|
+
super().handle(*args, **options)
|