cardo-python-utils 0.5.dev34__py3-none-any.whl → 0.5.dev35__py3-none-any.whl

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.
@@ -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,4 +1,4 @@
1
- cardo_python_utils-0.5.dev34.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
1
+ cardo_python_utils-0.5.dev35.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
2
2
  python_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  python_utils/choices.py,sha256=_sLNkSnQqhg55gGKNRsOQCJ75W6gnz8J8Q00528MEYk,2548
4
4
  python_utils/data_structures.py,sha256=ZqkZYPy20zyGYOVhwb9qst4vF_P7X2A9z5E36rMUC6I,16820
@@ -11,16 +11,16 @@ python_utils/math.py,sha256=p_v8a9nVSe9426nR8H_SM8hOQrkzESVpCnn3gntw7TA,5603
11
11
  python_utils/text.py,sha256=pw9CZeM_Lcw-6k4GyR-4D1Wix8A7F_V1u1IIZTIazW4,3792
12
12
  python_utils/time.py,sha256=7Wei3uJ02Bk-BFRf-e1axoG418XQOhrXPvTwNZgTdnw,9614
13
13
  python_utils/types_hinting.py,sha256=QVWzmXRgNxhvln14tEX_FbQYryuVYhjWJ0dVOnlF6G4,120
14
- python_utils/django/README.md,sha256=nebCSS4joARr7F1rlJtKNO7S0Y9ctM3IJCmya0zInGM,4068
14
+ python_utils/django/README.md,sha256=3_Cj0r7t8--FXls5tpDuSqM4TedvP4GZ527qHK1iL54,4749
15
15
  python_utils/django/__init__.py,sha256=uXyqF-_5gZAlSIKoQkUTedAeBjnUHqh6lR6SzA1DEOM,64
16
16
  python_utils/django/apps.py,sha256=rgGdSsvptNVQ5QOEfYTdX7eA9EBgEPqq3LKIBLxaFLo,103
17
- python_utils/django/oidc_settings.py,sha256=I6-dpo7vxI8Z7UQyyzDm0N1uLKCcOpgS337JlCpWpqU,2838
17
+ python_utils/django/oidc_settings.py,sha256=Moc3ZEOIb40sCcvJtl4Xo51SjC__9Ki85sRhFuoX9dg,3926
18
18
  python_utils/django/settings.py,sha256=tapr6NaZyYBN8XRq6CmzhLiTtdW_4v1XiytZP4rDLf4,646
19
19
  python_utils/django/tenant_context.py,sha256=cbCggpUUB1BUcUtoqbriEgtc5ZyYtuaU7aWPiobg4uU,2980
20
20
  python_utils/django/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- python_utils/django/admin/auth.py,sha256=Gd5CCZ-0FDPNi3-8fcDOr37fnS7K2kjm7B6xpzmROME,2783
21
+ python_utils/django/admin/auth.py,sha256=c3BunWJZKCvp0px27jCrqj_BtY7lkS7ATpjxbqdZQMA,2809
22
22
  python_utils/django/admin/user_group.py,sha256=7bt9IyE1o4u0u97vrbRWra6698SBsRrEh_9It1djn4k,6060
23
- python_utils/django/admin/views.py,sha256=Po2Kr7GyavxBtmPIyg-N8yLdQTas9tzRDV50clLJ_Ms,1784
23
+ python_utils/django/admin/views.py,sha256=D4Ez-RkZa_c7O8AN1ljosBBJWieSodrY2TAZ7-uw3e0,756
24
24
  python_utils/django/admin/templates/__init__.py,sha256=LxCKcnJ1Ty48CFDJ8XtAZYTW45xDw5o7H4GLsRo6iQk,53
25
25
  python_utils/django/admin/templates/user_groups_changelist.html,sha256=KbO6bsH-nh3DDYCq4UB8j25NdjLP_nh_GuGZ4lYSarM,182
26
26
  python_utils/django/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -48,7 +48,7 @@ python_utils/django/storage/__init__.py,sha256=mNn2YmD7pkXhBLHMM1444BLsCMq78YdYx
48
48
  python_utils/django/storage/tenant_aware_storage.py,sha256=5dDes6xLv7_R8hIBbFIzRvPL7HL9K_RM-G6LI8qUSxM,2550
49
49
  python_utils/django/tests/__init__.py,sha256=Nkt0a7LEHyjLvuEBZ7113VjjAWJlyZlMy-H-JZ5tNcs,252
50
50
  python_utils/django/tests/conftest.py,sha256=PTA2QAqOMtmeVE87C1FOTC2d_4FSRw_i1vMgLoxKkwA,3176
51
- cardo_python_utils-0.5.dev34.dist-info/METADATA,sha256=YdNukdBcqMLicjJSBpH8FxHu8KZleVmrl2ip4enTyhM,3007
52
- cardo_python_utils-0.5.dev34.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
53
- cardo_python_utils-0.5.dev34.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
54
- cardo_python_utils-0.5.dev34.dist-info/RECORD,,
51
+ cardo_python_utils-0.5.dev35.dist-info/METADATA,sha256=bIG6Xl8SLFcXMEuER43lVrmovSysxNX5dmjSOMhBp5I,3007
52
+ cardo_python_utils-0.5.dev35.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
53
+ cardo_python_utils-0.5.dev35.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
54
+ cardo_python_utils-0.5.dev35.dist-info/RECORD,,
@@ -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,
@@ -5,16 +5,9 @@ These views override the default mozilla-django-oidc views to dynamically
5
5
  resolve OIDC endpoints based on the current tenant context.
6
6
  """
7
7
 
8
- from mozilla_django_oidc.views import (
9
- OIDCAuthenticationCallbackView,
10
- OIDCAuthenticationRequestView,
11
- OIDCLogoutView,
12
- )
8
+ from mozilla_django_oidc.views import OIDCAuthenticationRequestView
13
9
 
14
- from ..oidc_settings import (
15
- get_oidc_op_authorization_endpoint,
16
- get_oidc_op_logout_endpoint,
17
- )
10
+ from ..oidc_settings import get_oidc_op_authorization_endpoint
18
11
 
19
12
 
20
13
  class TenantAwareOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
@@ -24,40 +17,8 @@ class TenantAwareOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
24
17
  Dynamically resolves the authorization endpoint based on the current tenant.
25
18
  """
26
19
 
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
20
  def get_settings(self, attr, *args):
33
21
  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
22
+ return get_oidc_op_authorization_endpoint()
46
23
 
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
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