maquinaweb-shared-auth 0.1.2__py3-none-any.whl → 0.1.4__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.
Potentially problematic release.
This version of maquinaweb-shared-auth might be problematic. Click here for more details.
- {maquinaweb_shared_auth-0.1.2.dist-info → maquinaweb_shared_auth-0.1.4.dist-info}/METADATA +2 -2
- maquinaweb_shared_auth-0.1.4.dist-info/RECORD +17 -0
- shared_auth/authentication.py +48 -0
- shared_auth/decorators.py +122 -0
- shared_auth/middleware.py +207 -0
- shared_auth/mixins.py +62 -2
- shared_auth/models.py +35 -0
- shared_auth/permissions.py +86 -0
- shared_auth/router.py +22 -0
- maquinaweb_shared_auth-0.1.2.dist-info/RECORD +0 -12
- {maquinaweb_shared_auth-0.1.2.dist-info → maquinaweb_shared_auth-0.1.4.dist-info}/WHEEL +0 -0
- {maquinaweb_shared_auth-0.1.2.dist-info → maquinaweb_shared_auth-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maquinaweb-shared-auth
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
524
|
+
# views
|
|
525
525
|
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
526
526
|
queryset = Rascunho.objects.all()
|
|
527
527
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
shared_auth/__init__.py,sha256=WbDmRRdOp0nn5I_ksYjhAa2HRtXe0wxtGY6qnaiPlB8,397
|
|
2
|
+
shared_auth/authentication.py,sha256=btrTjBszPrKKfKA4F0pOOzaxnfASgXb1y2cxQwOWbnk,1515
|
|
3
|
+
shared_auth/conf.py,sha256=-jdnCokMvWvVKllfsxNYCsPk1Vo3MDRq4Y1MsO16oeA,411
|
|
4
|
+
shared_auth/decorators.py,sha256=LTDHiVX36O65jbcCUqtoNPNkQ1suDH4fICXVyduZ1BM,3444
|
|
5
|
+
shared_auth/exceptions.py,sha256=eiII-REupK6GeFinisteYO3FsGUDAN5zAajXPhTREm8,404
|
|
6
|
+
shared_auth/fields.py,sha256=RAcmFh1D_nkbai_7t_OrPZhfhAipesy5kKnEj4LUvvM,1254
|
|
7
|
+
shared_auth/managers.py,sha256=NfMhsAFuVmo8MUY2fveB_xVAT5_67nlkVOXS_C8bhew,6920
|
|
8
|
+
shared_auth/middleware.py,sha256=OycDHgHh-1GkicU91xDaxXw_AR43JeQn8PsJXm4w_3E,5979
|
|
9
|
+
shared_auth/mixins.py,sha256=3ARghQ4TvtENKoA5UTOHTrRHa6ufMRVJJHGFkV8-BL8,7550
|
|
10
|
+
shared_auth/models.py,sha256=iszFHOhpD78tqcXaxxZ9_57wLdRq9i_ZsHWrfy5W9Nc,6997
|
|
11
|
+
shared_auth/permissions.py,sha256=kkEMtClmV9VB3EOhKDyinSr4r5v9g-3DCCKNtqpaTz8,2473
|
|
12
|
+
shared_auth/router.py,sha256=nYbqnqjoylD0yeSY7rqyH8N4H6j1MHYD2sMnrhi3sm4,646
|
|
13
|
+
shared_auth/serializers.py,sha256=TDpuZVsOL-6igINSOOOyELWbTUeet4XWRoBkvcMGjW4,4290
|
|
14
|
+
maquinaweb_shared_auth-0.1.4.dist-info/METADATA,sha256=nzMW_SX5YSnQrtpPH-f33d1jXc8_bmG6XHVnlKvy-OU,27150
|
|
15
|
+
maquinaweb_shared_auth-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
maquinaweb_shared_auth-0.1.4.dist-info/top_level.txt,sha256=msyYRy02ZV7zz7GR1raUI5LXGFIFn2TIkgkeKZqKufE,12
|
|
17
|
+
maquinaweb_shared_auth-0.1.4.dist-info/RECORD,,
|
|
@@ -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()
|
shared_auth/mixins.py
CHANGED
|
@@ -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
|
"""
|
shared_auth/models.py
CHANGED
|
@@ -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
|
shared_auth/router.py
ADDED
|
@@ -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
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
shared_auth/__init__.py,sha256=WbDmRRdOp0nn5I_ksYjhAa2HRtXe0wxtGY6qnaiPlB8,397
|
|
2
|
-
shared_auth/conf.py,sha256=-jdnCokMvWvVKllfsxNYCsPk1Vo3MDRq4Y1MsO16oeA,411
|
|
3
|
-
shared_auth/exceptions.py,sha256=eiII-REupK6GeFinisteYO3FsGUDAN5zAajXPhTREm8,404
|
|
4
|
-
shared_auth/fields.py,sha256=RAcmFh1D_nkbai_7t_OrPZhfhAipesy5kKnEj4LUvvM,1254
|
|
5
|
-
shared_auth/managers.py,sha256=NfMhsAFuVmo8MUY2fveB_xVAT5_67nlkVOXS_C8bhew,6920
|
|
6
|
-
shared_auth/mixins.py,sha256=fl7h5IWPXtrdzqXJUe8L1cPjYBjwOkQZ6Rs-ZG1iUJw,5334
|
|
7
|
-
shared_auth/models.py,sha256=gwJ9jCCJ-W21G0D-dcHjlYuKDv8GOwgosNj9QA_aMew,6094
|
|
8
|
-
shared_auth/serializers.py,sha256=TDpuZVsOL-6igINSOOOyELWbTUeet4XWRoBkvcMGjW4,4290
|
|
9
|
-
maquinaweb_shared_auth-0.1.2.dist-info/METADATA,sha256=qc3CdNSkNNOOF02f4Qm2B5H9USLzNvY5z_D9rN6gIdA,27153
|
|
10
|
-
maquinaweb_shared_auth-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
maquinaweb_shared_auth-0.1.2.dist-info/top_level.txt,sha256=msyYRy02ZV7zz7GR1raUI5LXGFIFn2TIkgkeKZqKufE,12
|
|
12
|
-
maquinaweb_shared_auth-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
{maquinaweb_shared_auth-0.1.2.dist-info → maquinaweb_shared_auth-0.1.4.dist-info}/top_level.txt
RENAMED
|
File without changes
|