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.
shared_auth/mixins.py ADDED
@@ -0,0 +1,475 @@
1
+ """
2
+ Mixins para facilitar a criação de models com referências ao sistema de auth
3
+ """
4
+
5
+ from django.db import models
6
+ from rest_framework import status, viewsets
7
+ from rest_framework.response import Response
8
+
9
+ from shared_auth.managers import BaseAuthManager
10
+
11
+
12
+ class OrganizationMixin(models.Model):
13
+ """
14
+ Mixin para models que pertencem a uma organização
15
+
16
+ Adiciona:
17
+ - Campo organization_id
18
+ - Property organization (lazy loading)
19
+ - Métodos úteis
20
+
21
+ Usage:
22
+ class Rascunho(OrganizationMixin):
23
+ titulo = models.CharField(max_length=200)
24
+
25
+ # Uso
26
+ rascunho.organization # Acessa organização automaticamente
27
+ rascunho.organization_members # Acessa membros
28
+ """
29
+
30
+ organization_id = models.IntegerField(
31
+ db_index=True,
32
+ help_text="ID da organização no sistema de autenticação",
33
+ null=True,
34
+ default=None,
35
+ )
36
+ objects = BaseAuthManager()
37
+
38
+ class Meta:
39
+ abstract = True
40
+ indexes = [
41
+ models.Index(fields=["organization_id"]),
42
+ ]
43
+
44
+ @classmethod
45
+ def prefetch_organizations(cls, queryset, request, org_ids=None):
46
+ if not hasattr(request, "_orgs_dict"):
47
+ from shared_auth.utils import get_organization_model
48
+
49
+ Organization = get_organization_model()
50
+ if org_ids is None:
51
+ org_ids = list(
52
+ queryset.values_list("organization_id", flat=True).distinct()
53
+ )
54
+ if not org_ids:
55
+ request._orgs_dict = {}
56
+ return queryset
57
+
58
+ orgs_qs = Organization.objects.filter(pk__in=org_ids)
59
+ request._orgs_dict = {org.pk: org for org in orgs_qs}
60
+
61
+ return queryset
62
+
63
+ @property
64
+ def organization(self):
65
+ if not hasattr(self, "_cached_organization"):
66
+ from shared_auth.utils import get_organization_model
67
+
68
+ Organization = get_organization_model()
69
+ self._cached_organization = Organization.objects.get_or_fail(
70
+ self.organization_id
71
+ )
72
+ return self._cached_organization
73
+
74
+ @property
75
+ def organization_members(self):
76
+ """Retorna membros da organização"""
77
+ return self.organization.members
78
+
79
+ @property
80
+ def organization_users(self):
81
+ """Retorna usuários da organização"""
82
+ return self.organization.users
83
+
84
+ def is_organization_active(self):
85
+ """Verifica se a organização está ativa"""
86
+ return self.organization.is_active()
87
+
88
+ def get_organization_name(self):
89
+ """Retorna nome da organização (safe)"""
90
+ try:
91
+ return self.organization.name
92
+ except Exception:
93
+ return None
94
+
95
+
96
+ class UserMixin(models.Model):
97
+ """
98
+ Mixin para models que pertencem a um usuário
99
+
100
+ Adiciona:
101
+ - Campo user_id
102
+ - Property user (lazy loading)
103
+ - Métodos úteis
104
+
105
+ Usage:
106
+ class Rascunho(UserMixin):
107
+ titulo = models.CharField(max_length=200)
108
+
109
+ # Uso
110
+ rascunho.user # Acessa usuário automaticamente
111
+ rascunho.user_email # Acessa email
112
+ """
113
+
114
+ user_id = models.IntegerField(
115
+ db_index=True,
116
+ help_text="ID do usuário no sistema de autenticação",
117
+ null=True,
118
+ default=None,
119
+ )
120
+ objects = BaseAuthManager()
121
+
122
+ class Meta:
123
+ abstract = True
124
+ indexes = [
125
+ models.Index(fields=["user_id"]),
126
+ ]
127
+
128
+ @property
129
+ def user(self):
130
+ """
131
+ Acessa usuário do banco de auth (lazy loading com cache)
132
+ """
133
+ if not hasattr(self, "_cached_user"):
134
+ from shared_auth.utils import get_user_model
135
+
136
+ User = get_user_model()
137
+ self._cached_user = User.objects.get_or_fail(self.user_id)
138
+ return self._cached_user
139
+
140
+ @property
141
+ def user_email(self):
142
+ """Retorna email do usuário (safe)"""
143
+ try:
144
+ return self.user.email
145
+ except Exception:
146
+ return None
147
+
148
+ @property
149
+ def user_full_name(self):
150
+ """Retorna nome completo do usuário (safe)"""
151
+ try:
152
+ return self.user.get_full_name()
153
+ except Exception:
154
+ return None
155
+
156
+ @property
157
+ def user_organizations(self):
158
+ """Retorna organizações do usuário"""
159
+ return self.user.organizations
160
+
161
+ def is_user_active(self):
162
+ """Verifica se o usuário está ativo"""
163
+ try:
164
+ return self.user.is_active and self.user.deleted_at is None
165
+ except Exception:
166
+ return False
167
+
168
+
169
+ class OrganizationUserMixin(OrganizationMixin, UserMixin):
170
+ """
171
+ Mixin combinado para models que pertencem a organização E usuário
172
+
173
+ Adiciona tudo dos dois mixins + validações
174
+
175
+ Usage:
176
+ class Rascunho(OrganizationUserMixin):
177
+ titulo = models.CharField(max_length=200)
178
+
179
+ # Uso
180
+ rascunho.organization # Organização
181
+ rascunho.user # Usuário
182
+ rascunho.validate_user_belongs_to_organization() # Validação
183
+ """
184
+
185
+ class Meta:
186
+ abstract = True
187
+ indexes = [
188
+ models.Index(fields=["organization_id", "user_id"]),
189
+ ]
190
+
191
+ def validate_user_belongs_to_organization(self):
192
+ """
193
+ Valida se o usuário pertence à organização
194
+
195
+ Returns:
196
+ bool: True se pertence, False caso contrário
197
+ """
198
+ from shared_auth.utils import get_member_model
199
+
200
+ Member = get_member_model()
201
+ return Member.objects.filter(
202
+ user_id=self.user_id, organization_id=self.organization_id
203
+ ).exists()
204
+
205
+ def user_can_access(self, user_id):
206
+ """
207
+ Verifica se um usuário pode acessar este registro
208
+ (se pertence à mesma organização)
209
+ """
210
+ from shared_auth.utils import get_member_model
211
+
212
+ Member = get_member_model()
213
+ return Member.objects.filter(
214
+ user_id=user_id, organization_id=self.organization_id
215
+ ).exists()
216
+
217
+ class RequirePermissionMixin:
218
+ required_permission = None
219
+ required_permissions = None
220
+ base_permission = None
221
+ translate_action_to_perm = {
222
+ 'list': 'view',
223
+ 'retrieve': 'view',
224
+ 'create': 'add',
225
+ 'update': 'change',
226
+ 'partial_update': 'change',
227
+ 'destroy': 'delete',
228
+ }
229
+
230
+ def check_permissions(self, request):
231
+ if hasattr(super(), 'check_permissions'):
232
+ super().check_permissions(request)
233
+
234
+ if self.base_permission and self.action in self.translate_action_to_perm:
235
+ perm = f"{self.translate_action_to_perm.get(self.action)}_{self.base_permission}"
236
+ if not self.check_permission(perm):
237
+ self.permission_denied(
238
+ request,
239
+ message=f"Permissão '{perm}' necessária."
240
+ )
241
+ if self.required_permission:
242
+ if not self.check_permission(self.required_permission):
243
+ self.permission_denied(
244
+ request,
245
+ message=f"Permissão '{self.required_permission}' necessária."
246
+ )
247
+ if self.required_permissions:
248
+ has_any = any(
249
+ self.check_permission(perm)
250
+ for perm in self.required_permissions
251
+ )
252
+ if not has_any:
253
+ self.permission_denied(
254
+ request,
255
+ message=f"Uma das permissões necessárias: {', '.join(self.required_permissions)}"
256
+ )
257
+
258
+ def check_permission(self, permission_codename):
259
+ """
260
+ Verifica se usuário tem permissão específica.
261
+
262
+ OTIMIZADO: Passa request para habilitar cache de permissões.
263
+
264
+ Args:
265
+ permission_codename: Código da permissão (ex: 'create_invoices')
266
+
267
+ Returns:
268
+ bool: True se tem permissão
269
+
270
+ Usage:
271
+ if self.check_permission('create_invoices'):
272
+ # Usuário pode criar faturas
273
+ pass
274
+ """
275
+ from shared_auth.permissions_helpers import user_has_permission
276
+
277
+ system_id = self.get_system_id()
278
+ if not system_id:
279
+ return False
280
+
281
+ organization_id = self.get_organization_id()
282
+ if not organization_id:
283
+ return False
284
+
285
+ user = self.get_user()
286
+ if not user or not user.is_authenticated:
287
+ return False
288
+
289
+ return user_has_permission(
290
+ user.id,
291
+ organization_id,
292
+ permission_codename,
293
+ system_id,
294
+ request=getattr(self, 'request', None)
295
+ )
296
+
297
+ def require_permission(self, permission_codename):
298
+ """
299
+ Retorna erro se usuário não tiver permissão.
300
+
301
+ Args:
302
+ permission_codename: Código da permissão
303
+
304
+ Returns:
305
+ Response com erro 403 ou None se tem permissão
306
+
307
+ Usage:
308
+ response = self.require_permission('create_invoices')
309
+ if response:
310
+ return response
311
+ """
312
+ if not self.check_permission(permission_codename):
313
+ return Response(
314
+ {"detail": f"Permissão '{permission_codename}' necessária."},
315
+ status=status.HTTP_403_FORBIDDEN
316
+ )
317
+ return None
318
+
319
+ def get_user_permissions(self):
320
+ """
321
+ Lista todas as permissões do usuário no sistema atual.
322
+
323
+ Returns:
324
+ list[Permission]: Permissões do usuário
325
+
326
+ Usage:
327
+ perms = self.get_user_permissions()
328
+ for perm in perms:
329
+ print(perm.codename)
330
+ """
331
+ from shared_auth.permissions_helpers import get_user_permissions
332
+
333
+ system_id = self.get_system_id()
334
+ if not system_id:
335
+ return []
336
+
337
+ organization_id = self.get_organization_id()
338
+ if not organization_id:
339
+ return []
340
+
341
+ user = self.get_user()
342
+ if not user or not user.is_authenticated:
343
+ return []
344
+
345
+ return get_user_permissions(
346
+ user.id,
347
+ organization_id,
348
+ system_id,
349
+ request=getattr(self, 'request', None)
350
+ )
351
+
352
+ def get_user_permission_codenames(self):
353
+ """
354
+ Lista codenames das permissões do usuário.
355
+
356
+ Returns:
357
+ List[str]: Lista de codenames
358
+
359
+ Usage:
360
+ codenames = self.get_user_permission_codenames()
361
+ # ['create_invoices', 'edit_invoices']
362
+ """
363
+ return [perm.codename for perm in self.get_user_permissions()]
364
+
365
+
366
+
367
+ class LoggedOrganizationPermMixin:
368
+ """
369
+ Mixin para ViewSets que dependem de uma organização logada.
370
+ Integra com a lib maquinaweb-shared-auth.
371
+
372
+ NÃO use diretamente. Use LoggedOrganizationViewSet.
373
+ """
374
+
375
+ def get_organization_id(self):
376
+ """Obtém o ID da organização logada via maquinaweb-shared-auth"""
377
+ return getattr(self.request, 'organization_id', None)
378
+
379
+ def get_organization_ids(self):
380
+ """Obtém os IDs das organizações permitidas via maquinaweb-shared-auth"""
381
+ return getattr(self.request, 'organization_ids', [])
382
+
383
+ def get_user(self):
384
+ """Obtém o usuário atual autenticado"""
385
+ return self.request.user
386
+
387
+ def get_system_id(self):
388
+ """
389
+ Obtém o ID do sistema.
390
+
391
+ Busca em:
392
+ 1. Settings SYSTEM_ID
393
+ 2. Header X-System-ID
394
+ """
395
+ from django.conf import settings
396
+
397
+ system_id = getattr(settings, 'SYSTEM_ID', None)
398
+ if system_id:
399
+ return system_id
400
+
401
+ # Tentar pegar do header
402
+ header_value = self.request.headers.get('X-System-ID')
403
+ if header_value:
404
+ try:
405
+ return int(header_value)
406
+ except (ValueError, TypeError):
407
+ pass
408
+
409
+ return None
410
+
411
+ def check_logged_organization(self):
412
+ """Verifica se há uma organização logada"""
413
+ return self.get_organization_id() is not None
414
+
415
+ def require_logged_organization(self):
416
+ """Retorna erro se não houver organização logada"""
417
+ if not self.check_logged_organization():
418
+ return Response(
419
+ {
420
+ "detail": "Nenhuma organização logada. Defina uma organização antes de continuar."
421
+ },
422
+ status=status.HTTP_403_FORBIDDEN,
423
+ )
424
+ return None
425
+
426
+ def get_queryset(self):
427
+ """Filtra os objetos pela organização logada, se aplicável"""
428
+ queryset = super().get_queryset()
429
+
430
+ response = self.require_logged_organization()
431
+ if response:
432
+ return queryset.none()
433
+
434
+ organization_id = self.get_organization_id()
435
+ if hasattr(queryset.model, "organization_id"):
436
+ return queryset.filter(organization_id=organization_id)
437
+ elif hasattr(queryset.model, "organization"):
438
+ return queryset.filter(organization_id=organization_id)
439
+ return queryset
440
+
441
+ def perform_create(self, serializer):
442
+ """Define a organização automaticamente ao criar um objeto"""
443
+ response = self.require_logged_organization()
444
+ if response:
445
+ # CORRIGIDO: Lançar exceção em vez de retornar Response
446
+ from rest_framework.exceptions import PermissionDenied
447
+ raise PermissionDenied("Nenhuma organização logada.")
448
+
449
+ organization_id = self.get_organization_id()
450
+
451
+ if "organization" in serializer.fields:
452
+ serializer.save(organization_id=organization_id)
453
+ else:
454
+ serializer.save()
455
+
456
+ class LoggedOrganizationMixin(RequirePermissionMixin, LoggedOrganizationPermMixin, viewsets.ModelViewSet):
457
+ pass
458
+
459
+
460
+ class PrefetchOrganizationsMixin(LoggedOrganizationMixin):
461
+ def get_queryset(self):
462
+ queryset = super().get_queryset()
463
+ return OrganizationMixin.prefetch_organizations(queryset, self.request)
464
+
465
+
466
+ class TimestampedMixin(models.Model):
467
+ """
468
+ Mixin para adicionar timestamps
469
+ """
470
+
471
+ created_at = models.DateTimeField(auto_now_add=True)
472
+ updated_at = models.DateTimeField(auto_now=True)
473
+
474
+ class Meta:
475
+ abstract = True
shared_auth/models.py ADDED
@@ -0,0 +1,191 @@
1
+ """
2
+ Models READ-ONLY para acesso aos dados de autenticação
3
+ ATENÇÃO: Estes models NÃO devem ser usados para criar migrations
4
+
5
+ Para customizar estes models, herde dos models abstratos em shared_auth.abstract_models
6
+ e configure no settings.py. Veja a documentação em abstract_models.py
7
+ """
8
+
9
+ from .abstract_models import (
10
+ AbstractGroupOrganizationPermissions,
11
+ AbstractGroupPermissions,
12
+ AbstractMemberSystemGroup,
13
+ AbstractOrganizationGroup,
14
+ AbstractPermission,
15
+ AbstractPlan,
16
+ AbstractSharedMember,
17
+ AbstractSharedOrganization,
18
+ AbstractSharedToken,
19
+ AbstractSubscription,
20
+ AbstractSystem,
21
+ AbstractUser,
22
+ )
23
+ from .conf import (
24
+ GROUP_ORG_PERMISSIONS_TABLE,
25
+ GROUP_PERMISSIONS_TABLE,
26
+ MEMBER_SYSTEM_GROUP_TABLE,
27
+ ORGANIZATION_GROUP_TABLE,
28
+ PERMISSION_TABLE,
29
+ PLAN_TABLE,
30
+ SUBSCRIPTION_TABLE,
31
+ SYSTEM_TABLE,
32
+ )
33
+ from .managers import (
34
+ GroupOrganizationPermissionsManager,
35
+ MemberSystemGroupManager,
36
+ PermissionManager,
37
+ SubscriptionManager,
38
+ SystemManager,
39
+ )
40
+
41
+
42
+ class SharedToken(AbstractSharedToken):
43
+ """
44
+ Model READ-ONLY padrão da tabela authtoken_token
45
+
46
+ Para customizar, crie seu próprio model herdando de AbstractSharedToken
47
+ """
48
+
49
+ class Meta(AbstractSharedToken.Meta):
50
+ pass
51
+
52
+
53
+ class SharedOrganization(AbstractSharedOrganization):
54
+ """
55
+ Model READ-ONLY padrão da tabela organization
56
+
57
+ Para customizar, crie seu próprio model herdando de AbstractSharedOrganization
58
+ """
59
+
60
+ class Meta(AbstractSharedOrganization.Meta):
61
+ pass
62
+
63
+
64
+ class User(AbstractUser):
65
+ """
66
+ Model READ-ONLY padrão da tabela auth_user
67
+
68
+ Para customizar, crie seu próprio model herdando de AbstractUser
69
+ """
70
+
71
+ class Meta(AbstractUser.Meta):
72
+ pass
73
+
74
+
75
+ class SharedMember(AbstractSharedMember):
76
+ """
77
+ Model READ-ONLY padrão da tabela organization_member
78
+
79
+ Para customizar, crie seu próprio model herdando de AbstractSharedMember
80
+ """
81
+
82
+ class Meta(AbstractSharedMember.Meta):
83
+ pass
84
+
85
+
86
+ class OrganizationGroup(AbstractOrganizationGroup):
87
+ """
88
+ Model READ-ONLY padrão da tabela organization_organizationgroup
89
+ Representa um grupo de organizações com assinatura compartilhada
90
+
91
+ Para customizar, crie seu próprio model herdando de AbstractOrganizationGroup
92
+ """
93
+
94
+ class Meta(AbstractOrganizationGroup.Meta):
95
+ db_table = ORGANIZATION_GROUP_TABLE
96
+
97
+
98
+
99
+ class System(AbstractSystem):
100
+ """
101
+ Model READ-ONLY padrão da tabela plans_system
102
+ Representa um sistema externo que usa este serviço de autenticação
103
+
104
+ Para customizar, crie seu próprio model herdando de AbstractSystem
105
+ """
106
+
107
+ objects = SystemManager()
108
+
109
+ class Meta(AbstractSystem.Meta):
110
+ db_table = SYSTEM_TABLE
111
+
112
+
113
+ class Permission(AbstractPermission):
114
+ """
115
+ Model READ-ONLY padrão da tabela organization_permissions
116
+ Define permissões específicas de cada sistema
117
+
118
+ Para customizar, crie seu próprio model herdando de AbstractPermission
119
+ """
120
+
121
+ objects = PermissionManager()
122
+
123
+ class Meta(AbstractPermission.Meta):
124
+ db_table = PERMISSION_TABLE
125
+
126
+
127
+ class GroupPermissions(AbstractGroupPermissions):
128
+ """
129
+ Model READ-ONLY padrão da tabela organization_grouppermissions
130
+ Grupos base de permissões (usados nos planos)
131
+
132
+ Para customizar, crie seu próprio model herdando de AbstractGroupPermissions
133
+ """
134
+
135
+ class Meta(AbstractGroupPermissions.Meta):
136
+ db_table = GROUP_PERMISSIONS_TABLE
137
+
138
+
139
+ class Plan(AbstractPlan):
140
+ """
141
+ Model READ-ONLY padrão da tabela plans_plan
142
+ Planos oferecidos por cada sistema, com conjunto de permissões
143
+
144
+ Para customizar, crie seu próprio model herdando de AbstractPlan
145
+ """
146
+
147
+ class Meta(AbstractPlan.Meta):
148
+ db_table = PLAN_TABLE
149
+
150
+
151
+ class Subscription(AbstractSubscription):
152
+ """
153
+ Model READ-ONLY padrão da tabela plans_subscription
154
+ Assinatura de uma organização a um plano
155
+
156
+ Para customizar, crie seu próprio model herdando de AbstractSubscription
157
+ """
158
+
159
+ objects = SubscriptionManager()
160
+
161
+ class Meta(AbstractSubscription.Meta):
162
+ db_table = SUBSCRIPTION_TABLE
163
+
164
+
165
+ class GroupOrganizationPermissions(AbstractGroupOrganizationPermissions):
166
+ """
167
+ Model READ-ONLY padrão da tabela organization_grouporganizationpermissions
168
+ Grupos de permissões criados pela organização para distribuir aos usuários
169
+
170
+ Para customizar, crie seu próprio model herdando de AbstractGroupOrganizationPermissions
171
+ """
172
+
173
+ objects = GroupOrganizationPermissionsManager()
174
+
175
+ class Meta(AbstractGroupOrganizationPermissions.Meta):
176
+ db_table = GROUP_ORG_PERMISSIONS_TABLE
177
+
178
+
179
+ class MemberSystemGroup(AbstractMemberSystemGroup):
180
+ """
181
+ Model READ-ONLY padrão da tabela organization_membersystemgroup
182
+ Relaciona um membro a um grupo de permissões em um sistema específico
183
+
184
+ Para customizar, crie seu próprio model herdando de AbstractMemberSystemGroup
185
+ """
186
+
187
+ objects = MemberSystemGroupManager()
188
+
189
+ class Meta(AbstractMemberSystemGroup.Meta):
190
+ db_table = MEMBER_SYSTEM_GROUP_TABLE
191
+