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
@@ -7,15 +7,21 @@ from django.utils.deprecation import MiddlewareMixin
7
7
 
8
8
  from django_tenants.utils import (
9
9
  get_public_schema_name,
10
- get_public_schema_urlconf
10
+ get_public_schema_urlconf,
11
+ get_tenant_types,
12
+ has_multi_type_tenants,
11
13
  )
12
14
  from oxutils.settings import oxi_settings
13
15
  from oxutils.constants import (
14
16
  ORGANIZATION_HEADER_KEY,
15
17
  ORGANIZATION_TOKEN_COOKIE_KEY
16
18
  )
19
+ from oxutils.oxiliere.utils import is_system_tenant
17
20
  from oxutils.jwt.models import TokenTenant
18
21
  from oxutils.jwt.tokens import OrganizationAccessToken
22
+ from oxutils.oxiliere.context import set_current_tenant_schema_name
23
+
24
+
19
25
 
20
26
 
21
27
  class TenantMainMiddleware(MiddlewareMixin):
@@ -45,9 +51,6 @@ class TenantMainMiddleware(MiddlewareMixin):
45
51
  connection.set_schema_to_public()
46
52
 
47
53
  oxi_id = self.get_org_id_from_request(request)
48
- if not oxi_id:
49
- from django.http import HttpResponseBadRequest
50
- return HttpResponseBadRequest('Missing X-Organization-ID header')
51
54
 
52
55
  # Try to get tenant from cookie token first
53
56
  tenant_token = request.COOKIES.get(ORGANIZATION_TOKEN_COOKIE_KEY)
@@ -57,21 +60,34 @@ class TenantMainMiddleware(MiddlewareMixin):
57
60
  if tenant_token:
58
61
  tenant = TokenTenant.for_token(tenant_token)
59
62
  # Verify the token's oxi_id matches the request
60
- if tenant and tenant.oxi_id != oxi_id:
63
+ if not is_system_tenant(tenant) and tenant.oxi_id != oxi_id:
61
64
  tenant = None
62
65
 
63
66
  # If no valid token, fetch from database
64
67
  if not tenant:
65
- tenant_model = connection.tenant_model
66
- try:
67
- tenant = self.get_tenant(tenant_model, oxi_id)
68
- # Mark that we need to set the cookie in the response
69
- request._should_set_tenant_cookie = True
70
- except tenant_model.DoesNotExist:
71
- default_tenant = self.no_tenant_found(request, oxi_id)
72
- return default_tenant
68
+ if oxi_id: # fetch with oxi_id on tenant
69
+ tenant_model = connection.tenant_model
70
+ try:
71
+ tenant = self.get_tenant(tenant_model, oxi_id)
72
+ # Mark that we need to set the cookie in the response
73
+ request._should_set_tenant_cookie = True
74
+ except tenant_model.DoesNotExist:
75
+ default_tenant = self.no_tenant_found(request, oxi_id)
76
+ return default_tenant
77
+ else: # try to return the system tenant
78
+ try:
79
+ from oxutils.oxiliere.caches import get_system_tenant
80
+ tenant = get_system_tenant()
81
+ request._should_set_tenant_cookie = True
82
+ except Exception as e:
83
+ from django.http import HttpResponseBadRequest
84
+ return HttpResponseBadRequest('Missing X-Organization-ID header')
85
+
86
+ if tenant.is_deleted or not tenant.is_active:
87
+ return self.no_tenant_found(request, oxi_id)
73
88
 
74
89
  request.tenant = tenant
90
+ set_current_tenant_schema_name(tenant.schema_name)
75
91
  connection.set_tenant(request.tenant)
76
92
  self.setup_url_routing(request)
77
93
 
@@ -1,25 +1,44 @@
1
+ import time
2
+ import uuid
3
+ import structlog
1
4
  from django.db import models
5
+ from django.utils import timezone
6
+ from django.contrib.auth.models import AbstractBaseUser
2
7
  from django.conf import settings
3
8
  from django.utils.translation import gettext_lazy as _
4
9
  from django_tenants.models import TenantMixin
5
10
  from oxutils.models import (
6
- TimestampMixin,
7
11
  BaseModelMixin,
8
- UUIDPrimaryKeyMixin,
9
12
  )
10
13
  from oxutils.oxiliere.enums import TenantStatus
14
+ from oxutils.oxiliere.exceptions import DeleteError
15
+ from oxutils.oxiliere.signals import (
16
+ tenant_user_removed,
17
+ tenant_user_added,
18
+ )
19
+ from oxutils.oxiliere.utils import (
20
+ is_system_tenant,
21
+ generate_schema_name,
22
+ )
11
23
 
24
+ logger = structlog.get_logger(__name__)
12
25
 
13
26
 
27
+ class TenantQuerySet(models.QuerySet):
28
+ def active(self):
29
+ return self.filter(is_deleted=False)
14
30
 
15
- tenant_model = getattr(settings, 'TENANT_MODEL', 'oxiliere.Tenant')
16
- tenant_user_model = getattr(settings, 'TENANT_USER_MODEL', 'oxiliere.TenantUser')
31
+ def deleted(self):
32
+ return self.filter(is_deleted=True)
17
33
 
34
+ class TenantManager(models.Manager):
35
+ def get_queryset(self):
36
+ return TenantQuerySet(self.model, using=self._db).active()
18
37
 
19
38
 
20
- class BaseTenant(TenantMixin, UUIDPrimaryKeyMixin, TimestampMixin):
39
+ class BaseTenant(TenantMixin, BaseModelMixin):
21
40
  name = models.CharField(max_length=100)
22
- oxi_id = models.CharField(unique=True)
41
+ oxi_id = models.CharField(unique=True, max_length=25)
23
42
  subscription_plan = models.CharField(max_length=255, null=True, blank=True)
24
43
  subscription_status = models.CharField(max_length=255, null=True, blank=True)
25
44
  subscription_end_date = models.DateTimeField(null=True, blank=True)
@@ -29,24 +48,126 @@ class BaseTenant(TenantMixin, UUIDPrimaryKeyMixin, TimestampMixin):
29
48
  default=TenantStatus.ACTIVE
30
49
  )
31
50
 
51
+ # soft delete
52
+ is_deleted = models.BooleanField(default=False)
53
+ deleted_at = models.DateTimeField(null=True, blank=True)
54
+
55
+ suffix = models.CharField(max_length=8, editable=False)
56
+
32
57
  # default true, schema will be automatically created and synced when it is saved
33
58
  auto_create_schema = True
59
+ # Schema will be automatically deleted when related tenant is deleted
60
+ auto_drop_schema = True
61
+
62
+ objects = models.Manager()
63
+ active = TenantManager()
64
+
65
+
66
+ def __str__(self):
67
+ return self.name
68
+
69
+ def save(self, *args, **kwargs):
70
+ if self._state.adding:
71
+ self.suffix = uuid.uuid4().hex[:8]
72
+ self.schema_name = generate_schema_name(self.oxi_id, self.suffix)
73
+ super().save(*args, **kwargs)
74
+
75
+ def delete(self, *args, force_drop: bool = False, **kwargs) -> None:
76
+ """Override deleting of Tenant object.
77
+
78
+ Args:
79
+ force_drop (bool): If True, forces the deletion of the object. Defaults to False.
80
+ *args: Variable length argument list.
81
+ **kwargs: Arbitrary keyword arguments.
82
+ """
83
+ if force_drop:
84
+ super().delete(force_drop, *args, **kwargs)
85
+ else:
86
+ logger.warning("Tenant deletion is not allowed. Use delete_tenant to delete the tenant.")
87
+ raise DeleteError(_("Tenant deletion is not allowed. Use delete_tenant to delete the tenant."))
88
+
89
+ def delete_tenant(self) -> None:
90
+ """Mark tenant for deletion."""
91
+
92
+ if self.is_deleted:
93
+ return
94
+
95
+ # Prevent public tenant schema from being deleted
96
+ if is_system_tenant(self):
97
+ logger.warning("Cannot delete public tenant schema.")
98
+ raise ValueError(_("Cannot delete public tenant schema"))
99
+
100
+ time_string = str(int(time.time()))
101
+ new_id = f"{time_string}-deleted-{self.oxi_id}"
102
+
103
+ self.oxi_id = new_id
104
+ self.deleted_at = timezone.now()
105
+ self.is_deleted = True
106
+ self.is_active = False
107
+ self.status = TenantStatus.DELETED
108
+
109
+ self.save(update_fields=[
110
+ 'oxi_id', 'deleted_at', 'is_deleted', 'is_active', 'status'
111
+ ])
112
+
113
+ def restore(self):
114
+ if not self.is_deleted:
115
+ return
116
+
117
+ oxi_id = self.oxi_id.split("-deleted-")[1]
118
+ self.oxi_id = oxi_id
119
+ self.is_deleted = False
120
+ self.deleted_at = None
121
+ self.is_active = True
122
+ self.status = TenantStatus.ACTIVE
123
+ self.save(update_fields=["oxi_id", "is_deleted", "deleted_at", "is_active", "status"])
124
+
125
+
126
+ def add_user(self, user: AbstractBaseUser, is_owner: bool = False, is_admin: bool = False):
127
+ """Add user to tenant."""
128
+
129
+ if self.users.filter(user=user).exists():
130
+ logger.warning("User is already a member of this tenant.")
131
+ raise ValueError(_("User is already a member of this tenant."))
132
+
133
+ self.users.create(user=user, is_owner=is_owner, is_admin=is_admin)
134
+ tenant_user_added.send(sender=self.__class__, tenant=self, user=user)
135
+
136
+
137
+ def remove_user(self, user: AbstractBaseUser):
138
+ """Remove user from tenant."""
139
+
140
+ if not self.users.filter(user=user).exists():
141
+ logger.warning("User is not a member of this tenant.")
142
+ raise ValueError("User is not a member of this tenant.")
143
+
144
+ self.users.filter(user=user).delete()
145
+ logger.info("User removed from tenant.")
146
+ tenant_user_removed.send(sender=self.__class__, tenant=self, user=user)
147
+
34
148
 
35
149
  class Meta:
36
150
  abstract = True
37
151
  verbose_name = _('Tenant')
38
152
  verbose_name_plural = _('Tenants')
39
153
  indexes = [
40
- models.Index(fields=['oxi_id'])
154
+ models.Index(fields=['schema_name']),
155
+ models.Index(fields=['oxi_id']),
156
+ models.Index(fields=['is_deleted']),
157
+ models.Index(fields=['oxi_id', 'is_deleted'])
41
158
  ]
42
159
 
43
160
 
44
161
  class BaseTenantUser(BaseModelMixin):
45
162
  tenant = models.ForeignKey(
46
- tenant_model, on_delete=models.CASCADE
163
+ settings.TENANT_MODEL,
164
+ on_delete=models.CASCADE,
165
+ related_name='users'
47
166
  )
48
167
  user = models.ForeignKey(
49
- settings.AUTH_USER_MODEL, on_delete=models.CASCADE
168
+ settings.AUTH_USER_MODEL,
169
+ on_delete=models.CASCADE,
170
+ related_name='tenants'
50
171
  )
51
172
  is_owner = models.BooleanField(default=False)
52
173
  is_admin = models.BooleanField(default=False)
@@ -69,13 +190,3 @@ class BaseTenantUser(BaseModelMixin):
69
190
  indexes = [
70
191
  models.Index(fields=['tenant', 'user'])
71
192
  ]
72
-
73
-
74
- class Tenant(BaseTenant):
75
- class Meta(BaseTenant.Meta):
76
- abstract = not tenant_model == 'oxiliere.Tenant'
77
-
78
-
79
- class TenantUser(BaseTenantUser):
80
- class Meta(BaseTenantUser.Meta):
81
- abstract = not tenant_user_model == 'oxiliere.TenantUser'
@@ -1,9 +1,10 @@
1
1
  from ninja_extra.permissions import BasePermission
2
- from oxutils.oxiliere.models import TenantUser
2
+ from oxutils.oxiliere.utils import get_tenant_user_model
3
3
  from oxutils.constants import OXILIERE_SERVICE_TOKEN
4
4
  from oxutils.jwt.tokens import OxilierServiceToken
5
5
 
6
6
 
7
+
7
8
  class TenantPermission(BasePermission):
8
9
  """
9
10
  Vérifie que l'utilisateur a accès au tenant actuel.
@@ -17,7 +18,7 @@ class TenantPermission(BasePermission):
17
18
  return False
18
19
 
19
20
  # Vérifier que l'utilisateur a accès à ce tenant
20
- return TenantUser.objects.filter(
21
+ return get_tenant_user_model().objects.filter(
21
22
  tenant__pk=request.tenant.pk,
22
23
  user__pk=request.user.pk
23
24
  ).exists()
@@ -34,7 +35,7 @@ class TenantOwnerPermission(BasePermission):
34
35
  if not hasattr(request, 'tenant'):
35
36
  return False
36
37
 
37
- return TenantUser.objects.filter(
38
+ return get_tenant_user_model().objects.filter(
38
39
  tenant__pk=request.tenant.pk,
39
40
  user__pk=request.user.pk,
40
41
  is_owner=True
@@ -52,7 +53,7 @@ class TenantAdminPermission(BasePermission):
52
53
  if not hasattr(request, 'tenant'):
53
54
  return False
54
55
 
55
- return TenantUser.objects.filter(
56
+ return get_tenant_user_model().objects.filter(
56
57
  tenant__pk=request.tenant.pk,
57
58
  user__pk=request.user.pk,
58
59
  is_admin=True
@@ -71,7 +72,7 @@ class TenantUserPermission(BasePermission):
71
72
  if not hasattr(request, 'tenant'):
72
73
  return False
73
74
 
74
- return TenantUser.objects.filter(
75
+ return get_tenant_user_model().objects.filter(
75
76
  tenant__pk=request.tenant.pk,
76
77
  user__pk=request.user.pk
77
78
  ).exists()
@@ -4,8 +4,10 @@ from ninja import Schema
4
4
  from django.db import transaction
5
5
  from django.contrib.auth import get_user_model
6
6
  from django_tenants.utils import get_tenant_model
7
- from oxutils.oxiliere.models import TenantUser
8
- from oxutils.oxiliere.utils import oxid_to_schema_name
7
+ from oxutils.oxiliere.utils import (
8
+ get_tenant_user_model,
9
+ )
10
+ from oxutils.oxiliere.authorization import grant_manager_access_to_owners
9
11
  import structlog
10
12
 
11
13
  logger = structlog.get_logger(__name__)
@@ -22,6 +24,8 @@ class TenantSchema(Schema):
22
24
 
23
25
  class TenantOwnerSchema(Schema):
24
26
  oxi_id: UUID
27
+ first_name: Optional[str] = None
28
+ last_name: Optional[str] = None
25
29
  email: str
26
30
 
27
31
 
@@ -34,6 +38,7 @@ class CreateTenantSchema(Schema):
34
38
  def create_tenant(self):
35
39
  UserModel = get_user_model()
36
40
  TenantModel = get_tenant_model()
41
+ TenantUserModel = get_tenant_user_model()
37
42
 
38
43
  if TenantModel.objects.filter(oxi_id=self.tenant.oxi_id).exists():
39
44
  logger.info("tenant_exists", oxi_id=self.tenant.oxi_id)
@@ -42,25 +47,29 @@ class CreateTenantSchema(Schema):
42
47
  user, _ = UserModel.objects.get_or_create(
43
48
  oxi_id=self.owner.oxi_id,
44
49
  defaults={
50
+ 'id': self.owner.oxi_id,
45
51
  'email': self.owner.email,
52
+ 'first_name': self.owner.first_name,
53
+ 'last_name': self.owner.last_name
46
54
  }
47
55
  )
48
56
 
49
57
  tenant = TenantModel.objects.create(
50
58
  name=self.tenant.name,
51
- schema_name=oxid_to_schema_name(self.tenant.oxi_id),
59
+ schema_name=self.tenant.oxi_id,
52
60
  oxi_id=self.tenant.oxi_id,
53
61
  subscription_plan=self.tenant.subscription_plan,
54
62
  subscription_status=self.tenant.subscription_status,
55
63
  subscription_end_date=self.tenant.subscription_end_date,
56
64
  )
57
65
 
58
- TenantUser.objects.create(
66
+ TenantUserModel.objects.create(
59
67
  tenant=tenant,
60
68
  user=user,
61
69
  is_owner=True,
62
70
  is_admin=True,
63
71
  )
64
72
 
73
+ grant_manager_access_to_owners(tenant)
65
74
  logger.info("tenant_created", oxi_id=self.tenant.oxi_id)
66
75
  return tenant
@@ -0,0 +1,5 @@
1
+ from django.dispatch import Signal
2
+
3
+
4
+ tenant_user_removed = Signal()
5
+ tenant_user_added = Signal()
oxutils/oxiliere/utils.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from typing import Any
2
+ import uuid
2
3
  from django.apps import apps
3
4
  from django.conf import settings
5
+ from .constants import OXI_SYSTEM_TENANT
4
6
 
5
7
 
6
8
 
@@ -20,6 +22,11 @@ def get_tenant_model() -> Any:
20
22
  def get_tenant_user_model() -> Any:
21
23
  return get_model('TENANT_USER_MODEL')
22
24
 
25
+ def is_system_tenant(tenant: Any) -> bool:
26
+ return tenant.oxi_id == get_system_tenant_oxi_id()
27
+
28
+ def get_system_tenant_oxi_id():
29
+ return getattr(settings, 'OXI_SYSTEM_TENANT', OXI_SYSTEM_TENANT)
23
30
 
24
31
  def oxid_to_schema_name(oxid: str) -> str:
25
32
  """
@@ -58,6 +65,13 @@ def oxid_to_schema_name(oxid: str) -> str:
58
65
  return schema_name
59
66
 
60
67
 
68
+ def generate_schema_name(oxi_id: str, suffix: str = None) -> str:
69
+ cleaned = oxid_to_schema_name(oxi_id)
70
+ if suffix:
71
+ return f"{cleaned}_{suffix}"
72
+ return f"{cleaned}_{uuid.uuid4().hex[:8]}"
73
+
74
+
61
75
  def update_tenant_user(oxi_org_id: str, oxi_user_id: str, data: dict):
62
76
  if not data or isinstance(data, dict) == False: return
63
77
  if not oxi_org_id or not oxi_user_id: return
File without changes