maquinaweb-shared-auth 0.2.60__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.
- maquinaweb_shared_auth-0.2.60.dist-info/METADATA +1003 -0
- maquinaweb_shared_auth-0.2.60.dist-info/RECORD +28 -0
- maquinaweb_shared_auth-0.2.60.dist-info/WHEEL +5 -0
- maquinaweb_shared_auth-0.2.60.dist-info/top_level.txt +1 -0
- shared_auth/__init__.py +7 -0
- shared_auth/abstract_models.py +897 -0
- shared_auth/app.py +9 -0
- shared_auth/authentication.py +55 -0
- shared_auth/conf.py +33 -0
- shared_auth/decorators.py +122 -0
- shared_auth/exceptions.py +23 -0
- shared_auth/fields.py +51 -0
- shared_auth/management/__init__.py +0 -0
- shared_auth/management/commands/__init__.py +0 -0
- shared_auth/management/commands/generate_permissions.py +147 -0
- shared_auth/managers.py +344 -0
- shared_auth/middleware.py +281 -0
- shared_auth/mixins.py +475 -0
- shared_auth/models.py +191 -0
- shared_auth/permissions.py +266 -0
- shared_auth/permissions_cache.py +249 -0
- shared_auth/permissions_helpers.py +251 -0
- shared_auth/router.py +22 -0
- shared_auth/serializers.py +439 -0
- shared_auth/storage_backend.py +6 -0
- shared_auth/urls.py +8 -0
- shared_auth/utils.py +356 -0
- shared_auth/views.py +40 -0
shared_auth/managers.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Managers customizados para os models compartilhados
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth.models import UserManager
|
|
6
|
+
from django.db import models
|
|
7
|
+
|
|
8
|
+
from .exceptions import OrganizationNotFoundError, UserNotFoundError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SharedOrganizationManager(models.Manager):
|
|
12
|
+
"""Manager para SharedOrganization com métodos úteis"""
|
|
13
|
+
|
|
14
|
+
def get_or_fail(self, organization_id):
|
|
15
|
+
"""
|
|
16
|
+
Busca organização ou lança exceção customizada
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
org = SharedOrganization.objects.get_or_fail(123)
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
return self.get(pk=organization_id)
|
|
23
|
+
except self.model.DoesNotExist:
|
|
24
|
+
raise OrganizationNotFoundError(
|
|
25
|
+
f"Organização com ID {organization_id} não encontrada"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def active(self):
|
|
29
|
+
"""Retorna apenas organizações ativas (não deletadas)"""
|
|
30
|
+
return self.filter(deleted_at__isnull=True)
|
|
31
|
+
|
|
32
|
+
def branches(self):
|
|
33
|
+
"""Retorna apenas filiais"""
|
|
34
|
+
return self.filter(is_branch=True)
|
|
35
|
+
|
|
36
|
+
def main_organizations(self):
|
|
37
|
+
"""Retorna apenas organizações principais"""
|
|
38
|
+
return self.filter(is_branch=False)
|
|
39
|
+
|
|
40
|
+
def by_cnpj(self, cnpj):
|
|
41
|
+
"""Busca por CNPJ"""
|
|
42
|
+
import re
|
|
43
|
+
|
|
44
|
+
clean_cnpj = re.sub(r"[^0-9]", "", cnpj)
|
|
45
|
+
return self.filter(cnpj__contains=clean_cnpj).first()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserManager(UserManager):
|
|
49
|
+
"""Manager para User"""
|
|
50
|
+
|
|
51
|
+
def get_or_fail(self, user_id):
|
|
52
|
+
"""Busca usuário ou lança exceção"""
|
|
53
|
+
try:
|
|
54
|
+
return self.get(pk=user_id)
|
|
55
|
+
except self.model.DoesNotExist:
|
|
56
|
+
raise UserNotFoundError(f"Usuário com ID {user_id} não encontrado")
|
|
57
|
+
|
|
58
|
+
def active(self):
|
|
59
|
+
"""Retorna usuários ativos"""
|
|
60
|
+
return self.filter(deleted_at__isnull=True, is_active=True)
|
|
61
|
+
|
|
62
|
+
def by_email(self, email):
|
|
63
|
+
"""Busca por email"""
|
|
64
|
+
return self.filter(email=email).first()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SharedMemberManager(models.Manager):
|
|
68
|
+
"""Manager para SharedMember"""
|
|
69
|
+
|
|
70
|
+
def for_user(self, user_id):
|
|
71
|
+
"""Retorna memberships de um usuário"""
|
|
72
|
+
return self.filter(user_id=user_id)
|
|
73
|
+
|
|
74
|
+
def for_organization(self, organization_id):
|
|
75
|
+
"""Retorna membros de uma organização"""
|
|
76
|
+
return self.filter(organization_id=organization_id)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OrganizationQuerySetMixin:
|
|
80
|
+
"""Mixin para QuerySets com métodos de organização"""
|
|
81
|
+
|
|
82
|
+
def for_organization(self, organization_id):
|
|
83
|
+
"""Filtra por organização"""
|
|
84
|
+
return self.filter(organization_id=organization_id)
|
|
85
|
+
|
|
86
|
+
def for_organizations(self, organization_ids):
|
|
87
|
+
"""Filtra por múltiplas organizações"""
|
|
88
|
+
return self.filter(organization_id__in=organization_ids)
|
|
89
|
+
|
|
90
|
+
def with_organization_data(self):
|
|
91
|
+
"""
|
|
92
|
+
Pré-carrega dados de organizações (evita N+1)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Lista de objetos com _cached_organization
|
|
96
|
+
"""
|
|
97
|
+
objects = list(self.all())
|
|
98
|
+
from .utils import get_organization_model
|
|
99
|
+
|
|
100
|
+
if not objects:
|
|
101
|
+
return objects
|
|
102
|
+
|
|
103
|
+
# Coletar IDs únicos
|
|
104
|
+
org_ids = set(obj.organization_id for obj in objects)
|
|
105
|
+
|
|
106
|
+
# Buscar todas de uma vez
|
|
107
|
+
Organization = get_organization_model()
|
|
108
|
+
organizations = {
|
|
109
|
+
org.pk: org for org in Organization.objects.filter(pk__in=org_ids)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Cachear nos objetos
|
|
113
|
+
for obj in objects:
|
|
114
|
+
obj._cached_organization = organizations.get(obj.organization_id)
|
|
115
|
+
|
|
116
|
+
return objects
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class UserQuerySetMixin:
|
|
120
|
+
"""Mixin para QuerySets com métodos de usuário"""
|
|
121
|
+
|
|
122
|
+
def for_user(self, user_id):
|
|
123
|
+
"""Filtra por usuário"""
|
|
124
|
+
return self.filter(user_id=user_id)
|
|
125
|
+
|
|
126
|
+
def for_users(self, user_ids):
|
|
127
|
+
"""Filtra por múltiplos usuários"""
|
|
128
|
+
return self.filter(user_id__in=user_ids)
|
|
129
|
+
|
|
130
|
+
def with_user_data(self):
|
|
131
|
+
"""
|
|
132
|
+
Pré-carrega dados de usuários (evita N+1)
|
|
133
|
+
"""
|
|
134
|
+
from .utils import get_user_model
|
|
135
|
+
|
|
136
|
+
objects = list(self.all())
|
|
137
|
+
|
|
138
|
+
if not objects:
|
|
139
|
+
return objects
|
|
140
|
+
|
|
141
|
+
user_ids = set(obj.user_id for obj in objects)
|
|
142
|
+
|
|
143
|
+
User = get_user_model()
|
|
144
|
+
users = {user.pk: user for user in User.objects.filter(pk__in=user_ids)}
|
|
145
|
+
|
|
146
|
+
for obj in objects:
|
|
147
|
+
obj._cached_user = users.get(obj.user_id)
|
|
148
|
+
|
|
149
|
+
return objects
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class OrganizationUserQuerySetMixin(OrganizationQuerySetMixin, UserQuerySetMixin):
|
|
153
|
+
"""Mixin combinado com todos os métodos"""
|
|
154
|
+
|
|
155
|
+
def with_auth_data(self):
|
|
156
|
+
"""
|
|
157
|
+
Pré-carrega dados de organizações E usuários (evita N+1)
|
|
158
|
+
"""
|
|
159
|
+
from .utils import get_organization_model, get_user_model
|
|
160
|
+
|
|
161
|
+
objects = list(self.all())
|
|
162
|
+
|
|
163
|
+
if not objects:
|
|
164
|
+
return objects
|
|
165
|
+
|
|
166
|
+
# Coletar IDs
|
|
167
|
+
org_ids = set(obj.organization_id for obj in objects)
|
|
168
|
+
user_ids = set(obj.user_id for obj in objects)
|
|
169
|
+
|
|
170
|
+
# Buscar em batch
|
|
171
|
+
Organization = get_organization_model()
|
|
172
|
+
User = get_user_model()
|
|
173
|
+
|
|
174
|
+
organizations = {
|
|
175
|
+
org.pk: org for org in Organization.objects.filter(pk__in=org_ids)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
users = {user.pk: user for user in User.objects.filter(pk__in=user_ids)}
|
|
179
|
+
|
|
180
|
+
# Cachear
|
|
181
|
+
for obj in objects:
|
|
182
|
+
obj._cached_organization = organizations.get(obj.organization_id)
|
|
183
|
+
obj._cached_user = users.get(obj.user_id)
|
|
184
|
+
|
|
185
|
+
return objects
|
|
186
|
+
|
|
187
|
+
def create_with_validation(self, organization_id, user_id, **kwargs):
|
|
188
|
+
"""
|
|
189
|
+
Cria objeto com validação de organização e usuário
|
|
190
|
+
"""
|
|
191
|
+
from .utils import get_member_model, get_organization_model
|
|
192
|
+
|
|
193
|
+
# Valida organização
|
|
194
|
+
Organization = get_organization_model()
|
|
195
|
+
Organization.objects.get_or_fail(organization_id)
|
|
196
|
+
|
|
197
|
+
# Valida usuário pertence à organização
|
|
198
|
+
Member = get_member_model()
|
|
199
|
+
if not Member.objects.filter(
|
|
200
|
+
user_id=user_id, organization_id=organization_id
|
|
201
|
+
).exists():
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Usuário {user_id} não pertence à organização {organization_id}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return self.create(organization_id=organization_id, user_id=user_id, **kwargs)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class BaseAuthManager(models.Manager):
|
|
210
|
+
"""Manager base com suporte aos mixins"""
|
|
211
|
+
|
|
212
|
+
def get_queryset(self):
|
|
213
|
+
# Detecta qual mixin está sendo usado
|
|
214
|
+
model_bases = [base.__name__ for base in self.model.__bases__]
|
|
215
|
+
|
|
216
|
+
if "OrganizationUserMixin" in model_bases:
|
|
217
|
+
qs_class = type(
|
|
218
|
+
"QuerySet", (OrganizationUserQuerySetMixin, models.QuerySet), {}
|
|
219
|
+
)
|
|
220
|
+
elif "OrganizationMixin" in model_bases:
|
|
221
|
+
qs_class = type(
|
|
222
|
+
"QuerySet", (OrganizationQuerySetMixin, models.QuerySet), {}
|
|
223
|
+
)
|
|
224
|
+
elif "UserMixin" in model_bases:
|
|
225
|
+
qs_class = type("QuerySet", (UserQuerySetMixin, models.QuerySet), {})
|
|
226
|
+
else:
|
|
227
|
+
return super().get_queryset()
|
|
228
|
+
|
|
229
|
+
return qs_class(self.model, using=self._db)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SystemManager(models.Manager):
|
|
233
|
+
"""Manager for System model"""
|
|
234
|
+
|
|
235
|
+
def get_or_fail(self, system_id):
|
|
236
|
+
"""Get system or raise custom exception"""
|
|
237
|
+
from .exceptions import SharedAuthError
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
return self.get(pk=system_id)
|
|
241
|
+
except self.model.DoesNotExist:
|
|
242
|
+
raise SharedAuthError(f"Sistema com ID {system_id} não encontrado")
|
|
243
|
+
|
|
244
|
+
def active(self):
|
|
245
|
+
"""Return only active systems"""
|
|
246
|
+
return self.filter(active=True)
|
|
247
|
+
|
|
248
|
+
def by_name(self, name):
|
|
249
|
+
"""Search by name"""
|
|
250
|
+
return self.filter(name__iexact=name).first()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class PermissionManager(models.Manager):
|
|
254
|
+
"""Manager for Permission model"""
|
|
255
|
+
|
|
256
|
+
def get_or_fail(self, permission_id):
|
|
257
|
+
"""Get permission or raise exception"""
|
|
258
|
+
from .exceptions import SharedAuthError
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
return self.get(pk=permission_id)
|
|
262
|
+
except self.model.DoesNotExist:
|
|
263
|
+
raise SharedAuthError(f"Permissão com ID {permission_id} não encontrada")
|
|
264
|
+
|
|
265
|
+
def for_system(self, system_id):
|
|
266
|
+
"""Get permissions for a system"""
|
|
267
|
+
return self.filter(system_id=system_id)
|
|
268
|
+
|
|
269
|
+
def by_codename(self, codename, system_id=None):
|
|
270
|
+
"""Search by codename"""
|
|
271
|
+
qs = self.filter(codename=codename)
|
|
272
|
+
if system_id:
|
|
273
|
+
qs = qs.filter(system_id=system_id)
|
|
274
|
+
return qs.first()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class SubscriptionManager(models.Manager):
|
|
278
|
+
"""Manager for Subscription model"""
|
|
279
|
+
|
|
280
|
+
def active(self):
|
|
281
|
+
"""Return only active subscriptions"""
|
|
282
|
+
return self.filter(active=True, paid=True)
|
|
283
|
+
|
|
284
|
+
def for_organization(self, organization_id):
|
|
285
|
+
"""Get subscriptions for an organization"""
|
|
286
|
+
return self.filter(organization_id=organization_id)
|
|
287
|
+
|
|
288
|
+
def for_system(self, system_id):
|
|
289
|
+
"""Get subscriptions for a system (via plan)"""
|
|
290
|
+
return self.filter(plan__system_id=system_id)
|
|
291
|
+
|
|
292
|
+
def valid_for_organization_and_system(self, organization_id, system_id):
|
|
293
|
+
"""
|
|
294
|
+
Get valid subscription for organization and system.
|
|
295
|
+
|
|
296
|
+
Returns the active, paid subscription that hasn't expired.
|
|
297
|
+
"""
|
|
298
|
+
from django.utils import timezone
|
|
299
|
+
|
|
300
|
+
return self.filter(
|
|
301
|
+
organization_id=organization_id,
|
|
302
|
+
plan__system_id=system_id,
|
|
303
|
+
active=True,
|
|
304
|
+
paid=True,
|
|
305
|
+
).filter(
|
|
306
|
+
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now())
|
|
307
|
+
).first()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class GroupOrganizationPermissionsManager(models.Manager):
|
|
311
|
+
"""Manager for GroupOrganizationPermissions model"""
|
|
312
|
+
|
|
313
|
+
def for_organization(self, organization_id):
|
|
314
|
+
"""Get groups for an organization"""
|
|
315
|
+
return self.filter(organization_id=organization_id)
|
|
316
|
+
|
|
317
|
+
def for_system(self, system_id):
|
|
318
|
+
"""Get groups for a system"""
|
|
319
|
+
return self.filter(system_id=system_id)
|
|
320
|
+
|
|
321
|
+
def for_organization_and_system(self, organization_id, system_id):
|
|
322
|
+
"""Get groups for organization and system"""
|
|
323
|
+
return self.filter(organization_id=organization_id, system_id=system_id)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class MemberSystemGroupManager(models.Manager):
|
|
327
|
+
"""Manager for MemberSystemGroup model"""
|
|
328
|
+
|
|
329
|
+
def for_member(self, member_id):
|
|
330
|
+
"""Get groups for a member"""
|
|
331
|
+
return self.filter(member_id=member_id)
|
|
332
|
+
|
|
333
|
+
def for_system(self, system_id):
|
|
334
|
+
"""Get assignments for a system"""
|
|
335
|
+
return self.filter(system_id=system_id)
|
|
336
|
+
|
|
337
|
+
def get_group_for_member_and_system(self, member_id, system_id):
|
|
338
|
+
"""
|
|
339
|
+
Get the group assigned to a member for a specific system.
|
|
340
|
+
|
|
341
|
+
Returns the MemberSystemGroup object or None.
|
|
342
|
+
"""
|
|
343
|
+
return self.filter(member_id=member_id, system_id=system_id).first()
|
|
344
|
+
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middlewares para autenticação compartilhada
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.http import JsonResponse
|
|
6
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
7
|
+
|
|
8
|
+
from .authentication import SharedTokenAuthentication
|
|
9
|
+
from .utils import (
|
|
10
|
+
get_member_model,
|
|
11
|
+
get_organization_model,
|
|
12
|
+
get_token_model,
|
|
13
|
+
get_user_model,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SharedAuthMiddleware(MiddlewareMixin):
|
|
18
|
+
"""
|
|
19
|
+
Middleware que autentica usuário baseado no token do header
|
|
20
|
+
|
|
21
|
+
Usage em settings.py:
|
|
22
|
+
MIDDLEWARE = [
|
|
23
|
+
...
|
|
24
|
+
'shared_auth.middleware.SharedAuthMiddleware',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
O middleware busca o token em:
|
|
28
|
+
- Header: Authorization: Token <token>
|
|
29
|
+
- Header: X-Auth-Token: <token>
|
|
30
|
+
- Cookie: auth_token
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def process_request(self, request):
|
|
34
|
+
from .permissions_cache import init_permissions_cache
|
|
35
|
+
init_permissions_cache(request)
|
|
36
|
+
|
|
37
|
+
# Caminhos que não precisam de autenticação
|
|
38
|
+
exempt_paths = getattr(
|
|
39
|
+
request,
|
|
40
|
+
"auth_exempt_paths",
|
|
41
|
+
[
|
|
42
|
+
"/api/auth/login/",
|
|
43
|
+
"/api/auth/register/",
|
|
44
|
+
"/health/",
|
|
45
|
+
"/static/",
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if any(request.path.startswith(path) for path in exempt_paths):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Extrair token
|
|
53
|
+
token = self._get_token_from_request(request)
|
|
54
|
+
|
|
55
|
+
if not token:
|
|
56
|
+
# request.user = None
|
|
57
|
+
request.auth = None
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Validar token e buscar usuário
|
|
61
|
+
Token = get_token_model()
|
|
62
|
+
User = get_user_model()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
token_obj = Token.objects.get(key=token)
|
|
66
|
+
user = User.objects.get(pk=token_obj.user_id)
|
|
67
|
+
|
|
68
|
+
if not user.is_active or user.deleted_at is not None:
|
|
69
|
+
# request.user = None
|
|
70
|
+
request.auth = None
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Adicionar ao request
|
|
74
|
+
if user:
|
|
75
|
+
request.user = user
|
|
76
|
+
request.auth = token_obj
|
|
77
|
+
|
|
78
|
+
except (Token.DoesNotExist, User.DoesNotExist):
|
|
79
|
+
# request.user = None
|
|
80
|
+
request.auth = None
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _get_token_from_request(self, request):
|
|
85
|
+
"""Extrai token do request"""
|
|
86
|
+
# Header: Authorization: Token <token>
|
|
87
|
+
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
|
|
88
|
+
if auth_header.startswith("Token "):
|
|
89
|
+
return auth_header.split(" ")[1]
|
|
90
|
+
|
|
91
|
+
# Header: X-Auth-Token
|
|
92
|
+
token = request.META.get("HTTP_X_AUTH_TOKEN")
|
|
93
|
+
if token:
|
|
94
|
+
return token
|
|
95
|
+
|
|
96
|
+
# Cookie
|
|
97
|
+
token = request.COOKIES.get("auth_token")
|
|
98
|
+
if token:
|
|
99
|
+
return token
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RequireAuthMiddleware(MiddlewareMixin):
|
|
105
|
+
"""
|
|
106
|
+
Middleware que FORÇA autenticação em todas as rotas
|
|
107
|
+
Retorna 401 se não estiver autenticado
|
|
108
|
+
|
|
109
|
+
Usage em settings.py:
|
|
110
|
+
MIDDLEWARE = [
|
|
111
|
+
'shared_auth.middleware.SharedAuthMiddleware',
|
|
112
|
+
'shared_auth.middleware.RequireAuthMiddleware',
|
|
113
|
+
]
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def process_request(self, request):
|
|
117
|
+
# Caminhos públicos
|
|
118
|
+
public_paths = getattr(
|
|
119
|
+
request,
|
|
120
|
+
"public_paths",
|
|
121
|
+
[
|
|
122
|
+
"/api/auth/",
|
|
123
|
+
"/health/",
|
|
124
|
+
"/docs/",
|
|
125
|
+
"/static/",
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if any(request.path.startswith(path) for path in public_paths):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Verificar se está autenticado
|
|
133
|
+
if not hasattr(request, "user") or request.user is None:
|
|
134
|
+
return JsonResponse(
|
|
135
|
+
{
|
|
136
|
+
"error": "Autenticação necessária",
|
|
137
|
+
"detail": "Token não fornecido ou inválido",
|
|
138
|
+
},
|
|
139
|
+
status=401,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class OrganizationMiddleware(MiddlewareMixin):
|
|
146
|
+
"""
|
|
147
|
+
Middleware que adiciona organização logada ao request
|
|
148
|
+
|
|
149
|
+
Adiciona:
|
|
150
|
+
- request.organization (objeto SharedOrganization)
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def process_request(self, request) -> None:
|
|
154
|
+
ip = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
155
|
+
if ip:
|
|
156
|
+
ip = ip.split(",")[0]
|
|
157
|
+
else:
|
|
158
|
+
ip = request.META.get("REMOTE_ADDR")
|
|
159
|
+
|
|
160
|
+
organization_id = self._determine_organization_id(request)
|
|
161
|
+
user = self._authenticate_user(request)
|
|
162
|
+
|
|
163
|
+
if not organization_id and not user:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if organization_id and user:
|
|
167
|
+
organization_id = self._validate_organization_membership(
|
|
168
|
+
user, organization_id
|
|
169
|
+
)
|
|
170
|
+
if not organization_id:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
organization_ids = self._determine_organization_ids(request)
|
|
174
|
+
|
|
175
|
+
request.organization_id = organization_id
|
|
176
|
+
request.organization_ids = organization_ids
|
|
177
|
+
Organization = get_organization_model()
|
|
178
|
+
request.organization = Organization.objects.filter(pk=organization_id).first()
|
|
179
|
+
|
|
180
|
+
if user and organization_id:
|
|
181
|
+
system_id = self._get_system_id(request)
|
|
182
|
+
if system_id:
|
|
183
|
+
from .permissions_cache import warmup_permissions_cache
|
|
184
|
+
warmup_permissions_cache(user.id, organization_id, system_id, request)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _authenticate_user(request):
|
|
188
|
+
try:
|
|
189
|
+
data = SharedTokenAuthentication().authenticate(request)
|
|
190
|
+
except Exception:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
return data[0] if data else None
|
|
194
|
+
|
|
195
|
+
def _determine_organization_id(self, request):
|
|
196
|
+
org_id = self._get_organization_from_header(request)
|
|
197
|
+
if org_id:
|
|
198
|
+
return org_id
|
|
199
|
+
|
|
200
|
+
return self._get_organization_from_user(request)
|
|
201
|
+
|
|
202
|
+
def _determine_organization_ids(self, request):
|
|
203
|
+
return self._get_organization_ids_from_user(request)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _get_organization_from_header(request):
|
|
207
|
+
if header_value := request.headers.get("X-Organization"):
|
|
208
|
+
try:
|
|
209
|
+
return int(header_value)
|
|
210
|
+
except (ValueError, TypeError):
|
|
211
|
+
pass
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _get_organization_from_user(request):
|
|
216
|
+
"""
|
|
217
|
+
Retorna a primeira organização do usuário autenticado
|
|
218
|
+
"""
|
|
219
|
+
if not request.user.is_authenticated:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# Buscar a primeira organização que o usuário pertence
|
|
223
|
+
Member = get_member_model()
|
|
224
|
+
member = Member.objects.filter(user_id=request.user.pk).first()
|
|
225
|
+
|
|
226
|
+
return member.organization_id if member else None
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _get_organization_ids_from_user(request):
|
|
230
|
+
if not request.user.is_authenticated:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
Member = get_member_model()
|
|
234
|
+
member = Member.objects.filter(user_id=request.user.pk)
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
list(member.values_list("organization_id", flat=True)) if member else None
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _validate_organization_membership(user, organization_id):
|
|
242
|
+
try:
|
|
243
|
+
member = get_member(user.pk, organization_id)
|
|
244
|
+
if not member and not user.is_superuser:
|
|
245
|
+
return None
|
|
246
|
+
return organization_id
|
|
247
|
+
except Exception:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _get_system_id(request):
|
|
252
|
+
"""
|
|
253
|
+
Obtém o system_id para warm-up de permissões.
|
|
254
|
+
|
|
255
|
+
Busca em:
|
|
256
|
+
1. Settings SYSTEM_ID
|
|
257
|
+
2. Header X-System-ID
|
|
258
|
+
"""
|
|
259
|
+
from django.conf import settings
|
|
260
|
+
|
|
261
|
+
system_id = getattr(settings, 'SYSTEM_ID', None)
|
|
262
|
+
if system_id:
|
|
263
|
+
return system_id
|
|
264
|
+
|
|
265
|
+
# Tentar pegar do header
|
|
266
|
+
header_value = request.headers.get('X-System-ID')
|
|
267
|
+
if header_value:
|
|
268
|
+
try:
|
|
269
|
+
return int(header_value)
|
|
270
|
+
except (ValueError, TypeError):
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_member(user_id, organization_id):
|
|
277
|
+
"""Busca membro usando o model configurado"""
|
|
278
|
+
Member = get_member_model()
|
|
279
|
+
return Member.objects.filter(
|
|
280
|
+
user_id=user_id, organization_id=organization_id
|
|
281
|
+
).first()
|