maquinaweb-shared-auth 0.1.2__tar.gz → 0.1.4__tar.gz

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.

Potentially problematic release.


This version of maquinaweb-shared-auth might be problematic. Click here for more details.

Files changed (23) hide show
  1. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/PKG-INFO +2 -2
  2. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/README.md +1 -1
  3. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/maquinaweb_shared_auth.egg-info/PKG-INFO +2 -2
  4. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/maquinaweb_shared_auth.egg-info/SOURCES.txt +5 -0
  5. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/pyproject.toml +1 -1
  6. maquinaweb_shared_auth-0.1.4/shared_auth/authentication.py +48 -0
  7. maquinaweb_shared_auth-0.1.4/shared_auth/decorators.py +122 -0
  8. maquinaweb_shared_auth-0.1.4/shared_auth/middleware.py +207 -0
  9. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/mixins.py +62 -2
  10. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/models.py +35 -0
  11. maquinaweb_shared_auth-0.1.4/shared_auth/permissions.py +86 -0
  12. maquinaweb_shared_auth-0.1.4/shared_auth/router.py +22 -0
  13. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/maquinaweb_shared_auth.egg-info/dependency_links.txt +0 -0
  14. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/maquinaweb_shared_auth.egg-info/requires.txt +0 -0
  15. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/maquinaweb_shared_auth.egg-info/top_level.txt +0 -0
  16. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/setup.cfg +0 -0
  17. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/setup.py +0 -0
  18. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/__init__.py +0 -0
  19. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/conf.py +0 -0
  20. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/exceptions.py +0 -0
  21. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/fields.py +0 -0
  22. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/managers.py +0 -0
  23. {maquinaweb_shared_auth-0.1.2 → maquinaweb_shared_auth-0.1.4}/shared_auth/serializers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maquinaweb-shared-auth
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Models read-only para autenticação compartilhada entre projetos Django.
5
5
  Author-email: Seu Nome <seuemail@dominio.com>
6
6
  License: MIT
@@ -521,7 +521,7 @@ class RascunhoDetailSerializer(OrganizationUserSerializerMixin, serializers.Mode
521
521
  ]
522
522
  read_only_fields = ['organization', 'user', 'created_at', 'updated_at']
523
523
 
524
- # views.py
524
+ # views
525
525
  class RascunhoViewSet(viewsets.ModelViewSet):
526
526
  queryset = Rascunho.objects.all()
527
527
 
@@ -500,7 +500,7 @@ class RascunhoDetailSerializer(OrganizationUserSerializerMixin, serializers.Mode
500
500
  ]
501
501
  read_only_fields = ['organization', 'user', 'created_at', 'updated_at']
502
502
 
503
- # views.py
503
+ # views
504
504
  class RascunhoViewSet(viewsets.ModelViewSet):
505
505
  queryset = Rascunho.objects.all()
506
506
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maquinaweb-shared-auth
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Models read-only para autenticação compartilhada entre projetos Django.
5
5
  Author-email: Seu Nome <seuemail@dominio.com>
6
6
  License: MIT
@@ -521,7 +521,7 @@ class RascunhoDetailSerializer(OrganizationUserSerializerMixin, serializers.Mode
521
521
  ]
522
522
  read_only_fields = ['organization', 'user', 'created_at', 'updated_at']
523
523
 
524
- # views.py
524
+ # views
525
525
  class RascunhoViewSet(viewsets.ModelViewSet):
526
526
  queryset = Rascunho.objects.all()
527
527
 
@@ -7,10 +7,15 @@ maquinaweb_shared_auth.egg-info/dependency_links.txt
7
7
  maquinaweb_shared_auth.egg-info/requires.txt
8
8
  maquinaweb_shared_auth.egg-info/top_level.txt
9
9
  shared_auth/__init__.py
10
+ shared_auth/authentication.py
10
11
  shared_auth/conf.py
12
+ shared_auth/decorators.py
11
13
  shared_auth/exceptions.py
12
14
  shared_auth/fields.py
13
15
  shared_auth/managers.py
16
+ shared_auth/middleware.py
14
17
  shared_auth/mixins.py
15
18
  shared_auth/models.py
19
+ shared_auth/permissions.py
20
+ shared_auth/router.py
16
21
  shared_auth/serializers.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maquinaweb-shared-auth"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Models read-only para autenticação compartilhada entre projetos Django."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -0,0 +1,48 @@
1
+ """
2
+ Backend de autenticação usando tokens do banco compartilhado
3
+ """
4
+
5
+ from rest_framework.authentication import TokenAuthentication
6
+ from rest_framework import exceptions
7
+ from django.utils.translation import gettext_lazy as _
8
+ from .models import SharedToken, SharedUser
9
+
10
+
11
+ class SharedTokenAuthentication(TokenAuthentication):
12
+ """
13
+ Autentica usando tokens do banco de dados compartilhado
14
+
15
+ Usage em settings.py:
16
+ REST_FRAMEWORK = {
17
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
18
+ 'shared_auth.authentication.SharedTokenAuthentication',
19
+ ]
20
+ }
21
+ """
22
+
23
+ model = SharedToken
24
+
25
+ def authenticate_credentials(self, key):
26
+ """
27
+ Valida o token no banco de dados compartilhado
28
+ """
29
+ try:
30
+ token = (
31
+ SharedToken.objects.using("auth_db").select_related("user").get(key=key)
32
+ )
33
+ except SharedToken.DoesNotExist:
34
+ raise exceptions.AuthenticationFailed(_("Token inválido."))
35
+
36
+ # Buscar usuário completo
37
+ try:
38
+ user = SharedUser.objects.using("auth_db").get(pk=token.user_id)
39
+ except SharedUser.DoesNotExist:
40
+ raise exceptions.AuthenticationFailed(_("Usuário não encontrado."))
41
+
42
+ if not user.is_active:
43
+ raise exceptions.AuthenticationFailed(_("Usuário inativo ou deletado."))
44
+
45
+ if user.deleted_at is not None:
46
+ raise exceptions.AuthenticationFailed(_("Usuário deletado."))
47
+
48
+ return (user, token)
@@ -0,0 +1,122 @@
1
+ """
2
+ Decorators para views funcionais
3
+ """
4
+
5
+ from functools import wraps
6
+ from django.http import JsonResponse
7
+ from .models import SharedToken, SharedUser, SharedOrganization
8
+
9
+
10
+ def require_auth(view_func):
11
+ """
12
+ Decorator que requer autenticação
13
+
14
+ Usage:
15
+ @require_auth
16
+ def my_view(request):
17
+ return JsonResponse({'user': request.user.email})
18
+ """
19
+
20
+ @wraps(view_func)
21
+ def wrapped_view(request, *args, **kwargs):
22
+ # Extrair token
23
+ token = _get_token_from_request(request)
24
+
25
+ if not token:
26
+ return JsonResponse({"error": "Token não fornecido"}, status=401)
27
+
28
+ # Validar token
29
+ try:
30
+ token_obj = SharedToken.objects.using("auth_db").get(key=token)
31
+ user = SharedUser.objects.using("auth_db").get(pk=token_obj.user_id)
32
+
33
+ if not user.is_active or user.deleted_at is not None:
34
+ return JsonResponse({"error": "Usuário inativo"}, status=401)
35
+
36
+ request.user = user
37
+ request.auth = token_obj
38
+
39
+ except (SharedToken.DoesNotExist, SharedUser.DoesNotExist):
40
+ return JsonResponse({"error": "Token inválido"}, status=401)
41
+
42
+ return view_func(request, *args, **kwargs)
43
+
44
+ return wrapped_view
45
+
46
+
47
+ def require_organization(view_func):
48
+ """
49
+ Decorator que requer organização ativa
50
+ """
51
+
52
+ @wraps(view_func)
53
+ @require_auth
54
+ def wrapped_view(request, *args, **kwargs):
55
+ if (
56
+ not hasattr(request.user, "logged_organization_id")
57
+ or not request.user.logged_organization_id
58
+ ):
59
+ return JsonResponse({"error": "Organização não definida"}, status=403)
60
+
61
+ # Buscar organização
62
+ try:
63
+ org = SharedOrganization.objects.using("auth_db").get(
64
+ pk=request.user.logged_organization_id
65
+ )
66
+
67
+ if not org.is_active():
68
+ return JsonResponse({"error": "Organização inativa"}, status=403)
69
+
70
+ request.organization = org
71
+
72
+ except SharedOrganization.DoesNotExist:
73
+ return JsonResponse({"error": "Organização não encontrada"}, status=404)
74
+
75
+ return view_func(request, *args, **kwargs)
76
+
77
+ return wrapped_view
78
+
79
+
80
+ def require_same_organization(view_func):
81
+ """
82
+ Decorator que verifica se objeto pertence à mesma organização
83
+
84
+ O objeto deve estar em kwargs['pk'] ou kwargs['id']
85
+ """
86
+
87
+ @wraps(view_func)
88
+ @require_organization
89
+ def wrapped_view(request, *args, **kwargs):
90
+ obj_id = kwargs.get("pk") or kwargs.get("id")
91
+
92
+ if not obj_id:
93
+ return view_func(request, *args, **kwargs)
94
+
95
+ # Aqui você precisa buscar o objeto e verificar
96
+ # Exemplo genérico - adapte conforme seu model
97
+ from django.apps import apps
98
+
99
+ # Tentar identificar o model pelo path
100
+ # Esta é uma implementação básica
101
+ # Em produção, você pode passar o model como parâmetro
102
+
103
+ return view_func(request, *args, **kwargs)
104
+
105
+ return wrapped_view
106
+
107
+
108
+ def _get_token_from_request(request):
109
+ """Helper para extrair token"""
110
+ auth_header = request.META.get("HTTP_AUTHORIZATION", "")
111
+ if auth_header.startswith("Token "):
112
+ return auth_header.split(" ")[1]
113
+
114
+ token = request.META.get("HTTP_X_AUTH_TOKEN")
115
+ if token:
116
+ return token
117
+
118
+ token = request.COOKIES.get("auth_token")
119
+ if token:
120
+ return token
121
+
122
+ return None
@@ -0,0 +1,207 @@
1
+ """
2
+ Middlewares para autenticação compartilhada
3
+ """
4
+
5
+ from django.utils.deprecation import MiddlewareMixin
6
+ from django.http import JsonResponse
7
+
8
+ from . import SharedMember
9
+ from .authentication import SharedTokenAuthentication
10
+ from .models import SharedToken, SharedUser, SharedOrganization
11
+
12
+
13
+ class SharedAuthMiddleware(MiddlewareMixin):
14
+ """
15
+ Middleware que autentica usuário baseado no token do header
16
+
17
+ Usage em settings.py:
18
+ MIDDLEWARE = [
19
+ ...
20
+ 'shared_auth.middleware.SharedAuthMiddleware',
21
+ ]
22
+
23
+ O middleware busca o token em:
24
+ - Header: Authorization: Token <token>
25
+ - Header: X-Auth-Token: <token>
26
+ - Cookie: auth_token
27
+ """
28
+
29
+ def process_request(self, request):
30
+ # Caminhos que não precisam de autenticação
31
+ exempt_paths = getattr(
32
+ request,
33
+ "auth_exempt_paths",
34
+ [
35
+ "/api/auth/login/",
36
+ "/api/auth/register/",
37
+ "/health/",
38
+ "/static/",
39
+ ],
40
+ )
41
+
42
+ if any(request.path.startswith(path) for path in exempt_paths):
43
+ return None
44
+
45
+ # Extrair token
46
+ token = self._get_token_from_request(request)
47
+
48
+ if not token:
49
+ request.user = None
50
+ request.auth = None
51
+ return None
52
+
53
+ # Validar token e buscar usuário
54
+ try:
55
+ token_obj = SharedToken.objects.using("auth_db").get(key=token)
56
+ user = SharedUser.objects.using("auth_db").get(pk=token_obj.user_id)
57
+
58
+ if not user.is_active or user.deleted_at is not None:
59
+ request.user = None
60
+ request.auth = None
61
+ return None
62
+
63
+ # Adicionar ao request
64
+ request.user = user
65
+ request.auth = token_obj
66
+
67
+ except (SharedToken.DoesNotExist, SharedUser.DoesNotExist):
68
+ request.user = None
69
+ request.auth = None
70
+
71
+ return None
72
+
73
+ def _get_token_from_request(self, request):
74
+ """Extrai token do request"""
75
+ # Header: Authorization: Token <token>
76
+ auth_header = request.META.get("HTTP_AUTHORIZATION", "")
77
+ if auth_header.startswith("Token "):
78
+ return auth_header.split(" ")[1]
79
+
80
+ # Header: X-Auth-Token
81
+ token = request.META.get("HTTP_X_AUTH_TOKEN")
82
+ if token:
83
+ return token
84
+
85
+ # Cookie
86
+ token = request.COOKIES.get("auth_token")
87
+ if token:
88
+ return token
89
+
90
+ return None
91
+
92
+
93
+ class RequireAuthMiddleware(MiddlewareMixin):
94
+ """
95
+ Middleware que FORÇA autenticação em todas as rotas
96
+ Retorna 401 se não estiver autenticado
97
+
98
+ Usage em settings.py:
99
+ MIDDLEWARE = [
100
+ 'shared_auth.middleware.SharedAuthMiddleware',
101
+ 'shared_auth.middleware.RequireAuthMiddleware',
102
+ ]
103
+ """
104
+
105
+ def process_request(self, request):
106
+ # Caminhos públicos
107
+ public_paths = getattr(
108
+ request,
109
+ "public_paths",
110
+ [
111
+ "/api/auth/",
112
+ "/health/",
113
+ "/docs/",
114
+ "/static/",
115
+ ],
116
+ )
117
+
118
+ if any(request.path.startswith(path) for path in public_paths):
119
+ return None
120
+
121
+ # Verificar se está autenticado
122
+ if not hasattr(request, "user") or request.user is None:
123
+ return JsonResponse(
124
+ {
125
+ "error": "Autenticação necessária",
126
+ "detail": "Token não fornecido ou inválido",
127
+ },
128
+ status=401,
129
+ )
130
+
131
+ return None
132
+
133
+
134
+ class OrganizationMiddleware(MiddlewareMixin):
135
+ """
136
+ Middleware que adiciona organização logada ao request
137
+
138
+ Adiciona:
139
+ - request.organization (objeto SharedOrganization)
140
+ """
141
+
142
+ def process_request(self, request) -> None:
143
+ ip = request.META.get("HTTP_X_FORWARDED_FOR")
144
+ if ip:
145
+ ip = ip.split(",")[0]
146
+ else:
147
+ ip = request.META.get("REMOTE_ADDR")
148
+
149
+ organization_id = self._determine_organization_id(request)
150
+ user = self._authenticate_user(request)
151
+
152
+ if organization_id and user:
153
+ organization_id = self._validate_organization_membership(
154
+ user, organization_id
155
+ )
156
+ if not organization_id:
157
+ return
158
+
159
+ request.organization_id = organization_id
160
+ request.organization = SharedOrganization.objects.get_or_fail(organization_id)
161
+
162
+ @staticmethod
163
+ def _authenticate_user(request):
164
+ data = SharedTokenAuthentication().authenticate(request)
165
+
166
+ return data[0] if data else None
167
+
168
+ def _determine_organization_id(self, request):
169
+ org_id = self._get_organization_from_header(request)
170
+ if org_id:
171
+ return org_id
172
+
173
+ return self._get_organization_from_user(request)
174
+ @staticmethod
175
+ def _get_organization_from_header(request):
176
+ if header_value := request.headers.get("X-Organization"):
177
+ try:
178
+ return int(header_value)
179
+ except (ValueError, TypeError):
180
+ pass
181
+ return None
182
+ @staticmethod
183
+ def _get_organization_from_user(request):
184
+ if not request.user.is_authenticated:
185
+ return None
186
+
187
+ if (
188
+ hasattr(request.user, "logged_organization")
189
+ and request.user.logged_organization
190
+ ):
191
+ return request.user.logged_organization.id
192
+
193
+ return None
194
+ @staticmethod
195
+ def _validate_organization_membership(
196
+ user, organization_id
197
+ ):
198
+ try:
199
+ member = get_member(user, organization_id)
200
+ if not member and not user.is_superuser:
201
+ return None
202
+ return organization_id
203
+ except Exception:
204
+ return None
205
+
206
+ def get_member(user, organization_id):
207
+ return SharedMember.objects.filter(user_id=user.pk, organization_id=organization_id).first()
@@ -3,6 +3,10 @@ Mixins para facilitar a criação de models com referências ao sistema de auth
3
3
  """
4
4
 
5
5
  from django.db import models
6
+ from rest_framework import viewsets, status
7
+ from rest_framework.response import Response
8
+
9
+ from shared_auth.managers import BaseAuthManager
6
10
 
7
11
 
8
12
  class OrganizationMixin(models.Model):
@@ -26,7 +30,7 @@ class OrganizationMixin(models.Model):
26
30
  organization_id = models.IntegerField(
27
31
  db_index=True, help_text="ID da organização no sistema de autenticação"
28
32
  )
29
-
33
+ objects = BaseAuthManager()
30
34
  class Meta:
31
35
  abstract = True
32
36
  indexes = [
@@ -89,7 +93,7 @@ class UserMixin(models.Model):
89
93
  user_id = models.IntegerField(
90
94
  db_index=True, help_text="ID do usuário no sistema de autenticação"
91
95
  )
92
-
96
+ objects = BaseAuthManager()
93
97
  class Meta:
94
98
  abstract = True
95
99
  indexes = [
@@ -188,6 +192,62 @@ class OrganizationUserMixin(OrganizationMixin, UserMixin):
188
192
  .exists()
189
193
  )
190
194
 
195
+ class LoggedOrganizationMixin(viewsets.ModelViewSet):
196
+ """
197
+ Mixin para ViewSets que dependem de uma organização logada.
198
+ Integra com a lib maquinaweb-shared-auth.
199
+ """
200
+
201
+ def get_organization_id(self):
202
+ """Obtém o ID da organização logada via maquinaweb-shared-auth"""
203
+ return self.request.organization_id
204
+
205
+ def get_user(self):
206
+ """Obtém o usuário atual autenticado"""
207
+ return self.request.user
208
+
209
+ def check_logged_organization(self):
210
+ """Verifica se há uma organização logada"""
211
+ return self.get_organization_id() is not None
212
+
213
+ def require_logged_organization(self):
214
+ """Retorna erro se não houver organização logada"""
215
+ if not self.check_logged_organization():
216
+ return Response(
217
+ {
218
+ "detail": "Nenhuma organização logada. Defina uma organização antes de continuar."
219
+ },
220
+ status=status.HTTP_403_FORBIDDEN,
221
+ )
222
+ return None
223
+
224
+ def get_queryset(self):
225
+ """Filtra os objetos pela organização logada, se aplicável"""
226
+ queryset = super().get_queryset()
227
+
228
+ response = self.require_logged_organization()
229
+ if response:
230
+ return queryset.none()
231
+
232
+ organization_id = self.get_organization_id()
233
+ if hasattr(queryset.model, "organization_id"):
234
+ return queryset.filter(organization_id=organization_id)
235
+ elif hasattr(queryset.model, "organization"):
236
+ return queryset.filter(organization_id=organization_id)
237
+ return queryset
238
+
239
+ def perform_create(self, serializer):
240
+ """Define a organização automaticamente ao criar um objeto"""
241
+ response = self.require_logged_organization()
242
+ if response:
243
+ return response
244
+
245
+ organization_id = self.get_organization_id()
246
+
247
+ if "organization" in serializer.fields:
248
+ serializer.save(organization_id=organization_id)
249
+ else:
250
+ serializer.save()
191
251
 
192
252
  class TimestampedMixin(models.Model):
193
253
  """
@@ -12,6 +12,41 @@ from .managers import (
12
12
  )
13
13
 
14
14
 
15
+ class SharedToken(models.Model):
16
+ """
17
+ Model READ-ONLY da tabela authtoken_token
18
+ Usado para validar tokens em outros sistemas
19
+ """
20
+
21
+ key = models.CharField(max_length=40, primary_key=True)
22
+ user_id = models.IntegerField()
23
+ created = models.DateTimeField()
24
+
25
+ objects = models.Manager()
26
+
27
+ class Meta:
28
+ managed = False
29
+ db_table = "authtoken_token"
30
+ app_label = "shared_auth"
31
+
32
+ def __str__(self):
33
+ return self.key
34
+
35
+ @property
36
+ def user(self):
37
+ """Acessa usuário do token"""
38
+ if not hasattr(self, "_cached_user"):
39
+ self._cached_user = SharedUser.objects.using("auth_db").get_or_fail(
40
+ self.user_id
41
+ )
42
+ return self._cached_user
43
+
44
+ def is_valid(self):
45
+ """Verifica se token ainda é válido"""
46
+ # Implementar lógica de expiração se necessário
47
+ return True
48
+
49
+
15
50
  class SharedOrganization(models.Model):
16
51
  """
17
52
  Model READ-ONLY da tabela organization
@@ -0,0 +1,86 @@
1
+ """
2
+ Permissões customizadas para DRF
3
+ """
4
+
5
+ from rest_framework import permissions
6
+
7
+
8
+ class IsAuthenticated(permissions.BasePermission):
9
+ """
10
+ Verifica se usuário está autenticado via SharedToken
11
+ """
12
+
13
+ message = "Autenticação necessária."
14
+
15
+ def has_permission(self, request, view):
16
+ return bool(
17
+ request.user and hasattr(request.user, "pk") and request.user.is_active
18
+ )
19
+
20
+
21
+ class HasActiveOrganization(permissions.BasePermission):
22
+ """
23
+ Verifica se usuário tem organização ativa
24
+ """
25
+
26
+ message = "Organização ativa necessária."
27
+
28
+ def has_permission(self, request, view):
29
+ if not request.user or not hasattr(request.user, "logged_organization_id"):
30
+ return False
31
+
32
+ if not request.user.logged_organization_id:
33
+ return False
34
+
35
+ # Verificar se organização está ativa
36
+ from .models import SharedOrganization
37
+
38
+ try:
39
+ org = SharedOrganization.objects.using("auth_db").get(
40
+ pk=request.user.logged_organization_id
41
+ )
42
+ return org.is_active()
43
+ except SharedOrganization.DoesNotExist:
44
+ return False
45
+
46
+
47
+ class IsSameOrganization(permissions.BasePermission):
48
+ """
49
+ Verifica se o objeto pertence à mesma organização do usuário
50
+
51
+ O model deve ter organization_id
52
+ """
53
+
54
+ message = "Você não tem permissão para acessar este recurso."
55
+
56
+ def has_object_permission(self, request, view, obj):
57
+ if not hasattr(request.user, "logged_organization_id"):
58
+ return False
59
+
60
+ if not hasattr(obj, "organization_id"):
61
+ return True # Se objeto não tem org, permite
62
+
63
+ return obj.organization_id == request.user.logged_organization_id
64
+
65
+
66
+ class IsOwnerOrSameOrganization(permissions.BasePermission):
67
+ """
68
+ Verifica se é o dono do objeto OU da mesma organização
69
+
70
+ O model deve ter user_id e/ou organization_id
71
+ """
72
+
73
+ message = "Você não tem permissão para acessar este recurso."
74
+
75
+ def has_object_permission(self, request, view, obj):
76
+ # Verificar se é o dono
77
+ if hasattr(obj, "user_id") and obj.user_id == request.user.pk:
78
+ return True
79
+
80
+ # Verificar se é da mesma organização
81
+ if hasattr(obj, "organization_id") and hasattr(
82
+ request.user, "logged_organization_id"
83
+ ):
84
+ return obj.organization_id == request.user.logged_organization_id
85
+
86
+ return False
@@ -0,0 +1,22 @@
1
+ class SharedAuthRouter:
2
+ """
3
+ Direciona queries dos models compartilhados para o banco correto
4
+ """
5
+
6
+ route_app_labels = {"shared_auth"}
7
+
8
+ def db_for_read(self, model, **hints):
9
+ if model._meta.app_label in self.route_app_labels:
10
+ return "auth_db"
11
+ return None
12
+
13
+ def db_for_write(self, model, **hints):
14
+ if model._meta.app_label in self.route_app_labels:
15
+ return None
16
+ return None
17
+
18
+ def allow_migrate(self, db, app_label, model_name=None, **hints):
19
+ """Bloqueia migrations"""
20
+ if app_label in self.route_app_labels:
21
+ return False
22
+ return None