cardo-python-utils 0.5.dev34__tar.gz → 0.5.dev35__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 (61) hide show
  1. {cardo_python_utils-0.5.dev34/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev35}/PKG-INFO +1 -1
  2. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35/cardo_python_utils.egg-info}/PKG-INFO +1 -1
  3. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/pyproject.toml +1 -1
  4. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/README.md +21 -1
  5. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/admin/auth.py +12 -17
  6. cardo_python_utils-0.5.dev35/python_utils/django/admin/views.py +24 -0
  7. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/oidc_settings.py +37 -6
  8. cardo_python_utils-0.5.dev34/python_utils/django/admin/views.py +0 -63
  9. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/LICENSE +0 -0
  10. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/MANIFEST.in +0 -0
  11. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/README.rst +0 -0
  12. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/cardo_python_utils.egg-info/SOURCES.txt +0 -0
  13. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  14. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/cardo_python_utils.egg-info/requires.txt +0 -0
  15. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/cardo_python_utils.egg-info/top_level.txt +0 -0
  16. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/__init__.py +0 -0
  17. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/choices.py +0 -0
  18. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/data_structures.py +0 -0
  19. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/db.py +0 -0
  20. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/__init__.py +0 -0
  21. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/admin/__init__.py +0 -0
  22. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/admin/templates/__init__.py +0 -0
  23. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/admin/templates/user_groups_changelist.html +0 -0
  24. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/admin/user_group.py +0 -0
  25. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/api/__init__.py +0 -0
  26. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/api/drf.py +0 -0
  27. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/api/ninja.py +0 -0
  28. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/api/utils.py +0 -0
  29. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/apps.py +0 -0
  30. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/auth/service.py +0 -0
  31. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/celery/__init__.py +0 -0
  32. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/celery/tenant_aware_task.py +0 -0
  33. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/db/__init__.py +0 -0
  34. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/db/routers.py +0 -0
  35. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/db/transaction.py +0 -0
  36. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/db/utils.py +0 -0
  37. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/management/__init__.py +0 -0
  38. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/management/commands/__init__.py +0 -0
  39. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/management/commands/migrateall.py +0 -0
  40. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/management/commands/tenant_aware_command.py +0 -0
  41. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/middleware/__init__.py +0 -0
  42. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/middleware/tenant_aware_http_middleware.py +0 -0
  43. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/models/__init__.py +0 -0
  44. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/models/user_group.py +0 -0
  45. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/redis/__init__.py +0 -0
  46. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/redis/key_function.py +0 -0
  47. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/settings.py +0 -0
  48. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/storage/__init__.py +0 -0
  49. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/storage/tenant_aware_storage.py +0 -0
  50. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/tenant_context.py +0 -0
  51. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/tests/__init__.py +0 -0
  52. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django/tests/conftest.py +0 -0
  53. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/django_utils.py +0 -0
  54. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/esma_choices.py +0 -0
  55. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/exceptions.py +0 -0
  56. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/imports.py +0 -0
  57. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/math.py +0 -0
  58. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/text.py +0 -0
  59. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/time.py +0 -0
  60. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/python_utils/types_hinting.py +0 -0
  61. {cardo_python_utils-0.5.dev34 → cardo_python_utils-0.5.dev35}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev34
3
+ Version: 0.5.dev35
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev34
3
+ Version: 0.5.dev35
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev34"
7
+ version = "0.5.dev35"
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"
@@ -111,15 +111,35 @@ KEYCLOAK_USER_GROUP_MODEL = "myapp.UserGroup"
111
111
  KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", f"{JWT_AUDIENCE}_confidential")
112
112
 
113
113
  OIDC_RP_CLIENT_ID = KEYCLOAK_CONFIDENTIAL_CLIENT_ID
114
- OIDC_RP_CLIENT_SECRET = None
115
114
  OIDC_RP_SIGN_ALGO = "RS256"
116
115
  OIDC_CREATE_USER = True
116
+ OIDC_AUTHENTICATE_CLASS = "python_utils.django.admin.views.TenantAwareOIDCAuthenticationRequestView"
117
117
 
118
118
  LOGIN_REDIRECT_URL = "/admin"
119
119
  SESSION_COOKIE_AGE = 60 * 30 # 30 minutes
120
120
  SESSION_SAVE_EVERY_REQUEST = True # Extend session on each request
121
121
  ```
122
122
 
123
+ ## urls.py file
124
+
125
+ The views of the `mozilla-django-oidc` package need to be exposed as well, for the OIDC auth:
126
+
127
+ ```python3
128
+ urlpatterns.append(path("oidc/", include("mozilla_django_oidc.urls")))
129
+ ```
130
+
131
+ ## admin.py file
132
+
133
+ The Django Admin Panel needs to be configured to automatically redirect to the OIDC login page:
134
+
135
+ ```python3
136
+ from python_utils.django.admin.auth import has_admin_site_permission
137
+ from python_utils.django.admin.views import TenantAwareOIDCAuthenticationRequestView
138
+
139
+ admin.site.login = TenantAwareOIDCAuthenticationRequestView.as_view()
140
+ admin.site.has_permission = has_admin_site_permission
141
+ ```
142
+
123
143
  ## With django-ninja
124
144
 
125
145
  If using `django-ninja`, apart from the settings configured above, auth utils are provided in the django/api/ninja.py module.
@@ -18,23 +18,18 @@ class AdminAuthenticationBackend(OIDCAuthenticationBackend):
18
18
  own Keycloak realm.
19
19
  """
20
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()
21
+ def get_settings(self, attr, *args):
22
+ if attr == "OIDC_OP_TOKEN_ENDPOINT":
23
+ return get_oidc_op_token_endpoint()
24
+ if attr == "OIDC_OP_USER_ENDPOINT":
25
+ return get_oidc_op_user_endpoint()
26
+ if attr == "OIDC_OP_JWKS_ENDPOINT":
27
+ return get_oidc_op_jwks_endpoint()
28
+ if attr == "OIDC_RP_CLIENT_SECRET":
29
+ # The explicit return of None is needed to prevent the parent class from raising an error
30
+ return None
31
+
32
+ return super().get_settings(attr, *args)
38
33
 
39
34
  def get_token(self, payload):
40
35
  # Instead of passing client_id and client_secret,
@@ -0,0 +1,24 @@
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 OIDCAuthenticationRequestView
9
+
10
+ from ..oidc_settings import get_oidc_op_authorization_endpoint
11
+
12
+
13
+ class TenantAwareOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
14
+ """
15
+ Tenant-aware OIDC authentication request view.
16
+
17
+ Dynamically resolves the authorization endpoint based on the current tenant.
18
+ """
19
+
20
+ def get_settings(self, attr, *args):
21
+ if attr == "OIDC_OP_AUTHORIZATION_ENDPOINT":
22
+ return get_oidc_op_authorization_endpoint()
23
+
24
+ return super().get_settings(attr, *args)
@@ -8,17 +8,30 @@ import json
8
8
  import os
9
9
  import requests
10
10
 
11
+ from django.conf import settings
11
12
  from .tenant_context import TenantContext
12
13
 
13
14
 
14
15
  KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL", None)
15
16
  KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", None)
17
+
18
+ OIDC_CLIENT_AUTH_METHOD = getattr(settings, "OIDC_CLIENT_AUTH_METHOD", "client_assertion")
19
+ if OIDC_CLIENT_AUTH_METHOD not in ("client_assertion", "client_secret"):
20
+ raise ValueError(
21
+ f"Invalid OIDC_CLIENT_AUTH_METHOD: {OIDC_CLIENT_AUTH_METHOD}. "
22
+ f"Supported methods are 'client_assertion' and 'client_secret'."
23
+ )
24
+
16
25
  KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS: dict[str, str] = json.loads(
17
26
  os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS", "{}")
18
27
  )
19
28
  KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
20
29
  KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
21
30
 
31
+ KEYCLOAK_CONFIDENTIAL_CLIENT_SECRETS: dict[str, str] = json.loads(
32
+ os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_SECRETS", "{}")
33
+ )
34
+
22
35
 
23
36
  def get_oidc_op_base_url() -> str:
24
37
  """Get the base URL for the OIDC provider (Keycloak realm URL)."""
@@ -65,20 +78,38 @@ def get_confidential_client_service_account_token() -> str:
65
78
  return token
66
79
 
67
80
 
81
+ def get_confidential_client_secret() -> str:
82
+ """
83
+ Retrieves the Keycloak confidential client secret for the current tenant.
84
+ """
85
+ tenant = TenantContext.get()
86
+ client_secret = KEYCLOAK_CONFIDENTIAL_CLIENT_SECRETS.get(tenant)
87
+ if not client_secret:
88
+ raise ValueError(f"Keycloak confidential client secret for tenant {tenant} not found.")
89
+
90
+ return client_secret
91
+
92
+
68
93
  def get_oidc_confidential_client_token(**kwargs) -> dict:
69
94
  """
70
95
  Obtains token for an OIDC confidential client with the client credentials grant,
71
96
  using a service account token for authentication.
72
97
  """
73
98
 
99
+ data = {
100
+ "grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
101
+ **kwargs,
102
+ }
103
+ if OIDC_CLIENT_AUTH_METHOD == "client_secret":
104
+ data["client_id"] = KEYCLOAK_CONFIDENTIAL_CLIENT_ID
105
+ data["client_secret"] = get_confidential_client_secret()
106
+ else:
107
+ data["client_assertion_type"] = KEYCLOAK_CLIENT_ASSERTION_TYPE
108
+ data["client_assertion"] = get_confidential_client_service_account_token()
109
+
74
110
  response = requests.post(
75
111
  get_oidc_op_token_endpoint(),
76
- data={
77
- "grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
78
- "client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
79
- "client_assertion": get_confidential_client_service_account_token(),
80
- **kwargs,
81
- },
112
+ data=data,
82
113
  )
83
114
  response.raise_for_status()
84
115
 
@@ -1,63 +0,0 @@
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)