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.
- {oxutils-0.1.5 → oxutils-0.1.11}/PKG-INFO +20 -1
- {oxutils-0.1.5 → oxutils-0.1.11}/pyproject.toml +30 -9
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/__init__.py +2 -1
- oxutils-0.1.11/src/oxutils/constants.py +8 -0
- oxutils-0.1.11/src/oxutils/jwt/auth.py +204 -0
- oxutils-0.1.11/src/oxutils/jwt/models.py +81 -0
- oxutils-0.1.11/src/oxutils/jwt/tokens.py +69 -0
- oxutils-0.1.11/src/oxutils/jwt/utils.py +45 -0
- oxutils-0.1.11/src/oxutils/logger/receivers.py +20 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/logger/settings.py +2 -2
- oxutils-0.1.11/src/oxutils/models/base.py +218 -0
- oxutils-0.1.11/src/oxutils/models/fields.py +79 -0
- oxutils-0.1.11/src/oxutils/oxiliere/apps.py +14 -0
- oxutils-0.1.11/src/oxutils/oxiliere/authorization.py +45 -0
- oxutils-0.1.11/src/oxutils/oxiliere/caches.py +36 -0
- oxutils-0.1.11/src/oxutils/oxiliere/checks.py +31 -0
- oxutils-0.1.11/src/oxutils/oxiliere/constants.py +3 -0
- oxutils-0.1.11/src/oxutils/oxiliere/context.py +16 -0
- oxutils-0.1.11/src/oxutils/oxiliere/exceptions.py +16 -0
- oxutils-0.1.11/src/oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/middleware.py +65 -11
- oxutils-0.1.11/src/oxutils/oxiliere/models.py +192 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/permissions.py +28 -35
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/schemas.py +16 -6
- oxutils-0.1.11/src/oxutils/oxiliere/signals.py +5 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/utils.py +36 -1
- oxutils-0.1.11/src/oxutils/pagination/cursor.py +367 -0
- oxutils-0.1.11/src/oxutils/permissions/actions.py +57 -0
- oxutils-0.1.11/src/oxutils/permissions/apps.py +10 -0
- oxutils-0.1.11/src/oxutils/permissions/caches.py +19 -0
- oxutils-0.1.11/src/oxutils/permissions/checks.py +188 -0
- oxutils-0.1.11/src/oxutils/permissions/controllers.py +344 -0
- oxutils-0.1.11/src/oxutils/permissions/exceptions.py +60 -0
- oxutils-0.1.11/src/oxutils/permissions/management/commands/__init__.py +0 -0
- oxutils-0.1.11/src/oxutils/permissions/management/commands/load_permission_preset.py +112 -0
- oxutils-0.1.11/src/oxutils/permissions/migrations/0001_initial.py +112 -0
- oxutils-0.1.11/src/oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
- oxutils-0.1.11/src/oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
- oxutils-0.1.11/src/oxutils/permissions/migrations/__init__.py +0 -0
- oxutils-0.1.11/src/oxutils/permissions/models.py +171 -0
- oxutils-0.1.11/src/oxutils/permissions/perms.py +95 -0
- oxutils-0.1.11/src/oxutils/permissions/queryset.py +92 -0
- oxutils-0.1.11/src/oxutils/permissions/schemas.py +276 -0
- oxutils-0.1.11/src/oxutils/permissions/services.py +663 -0
- oxutils-0.1.11/src/oxutils/permissions/utils.py +628 -0
- oxutils-0.1.11/src/oxutils/py.typed +0 -0
- oxutils-0.1.11/src/oxutils/s3/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/settings.py +10 -2
- oxutils-0.1.11/src/oxutils/users/__init__.py +0 -0
- oxutils-0.1.11/src/oxutils/users/admin.py +3 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/users/apps.py +1 -1
- oxutils-0.1.11/src/oxutils/users/migrations/0001_initial.py +47 -0
- oxutils-0.1.11/src/oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
- oxutils-0.1.11/src/oxutils/users/migrations/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/users/models.py +2 -0
- oxutils-0.1.11/src/oxutils/users/tests.py +3 -0
- oxutils-0.1.11/src/oxutils/utils.py +25 -0
- oxutils-0.1.5/src/oxutils/constants.py +0 -2
- oxutils-0.1.5/src/oxutils/jwt/auth.py +0 -55
- oxutils-0.1.5/src/oxutils/jwt/client.py +0 -123
- oxutils-0.1.5/src/oxutils/jwt/constants.py +0 -1
- oxutils-0.1.5/src/oxutils/logger/receivers.py +0 -16
- oxutils-0.1.5/src/oxutils/models/base.py +0 -116
- oxutils-0.1.5/src/oxutils/oxiliere/apps.py +0 -6
- oxutils-0.1.5/src/oxutils/oxiliere/caches.py +0 -33
- oxutils-0.1.5/src/oxutils/oxiliere/models.py +0 -55
- {oxutils-0.1.5 → oxutils-0.1.11}/README.md +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/apps.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/apps.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/export.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/masks.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/migrations/0001_initial.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/migrations/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/models.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/settings.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/audit/utils.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/base.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/celery/settings.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/conf.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/context/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/context/site_name_processor.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/admin.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/apps.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/controllers.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/enums.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/migrations/0001_initial.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/migrations/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/models.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/schemas.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/tests.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/currency/utils.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/audit.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/enums/invoices.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/exceptions.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/functions.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/jwt/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/locale/fr/LC_MESSAGES/django.po +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/logger/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/base.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/schemas.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/mixins/services.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/billing.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/models/invoice.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/admin.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/cacheops.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/controllers.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/enums.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/management/commands/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/migrations/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/settings.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/oxiliere/tests.py +0 -0
- {oxutils-0.1.5/src/oxutils/s3 → oxutils-0.1.11/src/oxutils/pagination}/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/__init__.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/printer.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/utils.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/pdf/views.py +0 -0
- {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/__init__.py +0 -0
- {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/admin.py +0 -0
- /oxutils-0.1.5/src/oxutils/py.typed → /oxutils-0.1.11/src/oxutils/permissions/constants.py +0 -0
- {oxutils-0.1.5/src/oxutils/users/migrations → oxutils-0.1.11/src/oxutils/permissions/management}/__init__.py +0 -0
- {oxutils-0.1.5/src/oxutils/users → oxutils-0.1.11/src/oxutils/permissions}/tests.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/s3/settings.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/s3/storages.py +0 -0
- {oxutils-0.1.5 → oxutils-0.1.11}/src/oxutils/types.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
[
|
|
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
|
-
|
|
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.
|
|
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":
|
|
32
|
+
"filename": oxi_settings.log_file_path,
|
|
33
33
|
"formatter": "json_formatter",
|
|
34
34
|
},
|
|
35
35
|
},
|