oxutils 0.1.8__py3-none-any.whl → 0.1.11__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.
Files changed (53) hide show
  1. oxutils/__init__.py +2 -1
  2. oxutils/constants.py +4 -0
  3. oxutils/jwt/auth.py +150 -1
  4. oxutils/jwt/models.py +13 -0
  5. oxutils/logger/receivers.py +10 -6
  6. oxutils/models/base.py +102 -0
  7. oxutils/models/fields.py +79 -0
  8. oxutils/oxiliere/apps.py +6 -1
  9. oxutils/oxiliere/authorization.py +45 -0
  10. oxutils/oxiliere/caches.py +7 -7
  11. oxutils/oxiliere/checks.py +31 -0
  12. oxutils/oxiliere/constants.py +3 -0
  13. oxutils/oxiliere/context.py +16 -0
  14. oxutils/oxiliere/exceptions.py +16 -0
  15. oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
  16. oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
  17. oxutils/oxiliere/middleware.py +29 -13
  18. oxutils/oxiliere/models.py +130 -19
  19. oxutils/oxiliere/permissions.py +6 -5
  20. oxutils/oxiliere/schemas.py +13 -4
  21. oxutils/oxiliere/signals.py +5 -0
  22. oxutils/oxiliere/utils.py +14 -0
  23. oxutils/pagination/__init__.py +0 -0
  24. oxutils/pagination/cursor.py +367 -0
  25. oxutils/permissions/__init__.py +0 -0
  26. oxutils/permissions/actions.py +57 -0
  27. oxutils/permissions/admin.py +3 -0
  28. oxutils/permissions/apps.py +10 -0
  29. oxutils/permissions/caches.py +19 -0
  30. oxutils/permissions/checks.py +188 -0
  31. oxutils/permissions/constants.py +0 -0
  32. oxutils/permissions/controllers.py +344 -0
  33. oxutils/permissions/exceptions.py +60 -0
  34. oxutils/permissions/management/__init__.py +0 -0
  35. oxutils/permissions/management/commands/__init__.py +0 -0
  36. oxutils/permissions/management/commands/load_permission_preset.py +112 -0
  37. oxutils/permissions/migrations/0001_initial.py +112 -0
  38. oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
  39. oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
  40. oxutils/permissions/migrations/__init__.py +0 -0
  41. oxutils/permissions/models.py +171 -0
  42. oxutils/permissions/perms.py +95 -0
  43. oxutils/permissions/queryset.py +92 -0
  44. oxutils/permissions/schemas.py +276 -0
  45. oxutils/permissions/services.py +663 -0
  46. oxutils/permissions/tests.py +3 -0
  47. oxutils/permissions/utils.py +628 -0
  48. oxutils/settings.py +1 -0
  49. oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
  50. oxutils/users/models.py +2 -0
  51. {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/METADATA +1 -1
  52. {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/RECORD +53 -19
  53. {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/WHEEL +0 -0
oxutils/__init__.py CHANGED
@@ -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.8"
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
oxutils/constants.py CHANGED
@@ -2,3 +2,7 @@ ORGANIZATION_QUERY_KEY = 'organization_id'
2
2
  ORGANIZATION_HEADER_KEY = 'X-Organization-ID'
3
3
  ORGANIZATION_TOKEN_COOKIE_KEY = 'organization_token'
4
4
  OXILIERE_SERVICE_TOKEN = 'X-Oxiliere-Token'
5
+
6
+ REFRESH_TOKEN_COOKIE = 'refresh_token'
7
+ ACCESS_TOKEN_COOKIE = 'access_token'
8
+
oxutils/jwt/auth.py CHANGED
@@ -1,11 +1,33 @@
1
1
  import os
2
- from typing import Dict, Any, Optional
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
+ )
3
11
  from jwcrypto import jwk
4
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
5
26
  from oxutils.settings import oxi_settings
6
27
 
7
28
 
8
29
 
30
+
9
31
  _public_jwk_cache: Optional[jwk.JWK] = None
10
32
 
11
33
 
@@ -53,3 +75,130 @@ def clear_jwk_cache() -> None:
53
75
  """Clear the cached JWK. Useful for testing or key rotation."""
54
76
  global _public_jwk_cache
55
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]
oxutils/jwt/models.py CHANGED
@@ -36,6 +36,14 @@ class TokenTenant:
36
36
  def pk(self):
37
37
  return self.id
38
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
+
39
47
  @classmethod
40
48
  def for_token(cls, token):
41
49
  try:
@@ -59,6 +67,11 @@ class TokenUser(DefaultTonkenUser):
59
67
  def id(self):
60
68
  return UUID(self.token[api_settings.USER_ID_CLAIM])
61
69
 
70
+ @property
71
+ def oxi_id(self):
72
+ # for compatibility with the User model
73
+ return self.id
74
+
62
75
  @cached_property
63
76
  def token_created_at(self):
64
77
  return self.token.get('cat', None)
@@ -3,14 +3,18 @@ from django.dispatch import receiver
3
3
  import structlog
4
4
  from django_structlog import signals
5
5
  from oxutils.settings import oxi_settings
6
-
6
+ from oxutils.oxiliere.context import get_current_tenant_schema_name
7
7
 
8
8
 
9
9
  @receiver(signals.bind_extra_request_metadata)
10
10
  def bind_domain(request, logger, **kwargs):
11
11
  current_site = RequestSite(request)
12
- structlog.contextvars.bind_contextvars(
13
- domain=current_site.domain,
14
- user_id=str(request.user.pk),
15
- service=oxi_settings.service_name
16
- )
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)
oxutils/models/base.py CHANGED
@@ -1,6 +1,24 @@
1
1
  import uuid
2
+ import time
2
3
  from django.db import models
4
+ from django.db import transaction
3
5
  from django.conf import settings
6
+ from oxutils.models.fields import MaskedBackupField
7
+
8
+
9
+
10
+ try:
11
+ from safedelete.models import SafeDeleteModel
12
+ from safedelete.models import SOFT_DELETE_CASCADE
13
+ from safedelete.signals import post_undelete
14
+ except ImportError:
15
+ from django.dispatch import Signal
16
+ post_undelete = Signal()
17
+ SOFT_DELETE_CASCADE = 2
18
+
19
+ class SafeDeleteModel(models.Model):
20
+ def __new__(cls, *args, **kwargs):
21
+ raise ImportError("django-safedelete is not installed, please install it to use SafeDeleteModel")
4
22
 
5
23
 
6
24
  class UUIDPrimaryKeyMixin(models.Model):
@@ -114,3 +132,87 @@ class BaseModelMixin(UUIDPrimaryKeyMixin, TimestampMixin, ActiveMixin):
114
132
  """
115
133
  class Meta:
116
134
  abstract = True
135
+
136
+
137
+ class SafeDeleteModelMixin(SafeDeleteModel):
138
+ _safedelete_policy = SOFT_DELETE_CASCADE
139
+ mask_fields = []
140
+
141
+ _masked_backup = MaskedBackupField(default=dict, editable=False)
142
+
143
+ class Meta:
144
+ abstract = True
145
+
146
+ @transaction.atomic
147
+ def delete(self, *args, **kwargs):
148
+ backup = {}
149
+
150
+ for field_name in self.mask_fields:
151
+ field = self._meta.get_field(field_name)
152
+ old_value = getattr(self, field_name)
153
+
154
+ if old_value is None:
155
+ continue
156
+
157
+ backup[field_name] = old_value
158
+ masked = self._mask_value(field, old_value)
159
+ setattr(self, field_name, masked)
160
+
161
+ if backup:
162
+ self._masked_backup = backup
163
+ self.save(update_fields=[*backup.keys(), "_masked_backup"])
164
+
165
+ return super().delete(*args, **kwargs)
166
+
167
+ def _mask_value(self, field: models.Field, old_value):
168
+ uid = uuid.uuid4().hex
169
+ ts = int(time.time())
170
+
171
+ if isinstance(field, models.EmailField):
172
+ return f"{ts}.{uid}.deleted@invalid.local"
173
+
174
+ if isinstance(field, models.URLField):
175
+ return f"https://deleted.invalid/{ts}/{uid}"
176
+
177
+ if isinstance(field, models.SlugField):
178
+ return f"deleted-{ts}-{uid}"
179
+
180
+ if isinstance(field, models.CharField):
181
+ return f"__deleted__{ts}__{uid}"
182
+
183
+ if isinstance(field, models.IntegerField):
184
+ return None # souvent OK, sinon adapte
185
+
186
+ # fallback générique
187
+ return f"deleted-{ts}-{uid}"
188
+
189
+ @transaction.atomic
190
+ def restore_masked_fields(self):
191
+ if not self._masked_backup:
192
+ return
193
+
194
+ for field_name, old_value in self._masked_backup.items():
195
+ field = self._meta.get_field(field_name)
196
+
197
+ # vérification collision
198
+ qs = self.__class__._default_manager.filter(
199
+ **{field_name: old_value}
200
+ ).exclude(pk=self.pk)
201
+
202
+ if qs.exists():
203
+ raise ValueError(
204
+ f"Collision détectée lors de la restauration du champ '{field_name}'"
205
+ )
206
+
207
+ setattr(self, field_name, old_value)
208
+
209
+ self._masked_backup = {}
210
+ self.save()
211
+
212
+
213
+ def _restore_masked_fields(sender, instance, **kwargs):
214
+ if isinstance(instance, SafeDeleteModelMixin):
215
+ instance.restore_masked_fields()
216
+
217
+
218
+ post_undelete.connect(_restore_masked_fields)
@@ -0,0 +1,79 @@
1
+ """# settings.py
2
+
3
+ FIELD_MASKING_CRYPTO_ENABLED = True # switch global
4
+
5
+ # optionnel (recommandé)
6
+ FIELD_MASKING_KEY = env("FIELD_MASKING_KEY", default=None)
7
+
8
+ """
9
+
10
+ import json
11
+ import base64
12
+ import hashlib
13
+ from django.utils.functional import cached_property
14
+ from django.conf import settings
15
+ from django.db import models
16
+
17
+
18
+
19
+
20
+
21
+ def get_field_masking_fernet():
22
+ from cryptography.fernet import Fernet
23
+
24
+ if not hasattr(settings, "FIELD_MASKING_CRYPTO_ENABLED") or not settings.FIELD_MASKING_CRYPTO_ENABLED:
25
+ return None
26
+
27
+ if not hasattr(settings, "FIELD_MASKING_KEY") or settings.FIELD_MASKING_KEY:
28
+ return Fernet(settings.FIELD_MASKING_KEY)
29
+
30
+ # fallback contrôlé
31
+ digest = hashlib.sha256(
32
+ (settings.SECRET_KEY + ":field-masking:v1").encode()
33
+ ).digest()
34
+
35
+ key = base64.urlsafe_b64encode(digest)
36
+ return Fernet(key)
37
+
38
+
39
+
40
+ class MaskedBackupField(models.TextField):
41
+ """
42
+ JSONField avec chiffrement optionnel
43
+ """
44
+
45
+ @cached_property
46
+ def fernet(self):
47
+ return get_field_masking_fernet()
48
+
49
+ def get_prep_value(self, value):
50
+ if value in (None, ""):
51
+ return None
52
+
53
+ raw = json.dumps(value).encode()
54
+
55
+ if not self.fernet:
56
+ return raw.decode()
57
+
58
+ return self.fernet.encrypt(raw).decode()
59
+
60
+ def from_db_value(self, value, expression, connection):
61
+ return self.to_python(value)
62
+
63
+ def to_python(self, value):
64
+ if isinstance(value, dict):
65
+ return value
66
+
67
+ if value in (None, ""):
68
+ return {}
69
+
70
+ try:
71
+ if self.fernet:
72
+ decrypted = self.fernet.decrypt(value.encode())
73
+ return json.loads(decrypted.decode())
74
+
75
+ return json.loads(value)
76
+
77
+ except Exception:
78
+ # sécurité : ne jamais casser un fetch DB
79
+ return {}
oxutils/oxiliere/apps.py CHANGED
@@ -6,4 +6,9 @@ class OxiliereConfig(AppConfig):
6
6
  name = 'oxutils.oxiliere'
7
7
 
8
8
  def ready(self):
9
- import oxutils.oxiliere.caches
9
+ import oxutils.oxiliere.checks
10
+
11
+ try:
12
+ import oxutils.oxiliere.caches
13
+ except LookupError:
14
+ pass
@@ -0,0 +1,45 @@
1
+ from django.conf import settings
2
+ from django.db import transaction
3
+ from oxutils.permissions.actions import ACTIONS
4
+ from oxutils.permissions.models import Grant, Group, UserGroup
5
+ from oxutils.oxiliere.utils import get_tenant_user_model
6
+ from oxutils.oxiliere.models import BaseTenant
7
+
8
+
9
+ @transaction.atomic
10
+ def grant_manager_access_to_owners(tenant: BaseTenant):
11
+ tenant_user_model = get_tenant_user_model()
12
+ tenant_users = tenant_user_model.objects.select_related("user").filter(tenant=tenant, is_owner=True)
13
+
14
+ access_scope = getattr(settings, 'ACCESS_MANAGER_SCOPE')
15
+ access_group = getattr(settings, 'ACCESS_MANAGER_GROUP')
16
+
17
+ if access_group:
18
+ try:
19
+ group = Group.objects.get(slug=access_group)
20
+ except Group.DoesNotExist:
21
+ group = None
22
+
23
+ bulk_grant = []
24
+ for tenant_user in tenant_users:
25
+ if group:
26
+ user_group, _ = UserGroup.objects.get_or_create(
27
+ user=tenant_user.user,
28
+ group=group,
29
+ )
30
+ else:
31
+ user_group = None
32
+
33
+ bulk_grant.append(
34
+ Grant(
35
+ user=tenant_user.user,
36
+ scope=access_scope,
37
+ role=None,
38
+ actions=ACTIONS,
39
+ context={},
40
+ user_group=user_group,
41
+ created_by=None,
42
+ )
43
+ )
44
+
45
+ Grant.objects.bulk_create(bulk_grant)
@@ -1,6 +1,11 @@
1
1
  from django.conf import settings
2
2
  from cacheops import cached_as, cached
3
- from oxutils.oxiliere.utils import get_tenant_model, get_tenant_user_model
3
+ from oxutils.oxiliere.utils import (
4
+ get_tenant_model,
5
+ get_tenant_user_model,
6
+ get_system_tenant_oxi_id
7
+ )
8
+
4
9
 
5
10
 
6
11
 
@@ -28,9 +33,4 @@ def get_tenant_user(oxi_org_id: str, oxi_user_id: str):
28
33
 
29
34
  @cached(timeout=60*15)
30
35
  def get_system_tenant():
31
- from oxutils.oxiliere.utils import oxid_to_schema_name
32
-
33
- system_schema_name = oxid_to_schema_name(
34
- getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
35
- )
36
- return get_tenant_model().objects.get(schema_name=system_schema_name)
36
+ return get_tenant_model().objects.get(oxi_id=get_system_tenant_oxi_id())
@@ -0,0 +1,31 @@
1
+ """
2
+ Check TENANT_MODEL & TENANT_USER_MODEL
3
+ """
4
+ from django.conf import settings
5
+ from django.core.checks import Error, register, Tags
6
+
7
+
8
+ @register(Tags.models)
9
+ def check_tenant_settings(app_configs, **kwargs):
10
+ """Check that TENANT_MODEL and TENANT_USER_MODEL are defined in settings."""
11
+ errors = []
12
+
13
+ if not hasattr(settings, 'TENANT_MODEL'):
14
+ errors.append(
15
+ Error(
16
+ 'TENANT_MODEL is not defined in settings',
17
+ hint='Add TENANT_MODEL = "app_label.ModelName" to your settings',
18
+ id='oxiliere.E001',
19
+ )
20
+ )
21
+
22
+ if not hasattr(settings, 'TENANT_USER_MODEL'):
23
+ errors.append(
24
+ Error(
25
+ 'TENANT_USER_MODEL is not defined in settings',
26
+ hint='Add TENANT_USER_MODEL = "app_label.ModelName" to your settings',
27
+ id='oxiliere.E002',
28
+ )
29
+ )
30
+
31
+ return errors
@@ -0,0 +1,3 @@
1
+ OXI_SYSTEM_TENANT = 'tenant_oxisystem'
2
+ OXI_SYSTEM_DOMAIN = 'system.oxiliere.com'
3
+ OXI_SYSTEM_OWNER_EMAIL = 'dev@oxiliere.com'
@@ -0,0 +1,16 @@
1
+ import contextvars
2
+ from oxutils.oxiliere.utils import get_system_tenant_oxi_id
3
+
4
+
5
+ current_tenant_schema_name: contextvars.ContextVar[str] = contextvars.ContextVar(
6
+ "current_tenant_schema_name",
7
+ default=f"[oxi_id] {get_system_tenant_oxi_id()}"
8
+ )
9
+
10
+
11
+ def get_current_tenant_schema_name() -> str:
12
+ return current_tenant_schema_name.get()
13
+
14
+
15
+ def set_current_tenant_schema_name(schema_name: str):
16
+ current_tenant_schema_name.set(schema_name)
@@ -0,0 +1,16 @@
1
+ # exceptions
2
+
3
+ class InactiveError(Exception):
4
+ pass
5
+
6
+
7
+ class ExistsError(Exception):
8
+ pass
9
+
10
+
11
+ class DeleteError(Exception):
12
+ pass
13
+
14
+
15
+ class SchemaError(Exception):
16
+ pass
@@ -0,0 +1,19 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.db import connection
3
+ from django_tenants.management.commands import InteractiveTenantOption
4
+ from oxutils.oxiliere.authorization import grant_manager_access_to_owners
5
+
6
+
7
+ class Command(InteractiveTenantOption, BaseCommand):
8
+ help = "Wrapper around django commands for use with an individual tenant"
9
+
10
+ def add_arguments(self, parser):
11
+ super().add_arguments(parser)
12
+
13
+ def handle(self, *args, **options):
14
+ tenant = self.get_tenant_from_options_or_interactive(**options)
15
+ connection.set_tenant(tenant)
16
+ options.pop('schema_name', None)
17
+
18
+ grant_manager_access_to_owners(tenant)
19
+ self.stdout.write(self.style.SUCCESS('Successfully granted manager access to owners'))
@@ -1,11 +1,22 @@
1
1
  import uuid
2
2
  from django.core.management.base import BaseCommand
3
3
  from django.conf import settings
4
- from django.db import transaction
4
+ from django.db import transaction, connection
5
5
  from django.contrib.auth import get_user_model
6
- from django_tenants.utils import get_tenant_model
7
- from oxutils.oxiliere.models import Domain, TenantUser
8
- from oxutils.oxiliere.utils import oxid_to_schema_name
6
+ from django_tenants.utils import (
7
+ get_tenant_model,
8
+ get_tenant_domain_model,
9
+ )
10
+ from oxutils.oxiliere.utils import (
11
+ oxid_to_schema_name,
12
+ get_tenant_user_model
13
+ )
14
+ from oxutils.oxiliere.constants import (
15
+ OXI_SYSTEM_TENANT,
16
+ OXI_SYSTEM_DOMAIN,
17
+ OXI_SYSTEM_OWNER_EMAIL
18
+ )
19
+ from oxutils.oxiliere.authorization import grant_manager_access_to_owners
9
20
 
10
21
 
11
22
 
@@ -18,17 +29,17 @@ class Command(BaseCommand):
18
29
  UserModel = get_user_model()
19
30
 
20
31
  # Configuration du tenant système depuis settings
21
- system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
32
+ system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', OXI_SYSTEM_TENANT)
22
33
  schema_name = oxid_to_schema_name(system_slug)
23
- system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', 'system.oxiliere.com')
24
- owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', 'dev@oxiliere.com')
34
+ system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', OXI_SYSTEM_DOMAIN)
35
+ owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', OXI_SYSTEM_OWNER_EMAIL)
25
36
  owner_oxi_id = uuid.uuid4()
26
37
 
27
38
  self.stdout.write(self.style.WARNING(f'Initialisation du tenant système...'))
28
39
 
29
40
  # Vérifier si le tenant système existe déjà
30
- if TenantModel.objects.filter(schema_name=schema_name).exists():
31
- self.stdout.write(self.style.ERROR(f'Le tenant système "{schema_name}" existe déjà!'))
41
+ if TenantModel.objects.filter(oxi_id=system_slug).exists():
42
+ self.stdout.write(self.style.ERROR(f'Le tenant système "{system_slug}" existe déjà!'))
32
43
  return
33
44
 
34
45
  # Créer le tenant système
@@ -44,12 +55,15 @@ class Command(BaseCommand):
44
55
 
45
56
  # Créer le domaine pour le tenant système
46
57
  self.stdout.write(f'Création du domaine: {system_domain}')
47
- domain = Domain.objects.create(
58
+
59
+ domain = get_tenant_domain_model().objects.create(
48
60
  domain=system_domain,
49
61
  tenant=tenant,
50
62
  is_primary=True
51
63
  )
52
64
  self.stdout.write(self.style.SUCCESS(f'✓ Domaine créé: {domain.domain}'))
65
+
66
+ connection.set_tenant(tenant)
53
67
 
54
68
  self.stdout.write(f'Création du superuser: {owner_email}')
55
69
  try:
@@ -66,7 +80,7 @@ class Command(BaseCommand):
66
80
 
67
81
  # Lier le superuser au tenant système
68
82
  self.stdout.write('Liaison du superuser au tenant système')
69
- tenant_user, created = TenantUser.objects.get_or_create(
83
+ tenant_user, created = get_tenant_user_model().objects.get_or_create(
70
84
  tenant=tenant,
71
85
  user=superuser,
72
86
  defaults={
@@ -76,6 +90,11 @@ class Command(BaseCommand):
76
90
  )
77
91
  if created:
78
92
  self.stdout.write(self.style.SUCCESS(f'✓ Superuser lié au tenant système'))
93
+ try:
94
+ grant_manager_access_to_owners(tenant)
95
+ self.stdout.write(self.style.SUCCESS(f'✓ Droits mis en place'))
96
+ except Exception as e:
97
+ self.stdout.write(self.style.ERROR(f'Erreur lors de la mise en place des droits: {str(e)}'))
79
98
  else:
80
99
  self.stdout.write(self.style.WARNING(f'⚠ Liaison existe déjà'))
81
100