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.
@@ -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()