cardo-python-utils 0.5.dev31__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.dev31/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev32}/PKG-INFO +3 -1
- {cardo_python_utils-0.5.dev31 → 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.dev31 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/requires.txt +2 -0
- {cardo_python_utils-0.5.dev31 → 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.dev31/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.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/admin/user_group.py +1 -1
- cardo_python_utils-0.5.dev32/python_utils/django/admin/views.py +63 -0
- {cardo_python_utils-0.5.dev31/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.dev31/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/middleware/__init__.py +6 -0
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/middleware/tenant_http_middleware.py → cardo_python_utils-0.5.dev32/python_utils/django/middleware/tenant_aware_http_middleware.py +1 -8
- 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.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/redis/key_function.py +1 -1
- cardo_python_utils-0.5.dev31/MANIFEST.in +0 -5
- cardo_python_utils-0.5.dev31/cardo_python_utils.egg-info/SOURCES.txt +0 -57
- cardo_python_utils-0.5.dev31/python_utils/keycloak/utils.py +0 -51
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/README.md +0 -62
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/__init__.py +0 -1
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/apps.py +0 -7
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/management/commands/__init__.py +0 -0
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/middleware/__init__.py +0 -6
- cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/redis/__init__.py +0 -4
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/LICENSE +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/README.rst +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/cardo_python_utils.egg-info/top_level.txt +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/choices.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/data_structures.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/db.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak/django → cardo_python_utils-0.5.dev32/python_utils/django/admin}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/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.dev31/python_utils/keycloak/django/admin → cardo_python_utils-0.5.dev32/python_utils/django/api}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/api/drf.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/api/ninja.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/celery/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/celery/tenant_aware_task.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/api → cardo_python_utils-0.5.dev32/python_utils/django/db}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/db/routers.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/db/transaction.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/db/utils.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/models → cardo_python_utils-0.5.dev32/python_utils/django/management}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/db → cardo_python_utils-0.5.dev32/python_utils/django/management/commands}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/management/commands/migrateall.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/management/commands/tenant_aware_command.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/management → cardo_python_utils-0.5.dev32/python_utils/django/models}/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/models/user_group.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/settings.py +1 -1
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/storage/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/storage/tenant_aware_storage.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev32/python_utils/django}/tenant_context.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/tests/__init__.py +0 -0
- {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev32/python_utils}/django/tests/conftest.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/django_utils.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/esma_choices.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/exceptions.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/imports.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/math.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/text.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/time.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/types_hinting.py +0 -0
- {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/setup.cfg +0 -0
{cardo_python_utils-0.5.dev31/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.dev31 → 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 = (
|
|
@@ -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)
|
|
@@ -98,19 +98,12 @@ class TenantAwareHttpMiddleware:
|
|
|
98
98
|
|
|
99
99
|
# Need at least 3 parts: <app>.<tenant>.<domain>
|
|
100
100
|
if len(parts) >= 3:
|
|
101
|
-
tenant =
|
|
101
|
+
tenant = parts[1].replace("-internal", "")
|
|
102
102
|
logger.debug(f"Tenant '{tenant}' extracted from subdomain: {host}")
|
|
103
103
|
return tenant
|
|
104
104
|
|
|
105
105
|
return None
|
|
106
106
|
|
|
107
|
-
@staticmethod
|
|
108
|
-
def _normalize_tenant(tenant: str) -> str:
|
|
109
|
-
# Remove -internal suffix if present
|
|
110
|
-
if tenant.endswith("-internal"):
|
|
111
|
-
return tenant[: -len("-internal")]
|
|
112
|
-
return tenant
|
|
113
|
-
|
|
114
107
|
@staticmethod
|
|
115
108
|
def _is_valid_tenant(tenant: str) -> bool:
|
|
116
109
|
"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides dynamic OIDC configuration based on the current tenant context.
|
|
3
|
+
Each tenant has its own Keycloak realm, and the OIDC endpoints are computed
|
|
4
|
+
dynamically based on the tenant.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .tenant_context import TenantContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL", None)
|
|
15
|
+
if not KEYCLOAK_SERVER_URL:
|
|
16
|
+
raise ValueError("KEYCLOAK_SERVER_URL env variable is not set.")
|
|
17
|
+
|
|
18
|
+
KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", None)
|
|
19
|
+
if not KEYCLOAK_CONFIDENTIAL_CLIENT_ID:
|
|
20
|
+
raise ValueError("KEYCLOAK_CONFIDENTIAL_CLIENT_ID env variable is not set.")
|
|
21
|
+
|
|
22
|
+
KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS: dict[str, str] = json.loads(
|
|
23
|
+
os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS")
|
|
24
|
+
)
|
|
25
|
+
KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
|
|
26
|
+
KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_oidc_op_base_url() -> str:
|
|
30
|
+
"""Get the base URL for the OIDC provider (Keycloak realm URL)."""
|
|
31
|
+
|
|
32
|
+
realm = TenantContext.get()
|
|
33
|
+
return f"{KEYCLOAK_SERVER_URL}/realms/{realm}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_oidc_op_authorization_endpoint() -> str:
|
|
37
|
+
return f"{get_oidc_op_base_url()}/protocol/openid-connect/auth"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_oidc_op_token_endpoint() -> str:
|
|
41
|
+
return f"{get_oidc_op_base_url()}/protocol/openid-connect/token"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_oidc_op_user_endpoint() -> str:
|
|
45
|
+
return f"{get_oidc_op_base_url()}/protocol/openid-connect/userinfo"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_oidc_op_jwks_endpoint() -> str:
|
|
49
|
+
return f"{get_oidc_op_base_url()}/protocol/openid-connect/certs"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_oidc_op_logout_endpoint() -> str:
|
|
53
|
+
return f"{get_oidc_op_base_url()}/protocol/openid-connect/logout"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_confidential_client_service_account_token() -> str:
|
|
57
|
+
"""
|
|
58
|
+
Reads the Keycloak confidential client service account token for the current tenant from a file.
|
|
59
|
+
"""
|
|
60
|
+
tenant = TenantContext.get()
|
|
61
|
+
token_file_path = KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS.get(tenant)
|
|
62
|
+
if not token_file_path or not os.path.isfile(token_file_path):
|
|
63
|
+
raise FileNotFoundError(f"Keycloak service account token file for tenant {tenant} not found: {token_file_path}")
|
|
64
|
+
|
|
65
|
+
with open(token_file_path, "r") as f:
|
|
66
|
+
token = f.read().strip()
|
|
67
|
+
|
|
68
|
+
if not token:
|
|
69
|
+
raise ValueError(f"Keycloak service account token for tenant {tenant} is empty.")
|
|
70
|
+
|
|
71
|
+
return token
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_oidc_confidential_client_token(**kwargs) -> dict:
|
|
75
|
+
"""
|
|
76
|
+
Obtains token for an OIDC confidential client with the client credentials grant,
|
|
77
|
+
using a service account token for authentication.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
response = requests.post(
|
|
81
|
+
get_oidc_op_token_endpoint(),
|
|
82
|
+
data={
|
|
83
|
+
"grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
|
|
84
|
+
"client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
|
|
85
|
+
"client_assertion": get_confidential_client_service_account_token(),
|
|
86
|
+
**kwargs,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
|
|
91
|
+
return response.json()
|
|
@@ -1,57 +0,0 @@
|
|
|
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/keycloak/utils.py
|
|
23
|
-
python_utils/keycloak/django/__init__.py
|
|
24
|
-
python_utils/keycloak/django/service.py
|
|
25
|
-
python_utils/keycloak/django/admin/__init__.py
|
|
26
|
-
python_utils/keycloak/django/admin/auth.py
|
|
27
|
-
python_utils/keycloak/django/admin/user_group.py
|
|
28
|
-
python_utils/keycloak/django/admin/user_groups_changelist.html
|
|
29
|
-
python_utils/keycloak/django/api/__init__.py
|
|
30
|
-
python_utils/keycloak/django/api/drf.py
|
|
31
|
-
python_utils/keycloak/django/api/ninja.py
|
|
32
|
-
python_utils/keycloak/django/api/utils.py
|
|
33
|
-
python_utils/keycloak/django/models/__init__.py
|
|
34
|
-
python_utils/keycloak/django/models/user_group.py
|
|
35
|
-
python_utils/keycloak/django/tests/__init__.py
|
|
36
|
-
python_utils/keycloak/django/tests/conftest.py
|
|
37
|
-
python_utils/multi_tenancy/README.md
|
|
38
|
-
python_utils/multi_tenancy/__init__.py
|
|
39
|
-
python_utils/multi_tenancy/apps.py
|
|
40
|
-
python_utils/multi_tenancy/settings.py
|
|
41
|
-
python_utils/multi_tenancy/tenant_context.py
|
|
42
|
-
python_utils/multi_tenancy/celery/__init__.py
|
|
43
|
-
python_utils/multi_tenancy/celery/tenant_aware_task.py
|
|
44
|
-
python_utils/multi_tenancy/db/__init__.py
|
|
45
|
-
python_utils/multi_tenancy/db/routers.py
|
|
46
|
-
python_utils/multi_tenancy/db/transaction.py
|
|
47
|
-
python_utils/multi_tenancy/db/utils.py
|
|
48
|
-
python_utils/multi_tenancy/management/__init__.py
|
|
49
|
-
python_utils/multi_tenancy/management/commands/__init__.py
|
|
50
|
-
python_utils/multi_tenancy/management/commands/migrateall.py
|
|
51
|
-
python_utils/multi_tenancy/management/commands/tenant_aware_command.py
|
|
52
|
-
python_utils/multi_tenancy/middleware/__init__.py
|
|
53
|
-
python_utils/multi_tenancy/middleware/tenant_http_middleware.py
|
|
54
|
-
python_utils/multi_tenancy/redis/__init__.py
|
|
55
|
-
python_utils/multi_tenancy/redis/key_function.py
|
|
56
|
-
python_utils/multi_tenancy/storage/__init__.py
|
|
57
|
-
python_utils/multi_tenancy/storage/tenant_aware_storage.py
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import requests
|
|
3
|
-
|
|
4
|
-
KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL")
|
|
5
|
-
KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM")
|
|
6
|
-
KEYCLOAK_REALM_URL = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}"
|
|
7
|
-
KEYCLOAK_TOKEN_ENDPOINT = f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/token"
|
|
8
|
-
|
|
9
|
-
KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID")
|
|
10
|
-
KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH = os.getenv(
|
|
11
|
-
"KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH"
|
|
12
|
-
)
|
|
13
|
-
KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
|
|
14
|
-
KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def get_confidential_client_service_account_token() -> str:
|
|
18
|
-
"""
|
|
19
|
-
Reads the Keycloak confidential client service account token from a file.
|
|
20
|
-
"""
|
|
21
|
-
token_file_path = KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH
|
|
22
|
-
if not token_file_path or not os.path.isfile(token_file_path):
|
|
23
|
-
raise FileNotFoundError(f"Keycloak service account token file not found: {token_file_path}")
|
|
24
|
-
|
|
25
|
-
with open(token_file_path, "r") as f:
|
|
26
|
-
token = f.read().strip()
|
|
27
|
-
|
|
28
|
-
if not token:
|
|
29
|
-
raise ValueError("Keycloak service account token is empty.")
|
|
30
|
-
|
|
31
|
-
return token
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_keycloak_confidential_client_token(**kwargs) -> dict:
|
|
35
|
-
"""
|
|
36
|
-
Obtains token for a Keycloak confidential client with the client credentials grant,
|
|
37
|
-
using a service account token for authentication.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
response = requests.post(
|
|
41
|
-
KEYCLOAK_TOKEN_ENDPOINT,
|
|
42
|
-
data={
|
|
43
|
-
"grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
|
|
44
|
-
"client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
|
|
45
|
-
"client_assertion": get_confidential_client_service_account_token(),
|
|
46
|
-
**kwargs,
|
|
47
|
-
},
|
|
48
|
-
)
|
|
49
|
-
response.raise_for_status()
|
|
50
|
-
|
|
51
|
-
return response.json()
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
This package adds multi-tenancy support to a Django application, with a separate database for each tenant.
|
|
2
|
-
Several components are provided to facilitate this, including: database routing, middleware, celery tasks etc.
|
|
3
|
-
|
|
4
|
-
It is heavily inspired by the implementation of multi-tenancy in the Mercury app by Klement Omeri.
|
|
5
|
-
|
|
6
|
-
# Usage
|
|
7
|
-
|
|
8
|
-
To use this package, add the following to your Django settings.py file:
|
|
9
|
-
|
|
10
|
-
```python3
|
|
11
|
-
INSTALLED_APPS = [
|
|
12
|
-
...
|
|
13
|
-
"python_utils.multi_tenancy",
|
|
14
|
-
...
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
MIDDLEWARE = [
|
|
18
|
-
"python_utils.multi_tenancy.middleware.TenantAwareHttpMiddleware",
|
|
19
|
-
...
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
# Include the database configuration for each tenant in the DATABASES setting.
|
|
23
|
-
# You can use the get_database_configs() function from python_utils.multi_tenancy.db.utils as a helper.
|
|
24
|
-
DATABASES = {
|
|
25
|
-
'tenant1': { ... },
|
|
26
|
-
'tenant2': { ... },
|
|
27
|
-
...
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
# If you want to override the database alias to use for local development (when DEBUG is True).
|
|
31
|
-
# By default, the first database defined in DATABASES is used.
|
|
32
|
-
DEVELOPMENT_TENANT = "development"
|
|
33
|
-
|
|
34
|
-
# This is required to use the tenant context when routing database queries
|
|
35
|
-
DATABASE_ROUTERS = [
|
|
36
|
-
"python_utils.multi_tenancy.db.routers.TenantAwareRouter"
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
# If using celery, set the task class to TenantAwareTask:
|
|
40
|
-
CELERY_TASK_CLS = "python_utils.multi_tenancy.celery.TenantAwareTask"
|
|
41
|
-
|
|
42
|
-
# If using Redis caching, configure the cache backend as follows:
|
|
43
|
-
CACHES = {
|
|
44
|
-
"default": {
|
|
45
|
-
"BACKEND": "django_redis.cache.RedisCache",
|
|
46
|
-
"LOCATION": REDIS_LOCATION,
|
|
47
|
-
"KEY_FUNCTION": "python_utils.multi_tenancy.redis.make_key",
|
|
48
|
-
**OPTIONS,
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
# If using Django Storages with S3, configure the storage backends as follows:
|
|
53
|
-
STORAGES = {
|
|
54
|
-
"default": {
|
|
55
|
-
"BACKEND": "python_utils.multi_tenancy.storage.TenantAwarePrivateS3Storage",
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# If you want to exclude certain paths from tenant processing, use TENANT_AWARE_EXCLUDED_PATHS:
|
|
60
|
-
# They are considered as prefixes, so all paths starting with the given strings will be excluded.
|
|
61
|
-
TENANT_AWARE_EXCLUDED_PATHS = ("/some/path",)
|
|
62
|
-
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
default_app_config = "python_utils.multi_tenancy.apps.MultiTenancyConfig"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev32}/python_utils/data_structures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
|
|
3
|
-
TENANT_DATABASES = set(settings.DATABASES.keys()) - {"default"}
|
|
4
3
|
TENANT_KEY = "tenant"
|
|
4
|
+
TENANT_DATABASES = set(settings.DATABASES.keys()) - {"default"}
|
|
5
5
|
|
|
6
6
|
TENANT_AWARE_EXCLUDED_PATHS = getattr(settings, "TENANT_AWARE_EXCLUDED_PATHS", ())
|
|
7
7
|
TENANT_AWARE_EXCLUDED_PATHS = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|