oxutils 0.1.12__py3-none-any.whl → 0.1.15__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.
@@ -1,10 +1,13 @@
1
1
  from django.conf import settings
2
+ from django.core.exceptions import ObjectDoesNotExist
2
3
  from django.db import connection
3
4
  from django.http import Http404
4
5
  from django.urls import set_urlconf
5
6
  from django.utils.module_loading import import_string
6
7
  from django.utils.deprecation import MiddlewareMixin
7
8
 
9
+ import structlog
10
+
8
11
  from django_tenants.utils import (
9
12
  get_public_schema_name,
10
13
  get_public_schema_urlconf,
@@ -23,6 +26,9 @@ from oxutils.oxiliere.context import set_current_tenant_schema_name
23
26
 
24
27
 
25
28
 
29
+ logger = structlog.get_logger(__name__)
30
+
31
+
26
32
 
27
33
  class TenantMainMiddleware(MiddlewareMixin):
28
34
  TENANT_NOT_FOUND_EXCEPTION = Http404
@@ -44,6 +50,22 @@ class TenantMainMiddleware(MiddlewareMixin):
44
50
  """
45
51
  return tenant_model.objects.get(oxi_id=oxi_id)
46
52
 
53
+ def get_tenant_user(self, tenant, user, raise_exception=False):
54
+ """ Get tenant user by tenant and user.
55
+ """
56
+ if not tenant or not user:
57
+ if raise_exception:
58
+ raise ObjectDoesNotExist("tenant_user_not_found, tenant or user is None")
59
+ return None
60
+
61
+ try:
62
+ return tenant.users.select_related('user').get(user__pk=user.id)
63
+ except ObjectDoesNotExist:
64
+ logger.error("tenant_user_not_found", tenant_id=tenant.id, user_id=user.id)
65
+ if raise_exception:
66
+ raise ObjectDoesNotExist("tenant_user_not_found")
67
+ return None
68
+
47
69
  def process_request(self, request):
48
70
  # Connection needs first to be at the public schema, as this is where
49
71
  # the tenant metadata is stored.
@@ -51,42 +73,61 @@ class TenantMainMiddleware(MiddlewareMixin):
51
73
  connection.set_schema_to_public()
52
74
 
53
75
  oxi_id = self.get_org_id_from_request(request)
76
+ tenant_model = connection.tenant_model
54
77
 
55
78
  # Try to get tenant from cookie token first
56
79
  tenant_token = request.COOKIES.get(ORGANIZATION_TOKEN_COOKIE_KEY)
57
80
  tenant = None
81
+ old_tenant = None
58
82
  request._should_set_tenant_cookie = False
59
83
 
60
84
  if tenant_token:
61
85
  tenant = TokenTenant.for_token(tenant_token)
62
86
  # Verify the token's oxi_id matches the request
63
87
  if not is_system_tenant(tenant) and tenant.oxi_id != oxi_id:
88
+ logger.info("tenant_token_oxi_id_doesnt_match_request_oxi_id", tenant_oxi_id=tenant.oxi_id, request_oxi_id=oxi_id)
89
+ old_tenant = tenant
64
90
  tenant = None
65
91
 
66
92
  # If no valid token, fetch from database
67
93
  if not tenant:
68
94
  if oxi_id: # fetch with oxi_id on tenant
69
- tenant_model = connection.tenant_model
70
95
  try:
71
96
  tenant = self.get_tenant(tenant_model, oxi_id)
97
+ tenant.user = self.get_tenant_user(tenant, request.user, raise_exception=True)
98
+
72
99
  # Mark that we need to set the cookie in the response
73
100
  request._should_set_tenant_cookie = True
74
- except tenant_model.DoesNotExist:
101
+
102
+ if old_tenant:
103
+ logger.info("tenant_changed", old_tenant=old_tenant.oxi_id, new_tenant=tenant.oxi_id)
104
+
105
+ except ObjectDoesNotExist as ex:
106
+ logger.error("tenant_not_found", oxi_id=oxi_id, error=str(ex))
75
107
  default_tenant = self.no_tenant_found(request, oxi_id)
76
108
  return default_tenant
77
109
  else: # try to return the system tenant
78
110
  try:
79
111
  from oxutils.oxiliere.caches import get_system_tenant
80
112
  tenant = get_system_tenant()
113
+ tenant.user = self.get_tenant_user(tenant, request.user, raise_exception=False)
81
114
  request._should_set_tenant_cookie = True
82
115
  except Exception as e:
116
+ logger.error("system_tenant_not_found", error=str(e))
83
117
  from django.http import HttpResponseBadRequest
84
118
  return HttpResponseBadRequest('Missing X-Organization-ID header')
85
119
 
86
120
  if tenant.is_deleted or not tenant.is_active:
121
+ logger.error("tenant_is_deleted_or_inactive", oxi_id=oxi_id)
87
122
  return self.no_tenant_found(request, oxi_id)
88
123
 
89
- request.tenant = tenant
124
+ if tenant and not isinstance(tenant, TokenTenant):
125
+ request.db_tenant = tenant
126
+ else:
127
+ request.db_tenant = None
128
+
129
+ request.tenant = TokenTenant.from_db(tenant)
130
+
90
131
  set_current_tenant_schema_name(tenant.schema_name)
91
132
  connection.set_tenant(request.tenant)
92
133
  self.setup_url_routing(request)
@@ -94,9 +135,9 @@ class TenantMainMiddleware(MiddlewareMixin):
94
135
  def process_response(self, request, response):
95
136
  """Set the tenant token cookie if needed."""
96
137
  if hasattr(request, '_should_set_tenant_cookie') and request._should_set_tenant_cookie:
97
- if hasattr(request, 'tenant') and not isinstance(request.tenant, TokenTenant):
138
+ if hasattr(request, 'db_tenant') and isinstance(request.db_tenant, connection.tenant_model):
98
139
  # Generate token from DB tenant
99
- token = OrganizationAccessToken.for_tenant(request.tenant)
140
+ token = OrganizationAccessToken.for_tenant(request.db_tenant)
100
141
  response.set_cookie(
101
142
  key=ORGANIZATION_TOKEN_COOKIE_KEY,
102
143
  value=str(token),
@@ -1,81 +1,95 @@
1
+ import structlog
2
+
1
3
  from ninja_extra.permissions import BasePermission
2
- from oxutils.oxiliere.utils import get_tenant_user_model
3
4
  from oxutils.constants import OXILIERE_SERVICE_TOKEN
4
5
  from oxutils.jwt.tokens import OxilierServiceToken
6
+ from oxutils.jwt.models import TokenTenant
7
+
8
+
5
9
 
10
+ logger = structlog.get_logger(__name__)
6
11
 
7
12
 
8
- class TenantPermission(BasePermission):
13
+
14
+
15
+ class TenantBasePermission(BasePermission):
9
16
  """
10
17
  Vérifie que l'utilisateur a accès au tenant actuel.
11
18
  L'utilisateur doit être authentifié et avoir un lien avec le tenant.
12
19
  """
20
+ def check_tenant_permission(self, request) -> bool:
21
+ raise NotImplementedError("Subclasses must implement this method")
22
+
13
23
  def has_permission(self, request, **kwargs):
14
24
  if not request.user or not request.user.is_authenticated:
15
25
  return False
16
26
 
17
27
  if not hasattr(request, 'tenant'):
28
+ logger.warning('tenant_permission', type="tenant_not_found", user=request.user)
29
+ return False
30
+
31
+ if not isinstance(request.tenant, TokenTenant):
32
+ logger.warning(
33
+ 'tenant_permission',
34
+ type="tenant_is_not_token_tenant",
35
+ tenant=request.tenant,
36
+ user=request.user
37
+ )
18
38
  return False
19
-
20
- # Vérifier que l'utilisateur a accès à ce tenant
21
- return get_tenant_user_model().objects.filter(
22
- tenant__pk=request.tenant.pk,
23
- user__pk=request.user.pk
24
- ).exists()
25
39
 
40
+ return self.check_tenant_permission(request)
26
41
 
27
- class TenantOwnerPermission(BasePermission):
42
+
43
+ class TenantUserPermission(TenantBasePermission):
28
44
  """
29
- Vérifie que l'utilisateur est propriétaire (owner) du tenant actuel.
45
+ Vérifie que l'utilisateur est un membre du tenant actuel.
46
+ Alias de TenantPermission pour plus de clarté sémantique.
30
47
  """
31
- def has_permission(self, request, **kwargs):
32
- if not request.user or not request.user.is_authenticated:
33
- return False
34
-
35
- if not hasattr(request, 'tenant'):
36
- return False
48
+ def check_tenant_permission(self, request) -> bool:
49
+ tenant: TokenTenant = request.tenant
50
+
51
+ logger.info(
52
+ 'tenant_permission',
53
+ type="tenant_user_access_permission",
54
+ tenant=tenant, user=request.user,
55
+ passed=tenant.is_tenant_user
56
+ )
37
57
 
38
- return get_tenant_user_model().objects.filter(
39
- tenant__pk=request.tenant.pk,
40
- user__pk=request.user.pk,
41
- is_owner=True
42
- ).exists()
58
+ return tenant.is_tenant_user
43
59
 
44
60
 
45
- class TenantAdminPermission(BasePermission):
61
+ class TenantOwnerPermission(TenantBasePermission):
46
62
  """
47
- Vérifie que l'utilisateur est admin ou owner du tenant actuel.
63
+ Vérifie que l'utilisateur est propriétaire (owner) du tenant actuel.
48
64
  """
49
- def has_permission(self, request, **kwargs):
50
- if not request.user or not request.user.is_authenticated:
51
- return False
52
-
53
- if not hasattr(request, 'tenant'):
54
- return False
65
+ def check_tenant_permission(self, request) -> bool:
66
+ tenant: TokenTenant = request.tenant
67
+
68
+ logger.info(
69
+ 'tenant_permission',
70
+ type="tenant_user_access_permission",
71
+ tenant=tenant, user=request.user,
72
+ passed=tenant.is_owner_user
73
+ )
55
74
 
56
- return get_tenant_user_model().objects.filter(
57
- tenant__pk=request.tenant.pk,
58
- user__pk=request.user.pk,
59
- is_admin=True
60
- ).exists()
75
+ return tenant.is_owner_user
61
76
 
62
77
 
63
- class TenantUserPermission(BasePermission):
78
+ class TenantAdminPermission(TenantBasePermission):
64
79
  """
65
- Vérifie que l'utilisateur est un membre du tenant actuel.
66
- Alias de TenantPermission pour plus de clarté sémantique.
80
+ Vérifie que l'utilisateur est admin ou owner du tenant actuel.
67
81
  """
68
- def has_permission(self, request, **kwargs):
69
- if not request.user or not request.user.is_authenticated:
70
- return False
71
-
72
- if not hasattr(request, 'tenant'):
73
- return False
82
+ def check_tenant_permission(self, request) -> bool:
83
+ tenant: TokenTenant = request.tenant
84
+
85
+ logger.info(
86
+ 'tenant_permission',
87
+ type="tenant_user_access_permission",
88
+ tenant=tenant, user=request.user,
89
+ passed=tenant.is_admin_user
90
+ )
74
91
 
75
- return get_tenant_user_model().objects.filter(
76
- tenant__pk=request.tenant.pk,
77
- user__pk=request.user.pk
78
- ).exists()
92
+ return tenant.is_admin_user
79
93
 
80
94
 
81
95
  class OxiliereServicePermission(BasePermission):
@@ -95,3 +109,10 @@ class OxiliereServicePermission(BasePermission):
95
109
  return True
96
110
  except Exception:
97
111
  return False
112
+
113
+
114
+
115
+ IsTenantUser = TenantUserPermission()
116
+ IsTenantOwner = TenantOwnerPermission()
117
+ IsTenantAdmin = TenantAdminPermission()
118
+ IsOxiliereService = OxiliereServicePermission()
@@ -1,6 +1,9 @@
1
1
  from typing import Optional
2
2
  from uuid import UUID
3
3
  from ninja import Schema
4
+ from django.conf import settings
5
+ from django.core.exceptions import ImproperlyConfigured
6
+ from django.utils.module_loading import import_string
4
7
  from django.db import transaction
5
8
  from django.contrib.auth import get_user_model
6
9
  from django_tenants.utils import get_tenant_model
@@ -13,6 +16,18 @@ import structlog
13
16
  logger = structlog.get_logger(__name__)
14
17
 
15
18
 
19
+
20
+ def get_tenant_schema() -> 'TenantSchema':
21
+ if hasattr(settings, 'OX_TENANT_SCHEMA'):
22
+ try:
23
+ return import_string(settings.OX_TENANT_SCHEMA)
24
+ except ImportError as e:
25
+ raise ImproperlyConfigured(
26
+ f"Error: OX_TENANT_SCHEMA import error: {settings.OX_TENANT_SCHEMA}, please check your settings"
27
+ ) from e
28
+ return TenantSchema
29
+
30
+
16
31
  class TenantSchema(Schema):
17
32
  name: str
18
33
  oxi_id: str
@@ -29,6 +44,22 @@ class TenantOwnerSchema(Schema):
29
44
  email: str
30
45
 
31
46
 
47
+ class UserSchema(Schema):
48
+ oxi_id: UUID
49
+ first_name: Optional[str] = None
50
+ last_name: Optional[str] = None
51
+ email: str
52
+ is_active: bool
53
+ photo: Optional[str] = None
54
+
55
+
56
+ class TenantUser(Schema):
57
+ user: UserSchema
58
+ is_owner: bool
59
+ is_admin: bool
60
+ status: str
61
+
62
+
32
63
  class CreateTenantSchema(Schema):
33
64
  tenant: TenantSchema
34
65
  owner: TenantOwnerSchema
@@ -7,13 +7,27 @@ CACHE_CHECK_PERMISSION = getattr(settings, 'CACHE_CHECK_PERMISSION', False)
7
7
  if CACHE_CHECK_PERMISSION:
8
8
  from cacheops import cached_as
9
9
  from .models import Grant
10
- from .utils import check
10
+ from .utils import check, any_action_check, any_permission_check
11
11
 
12
- @cached_as(Grant, timeout=60*5)
12
+ @cached_as(Grant, timeout=60*15)
13
13
  def cache_check(user, scope, actions, group = None, **context):
14
14
  return check(user, scope, actions, group, **context)
15
+
16
+ @cached_as(Grant, timeout=60*15)
17
+ def cache_any_action_check(user, scope, required, group = None, **context):
18
+ return any_action_check(user, scope, required, group, **context)
19
+
20
+ @cached_as(Grant, timeout=60*15)
21
+ def cache_any_permission_check(user, *str_perms):
22
+ return any_permission_check(user, *str_perms)
15
23
  else:
16
- from .utils import check
24
+ from .utils import check, any_action_check, any_permission_check
17
25
 
18
26
  def cache_check(user, scope, actions, group = None, **context):
19
27
  return check(user, scope, actions, group, **context)
28
+
29
+ def cache_any_action_check(user, scope, required, group = None, **context):
30
+ return any_action_check(user, scope, required, group, **context)
31
+
32
+ def cache_any_permission_check(user, *str_perms):
33
+ return any_permission_check(user, *str_perms)
@@ -45,6 +45,112 @@ class ScopePermission(BasePermission):
45
45
  return str_check(request.user, self.perm, **self.ctx)
46
46
 
47
47
 
48
+ class ScopeAnyPermission(BasePermission):
49
+ """
50
+ Permission class for checking if user has at least one of multiple permissions.
51
+
52
+ Vérifie si l'utilisateur possède au moins une des permissions fournies.
53
+ Utilise any_permission_check pour une vérification optimisée en une seule requête.
54
+
55
+ Example:
56
+ @api_controller('/articles', permissions=[
57
+ ScopeAnyPermission('articles:r', 'articles:w:staff', 'articles:d:admin')
58
+ ])
59
+ class ArticleController:
60
+ # User needs either read access, OR staff write access, OR admin delete access
61
+ pass
62
+ """
63
+
64
+ def __init__(self, *perms: str):
65
+ """
66
+ Initialize the permission checker with multiple permission strings.
67
+
68
+ Args:
69
+ *perms: Variable number of permission strings in format "<scope>:<actions>:<group>?context"
70
+ """
71
+ if not perms:
72
+ raise ValueError("At least one permission string must be provided")
73
+ self.perms = perms
74
+
75
+ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
76
+ """
77
+ Check if the user has at least one of the required permissions.
78
+
79
+ Args:
80
+ request: HTTP request object
81
+ controller: Controller instance
82
+
83
+ Returns:
84
+ True if user has at least one permission, False otherwise
85
+ """
86
+ from oxutils.permissions.caches import cache_any_permission_check
87
+ return cache_any_permission_check(request.user, *self.perms)
88
+
89
+
90
+ class ScopeAnyActionPermission(BasePermission):
91
+ """
92
+ Permission class for checking if user has at least one of multiple actions on a scope.
93
+
94
+ Vérifie si l'utilisateur possède au moins une des actions requises pour un scope donné.
95
+ La chaîne d'actions contient plusieurs actions dont au moins une est requise.
96
+
97
+ Example:
98
+ @api_controller('/articles', permissions=[
99
+ ScopeAnyActionPermission('articles:rwd:staff')
100
+ ])
101
+ class ArticleController:
102
+ # User needs read OR write OR delete access on articles in staff group
103
+ pass
104
+
105
+ @api_controller('/invoices', permissions=[
106
+ ScopeAnyActionPermission('invoices:rw?tenant_id=123')
107
+ ])
108
+ class InvoiceController:
109
+ # User needs read OR write access on invoices with tenant_id=123
110
+ pass
111
+ """
112
+
113
+ def __init__(self, perm: str, ctx: Optional[dict] = None):
114
+ """
115
+ Initialize the permission checker with a permission string.
116
+
117
+ Args:
118
+ perm: Permission string in format "<scope>:<actions>:<group>?context"
119
+ where actions contains multiple characters (e.g., 'rwd' for read OR write OR delete)
120
+ ctx: Optional additional context dict
121
+ """
122
+ if not perm:
123
+ raise ValueError("Permission string must be provided")
124
+
125
+ self.perm = perm
126
+ self.ctx = ctx if ctx else dict()
127
+
128
+ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
129
+ """
130
+ Check if the user has at least one of the required actions.
131
+
132
+ Args:
133
+ request: HTTP request object
134
+ controller: Controller instance
135
+
136
+ Returns:
137
+ True if user has at least one action, False otherwise
138
+ """
139
+ from oxutils.permissions.caches import cache_any_action_check
140
+ from oxutils.permissions.utils import parse_permission
141
+
142
+ scope, actions, group, query_context = parse_permission(self.perm)
143
+ final_context = {**query_context, **self.ctx}
144
+
145
+ return cache_any_action_check(
146
+ request.user,
147
+ scope,
148
+ actions,
149
+ group,
150
+ **final_context
151
+ )
152
+
153
+
48
154
  def access_manager(actions: str):
49
155
  """
50
156
  Factory function for creating ScopePermission instances for access manager.