oxutils 0.1.5__py3-none-any.whl → 0.1.12__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.
- oxutils/__init__.py +2 -2
- oxutils/audit/migrations/0001_initial.py +2 -2
- oxutils/audit/models.py +2 -2
- oxutils/constants.py +6 -0
- oxutils/jwt/auth.py +150 -1
- oxutils/jwt/models.py +81 -0
- oxutils/jwt/tokens.py +69 -0
- oxutils/jwt/utils.py +45 -0
- oxutils/logger/__init__.py +10 -0
- oxutils/logger/receivers.py +10 -6
- oxutils/logger/settings.py +2 -2
- oxutils/models/base.py +102 -0
- oxutils/models/fields.py +79 -0
- oxutils/oxiliere/apps.py +9 -1
- oxutils/oxiliere/authorization.py +45 -0
- oxutils/oxiliere/caches.py +13 -11
- oxutils/oxiliere/checks.py +31 -0
- oxutils/oxiliere/constants.py +3 -0
- oxutils/oxiliere/context.py +16 -0
- oxutils/oxiliere/exceptions.py +16 -0
- oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
- oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
- oxutils/oxiliere/middleware.py +65 -11
- oxutils/oxiliere/models.py +146 -9
- oxutils/oxiliere/permissions.py +28 -35
- oxutils/oxiliere/schemas.py +16 -6
- oxutils/oxiliere/signals.py +5 -0
- oxutils/oxiliere/utils.py +36 -1
- oxutils/pagination/cursor.py +367 -0
- oxutils/permissions/__init__.py +0 -0
- oxutils/permissions/actions.py +57 -0
- oxutils/permissions/admin.py +3 -0
- oxutils/permissions/apps.py +10 -0
- oxutils/permissions/caches.py +19 -0
- oxutils/permissions/checks.py +188 -0
- oxutils/permissions/constants.py +0 -0
- oxutils/permissions/controllers.py +344 -0
- oxutils/permissions/exceptions.py +60 -0
- oxutils/permissions/management/__init__.py +0 -0
- oxutils/permissions/management/commands/__init__.py +0 -0
- oxutils/permissions/management/commands/load_permission_preset.py +112 -0
- oxutils/permissions/migrations/0001_initial.py +112 -0
- oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
- oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
- oxutils/permissions/migrations/__init__.py +0 -0
- oxutils/permissions/models.py +171 -0
- oxutils/permissions/perms.py +95 -0
- oxutils/permissions/queryset.py +92 -0
- oxutils/permissions/schemas.py +276 -0
- oxutils/permissions/services.py +663 -0
- oxutils/permissions/tests.py +3 -0
- oxutils/permissions/utils.py +628 -0
- oxutils/settings.py +14 -194
- oxutils/users/apps.py +1 -1
- oxutils/users/migrations/0001_initial.py +47 -0
- oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
- oxutils/users/models.py +2 -0
- oxutils/utils.py +25 -0
- {oxutils-0.1.5.dist-info → oxutils-0.1.12.dist-info}/METADATA +21 -11
- oxutils-0.1.12.dist-info/RECORD +122 -0
- oxutils/jwt/client.py +0 -123
- oxutils/jwt/constants.py +0 -1
- oxutils/s3/settings.py +0 -34
- oxutils/s3/storages.py +0 -130
- oxutils-0.1.5.dist-info/RECORD +0 -88
- /oxutils/{s3 → pagination}/__init__.py +0 -0
- {oxutils-0.1.5.dist-info → oxutils-0.1.12.dist-info}/WHEEL +0 -0
|
@@ -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)
|
oxutils/oxiliere/caches.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
from cacheops import cached_as, cached
|
|
2
|
-
from oxutils.oxiliere.
|
|
3
|
-
|
|
2
|
+
from oxutils.oxiliere.utils import (
|
|
3
|
+
get_tenant_model,
|
|
4
|
+
get_tenant_user_model,
|
|
5
|
+
get_system_tenant_oxi_id
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
TenantModel = get_tenant_model()
|
|
13
|
+
TenantUserModel = get_tenant_user_model()
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
@cached_as(TenantModel, timeout=60*15)
|
|
@@ -12,22 +19,17 @@ def get_tenant_by_oxi_id(oxi_id: str):
|
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
@cached_as(TenantModel, timeout=60*15)
|
|
15
|
-
def get_tenant_by_schema_name(schema_name: str)
|
|
22
|
+
def get_tenant_by_schema_name(schema_name: str):
|
|
16
23
|
return TenantModel.objects.get(schema_name=schema_name)
|
|
17
24
|
|
|
18
25
|
|
|
19
|
-
@cached_as(
|
|
26
|
+
@cached_as(TenantUserModel, timeout=60*15)
|
|
20
27
|
def get_tenant_user(oxi_org_id: str, oxi_user_id: str):
|
|
21
|
-
return
|
|
28
|
+
return TenantUserModel.objects.get(
|
|
22
29
|
tenant__oxi_id=oxi_org_id,
|
|
23
30
|
user__oxi_id=oxi_user_id
|
|
24
31
|
)
|
|
25
32
|
|
|
26
33
|
@cached(timeout=60*15)
|
|
27
34
|
def get_system_tenant():
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
system_schema_name = oxid_to_schema_name(
|
|
31
|
-
getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
|
|
32
|
-
)
|
|
33
|
-
return get_tenant_model().objects.get(schema_name=system_schema_name)
|
|
35
|
+
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,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,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
|
|
7
|
-
|
|
8
|
-
|
|
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',
|
|
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',
|
|
24
|
-
owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL',
|
|
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(
|
|
31
|
-
self.stdout.write(self.style.ERROR(f'Le tenant système "{
|
|
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
|
-
|
|
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 =
|
|
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
|
|
oxutils/oxiliere/middleware.py
CHANGED
|
@@ -7,9 +7,22 @@ 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
|
-
from oxutils.
|
|
14
|
+
from oxutils.settings import oxi_settings
|
|
15
|
+
from oxutils.constants import (
|
|
16
|
+
ORGANIZATION_HEADER_KEY,
|
|
17
|
+
ORGANIZATION_TOKEN_COOKIE_KEY
|
|
18
|
+
)
|
|
19
|
+
from oxutils.oxiliere.utils import is_system_tenant
|
|
20
|
+
from oxutils.jwt.models import TokenTenant
|
|
21
|
+
from oxutils.jwt.tokens import OrganizationAccessToken
|
|
22
|
+
from oxutils.oxiliere.context import set_current_tenant_schema_name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
|
|
14
27
|
class TenantMainMiddleware(MiddlewareMixin):
|
|
15
28
|
TENANT_NOT_FOUND_EXCEPTION = Http404
|
|
@@ -38,21 +51,62 @@ class TenantMainMiddleware(MiddlewareMixin):
|
|
|
38
51
|
connection.set_schema_to_public()
|
|
39
52
|
|
|
40
53
|
oxi_id = self.get_org_id_from_request(request)
|
|
41
|
-
if not oxi_id:
|
|
42
|
-
from django.http import HttpResponseBadRequest
|
|
43
|
-
return HttpResponseBadRequest('Missing X-Organization-ID header')
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
# Try to get tenant from cookie token first
|
|
56
|
+
tenant_token = request.COOKIES.get(ORGANIZATION_TOKEN_COOKIE_KEY)
|
|
57
|
+
tenant = None
|
|
58
|
+
request._should_set_tenant_cookie = False
|
|
59
|
+
|
|
60
|
+
if tenant_token:
|
|
61
|
+
tenant = TokenTenant.for_token(tenant_token)
|
|
62
|
+
# Verify the token's oxi_id matches the request
|
|
63
|
+
if not is_system_tenant(tenant) and tenant.oxi_id != oxi_id:
|
|
64
|
+
tenant = None
|
|
65
|
+
|
|
66
|
+
# If no valid token, fetch from database
|
|
67
|
+
if not 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)
|
|
51
88
|
|
|
52
89
|
request.tenant = tenant
|
|
90
|
+
set_current_tenant_schema_name(tenant.schema_name)
|
|
53
91
|
connection.set_tenant(request.tenant)
|
|
54
92
|
self.setup_url_routing(request)
|
|
55
93
|
|
|
94
|
+
def process_response(self, request, response):
|
|
95
|
+
"""Set the tenant token cookie if needed."""
|
|
96
|
+
if hasattr(request, '_should_set_tenant_cookie') and request._should_set_tenant_cookie:
|
|
97
|
+
if hasattr(request, 'tenant') and not isinstance(request.tenant, TokenTenant):
|
|
98
|
+
# Generate token from DB tenant
|
|
99
|
+
token = OrganizationAccessToken.for_tenant(request.tenant)
|
|
100
|
+
response.set_cookie(
|
|
101
|
+
key=ORGANIZATION_TOKEN_COOKIE_KEY,
|
|
102
|
+
value=str(token),
|
|
103
|
+
max_age=60 * oxi_settings.jwt_org_access_token_lifetime,
|
|
104
|
+
httponly=True,
|
|
105
|
+
secure=getattr(settings, 'SESSION_COOKIE_SECURE', False),
|
|
106
|
+
samesite='Lax',
|
|
107
|
+
)
|
|
108
|
+
return response
|
|
109
|
+
|
|
56
110
|
def no_tenant_found(self, request, oxi_id):
|
|
57
111
|
""" What should happen if no tenant is found.
|
|
58
112
|
This makes it easier if you want to override the default behavior """
|
oxutils/oxiliere/models.py
CHANGED
|
@@ -1,18 +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
|
-
from
|
|
8
|
+
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
from django_tenants.models import TenantMixin
|
|
4
10
|
from oxutils.models import (
|
|
5
|
-
TimestampMixin,
|
|
6
11
|
BaseModelMixin,
|
|
7
12
|
)
|
|
8
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
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = structlog.get_logger(__name__)
|
|
25
|
+
|
|
9
26
|
|
|
27
|
+
class TenantQuerySet(models.QuerySet):
|
|
28
|
+
def active(self):
|
|
29
|
+
return self.filter(is_deleted=False)
|
|
10
30
|
|
|
31
|
+
def deleted(self):
|
|
32
|
+
return self.filter(is_deleted=True)
|
|
11
33
|
|
|
34
|
+
class TenantManager(models.Manager):
|
|
35
|
+
def get_queryset(self):
|
|
36
|
+
return TenantQuerySet(self.model, using=self._db).active()
|
|
12
37
|
|
|
13
|
-
|
|
38
|
+
|
|
39
|
+
class BaseTenant(TenantMixin, BaseModelMixin):
|
|
14
40
|
name = models.CharField(max_length=100)
|
|
15
|
-
oxi_id = models.
|
|
41
|
+
oxi_id = models.CharField(unique=True, max_length=25)
|
|
16
42
|
subscription_plan = models.CharField(max_length=255, null=True, blank=True)
|
|
17
43
|
subscription_status = models.CharField(max_length=255, null=True, blank=True)
|
|
18
44
|
subscription_end_date = models.DateTimeField(null=True, blank=True)
|
|
@@ -22,21 +48,126 @@ class Tenant(TenantMixin, TimestampMixin):
|
|
|
22
48
|
default=TenantStatus.ACTIVE
|
|
23
49
|
)
|
|
24
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
|
+
|
|
25
57
|
# default true, schema will be automatically created and synced when it is saved
|
|
26
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."""
|
|
27
91
|
|
|
92
|
+
if self.is_deleted:
|
|
93
|
+
return
|
|
28
94
|
|
|
29
|
-
|
|
30
|
-
|
|
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"))
|
|
31
99
|
|
|
100
|
+
time_string = str(int(time.time()))
|
|
101
|
+
new_id = f"{time_string}-deleted-{self.oxi_id}"
|
|
32
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
|
|
33
108
|
|
|
34
|
-
|
|
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
|
+
|
|
148
|
+
|
|
149
|
+
class Meta:
|
|
150
|
+
abstract = True
|
|
151
|
+
verbose_name = _('Tenant')
|
|
152
|
+
verbose_name_plural = _('Tenants')
|
|
153
|
+
indexes = [
|
|
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'])
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class BaseTenantUser(BaseModelMixin):
|
|
35
162
|
tenant = models.ForeignKey(
|
|
36
|
-
settings.TENANT_MODEL,
|
|
163
|
+
settings.TENANT_MODEL,
|
|
164
|
+
on_delete=models.CASCADE,
|
|
165
|
+
related_name='users'
|
|
37
166
|
)
|
|
38
167
|
user = models.ForeignKey(
|
|
39
|
-
settings.AUTH_USER_MODEL,
|
|
168
|
+
settings.AUTH_USER_MODEL,
|
|
169
|
+
on_delete=models.CASCADE,
|
|
170
|
+
related_name='tenants'
|
|
40
171
|
)
|
|
41
172
|
is_owner = models.BooleanField(default=False)
|
|
42
173
|
is_admin = models.BooleanField(default=False)
|
|
@@ -47,9 +178,15 @@ class TenantUser(BaseModelMixin):
|
|
|
47
178
|
)
|
|
48
179
|
|
|
49
180
|
class Meta:
|
|
181
|
+
abstract = True
|
|
182
|
+
verbose_name = 'Tenant User'
|
|
183
|
+
verbose_name_plural = 'Tenant Users'
|
|
50
184
|
constraints = [
|
|
51
185
|
models.UniqueConstraint(
|
|
52
186
|
fields=['tenant', 'user'],
|
|
53
187
|
name='unique_tenant_user'
|
|
54
188
|
)
|
|
55
189
|
]
|
|
190
|
+
indexes = [
|
|
191
|
+
models.Index(fields=['tenant', 'user'])
|
|
192
|
+
]
|