feature/rbac permisos y roles implementados

This commit is contained in:
2026-05-21 07:54:59 -06:00
parent 9bbed42cf3
commit a318b70324
38 changed files with 2596 additions and 901 deletions

View File

@@ -1,18 +1,22 @@
from django.contrib import admin
from .models import Organizacion
# Register your models here.
@admin.register(Organizacion)
class OrganizacionAdmin(admin.ModelAdmin):
list_display = ('id', 'nombre', 'rfc', 'email', 'telefono', 'is_active', 'is_verified', 'inicia', 'vencimiento')
list_display = ('nombre', 'rfc', 'email', 'telefono', 'owner', 'is_active', 'is_verified', 'inicio', 'vencimiento')
search_fields = ('nombre', 'rfc', 'email')
list_filter = ('is_active', 'is_verified')
list_filter = ('is_active', 'is_verified', 'is_agente_aduanal')
ordering = ('nombre',)
# class UsuarioOrganizacionAdmin(admin.ModelAdmin):
# list_display = ('id', 'email', 'telefono', 'puesto', 'is_active', 'is_verified')
# search_fields = ('email', 'telefono', 'puesto')
# list_filter = ('is_active', 'is_verified')
# ordering = ('email',)
admin.site.register(Organizacion)
# admin.site.register(UsuarioOrganizacion)
autocomplete_fields = ('owner',)
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {'fields': ('nombre', 'rfc', 'titular', 'licencia')}),
('Contacto', {'fields': ('email', 'telefono', 'estado', 'ciudad')}),
('Administrador maestro', {'fields': ('owner',)}),
('Estado', {'fields': ('is_active', 'is_verified', 'is_agente_aduanal', 'apply_auto_download')}),
('Vigencia', {'fields': ('inicio', 'vencimiento')}),
('Observaciones', {'fields': ('observaciones',)}),
('Auditoría', {'fields': ('created_at', 'updated_at')}),
)

View File

@@ -40,6 +40,16 @@ class Organizacion(models.Model):
estado = models.CharField(max_length=50)
ciudad = models.CharField(max_length=50)
# Administrador maestro: acceso total a su org, no puede ser removido de su rol por otros admins.
# on_delete=PROTECT: no se puede eliminar el usuario sin reasignar el ownership primero.
owner = models.ForeignKey(
'cuser.CustomUser',
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='organizaciones_que_administra',
)
is_active = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False)
apply_auto_download = models.BooleanField(default=False)

View File

@@ -1,8 +1,28 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Organizacion, UsoAlmacenamiento
@receiver(post_save, sender=Organizacion)
def crear_uso_almacenamiento(sender, instance, created, **kwargs):
if created:
UsoAlmacenamiento.objects.create(organizacion=instance, espacio_utilizado=0)
UsoAlmacenamiento.objects.create(organizacion=instance, espacio_utilizado=0)
@receiver(post_save, sender=Organizacion)
def crear_roles_default(sender, instance, created, **kwargs):
"""Al crear una organización nueva, genera automáticamente los 5 roles por defecto
con sus permisos. Depende de que el catálogo RolePermission ya exista (post-migration)."""
if not created:
return
try:
from api.rbac.roles import crear_roles_para_organizacion
crear_roles_para_organizacion(instance)
except Exception:
# Si la app rbac aún no está migrada (ej. primer deploy), no bloquear la creación de org
import logging
logging.getLogger(__name__).warning(
'No se pudieron crear roles para org %s — verifica que rbac esté migrado.',
instance.id,
)

View File

@@ -6,10 +6,13 @@ from rest_framework.response import Response
from api.record.models import Document
from core.permissions import (
IsSameOrganization,
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
IsSuperUser,
get_org_context,
is_internal_service_request,
user_has_permission,
)
from .serializers import OrganizacionSerializer, UsoAlmacenamientoSerializer
from .models import Organizacion, UsoAlmacenamiento
@@ -32,21 +35,19 @@ class ViewSetOrganizacion(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltr
my_tags = ['Organizaciones']
def get_queryset(self):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
user = self.request.user
if not user.is_authenticated:
return Organizacion.objects.none()
if self.request.user.is_superuser:
# Superuser can see all organizations
if is_internal_service_request(self.request):
return Organizacion.objects.all()
if (self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter('developer').exists() or self.request.user.groups.filter('user')) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Importers can only see their own organization
return Organizacion.objects.filter(users=self.request.user)
if self.request.user.groups.filter(name='importador').exists():
return Organizacion.objects.filter(users=self.request.user)
return Organizacion.objects.none()
org = get_org_context(user)
if not org:
return Organizacion.objects.none()
# Superuser ve solo su org activa, no todas
return Organizacion.objects.filter(id=org.id)
class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
"""
@@ -60,31 +61,26 @@ class UsoAlmacenamientoViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
my_tags = ['Uso de Almacenamiento']
def get_queryset(self):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
if not self.request.user.is_authenticated:
return UsoAlmacenamiento.objects.none()
if self.request.user.is_superuser:
# Superuser can see all storage usage
if is_internal_service_request(self.request):
return UsoAlmacenamiento.objects.all()
if (self.request.user.groups.filter(name='developer').exists() or
self.request.user.groups.filter(name='admin').exists() or
self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Developers, Admins, and Users can see their organization's storage usage
return UsoAlmacenamiento.objects.filter(organizacion=self.request.user.organizacion)
if self.request.user.groups.filter(name='importador').exists():
# Importers can only see their own organization's storage usage
org = get_org_context(self.request.user)
if not org:
return UsoAlmacenamiento.objects.none()
if self.request.user.is_importador:
raise PermissionDenied("Los importadores no tienen acceso al uso de almacenamiento.")
return UsoAlmacenamiento.objects.none()
return UsoAlmacenamiento.objects.filter(organizacion=org)
@action(detail=False, methods=['get'])
def mi_organizacion(self, request):
"""Obtiene el uso de almacenamiento de la organización del usuario actual"""
organizacion = request.user.organizacion
organizacion = get_org_context(request.user)
# Obtener o crear el registro de uso
uso, created = UsoAlmacenamiento.objects.get_or_create(