oxutils 0.1.5__tar.gz → 0.1.11__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 (134) hide show
  1. {oxutils-0.1.5 → oxutils-0.1.11}/PKG-INFO +20 -1
  2. {oxutils-0.1.5 → oxutils-0.1.11}/pyproject.toml +30 -9
  3. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/__init__.py +2 -1
  4. oxutils-0.1.11/src/oxutils/constants.py +8 -0
  5. oxutils-0.1.11/src/oxutils/jwt/auth.py +204 -0
  6. oxutils-0.1.11/src/oxutils/jwt/models.py +81 -0
  7. oxutils-0.1.11/src/oxutils/jwt/tokens.py +69 -0
  8. oxutils-0.1.11/src/oxutils/jwt/utils.py +45 -0
  9. oxutils-0.1.11/src/oxutils/logger/receivers.py +20 -0
  10. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/logger/settings.py +2 -2
  11. oxutils-0.1.11/src/oxutils/models/base.py +218 -0
  12. oxutils-0.1.11/src/oxutils/models/fields.py +79 -0
  13. oxutils-0.1.11/src/oxutils/oxiliere/apps.py +14 -0
  14. oxutils-0.1.11/src/oxutils/oxiliere/authorization.py +45 -0
  15. oxutils-0.1.11/src/oxutils/oxiliere/caches.py +36 -0
  16. oxutils-0.1.11/src/oxutils/oxiliere/checks.py +31 -0
  17. oxutils-0.1.11/src/oxutils/oxiliere/constants.py +3 -0
  18. oxutils-0.1.11/src/oxutils/oxiliere/context.py +16 -0
  19. oxutils-0.1.11/src/oxutils/oxiliere/exceptions.py +16 -0
  20. oxutils-0.1.11/src/oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
  21. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
  22. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/middleware.py +65 -11
  23. oxutils-0.1.11/src/oxutils/oxiliere/models.py +192 -0
  24. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/permissions.py +28 -35
  25. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/schemas.py +16 -6
  26. oxutils-0.1.11/src/oxutils/oxiliere/signals.py +5 -0
  27. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/utils.py +36 -1
  28. oxutils-0.1.11/src/oxutils/pagination/cursor.py +367 -0
  29. oxutils-0.1.11/src/oxutils/permissions/actions.py +57 -0
  30. oxutils-0.1.11/src/oxutils/permissions/apps.py +10 -0
  31. oxutils-0.1.11/src/oxutils/permissions/caches.py +19 -0
  32. oxutils-0.1.11/src/oxutils/permissions/checks.py +188 -0
  33. oxutils-0.1.11/src/oxutils/permissions/controllers.py +344 -0
  34. oxutils-0.1.11/src/oxutils/permissions/exceptions.py +60 -0
  35. oxutils-0.1.11/src/oxutils/permissions/management/commands/__init__.py +0 -0
  36. oxutils-0.1.11/src/oxutils/permissions/management/commands/load_permission_preset.py +112 -0
  37. oxutils-0.1.11/src/oxutils/permissions/migrations/0001_initial.py +112 -0
  38. oxutils-0.1.11/src/oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
  39. oxutils-0.1.11/src/oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
  40. oxutils-0.1.11/src/oxutils/permissions/migrations/__init__.py +0 -0
  41. oxutils-0.1.11/src/oxutils/permissions/models.py +171 -0
  42. oxutils-0.1.11/src/oxutils/permissions/perms.py +95 -0
  43. oxutils-0.1.11/src/oxutils/permissions/queryset.py +92 -0
  44. oxutils-0.1.11/src/oxutils/permissions/schemas.py +276 -0
  45. oxutils-0.1.11/src/oxutils/permissions/services.py +663 -0
  46. oxutils-0.1.11/src/oxutils/permissions/utils.py +628 -0
  47. oxutils-0.1.11/src/oxutils/py.typed +0 -0
  48. oxutils-0.1.11/src/oxutils/s3/__init__.py +0 -0
  49. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/settings.py +10 -2
  50. oxutils-0.1.11/src/oxutils/users/__init__.py +0 -0
  51. oxutils-0.1.11/src/oxutils/users/admin.py +3 -0
  52. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/users/apps.py +1 -1
  53. oxutils-0.1.11/src/oxutils/users/migrations/0001_initial.py +47 -0
  54. oxutils-0.1.11/src/oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
  55. oxutils-0.1.11/src/oxutils/users/migrations/__init__.py +0 -0
  56. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/users/models.py +2 -0
  57. oxutils-0.1.11/src/oxutils/users/tests.py +3 -0
  58. oxutils-0.1.11/src/oxutils/utils.py +25 -0
  59. oxutils-0.1.5/src/oxutils/constants.py +0 -2
  60. oxutils-0.1.5/src/oxutils/jwt/auth.py +0 -55
  61. oxutils-0.1.5/src/oxutils/jwt/client.py +0 -123
  62. oxutils-0.1.5/src/oxutils/jwt/constants.py +0 -1
  63. oxutils-0.1.5/src/oxutils/logger/receivers.py +0 -16
  64. oxutils-0.1.5/src/oxutils/models/base.py +0 -116
  65. oxutils-0.1.5/src/oxutils/oxiliere/apps.py +0 -6
  66. oxutils-0.1.5/src/oxutils/oxiliere/caches.py +0 -33
  67. oxutils-0.1.5/src/oxutils/oxiliere/models.py +0 -55
  68. {oxutils-0.1.5 → oxutils-0.1.11}/README.md +0 -0
  69. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/apps.py +0 -0
  70. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/__init__.py +0 -0
  71. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/apps.py +0 -0
  72. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/export.py +0 -0
  73. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/masks.py +0 -0
  74. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/migrations/0001_initial.py +0 -0
  75. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/migrations/__init__.py +0 -0
  76. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/models.py +0 -0
  77. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/settings.py +0 -0
  78. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/utils.py +0 -0
  79. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/__init__.py +0 -0
  80. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/base.py +0 -0
  81. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/settings.py +0 -0
  82. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/conf.py +0 -0
  83. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/context/__init__.py +0 -0
  84. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/context/site_name_processor.py +0 -0
  85. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/__init__.py +0 -0
  86. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/admin.py +0 -0
  87. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/apps.py +0 -0
  88. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/controllers.py +0 -0
  89. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/enums.py +0 -0
  90. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/migrations/0001_initial.py +0 -0
  91. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/migrations/__init__.py +0 -0
  92. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/models.py +0 -0
  93. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/schemas.py +0 -0
  94. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/tests.py +0 -0
  95. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/utils.py +0 -0
  96. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/__init__.py +0 -0
  97. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/audit.py +0 -0
  98. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/invoices.py +0 -0
  99. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/exceptions.py +0 -0
  100. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/functions.py +0 -0
  101. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/jwt/__init__.py +0 -0
  102. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/locale/fr/LC_MESSAGES/django.po +0 -0
  103. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/logger/__init__.py +0 -0
  104. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/__init__.py +0 -0
  105. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/base.py +0 -0
  106. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/schemas.py +0 -0
  107. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/services.py +0 -0
  108. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/__init__.py +0 -0
  109. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/billing.py +0 -0
  110. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/invoice.py +0 -0
  111. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/__init__.py +0 -0
  112. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/admin.py +0 -0
  113. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/cacheops.py +0 -0
  114. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/controllers.py +0 -0
  115. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/enums.py +0 -0
  116. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/__init__.py +0 -0
  117. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/commands/__init__.py +0 -0
  118. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/migrations/__init__.py +0 -0
  119. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/settings.py +0 -0
  120. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/tests.py +0 -0
  121. {oxutils-0.1.5/src/oxutils/s3 → oxutils-0.1.11/src/oxutils/pagination}/__init__.py +0 -0
  122. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/__init__.py +0 -0
  123. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/printer.py +0 -0
  124. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/utils.py +0 -0
  125. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/views.py +0 -0
  126. {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/__init__.py +0 -0
  127. {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/admin.py +0 -0
  128. /oxutils-0.1.5/src/oxutils/py.typed → /oxutils-0.1.11/src/oxutils/permissions/constants.py +0 -0
  129. {oxutils-0.1.5/src/oxutils/users/migrations → oxutils-0.1.11/src/oxutils/permissions/management}/__init__.py +0 -0
  130. {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/tests.py +0 -0
  131. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/s3/settings.py +0 -0
  132. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/s3/storages.py +0 -0
  133. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/types.py +0 -0
  134. {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/users/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oxutils
3
- Version: 0.1.5
3
+ Version: 0.1.11
4
4
  Summary: Production-ready utilities for Django applications in the Oxiliere ecosystem
5
5
  Keywords: django,utilities,jwt,s3,audit,logging,celery,structlog
6
6
  Author: Edimedia Mutoke
@@ -30,12 +30,31 @@ Requires-Dist: jwcrypto>=1.5.6
30
30
  Requires-Dist: pydantic-settings>=2.12.0
31
31
  Requires-Dist: pyjwt>=2.10.1
32
32
  Requires-Dist: requests>=2.32.5
33
+ Requires-Dist: bcc-rates>=1.1.0 ; extra == 'all'
34
+ Requires-Dist: django-cacheops>=7.2 ; extra == 'all'
35
+ Requires-Dist: django-tenants>=3.9.0 ; extra == 'all'
36
+ Requires-Dist: django-safedelete>=1.4.1 ; extra == 'all'
37
+ Requires-Dist: django-auditlog>=3.4.1 ; extra == 'all'
38
+ Requires-Dist: django-ninja-jwt>=5.4.2 ; extra == 'all'
39
+ Requires-Dist: weasyprint>=67.0 ; extra == 'all'
40
+ Requires-Dist: bcc-rates>=1.1.0 ; extra == 'currency'
41
+ Requires-Dist: django-ninja-jwt>=5.4.2 ; extra == 'jwt'
42
+ Requires-Dist: django-cacheops>=7.2 ; extra == 'oxiliere'
43
+ Requires-Dist: django-tenants>=3.9.0 ; extra == 'oxiliere'
44
+ Requires-Dist: django-safedelete>=1.4.1 ; extra == 'oxiliere'
45
+ Requires-Dist: django-auditlog>=3.4.1 ; extra == 'oxiliere'
46
+ Requires-Dist: weasyprint>=67.0 ; extra == 'pdf'
33
47
  Requires-Python: >=3.12
34
48
  Project-URL: Changelog, https://github.com/oxiliere/oxutils/blob/main/CHANGELOG.md
35
49
  Project-URL: Documentation, https://github.com/oxiliere/oxutils/tree/main/docs
36
50
  Project-URL: Homepage, https://github.com/oxiliere/oxutils
37
51
  Project-URL: Issues, https://github.com/oxiliere/oxutils/issues
38
52
  Project-URL: Repository, https://github.com/oxiliere/oxutils
53
+ Provides-Extra: all
54
+ Provides-Extra: currency
55
+ Provides-Extra: jwt
56
+ Provides-Extra: oxiliere
57
+ Provides-Extra: pdf
39
58
  Description-Content-Type: text/markdown
40
59
 
41
60
  # OxUtils
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oxutils"
3
- version = "0.1.5"
3
+ version = "0.1.11"
4
4
  description = "Production-ready utilities for Django applications in the Oxiliere ecosystem"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -57,10 +57,36 @@ members = [
57
57
  [tool.uv.sources]
58
58
  oxutils = { workspace = true }
59
59
 
60
- [dependency-groups]
60
+ [project.optional-dependencies]
61
61
  currency = [
62
62
  "bcc-rates>=1.1.0",
63
63
  ]
64
+
65
+ oxiliere = [
66
+ "django-cacheops>=7.2",
67
+ "django-tenants>=3.9.0",
68
+ "django-safedelete>=1.4.1",
69
+ "django-auditlog>=3.4.1",
70
+ ]
71
+ pdf = [
72
+ "weasyprint>=67.0",
73
+ ]
74
+
75
+ jwt = [
76
+ "django-ninja-jwt>=5.4.2",
77
+ ]
78
+
79
+ all = [
80
+ "bcc-rates>=1.1.0",
81
+ "django-cacheops>=7.2",
82
+ "django-tenants>=3.9.0",
83
+ "django-safedelete>=1.4.1",
84
+ "django-auditlog>=3.4.1",
85
+ "django-ninja-jwt>=5.4.2",
86
+ "weasyprint>=67.0",
87
+ ]
88
+
89
+ [dependency-groups]
64
90
  dev = [
65
91
  "pytest>=8.0.0",
66
92
  "pytest-django>=4.8.0",
@@ -69,14 +95,9 @@ dev = [
69
95
  "coverage>=7.4.0",
70
96
  "pillow>=12.0.0",
71
97
  "ruff>=0.8.0",
98
+ "psycopg[binary]>=3.3.2",
72
99
  ]
73
- oxiliere = [
74
- "django-cacheops>=7.2",
75
- "django-tenants>=3.9.0",
76
- ]
77
- pdf = [
78
- "weasyprint>=67.0",
79
- ]
100
+
80
101
 
81
102
  [tool.ruff]
82
103
  line-length = 100
@@ -8,9 +8,10 @@ This package provides:
8
8
  - Celery integration
9
9
  - Django model mixins
10
10
  - Custom exceptions
11
+ - Permission management
11
12
  """
12
13
 
13
- __version__ = "0.1.5"
14
+ __version__ = "0.1.10"
14
15
 
15
16
  from oxutils.settings import oxi_settings
16
17
  from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
@@ -0,0 +1,8 @@
1
+ ORGANIZATION_QUERY_KEY = 'organization_id'
2
+ ORGANIZATION_HEADER_KEY = 'X-Organization-ID'
3
+ ORGANIZATION_TOKEN_COOKIE_KEY = 'organization_token'
4
+ OXILIERE_SERVICE_TOKEN = 'X-Oxiliere-Token'
5
+
6
+ REFRESH_TOKEN_COOKIE = 'refresh_token'
7
+ ACCESS_TOKEN_COOKIE = 'access_token'
8
+
@@ -0,0 +1,204 @@
1
+ import os
2
+ from typing import Dict, Any, Optional, Type, Tuple, List
3
+ from django.utils.translation import gettext_lazy as _
4
+ from django.http import HttpRequest
5
+ from django.contrib.auth.models import AbstractUser
6
+ from django.contrib.auth import (
7
+ authenticate as django_authenticate,
8
+ login as django_login,
9
+ get_user_model
10
+ )
11
+ from jwcrypto import jwk
12
+ from django.core.exceptions import ImproperlyConfigured
13
+
14
+ from ninja.security.base import AuthBase
15
+ from ninja_jwt.authentication import (
16
+ JWTBaseAuthentication,
17
+ JWTStatelessUserAuthentication
18
+ )
19
+ from ninja.security import (
20
+ APIKeyCookie,
21
+ HttpBasicAuth,
22
+ )
23
+ from ninja_jwt.exceptions import InvalidToken
24
+ from ninja_jwt.settings import api_settings
25
+ from oxutils.constants import ACCESS_TOKEN_COOKIE
26
+ from oxutils.settings import oxi_settings
27
+
28
+
29
+
30
+
31
+ _public_jwk_cache: Optional[jwk.JWK] = None
32
+
33
+
34
+
35
+ def get_jwks() -> Dict[str, Any]:
36
+ """
37
+ Get JSON Web Key Set (JWKS) for JWT verification.
38
+
39
+ Returns:
40
+ Dict containing the public JWK in JWKS format.
41
+
42
+ Raises:
43
+ ImproperlyConfigured: If jwt_verifying_key is not configured or file doesn't exist.
44
+ """
45
+ global _public_jwk_cache
46
+
47
+ if oxi_settings.jwt_verifying_key is None:
48
+ raise ImproperlyConfigured(
49
+ "JWT verifying key is not configured. Set OXI_JWT_VERIFYING_KEY environment variable."
50
+ )
51
+
52
+ key_path = oxi_settings.jwt_verifying_key
53
+
54
+ if not os.path.exists(key_path):
55
+ raise ImproperlyConfigured(
56
+ f"JWT verifying key file not found at: {key_path}"
57
+ )
58
+
59
+ if _public_jwk_cache is None:
60
+ try:
61
+ with open(key_path, 'r') as f:
62
+ key_data = f.read()
63
+
64
+ _public_jwk_cache = jwk.JWK.from_pem(key_data.encode('utf-8'))
65
+ _public_jwk_cache.update(kid='main')
66
+ except Exception as e:
67
+ raise ImproperlyConfigured(
68
+ f"Failed to load JWT verifying key from {key_path}: {str(e)}"
69
+ )
70
+
71
+ return {"keys": [_public_jwk_cache.export(as_dict=True)]}
72
+
73
+
74
+ def clear_jwk_cache() -> None:
75
+ """Clear the cached JWK. Useful for testing or key rotation."""
76
+ global _public_jwk_cache
77
+ _public_jwk_cache = None
78
+
79
+
80
+ class AuthMixin:
81
+ def jwt_authenticate(self, request: HttpRequest, token: str) -> AbstractUser:
82
+ """
83
+ Add token_user to the request object, witch will be erased by the jwt_allauth.utils.popolate_user
84
+ function.
85
+ """
86
+ token_user = super().jwt_authenticate(request, token)
87
+ request.token_user = token_user
88
+ return token_user
89
+
90
+
91
+ class JWTAuth(AuthMixin, JWTStatelessUserAuthentication):
92
+ pass
93
+
94
+
95
+ class JWTCookieAuth(AuthMixin, JWTBaseAuthentication, APIKeyCookie):
96
+ """
97
+ An authentication plugin that authenticates requests through a JSON web
98
+ token provided in a request header without performing a database lookup to obtain a user instance.
99
+ """
100
+
101
+ param_name = ACCESS_TOKEN_COOKIE
102
+
103
+ def authenticate(self, request: HttpRequest, token: str) -> Any:
104
+ return self.jwt_authenticate(request, token)
105
+
106
+ def get_user(self, validated_token: Any) -> Type[AbstractUser]:
107
+ """
108
+ Returns a stateless user object which is backed by the given validated
109
+ token.
110
+ """
111
+ if api_settings.USER_ID_CLAIM not in validated_token:
112
+ # The TokenUser class assumes tokens will have a recognizable user
113
+ # identifier claim.
114
+ raise InvalidToken(_("Token contained no recognizable user identification"))
115
+
116
+ return api_settings.TOKEN_USER_CLASS(validated_token)
117
+
118
+
119
+ def authenticate_by_x_session_token(token: str) -> Optional[Tuple]:
120
+ """
121
+ Copied from allauth.headless.internal.sessionkit, to "select_related"
122
+ """
123
+ from allauth.headless import app_settings
124
+
125
+
126
+ session = app_settings.TOKEN_STRATEGY.lookup_session(token)
127
+ if not session:
128
+ return None
129
+ user_id_str = session.get(SESSION_KEY)
130
+ if user_id_str:
131
+ meta_pk = get_user_model()._meta.pk
132
+ if meta_pk:
133
+ user_id = meta_pk.to_python(user_id_str)
134
+ user = get_user_model().objects.filter(pk=user_id).first()
135
+ if user and user.is_active:
136
+ return (user, session)
137
+ return None
138
+
139
+
140
+ class XSessionTokenAuth(AuthBase):
141
+ """
142
+ This security class uses the X-Session-Token that django-allauth
143
+ is using for authentication purposes.
144
+ """
145
+
146
+ openapi_type: str = "apiKey"
147
+
148
+ def __call__(self, request: HttpRequest):
149
+ token = self.get_session_token(request)
150
+ if token:
151
+ user_session = authenticate_by_x_session_token(token)
152
+ if user_session:
153
+ return user_session[0]
154
+ return None
155
+
156
+ def get_session_token(self, request: HttpRequest) -> Optional[str]:
157
+ """
158
+ Returns the session token for the given request, by looking up the
159
+ ``X-Session-Token`` header. Override this if you want to extract the token
160
+ from e.g. the ``Authorization`` header.
161
+ """
162
+ if request.session.session_key:
163
+ return request.session.session_key
164
+
165
+ return request.headers.get("X-Session-Token")
166
+
167
+
168
+ class BasicAuth(HttpBasicAuth):
169
+ def authenticate(self, request: HttpRequest, username: str, password: str) -> Optional[Any]:
170
+ user = django_authenticate(email=username, password=password)
171
+ if user and user.is_active:
172
+ django_login(request, user)
173
+ return user
174
+ return None
175
+
176
+
177
+ class BasicNoPasswordAuth(HttpBasicAuth):
178
+ def authenticate(self, request: HttpRequest, username: str, password: str) -> Optional[Any]:
179
+ try:
180
+ user = get_user_model().objects.get(email=username)
181
+ if user and user.is_active:
182
+ django_login(request, user)
183
+ return user
184
+ return None
185
+ except Exception as e:
186
+ return None
187
+
188
+ x_session_token_auth = XSessionTokenAuth()
189
+ basic_auth = BasicAuth()
190
+ basic_no_password_auth = BasicNoPasswordAuth()
191
+ jwt_auth = JWTAuth()
192
+ jwt_cookie_auth = JWTCookieAuth()
193
+
194
+
195
+
196
+
197
+ def get_auth_handlers(auths: List[AuthBase] = []) -> List[AuthBase]:
198
+ """Auth handler switcher based on settings.DEBUG"""
199
+ from django.conf import settings
200
+
201
+ if settings.DEBUG:
202
+ return auths
203
+
204
+ return [jwt_auth, jwt_cookie_auth]
@@ -0,0 +1,81 @@
1
+ from uuid import UUID
2
+ from django.utils.functional import cached_property
3
+ from ninja_jwt.models import TokenUser as DefaultTonkenUser
4
+ from ninja_jwt.settings import api_settings
5
+
6
+ import structlog
7
+ from .tokens import OrganizationAccessToken
8
+
9
+
10
+
11
+ logger = structlog.get_logger(__name__)
12
+
13
+
14
+ class TokenTenant:
15
+
16
+ def __init__(
17
+ self,
18
+ schema_name: str,
19
+ tenant_id: int,
20
+ oxi_id: str,
21
+ subscription_plan: str,
22
+ subscription_status: str,
23
+ status: str,
24
+ ):
25
+ self.schema_name = schema_name
26
+ self.id = tenant_id
27
+ self.oxi_id = oxi_id
28
+ self.subscription_plan = subscription_plan
29
+ self.subscription_status = subscription_status
30
+ self.status = status
31
+
32
+ def __str__(self):
33
+ return f"{self.schema_name} - {self.oxi_id}"
34
+
35
+ @property
36
+ def pk(self):
37
+ return self.id
38
+
39
+ @property
40
+ def is_active(self):
41
+ return self.status == 'active'
42
+
43
+ @property
44
+ def is_deleted(self):
45
+ return self.status == 'deleted'
46
+
47
+ @classmethod
48
+ def for_token(cls, token):
49
+ try:
50
+ token_obj = OrganizationAccessToken(token=token)
51
+ tenant = cls(
52
+ schema_name=token_obj['schema_name'],
53
+ tenant_id=token_obj['tenant_id'],
54
+ oxi_id=token_obj['oxi_id'],
55
+ subscription_plan=token_obj['subscription_plan'],
56
+ subscription_status=token_obj['subscription_status'],
57
+ status=token_obj['status'],
58
+ )
59
+ return tenant
60
+ except Exception:
61
+ logger.exception('Failed to create TokenTenant from token', token=token)
62
+ return None
63
+
64
+
65
+ class TokenUser(DefaultTonkenUser):
66
+ @cached_property
67
+ def id(self):
68
+ return UUID(self.token[api_settings.USER_ID_CLAIM])
69
+
70
+ @property
71
+ def oxi_id(self):
72
+ # for compatibility with the User model
73
+ return self.id
74
+
75
+ @cached_property
76
+ def token_created_at(self):
77
+ return self.token.get('cat', None)
78
+
79
+ @cached_property
80
+ def token_session(self):
81
+ return self.token.get('session', None)
@@ -0,0 +1,69 @@
1
+ from django.conf import settings
2
+ from ninja_jwt.tokens import Token, AccessToken
3
+ from ninja_jwt.backends import TokenBackend
4
+ from ninja_jwt.settings import api_settings
5
+ from datetime import timedelta
6
+ from oxutils.settings import oxi_settings
7
+
8
+
9
+
10
+
11
+ __all__ = [
12
+ 'AccessToken',
13
+ 'OrganizationAccessToken',
14
+ ]
15
+
16
+
17
+
18
+ token_backend = TokenBackend(
19
+ algorithm='HS256',
20
+ signing_key=settings.SECRET_KEY,
21
+ verifying_key="",
22
+ audience=None,
23
+ issuer=None,
24
+ jwk_url=None,
25
+ leeway=api_settings.LEEWAY,
26
+ json_encoder=api_settings.JSON_ENCODER,
27
+ )
28
+
29
+
30
+
31
+ class OxilierServiceToken(Token):
32
+ token_type = oxi_settings.jwt_service_token_key
33
+ lifetime = timedelta(minutes=oxi_settings.jwt_service_token_lifetime)
34
+
35
+
36
+ @classmethod
37
+ def for_service(cls, payload: dict = {}) -> Token:
38
+ token = cls()
39
+
40
+ for key, value in payload.items():
41
+ token[key] = value
42
+
43
+ return token
44
+
45
+
46
+ class OrganizationAccessToken(Token):
47
+ token_type = oxi_settings.jwt_org_access_token_key
48
+ lifetime = timedelta(minutes=oxi_settings.jwt_org_access_token_lifetime)
49
+
50
+ @classmethod
51
+ def for_tenant(cls, tenant) -> Token:
52
+ token = cls()
53
+ token.payload['tenant_id'] = str(tenant.id)
54
+ token.payload['oxi_id'] = str(tenant.oxi_id)
55
+ token.payload['schema_name'] = str(tenant.schema_name)
56
+ token.payload['subscription_plan'] = str(tenant.subscription_plan)
57
+ token.payload['subscription_status'] = str(tenant.subscription_status)
58
+ token.payload['subscription_end_date'] = str(tenant.subscription_end_date)
59
+ token.payload['status'] = str(tenant.status)
60
+
61
+ return token
62
+
63
+ @property
64
+ def token_backend(self):
65
+ return token_backend
66
+
67
+ def get_token_backend(self):
68
+ # Backward compatibility.
69
+ return self.token_backend
@@ -0,0 +1,45 @@
1
+ from functools import wraps
2
+ import structlog
3
+ from django.contrib.auth import get_user_model
4
+ from django.http import HttpRequest
5
+
6
+ from ninja_jwt.exceptions import InvalidToken
7
+
8
+
9
+
10
+ logger = structlog.getLogger("django")
11
+ User = get_user_model()
12
+
13
+
14
+ def load_user(f):
15
+ """
16
+ Decorator that loads the complete user object from the database for stateless JWT authentication.
17
+ This is necessary because JWT tokens only contain the user ID, and the full user object
18
+ might be needed in the view methods.
19
+
20
+ Usage:
21
+
22
+ .. code-block:: python
23
+
24
+ @load_user
25
+ def my_view_method(self, *args, **kwargs):
26
+ # self.request.user will be the complete user object
27
+ pass
28
+ """
29
+ @wraps(f)
30
+ def wrapper(self, *args, **kwargs):
31
+ populate_user(self.context.request)
32
+ res = f(self, *args, **kwargs)
33
+ return res
34
+ return wrapper
35
+
36
+
37
+ def populate_user(request: HttpRequest):
38
+ if isinstance(request.user, User):
39
+ return
40
+
41
+ try:
42
+ request.user = User.objects.get(oxi_id=request.user.id)
43
+ except User.DoesNotExist as exc:
44
+ logger.exception('user_not_found', oxi_id=request.user.id, message=str(exc))
45
+ raise InvalidToken()
@@ -0,0 +1,20 @@
1
+ from django.contrib.sites.shortcuts import RequestSite
2
+ from django.dispatch import receiver
3
+ import structlog
4
+ from django_structlog import signals
5
+ from oxutils.settings import oxi_settings
6
+ from oxutils.oxiliere.context import get_current_tenant_schema_name
7
+
8
+
9
+ @receiver(signals.bind_extra_request_metadata)
10
+ def bind_domain(request, logger, **kwargs):
11
+ current_site = RequestSite(request)
12
+ ctx = {
13
+ 'domain': current_site.domain,
14
+ 'user_id': str(request.user.pk),
15
+ 'service': oxi_settings.service_name
16
+ }
17
+ if oxi_settings.multitenancy:
18
+ ctx['tenant'] = get_current_tenant_schema_name()
19
+
20
+ structlog.contextvars.bind_contextvars(**ctx)
@@ -1,5 +1,5 @@
1
1
  import structlog
2
-
2
+ from oxutils.settings import oxi_settings
3
3
 
4
4
 
5
5
 
@@ -29,7 +29,7 @@ LOGGING = {
29
29
  },
30
30
  "json_file": {
31
31
  "class": "logging.handlers.WatchedFileHandler",
32
- "filename": "logs/json.log",
32
+ "filename": oxi_settings.log_file_path,
33
33
  "formatter": "json_formatter",
34
34
  },
35
35
  },