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/app.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend de autenticação usando tokens do banco compartilhado
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
|
6
|
+
from rest_framework import exceptions
|
|
7
|
+
from rest_framework.authentication import TokenAuthentication
|
|
8
|
+
|
|
9
|
+
from .utils import get_token_model, get_user_model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SharedTokenAuthentication(TokenAuthentication):
|
|
13
|
+
"""
|
|
14
|
+
Autentica usando tokens do banco de dados compartilhado
|
|
15
|
+
|
|
16
|
+
Usa get_token_model() e get_user_model() para suportar models customizados.
|
|
17
|
+
|
|
18
|
+
Usage em settings.py:
|
|
19
|
+
REST_FRAMEWORK = {
|
|
20
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
21
|
+
'shared_auth.authentication.SharedTokenAuthentication',
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def model(self):
|
|
28
|
+
"""Retorna o model de Token configurado"""
|
|
29
|
+
return get_token_model()
|
|
30
|
+
|
|
31
|
+
def authenticate_credentials(self, key):
|
|
32
|
+
"""
|
|
33
|
+
Valida o token no banco de dados compartilhado
|
|
34
|
+
"""
|
|
35
|
+
Token = get_token_model()
|
|
36
|
+
User = get_user_model()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
token = Token.objects.get(key=key)
|
|
40
|
+
except Token.DoesNotExist:
|
|
41
|
+
raise exceptions.AuthenticationFailed(_("Token inválido."))
|
|
42
|
+
|
|
43
|
+
# Buscar usuário completo
|
|
44
|
+
try:
|
|
45
|
+
user = User.objects.get(pk=token.user_id)
|
|
46
|
+
except User.DoesNotExist:
|
|
47
|
+
raise exceptions.AuthenticationFailed(_("Usuário não encontrado."))
|
|
48
|
+
|
|
49
|
+
if not user.is_active:
|
|
50
|
+
raise exceptions.AuthenticationFailed(_("Usuário inativo ou deletado."))
|
|
51
|
+
|
|
52
|
+
if user.deleted_at is not None:
|
|
53
|
+
raise exceptions.AuthenticationFailed(_("Usuário deletado."))
|
|
54
|
+
|
|
55
|
+
return (user, token)
|
shared_auth/conf.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
def get_setting(name, default):
|
|
4
|
+
"""Retorna valor configurado no settings ou o padrão"""
|
|
5
|
+
return getattr(settings, name, default)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Auth tables
|
|
9
|
+
ORGANIZATION_TABLE = get_setting("SHARED_AUTH_ORGANIZATION_TABLE", "organization_organization")
|
|
10
|
+
ORGANIZATION_GROUP_TABLE = get_setting("SHARED_AUTH_ORGANIZATION_GROUP_TABLE", "organization_organizationgroup")
|
|
11
|
+
USER_TABLE = get_setting("SHARED_AUTH_USER_TABLE", "auth_user")
|
|
12
|
+
MEMBER_TABLE = get_setting("SHARED_AUTH_MEMBER_TABLE", "organization_member")
|
|
13
|
+
TOKEN_TABLE = get_setting("SHARED_AUTH_TOKEN_TABLE", "organization_multitoken")
|
|
14
|
+
|
|
15
|
+
# Permission system tables
|
|
16
|
+
SYSTEM_TABLE = get_setting("SHARED_AUTH_SYSTEM_TABLE", "plans_system")
|
|
17
|
+
PERMISSION_TABLE = get_setting("SHARED_AUTH_PERMISSION_TABLE", "organization_permissions")
|
|
18
|
+
PLAN_TABLE = get_setting("SHARED_AUTH_PLAN_TABLE", "plans_plan")
|
|
19
|
+
SUBSCRIPTION_TABLE = get_setting("SHARED_AUTH_SUBSCRIPTION_TABLE", "plans_subscription")
|
|
20
|
+
GROUP_PERMISSIONS_TABLE = get_setting("SHARED_AUTH_GROUP_PERMISSIONS_TABLE", "organization_grouppermissions")
|
|
21
|
+
GROUP_ORG_PERMISSIONS_TABLE = get_setting("SHARED_AUTH_GROUP_ORG_PERMISSIONS_TABLE", "organization_grouporganizationpermissions")
|
|
22
|
+
MEMBER_SYSTEM_GROUP_TABLE = get_setting("SHARED_AUTH_MEMBER_SYSTEM_GROUP_TABLE", "organization_membersystemgroup")
|
|
23
|
+
|
|
24
|
+
# и т.д.
|
|
25
|
+
# ManyToMany tables
|
|
26
|
+
PLAN_GROUP_PERMISSIONS_TABLE = get_setting("SHARED_AUTH_PLAN_GROUP_PERMISSIONS_TABLE", "plans_plan_group_permissions")
|
|
27
|
+
GROUP_PERMISSIONS_PERMISSIONS_TABLE = get_setting("SHARED_AUTH_GROUP_PERMISSIONS_PERMISSIONS_TABLE", "organization_grouppermissions_permissions")
|
|
28
|
+
GROUP_ORG_PERMISSIONS_PERMISSIONS_TABLE = get_setting("SHARED_AUTH_GROUP_ORG_PERMISSIONS_PERMISSIONS_TABLE", "organization_grouporganizationpermissions_permissions")
|
|
29
|
+
|
|
30
|
+
# Other settings
|
|
31
|
+
CLOUDFRONT_DOMAIN = get_setting("CLOUDFRONT_DOMAIN", "")
|
|
32
|
+
CUSTOM_DOMAIN_AUTH = get_setting("CUSTOM_DOMAIN_AUTH", CLOUDFRONT_DOMAIN)
|
|
33
|
+
SYSTEM_ID = get_setting("SYSTEM_ID", None)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators para views funcionais
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
from django.http import JsonResponse
|
|
8
|
+
|
|
9
|
+
from .utils import get_organization_model, get_token_model, get_user_model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def require_auth(view_func):
|
|
13
|
+
"""
|
|
14
|
+
Decorator que requer autenticação
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
@require_auth
|
|
18
|
+
def my_view(request):
|
|
19
|
+
return JsonResponse({'user': request.user.email})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@wraps(view_func)
|
|
23
|
+
def wrapped_view(request, *args, **kwargs):
|
|
24
|
+
# Extrair token
|
|
25
|
+
token = _get_token_from_request(request)
|
|
26
|
+
|
|
27
|
+
if not token:
|
|
28
|
+
return JsonResponse({"error": "Token não fornecido"}, status=401)
|
|
29
|
+
|
|
30
|
+
# Validar token
|
|
31
|
+
Token = get_token_model()
|
|
32
|
+
User = get_user_model()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
token_obj = Token.objects.get(key=token)
|
|
36
|
+
user = User.objects.get(pk=token_obj.user_id)
|
|
37
|
+
|
|
38
|
+
if not user.is_active or user.deleted_at is not None:
|
|
39
|
+
return JsonResponse({"error": "Usuário inativo"}, status=401)
|
|
40
|
+
|
|
41
|
+
request.user = user
|
|
42
|
+
request.auth = token_obj
|
|
43
|
+
|
|
44
|
+
except (Token.DoesNotExist, User.DoesNotExist):
|
|
45
|
+
return JsonResponse({"error": "Token inválido"}, status=401)
|
|
46
|
+
|
|
47
|
+
return view_func(request, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
return wrapped_view
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def require_organization(view_func):
|
|
53
|
+
"""
|
|
54
|
+
Decorator que requer organização ativa
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@wraps(view_func)
|
|
58
|
+
@require_auth
|
|
59
|
+
def wrapped_view(request, *args, **kwargs):
|
|
60
|
+
if not hasattr(request, "organization_id") or not request.organization_id:
|
|
61
|
+
return JsonResponse({"error": "Organização não definida"}, status=403)
|
|
62
|
+
|
|
63
|
+
# Buscar organização
|
|
64
|
+
Organization = get_organization_model()
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
org = Organization.objects.get(pk=request.organization_id)
|
|
68
|
+
|
|
69
|
+
if not org.is_active():
|
|
70
|
+
return JsonResponse({"error": "Organização inativa"}, status=403)
|
|
71
|
+
|
|
72
|
+
request.organization = org
|
|
73
|
+
|
|
74
|
+
except Organization.DoesNotExist:
|
|
75
|
+
return JsonResponse({"error": "Organização não encontrada"}, status=404)
|
|
76
|
+
|
|
77
|
+
return view_func(request, *args, **kwargs)
|
|
78
|
+
|
|
79
|
+
return wrapped_view
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def require_same_organization(view_func):
|
|
83
|
+
"""
|
|
84
|
+
Decorator que verifica se objeto pertence à mesma organização
|
|
85
|
+
|
|
86
|
+
O objeto deve estar em kwargs['pk'] ou kwargs['id']
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@wraps(view_func)
|
|
90
|
+
@require_organization
|
|
91
|
+
def wrapped_view(request, *args, **kwargs):
|
|
92
|
+
obj_id = kwargs.get("pk") or kwargs.get("id")
|
|
93
|
+
|
|
94
|
+
if not obj_id:
|
|
95
|
+
return view_func(request, *args, **kwargs)
|
|
96
|
+
|
|
97
|
+
# Aqui você precisa buscar o objeto e verificar
|
|
98
|
+
# Exemplo genérico - adapte conforme seu model
|
|
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,23 @@
|
|
|
1
|
+
from rest_framework.exceptions import APIException
|
|
2
|
+
"""
|
|
3
|
+
Exceções customizadas
|
|
4
|
+
"""
|
|
5
|
+
class SharedAuthError(APIException):
|
|
6
|
+
"""Erro base da biblioteca"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OrganizationNotFoundError(SharedAuthError):
|
|
11
|
+
status_code = 404
|
|
12
|
+
default_detail = 'Organização não encontrada.'
|
|
13
|
+
default_code = 'organization_not_found'
|
|
14
|
+
|
|
15
|
+
class UserNotFoundError(SharedAuthError):
|
|
16
|
+
status_code = 404
|
|
17
|
+
default_detail = 'Usuário não encontrado.'
|
|
18
|
+
default_code = 'user_not_found'
|
|
19
|
+
|
|
20
|
+
class DatabaseConnectionError(SharedAuthError):
|
|
21
|
+
status_code = 500
|
|
22
|
+
default_detail = 'Erro interno'
|
|
23
|
+
default_code = 'internal_error'
|
shared_auth/fields.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
"""
|
|
3
|
+
Fields customizados para facilitar ainda mais
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Fields customizados para facilitar ainda mais
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OrganizationField(serializers.Field):
|
|
12
|
+
"""
|
|
13
|
+
Field que retorna dados completos da organização
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
class RascunhoSerializer(serializers.ModelSerializer):
|
|
17
|
+
organization = OrganizationField(source='*')
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def to_representation(self, obj):
|
|
21
|
+
try:
|
|
22
|
+
org = obj.organization
|
|
23
|
+
return {
|
|
24
|
+
"id": org.pk,
|
|
25
|
+
"name": org.name,
|
|
26
|
+
"fantasy_name": org.fantasy_name,
|
|
27
|
+
"cnpj": org.cnpj,
|
|
28
|
+
"email": org.email,
|
|
29
|
+
"is_active": org.is_active(),
|
|
30
|
+
}
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserField(serializers.Field):
|
|
36
|
+
"""
|
|
37
|
+
Field que retorna dados completos do usuário
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def to_representation(self, obj):
|
|
41
|
+
try:
|
|
42
|
+
user = obj.user
|
|
43
|
+
return {
|
|
44
|
+
"id": user.pk,
|
|
45
|
+
"username": user.username,
|
|
46
|
+
"email": user.email,
|
|
47
|
+
"full_name": user.get_full_name(),
|
|
48
|
+
"is_active": user.is_active,
|
|
49
|
+
}
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
6
|
+
|
|
7
|
+
from shared_auth.conf import get_setting
|
|
8
|
+
|
|
9
|
+
DEFAULT_ACTIONS = [
|
|
10
|
+
("add", "Adicionar"),
|
|
11
|
+
("view", "Visualizar"),
|
|
12
|
+
("change", "Editar"),
|
|
13
|
+
("delete", "Excluir"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(BaseCommand):
|
|
18
|
+
help = "Generate permissions from permissions.yml"
|
|
19
|
+
|
|
20
|
+
def add_arguments(self, parser):
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--file",
|
|
23
|
+
default="permissions.yml",
|
|
24
|
+
help="Path to permissions YAML file (default: permissions.yml)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--dry-run",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Show what would be done without making changes",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def handle(self, *args, **options):
|
|
33
|
+
from shared_auth.utils import get_permission_model, get_system_model
|
|
34
|
+
|
|
35
|
+
system_id = get_setting("SYSTEM_ID", None)
|
|
36
|
+
if not system_id:
|
|
37
|
+
raise CommandError("SYSTEM_ID not configured in settings.")
|
|
38
|
+
|
|
39
|
+
System = get_system_model()
|
|
40
|
+
try:
|
|
41
|
+
system = System.objects.using("auth_db").get(id=system_id)
|
|
42
|
+
except System.DoesNotExist:
|
|
43
|
+
raise CommandError(f"System with ID {system_id} not found.")
|
|
44
|
+
|
|
45
|
+
self.stdout.write(f"System: {system.name} (ID: {system_id})")
|
|
46
|
+
|
|
47
|
+
# Find and parse YAML
|
|
48
|
+
yaml_path = self._find_yaml(options["file"])
|
|
49
|
+
if not yaml_path:
|
|
50
|
+
raise CommandError(f"File not found: {options['file']}")
|
|
51
|
+
|
|
52
|
+
with open(yaml_path, encoding="utf-8") as f:
|
|
53
|
+
data = yaml.safe_load(f)
|
|
54
|
+
|
|
55
|
+
scopes = data.get("scopes", {})
|
|
56
|
+
if not scopes:
|
|
57
|
+
raise CommandError("No scopes found in YAML file.")
|
|
58
|
+
|
|
59
|
+
Permission = get_permission_model()
|
|
60
|
+
dry_run = options["dry_run"]
|
|
61
|
+
created, updated = 0, 0
|
|
62
|
+
|
|
63
|
+
for scope_key, scope_data in scopes.items():
|
|
64
|
+
scope_name = scope_data.get("name", scope_key)
|
|
65
|
+
models = scope_data.get("models", [])
|
|
66
|
+
|
|
67
|
+
self.stdout.write(f"\n[{scope_key}] {scope_name}")
|
|
68
|
+
|
|
69
|
+
for model_config in models:
|
|
70
|
+
model_name = model_config.get("model")
|
|
71
|
+
model_display_name = model_config.get("name", model_name)
|
|
72
|
+
|
|
73
|
+
if not model_name:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Create default CRUD permissions
|
|
77
|
+
for action, action_name in DEFAULT_ACTIONS:
|
|
78
|
+
codename = f"{action}_{model_name}"
|
|
79
|
+
name = f"{action_name} {model_display_name}"
|
|
80
|
+
|
|
81
|
+
c, u = self._create_permission(
|
|
82
|
+
Permission, codename, name, scope_key, system_id, dry_run
|
|
83
|
+
)
|
|
84
|
+
created += c
|
|
85
|
+
updated += u
|
|
86
|
+
|
|
87
|
+
# Create custom permissions
|
|
88
|
+
custom_permissions = model_config.get("custom_permissions", [])
|
|
89
|
+
for custom_perm in custom_permissions:
|
|
90
|
+
action = custom_perm.get("action")
|
|
91
|
+
perm_name = custom_perm.get("name")
|
|
92
|
+
|
|
93
|
+
if not action:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
codename = f"{action}_{model_name}"
|
|
97
|
+
name = perm_name or f"{action} {model_display_name}"
|
|
98
|
+
|
|
99
|
+
c, u = self._create_permission(
|
|
100
|
+
Permission, codename, name, scope_key, system_id, dry_run
|
|
101
|
+
)
|
|
102
|
+
created += c
|
|
103
|
+
updated += u
|
|
104
|
+
|
|
105
|
+
self.stdout.write(f"\nCreated: {created} | Updated: {updated}")
|
|
106
|
+
|
|
107
|
+
def _create_permission(
|
|
108
|
+
self, Permission, codename, name, scope, system_id, dry_run
|
|
109
|
+
) -> tuple[int, int]:
|
|
110
|
+
"""Create or update a permission. Returns (created_count, updated_count)."""
|
|
111
|
+
defaults = {"name": name, "scope": scope}
|
|
112
|
+
|
|
113
|
+
if dry_run:
|
|
114
|
+
exists = (
|
|
115
|
+
Permission.objects.using("auth_db")
|
|
116
|
+
.filter(codename=codename, system_id=system_id)
|
|
117
|
+
.exists()
|
|
118
|
+
)
|
|
119
|
+
status = "exists" if exists else "new"
|
|
120
|
+
self.stdout.write(f" [{status}] {codename}")
|
|
121
|
+
return (0, 0)
|
|
122
|
+
|
|
123
|
+
obj, is_new = Permission.objects.using("auth_db").update_or_create(
|
|
124
|
+
codename=codename,
|
|
125
|
+
system_id=system_id,
|
|
126
|
+
defaults=defaults,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if is_new:
|
|
130
|
+
self.stdout.write(self.style.SUCCESS(f" + {codename}"))
|
|
131
|
+
return (1, 0)
|
|
132
|
+
|
|
133
|
+
self.stdout.write(f" ~ {codename}")
|
|
134
|
+
return (0, 1)
|
|
135
|
+
|
|
136
|
+
def _find_yaml(self, file_path: str) -> Path | None:
|
|
137
|
+
paths = [
|
|
138
|
+
Path(file_path),
|
|
139
|
+
Path(settings.BASE_DIR) / file_path
|
|
140
|
+
if hasattr(settings, "BASE_DIR")
|
|
141
|
+
else None,
|
|
142
|
+
Path.cwd() / file_path,
|
|
143
|
+
]
|
|
144
|
+
for p in paths:
|
|
145
|
+
if p and p.exists():
|
|
146
|
+
return p
|
|
147
|
+
return None
|