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

@@ -8,10 +8,9 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
get_org_context,
require_permission,
user_has_permission,
)
from api.organization.models import UsoAlmacenamiento, Organizacion
@@ -34,7 +33,7 @@ class DocumentUtilInformation(LoggingMixin, APIView, FiltroPorOrganizacionMixin)
View to get the total storage used by the organization and stats of documents added in last 1, 7, and 30 days.
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('cards.view')]
model = Document
my_tags = ['Cards']
@@ -100,7 +99,7 @@ class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrgan
View para obtener información de uso de servicios relacionados con pedimentos.
Devuelve la cantidad de procesos por estado (1: espera, 2: proceso, 3: finalizado, 4: error) para la organización.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('cards.view')]
model = Document
my_tags = ['Cards']
@@ -140,29 +139,17 @@ class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrgan
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
return None
# Si es super usuario, devuelve todos los procesos
if self.request.user.is_superuser:
return ProcesamientoPedimento.objects.all()
org = get_org_context(self.request.user)
if not org:
return ProcesamientoPedimento.objects.none()
# Si es Administrador de la organizacion devuelve todos los servicios de la organizacion
if self.request.user.is_authenticated and self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
if self.request.user.is_importador:
return ProcesamientoPedimento.objects.filter(
pedimento__organizacion=org,
pedimento__contribuyente__in=self.request.user.rfc.all(),
)
# Si es Desarrollador de la organizacion devuelve todos los servicios de la organizacion
if self.request.user.is_authenticated and self.request.user.groups.filter(name='developer').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return self.request.user.organizacion.procesamiento_pedimentos.all()
if self.request.user.is_authenticated and self.request.user.groups.filter(name='user').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return self.request.user.organizacion.procesamiento_pedimentos.all()
# Si es importador de la organizacion, devuelve los servicios relacionados con sus pedimentos
if self.request.user.is_authenticated and self.request.user.groups.filter(name='importador').exists() and self.request.user.is_importador and self.request.user.groups.filter(name='user').exists():
return self.request.user.organizacion.procesamiento_pedimentos.filter(pedimento__contribuyente__in=self.request.user.rfc.all())
# Si es parte de una organización, filtrar por esa organización
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=org)
def get(self, request):
queryset = self.get_queryset()
@@ -193,12 +180,21 @@ class UserActivityAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
Endpoint para análisis de actividades de usuario.
Devuelve el conteo de acciones por tipo y los 5 usuarios más activos.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('cards.view')]
model = UserActivity
campo_organizacion = 'user__organizacion'
my_tags = ['Cards']
def get_queryset_importador(self):
# Importadores solo ven sus propias actividades
user = self.request.user
org = get_org_context(user)
if not org:
return UserActivity.objects.none()
return UserActivity.objects.filter(user__organizacion=org, user=user)
@swagger_auto_schema(
operation_description="Get analysis of user activities. Permite filtrar por fecha de actividades.",
manual_parameters=[
@@ -253,7 +249,9 @@ class UserActivityAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
}
)
def get_queryset(self):
return self.get_queryset_filtrado()
if self.request.user.is_importador:
return self.get_queryset_importador()
return self.get_queryset_filtrado()
def get(self, request):
queryset = self.get_queryset()
@@ -289,11 +287,20 @@ class RequestLogAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
Endpoint para análisis de logs de peticiones.
Devuelve el conteo por método, los paths más solicitados y el promedio de tiempo de respuesta.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('cards.view')]
model = RequestLog
campo_organizacion = 'user__organizacion'
my_tags = ['Cards']
def get_queryset_importador(self):
# Importadores solo ven sus propios logs
user = self.request.user
org = get_org_context(user)
if not org:
return RequestLog.objects.none()
return RequestLog.objects.filter(user__organizacion=org, user=user)
@swagger_auto_schema(
operation_description="Get analysis of request logs. Permite filtrar por fecha de logs.",
manual_parameters=[
@@ -345,6 +352,8 @@ class RequestLogAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
}
)
def get_queryset(self):
if self.request.user.is_importador:
return self.get_queryset_importador()
return self.get_queryset_filtrado()
def get(self, request):
@@ -376,7 +385,7 @@ class LastDocumentView(LoggingMixin, APIView, DocumentosFiltradosMixin):
View que obtiene los ultimos 10 documentos agregados.
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('cards.view')]
model = Document
my_tags = ['Cards']

View File

@@ -30,7 +30,7 @@ class CustomUserAdmin(UserAdmin):
# Fieldsets para editar un usuario
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'profile_picture', 'is_importador', 'rfc')}),
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'active_organization', 'profile_picture', 'is_importador', 'rfc')}),
('Permisos', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Fechas importantes', {'fields': ('last_login', 'date_joined')}),
)

View File

@@ -11,6 +11,17 @@ class CustomUser(AbstractUser):
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, null=True, blank=True, related_name='users')
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
# Contexto de trabajo activo para superusuarios. Filtra datos igual que un usuario normal.
# Sin este campo activo, el superuser no puede consultar datos — debe hacer switch primero.
active_organization = models.ForeignKey(
'organization.Organizacion',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='superusers_activos',
help_text="Solo superusuarios: organización activa para contexto de trabajo",
)
is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer")
rfc = models.ManyToManyField('customs.Importador', blank=True, related_name='users', help_text="RFCs de importadores asociados al usuario")

View File

@@ -17,10 +17,14 @@ from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import PermissionDenied
from core.permissions import (
IsSameOrganization,
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
IsSuperUser,
get_org_context,
is_internal_service_request,
user_has_permission,
require_permission,
)
from .serializers import CustomUserSerializer
@@ -74,78 +78,62 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for CustomUser model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSameOrganization )]
pagination_class = CustomPagination
model = CustomUser
serializer_class = CustomUserSerializer
filterset_fields = ['username', 'email', 'first_name', 'last_name', 'organizacion', 'is_importador']
my_tags = ['User Profile']
def get_permissions(self):
# Permitir eliminar usuarios solo a admin, Agente Aduanal y user de la misma organización
if self.action == 'destroy':
user = self.request.user
if not (
user.is_superuser or
user.groups.filter(name='admin').exists() or
user.groups.filter(name='Agente Aduanal').exists() or
user.groups.filter(name='user').exists()
):
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin, Agente Aduanal o user pueden eliminar usuarios.")
elif self.action in ['create', 'update', 'partial_update']:
if not (self.request.user.is_superuser or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Importador').exists()) :
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin o superusuario pueden modificar usuarios.")
return super().get_permissions()
if self.action in ('me', 'change_password'):
return [IsAuthenticated()]
perms = {
'list': 'usuarios.view',
'retrieve': 'usuarios.view',
'create': 'usuarios.create',
'update': 'usuarios.edit',
'partial_update': 'usuarios.edit',
'destroy': 'usuarios.delete',
}
codename = perms.get(self.action, 'usuarios.view')
return [IsAuthenticated(), require_permission(codename)()]
def perform_destroy(self, instance):
# Solo permitir eliminar usuarios de la misma organización
if self.request.user.is_superuser or instance.organizacion == self.request.user.organizacion:
user = self.request.user
org = get_org_context(user)
if user.is_superuser or instance.organizacion == org:
instance.delete()
else:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo puedes eliminar usuarios de tu organización.")
def get_queryset(self):
# Si es importador, solo puede ver su propio usuario
if self.request.user.groups.filter(name='importador').exists() or self.request.user.groups.filter(name='Importador').exists():
return CustomUser.objects.filter(pk=self.request.user.pk)
# Otros roles: filtrar por organización
return self.get_queryset_filtrado_por_organizacion()
user = self.request.user
if is_internal_service_request(self.request):
return CustomUser.objects.all()
if not user_has_permission(user, 'usuarios.view'):
return CustomUser.objects.none()
org = get_org_context(user)
if not org:
return CustomUser.objects.none()
return CustomUser.objects.filter(organizacion=org)
def perform_create(self, serializer):
# Always assign the creator's organization
if self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
if not self.request.user.organizacion:
raise PermissionDenied("Los administradores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
creator = self.request.user
if self.request.user.is_superuser:
# If superuser, allow creating users without organization
if creator.is_superuser:
user = serializer.save(is_active=False)
send_activation_email(user, self.request) # Usa template HTML
send_activation_email(user, self.request)
return
if self.request.user.groups.filter(name='developer').exists():
# Developers can create users but must assign an organization
if not self.request.user.organizacion:
raise PermissionDenied("Los desarrolladores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.groups.filter(name='importador').exists():
# No puedes crear un usuario si eres importador
if creator.is_importador:
raise PermissionDenied("Los importadores no pueden crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
org = get_org_context(creator)
if not org:
raise PermissionDenied("Debes tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=org, is_active=False)
send_activation_email(user, self.request)
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def me(self, request):
@@ -167,8 +155,11 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
user = self.get_object()
current_user = request.user
# Solo el propio usuario, admin o superuser pueden cambiar la contraseña
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists() or user == current_user):
puede_cambiar_ajena = (
current_user.is_superuser or
user_has_permission(current_user, 'usuarios.change_password')
)
if not (puede_cambiar_ajena or user == current_user):
raise PermissionDenied("No tienes permiso para cambiar la contraseña de este usuario.")
old_password = request.data.get('old_password')
@@ -176,8 +167,7 @@ class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
if not new_password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
# Si no es admin/superuser, debe validar old_password
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists()):
if not puede_cambiar_ajena:
if not old_password or not user.check_password(old_password):
return Response({'detail': 'La contraseña actual es incorrecta.'}, status=400)
@@ -226,11 +216,11 @@ class ProfilePictureView(LoggingMixin, APIView):
my_tags = ['User Profile']
def get(self, request, user_id):
# Obtiene el usuario (automáticamente 404 si no existe)
user = get_object_or_404(CustomUser, pk=user_id)
# El permiso IsOwnerOrAdmin ya verificó que request.user == user o es admin
# Así que no necesitas validar manualmente los permisos aquí.
org = get_org_context(request.user)
if not request.user.is_superuser and user.organizacion != org:
raise Http404("No autorizado")
if not user.profile_picture:
raise Http404("El usuario no tiene imagen de perfil")
@@ -267,6 +257,8 @@ class PasswordResetConfirmView(APIView):
return Response({'detail': 'Enlace inválido.'}, status=400)
if not default_token_generator.check_token(user, token):
return Response({'detail': 'Token inválido o expirado.'}, status=400)
if not user.is_active:
return Response({'detail': 'La cuenta de usuario no está activa.'}, status=400)
password = request.data.get('password')
if not password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)

View File

@@ -10,12 +10,20 @@ from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from django.http import HttpResponse
import django_filters
import io
import openpyxl
from rest_framework.filters import SearchFilter, OrderingFilter
from core.permissions import (
IsSameOrganization,
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
IsSuperUser,
get_org_context,
require_permission,
user_has_permission,
is_internal_service_request,
)
from api.customs.models import (
Pedimento,
@@ -244,6 +252,19 @@ class PedimentoPagination(PageNumberPagination):
return super().paginate_queryset(queryset, request, view)
# Create your views here.
class PedimentoFilter(django_filters.FilterSet):
# Rango de fecha de pago: ?fecha_pago_desde=YYYY-MM-DD&fecha_pago_hasta=YYYY-MM-DD
fecha_pago_desde = django_filters.DateFilter(field_name='fecha_pago', lookup_expr='gte')
fecha_pago_hasta = django_filters.DateFilter(field_name='fecha_pago', lookup_expr='lte')
class Meta:
model = Pedimento
fields = [
'patente', 'aduana', 'tipo_operacion', 'clave_pedimento',
'pedimento', 'existe_expediente', 'contribuyente',
'curp_apoderado', 'fecha_pago', 'pedimento_app',
]
class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Pendiente de permisos de creacion
"""
ViewSet for Pedimento model.
@@ -257,53 +278,124 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
- existe_expediente: Filtro por expediente (True/False)
- contribuyente: Filtro por contribuyente
- curp_apoderado: Filtro por curp del apoderado
- fecha_pago: Filtro por fecha de pago (YYYY-MM-DD)
- fecha_pago: Filtro por fecha de pago exacta (YYYY-MM-DD)
- fecha_pago_desde: Rango inicio de fecha de pago (YYYY-MM-DD)
- fecha_pago_hasta: Rango fin de fecha de pago (YYYY-MM-DD)
- patente: Filtro por patente
- aduana: Filtro por aduana
- tipo_operacion: Filtro por tipo de operación
- clave_pedimento: Filtro por clave de pedimento
- ordering: Ordenar por campo (ej: -created_at, pedimento)
Ejemplos:
- /pedimentos/ → Devuelve TODOS los pedimentos
- /pedimentos/?page_size=10 → Devuelve los primeros 10
- /pedimentos/?page_size=10&page=2 → Devuelve los pedimentos 11-20
- /pedimentos/?pedimento=12345678 → Filtra por número de pedimento
- /pedimentos/?existe_expediente=true → Filtra por expediente existente
- /pedimentos/?contribuyente=EMPRESA → Filtra por contribuyente
- /pedimentos/?curp_apoderado=XXXX → Filtra por curp apoderado
- /pedimentos/?fecha_pago=2025-07-18 → Filtra por fecha de pago
- /pedimentos/?fecha_pago_desde=2025-01-01&fecha_pago_hasta=2025-12-31 → Rango de fechas
- /pedimentos/export-excel/?contribuyente=EMPRESA → Descarga Excel con filtros
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = PedimentoSerializer
pagination_class = PedimentoPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
model = Pedimento
filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app']
filterset_class = PedimentoFilter
search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento']
# AGREGAR ESTOS CAMPOS PARA ORDENACIÓN
ordering_fields = ['created_at', 'pedimento', 'fecha_pago', 'aduana', 'patente']
ordering = ['-created_at'] # Orden descendente por fecha de creación por defecto
ordering = ['-created_at']
def get_permissions(self):
perms = {
'list': 'pedimentos.view',
'retrieve': 'pedimentos.view',
'create': 'pedimentos.create',
'update': 'pedimentos.edit',
'partial_update': 'pedimentos.edit',
'destroy': 'pedimentos.delete',
'procesar_completo': 'pedimentos.process',
'procesar_partidas': 'pedimentos.process',
'procesar_coves': 'pedimentos.process',
'procesar_acuse_coves': 'pedimentos.process',
'procesar_edocs': 'pedimentos.process',
'procesar_acuses': 'pedimentos.process',
'procesar_remesas': 'pedimentos.process',
'bulk_delete': 'pedimentos.delete',
'bulk_create': 'pedimentos.create',
'bulk_create_pedimento_desk': 'pedimentos.create',
'bulk_upload_record': 'documentos.upload',
'bulk_upload_record_async': 'documentos.upload',
}
codename = perms.get(self.action, 'pedimentos.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
if not user_has_permission(self.request.user, 'pedimentos.view'):
return Pedimento.objects.none()
return self.get_queryset_filtrado_por_organizacion()
# pedimento_app_filter = self.request.GET.get('pedimento_app', None)
@action(detail=False, methods=['get'], url_path='export-excel')
def export_excel(self, request):
"""Exporta a Excel todos los pedimentos que coincidan con los filtros activos."""
queryset = self.filter_queryset(self.get_queryset())
# if pedimento_app_filter:
# print(f"Filtro por pedimento_app: {pedimento_app_filter}")
# queryset = queryset.filter(pedimento_app__icontains=pedimento_app_filter)
columnas = [
('pedimento_app', 'Pedimento'),
('fecha_pago', 'Fecha Pago'),
('aduana', 'Aduana'),
('patente', 'Patente'),
('contribuyente', 'Contribuyente'),
('curp_apoderado','CURP Apoderado'),
('numero_partidas','Partidas'),
('created_at', 'F. Carga'),
('tipo_operacion','Tipo Op.'),
('clave_pedimento','Clave Pedimento'),
('documentos_count', 'Archivos'),
('existe_expediente','Expediente'),
]
return queryset
def safe_value(val):
if val is None:
return ''
if isinstance(val, bool):
return '' if val else 'No'
if isinstance(val, (int, float)):
return val
if isinstance(val, (datetime, date)):
return str(val)[:10]
# ForeignKey instances u otros objetos Django → su representación string
return str(val)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = 'Pedimentos'
ws.append([label for _, label in columnas])
for ped in queryset.iterator():
fila = []
for campo, _ in columnas:
val = getattr(ped, campo, None)
fila.append(safe_value(val))
ws.append(fila)
# Autoajuste de ancho de columnas
for col in ws.columns:
max_len = max((len(str(cell.value or '')) for cell in col), default=10)
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 50)
output = io.BytesIO()
wb.save(output)
output.seek(0)
filename = f"pedimentos_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
)
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un pedimento.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
org = get_org_context(self.request.user)
data = serializer.validated_data
if not data.get('pedimento_app'):
fecha_pago = data.get('fecha_pago')
@@ -312,7 +404,7 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
pedimento = data.get('pedimento')
if fecha_pago and aduana and patente and pedimento:
pedimento_app = f"{str(fecha_pago.year)[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento).zfill(7)[-7:]}"
serializer.save(organizacion=self.request.user.organizacion, pedimento_app=pedimento_app)
serializer.save(organizacion=org, pedimento_app=pedimento_app)
try:
# Usar el nombre del servicio de Docker Compose en lugar de localhost
@@ -375,6 +467,9 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
]
}
def perform_destroy(self, instance):
instance.delete()
@action(detail=True, methods=['post'], url_path='procesar-completo')
def procesar_completo(self, request, pk=None):
"""
@@ -2197,33 +2292,70 @@ class PartidaViewSet(viewsets.ModelViewSet):
Permite filtrar por:
- pedimento: UUID del pedimento (query parameter principal)
- pedimento__id: UUID del pedimento (alternativo)
Ejemplo: GET /api/partidas/?pedimento=6782d22e-5e97-4efc-87c9-bd8497c8ac7e
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = Partida.objects.all()
serializer_class = PartidaSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = {
'pedimento': ['exact'], # Filtro directo por UUID del pedimento
'pedimento__id': ['exact'], # Filtro alternativo
'numero_partida': ['exact', 'gte', 'lte'], # Filtros por número de partida
'descargado': ['exact'], # Filtro por estado de descarga
'created_at': ['exact', 'gte', 'lte'], # Filtros por fecha de creación
'updated_at': ['exact', 'gte', 'lte'] # Filtros por fecha de actualización
'pedimento': ['exact'],
'pedimento__id': ['exact'],
'numero_partida': ['exact', 'gte', 'lte'],
'descargado': ['exact'],
'created_at': ['exact', 'gte', 'lte'],
'updated_at': ['exact', 'gte', 'lte'],
}
search_fields = ['pedimento__pedimento', 'pedimento__pedimento_app']
ordering_fields = ['numero_partida', 'pedimento__pedimento', 'id', 'created_at', 'updated_at']
ordering = ['numero_partida'] # Ordenar por número de partida por defecto
ordering = ['numero_partida']
my_tags = ['Partidas']
def get_permissions(self):
perms = {
'list': 'partidas.view',
'retrieve': 'partidas.view',
'create': 'partidas.create',
'update': 'partidas.edit',
'partial_update': 'partidas.edit',
'destroy': 'partidas.delete',
'bulk_delete_partidas_vu': 'partidas.delete',
}
codename = perms.get(self.action, 'partidas.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
user = self.request.user
if is_internal_service_request(self.request):
return Partida.objects.all()
if not user_has_permission(user, 'partidas.view'):
return Partida.objects.none()
org = get_org_context(user)
if not org:
return Partida.objects.none()
qs = Partida.objects.filter(pedimento__organizacion=org)
if user.is_importador:
qs = qs.filter(pedimento__contribuyente__in=user.rfc.all())
return qs
def perform_create(self, serializer):
if is_internal_service_request(self.request):
serializer.save()
return
pedimento = serializer.validated_data.get('pedimento')
org = get_org_context(self.request.user)
if pedimento and pedimento.organizacion != org:
raise PermissionDenied("El pedimento no pertenece a tu organización.")
serializer.save()
def perform_destroy(self, instance):
instance.delete()
class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
"""
ViewSet for TipoOperacion model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('pedimentos.view')]
queryset = TipoOperacion.objects.all()
serializer_class = TipoOperacionSerializer
@@ -2233,9 +2365,17 @@ class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
search_fields = ['tipo', 'descripcion']
ordering_fields = ['tipo', 'descripcion']
ordering = ['tipo']
my_tags = ['Tipos_Operacion']
def get_queryset(self):
if is_internal_service_request(self.request):
return TipoOperacion.objects.all()
org = get_org_context(self.request.user)
if not org:
return TipoOperacion.objects.none()
return TipoOperacion.objects.filter(organizacion=org)
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un tipo de operación.
@@ -2276,7 +2416,6 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
- /procesamientopedimentos/ → Devuelve TODOS los procesamientos
- /procesamientopedimentos/?page_size=5 → Devuelve los primeros 5
"""
permission_classes = [IsAuthenticated, IsSuperUser | IsSameOrganizationDeveloper ]
serializer_class = ProcesamientoPedimentoSerializer
pagination_class = CustomPagination
model = ProcesamientoPedimento
@@ -2291,60 +2430,61 @@ class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizaci
search_fields = ['pedimento__pedimento_app', 'pedimento__pedimento']
ordering_fields = ['created_at', 'updated_at']
ordering = ['-created_at']
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna siempre la organización al crear un procesamiento de pedimento.
- Para superusuarios: requiere que la organización venga explícitamente en los datos validados.
- Para usuarios normales: asigna la organización del usuario autenticado.
"""
user = self.request.user
if not user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, debe venir la organización en los datos validados
if user.is_superuser:
organizacion = serializer.validated_data.get('organizacion', None)
if not organizacion:
raise ValueError("El superusuario debe especificar una organización al crear el procesamiento de pedimento.")
def get_permissions(self):
perms = {
'list': 'pedimentos.view',
'retrieve': 'pedimentos.view',
'create': 'pedimentos.process',
'update': 'pedimentos.process',
'partial_update': 'pedimentos.process',
'destroy': 'pedimentos.process',
}
codename = perms.get(self.action, 'pedimentos.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
user = self.request.user
if is_internal_service_request(self.request):
return ProcesamientoPedimento.objects.all()
if not user_has_permission(user, 'pedimentos.view'):
return ProcesamientoPedimento.objects.none()
org = get_org_context(user)
if not org:
return ProcesamientoPedimento.objects.none()
if user.is_importador:
return ProcesamientoPedimento.objects.filter(
organizacion=org,
pedimento__contribuyente__in=user.rfc.all()
)
return ProcesamientoPedimento.objects.filter(organizacion=org)
def perform_create(self, serializer):
if is_internal_service_request(self.request):
serializer.save()
return
# Para usuarios normales, asignar siempre la organización del usuario
if not hasattr(user, 'organizacion') or not user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=user.organizacion)
org = get_org_context(self.request.user)
if not org:
raise PermissionDenied("Sin organización activa.")
serializer.save(organizacion=org)
def perform_update(self, serializer):
"""
Permite actualizar un procesamiento de pedimento, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
if self.request.user.is_superuser:
if is_internal_service_request(self.request):
serializer.save()
return
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
if not user_has_permission(self.request.user, 'pedimentos.process'):
raise PermissionDenied("Se requiere el permiso pedimentos.process.")
org = get_org_context(self.request.user)
if not org:
raise PermissionDenied("Sin organización activa.")
serializer.save(organizacion=org)
raise ValueError("Usuario no autenticado o sin permisos para actualizar ProcesamientoPedimento")
my_tags = ['Procesamientos_Pedimentos']
class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for EDocument model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = EDocumentSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@@ -2353,60 +2493,48 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
ordering_fields = ['created_at', 'updated_at', 'numero_edocument']
ordering = ['-created_at']
model = EDocument
campo_contribuyente = 'pedimento__contribuyente'
my_tags = ['EDocuments']
def get_permissions(self):
perms = {
'list': 'edocuments.view',
'retrieve': 'edocuments.view',
'create': 'edocuments.create',
'update': 'edocuments.edit',
'partial_update': 'edocuments.edit',
'destroy': 'edocuments.delete',
'bulk_delete_edocs_vu': 'edocuments.delete',
}
codename = perms.get(self.action, 'edocuments.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
if not user_has_permission(self.request.user, 'edocuments.view'):
return EDocument.objects.none()
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un EDocument.
Para superusuarios, permite especificar una organización diferente.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario y se especifica organizacion en los datos validados
if self.request.user.is_superuser:
# Permitir que el superusuario especifique la organización
if is_internal_service_request(self.request):
serializer.save()
return
print(f"self.request.user.groups >>>> {self.request.user.groups}")
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para crear EDocument")
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
def perform_update(self, serializer):
"""
Permite actualizar un EDocument, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
if is_internal_service_request(self.request):
serializer.save()
return
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
raise ValueError("Usuario no autenticado o sin permisos para actualizar EDocument")
def perform_destroy(self, instance):
instance.delete()
class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for Cove model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser |IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
serializer_class = CoveSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@@ -2415,61 +2543,48 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
ordering_fields = ['created_at', 'updated_at', 'numero_cove']
ordering = ['-created_at']
model = Cove
campo_contribuyente = 'pedimento__contribuyente'
my_tags = ['Coves']
def get_permissions(self):
perms = {
'list': 'coves.view',
'retrieve': 'coves.view',
'create': 'coves.create',
'update': 'coves.edit',
'partial_update': 'coves.edit',
'destroy': 'coves.delete',
'bulk_delete_coves_vu': 'coves.delete',
}
codename = perms.get(self.action, 'coves.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
if not user_has_permission(self.request.user, 'coves.view'):
return Cove.objects.none()
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un Cove.
Para superusuarios, permite especificar una organización diferente.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario y se especifica organizacion en los datos validados
if self.request.user.is_superuser:
# Permitir que el superusuario especifique la organización
if is_internal_service_request(self.request):
serializer.save()
return
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para crear Cove")
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
def perform_update(self, serializer):
"""
Permite actualizar un Cove, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
if is_internal_service_request(self.request):
serializer.save()
return
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
def perform_destroy(self, instance):
instance.delete()
class ImportadorViewSet(viewsets.ModelViewSet):
"""
ViewSet for Importador model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = ImportadorSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@@ -2477,69 +2592,69 @@ class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
search_fields = ['rfc', 'nombre']
ordering_fields = ['created_at', 'updated_at', 'rfc']
ordering = ['-created_at']
model = Importador
my_tags = ['Importadores']
def get_permissions(self):
# list/retrieve: solo IsAuthenticated — el queryset filtra según permisos
if self.action in ('list', 'retrieve'):
return [IsAuthenticated()]
perms = {
'create': 'importadores.create',
'update': 'importadores.edit',
'partial_update': 'importadores.edit',
'destroy': 'importadores.delete',
}
codename = perms.get(self.action, 'importadores.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
user = self.request.user
grupos = user.groups.values_list('name', flat=True)
if user.is_superuser:
if is_internal_service_request(self.request):
return Importador.objects.all()
if 'Importador' in grupos:
return user.rfc.all()
return self.get_queryset_filtrado_por_organizacion()
org = get_org_context(user)
if not org:
return Importador.objects.none()
# Con permiso ve todos; sin permiso solo los asignados al usuario
if user_has_permission(user, 'importadores.view'):
return Importador.objects.filter(organizacion=org)
return Importador.objects.filter(organizacion=org, users=user)
def perform_create(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
serializer.save(organizacion=self.request.user.organizacion)
def perform_update(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
if is_internal_service_request(self.request):
serializer.save()
return
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():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador")
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
my_tags = ['Importadores']
def perform_update(self, serializer):
if is_internal_service_request(self.request):
serializer.save()
return
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
def perform_destroy(self, instance):
instance.delete()
class EjecutarComandoView(APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
"""
View para ejecutar el comando de microservicios desde una petición HTTP.
"""
def post(self, request):
permission_classes = [IsAuthenticated, require_permission('pedimentos.process')]
# Obtener organizacion_id del request (si se envía)
organizacion_id_request = request.data.get('organizacionid', None)
def post(self, request):
procesamiento = request.data.get('procesamiento', None)
todos = request.data.get('todos', False)
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
if organizacion_id_request is None:
org = get_org_context(request.user)
if not org:
return Response(
{"error": 'No se proporcionó la organización a ejecutar el proceso.'},
status=status.HTTP_400_BAD_REQUEST
)
{"error": "Sin organización activa."},
status=status.HTTP_403_FORBIDDEN
)
# organizacion_id = self.request.user.organizacion.id
organizacion_id = organizacion_id_request
nombre_organizacion = self.request.user.organizacion.nombre
organizacion_id = str(org.id)
nombre_organizacion = org.nombre
if procesamiento is None and todos == False:
return Response(

View File

@@ -5,7 +5,7 @@ from rest_framework.response import Response
from rest_framework import status
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
from core.permissions import require_permission
from .tasks.auditoria import (
crear_partidas,
auditar_coves,
@@ -84,7 +84,7 @@ def get_document_path(documento):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def crear_partidas_organizacion(request):
organizacion_id = request.data.get('organizacion_id')
@@ -122,7 +122,7 @@ def crear_partidas_organizacion(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def crear_partidas_pedimento(request):
pedimento_id = request.data.get('pedimento_id')
@@ -202,7 +202,7 @@ def crear_partidas_pedimento(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_pedimentos_endpoint(request):
"""
Inicia una tarea de auditoría para todos los pedimentos de una organización.
@@ -252,7 +252,7 @@ def auditar_pedimentos_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_procesamiento_remesa_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
@@ -339,7 +339,7 @@ def _lanzar_auditoria_organizacion(request, task_fn, label):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_coves_endpoint(request):
return _lanzar_auditoria_organizacion(request, auditar_coves, 'COVEs')
@@ -359,7 +359,7 @@ def auditar_coves_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_acuse_cove_endpoint(request):
return _lanzar_auditoria_organizacion(request, auditar_acuse_cove, 'acuses de COVE')
@@ -379,7 +379,7 @@ def auditar_acuse_cove_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_edocuments_endpoint(request):
return _lanzar_auditoria_organizacion(request, auditar_edocuments, 'EDocuments')
@@ -399,7 +399,7 @@ def auditar_edocuments_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_acuse_endpoint(request):
return _lanzar_auditoria_organizacion(request, auditar_acuse, 'acuses de EDocument')
@@ -419,7 +419,7 @@ def auditar_acuse_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_remesas_endpoint(request):
return _lanzar_auditoria_organizacion(request, auditar_remesas, 'remesas')
@@ -442,7 +442,7 @@ def auditar_remesas_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_cove_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
@@ -504,7 +504,7 @@ def auditar_cove_pedimento_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_acuse_cove_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
@@ -566,7 +566,7 @@ def auditar_acuse_cove_pedimento_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_edocument_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
@@ -628,7 +628,7 @@ def auditar_edocument_pedimento_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_acuse_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
@@ -687,7 +687,7 @@ def auditar_acuse_pedimento_endpoint(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditor_procesar_pedimentos_organizacion(request):
"""
Inicia una tarea de procesamiento para todos los pedimentos de todas las organizaciones.
@@ -739,7 +739,7 @@ def auditor_procesar_pedimentos_organizacion(request):
### Fin Procesamiento de pedimentos ###
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditar_peticion_respuesta_pedimento_completo(request):
"""
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
@@ -884,7 +884,7 @@ def auditar_peticion_respuesta_pedimento_completo(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_pedimento_vu(request):
"""
Backend endpoint para obtener las peticiones y respuestas asociadas a un pedimento.
@@ -938,7 +938,7 @@ def auditor_obtener_peticion_pedimento_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_pedimento_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a un pedimento.
@@ -991,7 +991,7 @@ def auditor_obtener_respuesta_pedimento_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_remesa_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1045,7 +1045,7 @@ def auditor_obtener_peticion_remesa_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_remesa_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1098,7 +1098,7 @@ def auditor_obtener_respuesta_remesa_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_partidas_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1178,7 +1178,7 @@ def auditor_obtener_peticion_partidas_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_partidas_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1231,7 +1231,7 @@ def auditor_obtener_respuesta_partidas_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_acuse_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1285,7 +1285,7 @@ def auditor_obtener_peticion_acuse_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_acuse_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1338,7 +1338,7 @@ def auditor_obtener_respuesta_acuse_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_cove_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1392,7 +1392,7 @@ def auditor_obtener_peticion_cove_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_cove_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1445,7 +1445,7 @@ def auditor_obtener_respuesta_cove_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_acuse_cove_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1499,7 +1499,7 @@ def auditor_obtener_peticion_acuse_cove_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_acuse_cove_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1552,7 +1552,7 @@ def auditor_obtener_respuesta_acuse_cove_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_peticion_edocument_vu(request):
"""
Backend endpoint para obtener las peticiones asociadas a una remesa.
@@ -1606,7 +1606,7 @@ def auditor_obtener_peticion_edocument_vu(request):
}, status=status.HTTP_200_OK)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.view')])
def auditor_obtener_respuesta_edocument_vu(request):
"""
Backend endpoint para obtener las respuestas asociadas a una remesa.
@@ -1677,7 +1677,7 @@ def auditor_obtener_respuesta_edocument_vu(request):
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_pedimento_endpoint(request):
"""
Audita un pedimento específico verificando si existe su XML y extrayendo información.

View File

@@ -12,106 +12,73 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from django.http import FileResponse, Http404
import os
from .models import DataStage
from .serializer import DataStageSerializer
from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from core.permissions import get_org_context, is_internal_service_request, require_permission
# Create your views here.
class DataStagePagination(PageNumberPagination):
page_size = 20 # Valor por defecto
page_size_query_param = 'page_size'
max_page_size = 1000
class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet):
"""
ViewSet for managing DataStage instances.
Provides CRUD operations for DataStage.
"""
serializer_class = DataStageSerializer
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = DataStage
my_tags = ['DataStage']
pagination_class = DataStagePagination
def get_permissions(self):
perms = {
'list': 'datastage.view',
'retrieve': 'datastage.view',
'create': 'datastage.create',
'update': 'datastage.create',
'partial_update': 'datastage.create',
'destroy': 'datastage.delete',
'procesar': 'datastage.process',
'download_datastage': 'datastage.view',
'task_status': 'datastage.view',
}
codename = perms.get(self.action, 'datastage.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
if self.request.user.is_superuser:
if is_internal_service_request(self.request):
return DataStage.objects.all().order_by('-created_at')
org = get_org_context(self.request.user)
if not org:
return DataStage.objects.none()
return DataStage.objects.filter(organizacion=org).order_by('-created_at')
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='Agente Aduanal').exists():
return DataStage.objects.filter(organizacion=self.request.user.organizacion).order_by('-created_at')
return self.get_queryset_filtrado_por_organizacion().order_by('-created_at')
def perform_create(self, serializer):
"""
Permite que la organización sea opcional en el request, pero si no se envía, se asigna la del usuario autenticado.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
org = get_org_context(self.request.user)
datastage = serializer.save(organizacion=org)
self._trigger_processing(datastage)
data = serializer.validated_data
organizacion = data.get('organizacion')
if self.request.user.is_superuser:
# Permitir que el superusuario cree sin organización o la especifique
datastage = serializer.save()
self._trigger_processing(datastage)
return
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():
if not organizacion:
datastage = serializer.save(organizacion=self.request.user.organizacion)
else:
datastage = serializer.save()
self._trigger_processing(datastage)
return
raise ValueError("No cuentas con los permisos necesarios para crear un DataStage")
def _trigger_processing(self, datastage):
"""
Método helper para disparar el procesamiento.
"""
from api.datastage.tasks import procesar_datastage_task
user_organizacion = getattr(self.request.user, 'organizacion', None)
user_organizacion_id = user_organizacion.id if user_organizacion else None
org = get_org_context(self.request.user)
datastage.procesado = True
datastage.save()
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
def perform_update(self, serializer):
"""
Override to ensure organization is set on update.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
procesar_datastage_task.delay(datastage.id, org.id if org else None)
if self.request.user.is_superuser:
# Allow superuser to update without organization
def perform_update(self, serializer):
if is_internal_service_request(self.request):
serializer.save()
return
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():
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("No cuentas con los permisos necesarios para actualizar un DataStage")
org = get_org_context(self.request.user)
serializer.save(organizacion=org)
def perform_destroy(self, instance):
if instance.archivo:
storage_service.delete_file(instance.archivo)
instance.delete()
@action(detail=True, methods=['get'], url_path='download-datastage', url_name='download-datastage')
def download_datastage(self, request, pk=None):
@@ -182,12 +149,10 @@ class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
"""
Endpoint para procesar el DataStage de forma asíncrona usando Celery.
"""
# ojo aqui
from api.datastage.tasks import procesar_datastage_task
datastage = self.get_object()
user_organizacion = getattr(self.request.user, 'organizacion', None)
user_organizacion_id = user_organizacion.id if user_organizacion else None
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
org = get_org_context(self.request.user)
task = procesar_datastage_task.delay(datastage.id, org.id if org else None)
return Response({
'task_id': task.id,
'detail': 'Procesamiento iniciado. Puede consultar el estado con el task_id.'

View File

@@ -58,8 +58,7 @@ class UserActivityViewSet(viewsets.ReadOnlyModelViewSet):
if not self.request.user.is_authenticated:
return UserActivity.objects.none()
# Los usuarios normales solo ven su propia actividad
if self.request.user.is_staff:
if self.request.user.is_superuser:
return UserActivity.objects.all()
return UserActivity.objects.filter(user=self.request.user)

View File

@@ -4,31 +4,43 @@ from django.dispatch import receiver
from api.notificaciones.models import Notificacion
from api.record.models import Document
@receiver(post_save, sender=Document)
def trigger_notificacion(sender, instance, created, **kwargs):
if created:
from api.cuser.models import CustomUser
from api.customs.models import Pedimento
from api.notificaciones.models import TipoNotificacion
if not created:
return
# Obtener el tipo de notificación (puedes ajustar el nombre si tienes tipos definidos)
tipo_info, _ = TipoNotificacion.objects.get_or_create(tipo="info", defaults={"descripcion": "Notificación informativa"})
from api.cuser.models import CustomUser
from api.notificaciones.models import TipoNotificacion
from core.permissions import user_has_permission
# Notificar a todos los usuarios de la organización
usuarios_org = CustomUser.objects.filter(organizacion=instance.organizacion)
for usuario in usuarios_org:
# Notificar solo a importadores cuyo RFC coincide
if (usuario.is_importador or usuario.groups.filter(name='Importador').exists()):
if instance.pedimento.contribuyente in usuario.rfc.all():
Notificacion.objects.create(
tipo=tipo_info,
dirigido=usuario,
mensaje=f"Se agregó el documento {instance.archivo} al pedimento {instance.pedimento.pedimento} \n {instance.document_type.nombre}",
)
# Notificar a otros roles (no importadores)
elif (usuario.is_superuser or usuario.groups.filter(name='Agente Aduanal').exists() or usuario.groups.filter(name='admin').exists()):
Notificacion.objects.create(
tipo=tipo_info,
dirigido=usuario,
mensaje=f"Se agregó el documento {instance.archivo} al pedimento {instance.pedimento.pedimento} \n {instance.document_type.nombre}",
)
tipo_info, _ = TipoNotificacion.objects.get_or_create(
tipo='info',
defaults={'descripcion': 'Notificación informativa'},
)
mensaje = (
f"Se agregó el documento {instance.archivo} "
f"al pedimento {instance.pedimento.pedimento}\n"
f"{instance.document_type.nombre}"
)
usuarios_org = CustomUser.objects.filter(
organizacion=instance.organizacion,
is_active=True,
).prefetch_related('rfc')
for usuario in usuarios_org:
if not user_has_permission(usuario, 'notificaciones.receive'):
continue
# Importadores: solo si el pedimento corresponde a uno de sus RFC
if usuario.is_importador:
if instance.pedimento.contribuyente not in usuario.rfc.all():
continue
Notificacion.objects.create(
tipo=tipo_info,
dirigido=usuario,
mensaje=mensaje,
)

View File

@@ -1,39 +1,36 @@
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import PermissionDenied
from .models import Notificacion, TipoNotificacion
from .serializers import NotificacionSerializer, TipoNotificacionSerializer
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
# Create your views here.
from core.permissions import require_permission
class TipoNotificacionViewSet(viewsets.ModelViewSet):
queryset = TipoNotificacion.objects.all()
serializer_class = TipoNotificacionSerializer
http_method_names = ['get']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated]
my_tags = ['Notificaciones']
def get_queryset(self):
return self.queryset.order_by('tipo')
class NotificacionViewSet(viewsets.ModelViewSet):
queryset = Notificacion.objects.all()
serializer_class = NotificacionSerializer
http_method_names = ['get', 'post', 'put', 'patch', 'delete']
filterset_fields = ['visto']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
my_tags = ['Notificaciones']
def get_permissions(self):
if self.action in ('list', 'retrieve'):
return [IsAuthenticated(), require_permission('notificaciones.view')()]
return [IsAuthenticated()]
def get_queryset(self):
# Evita error en generación de esquema Swagger
if getattr(self, 'swagger_fake_view', False):
return Notificacion.objects.none()
user = self.request.user
@@ -45,6 +42,6 @@ class NotificacionViewSet(viewsets.ModelViewSet):
if not self.request.user.is_authenticated:
raise PermissionDenied("Usuario no autenticado")
if self.request.user.is_superuser:
# Allow superusers and admins to create notifications for any user
serializer.save()
raise PermissionDenied("No tienes permiso para crear notificaciones para otros usuarios")
return
raise PermissionDenied("No tienes permiso para crear notificaciones para otros usuarios")

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(

0
api/rbac/__init__.py Normal file
View File

99
api/rbac/admin.py Normal file
View File

@@ -0,0 +1,99 @@
from django.contrib import admin
from .models import OrganizationRole, RolePermission, UserPermission, UserRole
@admin.register(RolePermission)
class RolePermissionAdmin(admin.ModelAdmin):
list_display = ('codename', 'modulo', 'descripcion')
list_filter = ('modulo',)
search_fields = ('codename', 'descripcion')
ordering = ('modulo', 'codename')
def get_readonly_fields(self, request, obj=None):
# Al editar un permiso existente los campos son readonly para evitar inconsistencias
if obj:
return ('codename', 'modulo', 'descripcion')
return ()
def has_add_permission(self, request):
return request.user.is_superuser
def has_change_permission(self, request, obj=None):
return request.user.is_superuser
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
class UserRoleInline(admin.TabularInline):
model = UserRole
extra = 0
autocomplete_fields = ('user',)
readonly_fields = ('created_at',)
@admin.register(OrganizationRole)
class OrganizationRoleAdmin(admin.ModelAdmin):
list_display = ('nombre', 'organizacion', 'is_admin_role', 'permisos_count', 'usuarios_count')
list_filter = ('organizacion', 'is_admin_role')
search_fields = ('nombre', 'organizacion__nombre')
filter_horizontal = ('permissions',)
inlines = (UserRoleInline,)
readonly_fields = ('created_at', 'updated_at')
def permisos_count(self, obj):
return obj.permissions.count()
permisos_count.short_description = 'Permisos'
def usuarios_count(self, obj):
return obj.user_roles.count()
usuarios_count.short_description = 'Usuarios'
def has_add_permission(self, request):
return request.user.is_superuser
def has_delete_permission(self, request, obj=None):
if obj and obj.is_admin_role:
return False
return request.user.is_superuser
@admin.register(UserRole)
class UserRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'organizacion', 'created_at')
list_filter = ('role__organizacion', 'role__nombre')
search_fields = ('user__username', 'user__email', 'role__nombre')
autocomplete_fields = ('user',)
readonly_fields = ('created_at',)
def organizacion(self, obj):
return obj.role.organizacion
organizacion.short_description = 'Organización'
def save_model(self, request, obj, form, change):
# Bloquear remoción del rol admin_role al owner de la org
if change and obj.role.is_admin_role:
org = obj.role.organizacion
if hasattr(org, 'owner') and org.owner == obj.user:
from django.contrib import messages
self.message_user(
request,
'No se puede remover el rol de administrador maestro al owner de la organización.',
level=messages.ERROR,
)
return
super().save_model(request, obj, form, change)
@admin.register(UserPermission)
class UserPermissionAdmin(admin.ModelAdmin):
list_display = ('user', 'permission', 'granted', 'organizacion', 'created_at')
list_filter = ('granted', 'permission__modulo')
search_fields = ('user__username', 'user__email', 'permission__codename')
autocomplete_fields = ('user',)
readonly_fields = ('created_at',)
def organizacion(self, obj):
return getattr(obj.user, 'organizacion', '')
organizacion.short_description = 'Organización'

8
api/rbac/apps.py Normal file
View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class RbacConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.rbac'
label = 'rbac'
verbose_name = 'RBAC'

View File

View File

View File

@@ -0,0 +1,101 @@
"""
Sincroniza el catálogo de permisos de roles.py con la base de datos.
Uso básico (solo catálogo):
python manage.py sync_rbac
Con propagación a roles existentes (agrega permisos nuevos a roles que ya existen):
python manage.py sync_rbac --roles
Con listado de lo que hay actualmente:
python manage.py sync_rbac --list
"""
from django.core.management.base import BaseCommand
from api.rbac.roles import DEFAULT_ROLES, PERMISSIONS_CATALOG
class Command(BaseCommand):
help = 'Sincroniza el catálogo de permisos (roles.py → BD) sin necesidad de migración.'
def add_arguments(self, parser):
parser.add_argument(
'--roles',
action='store_true',
help='Propaga los permisos nuevos a los OrganizationRoles existentes que coincidan con DEFAULT_ROLES.',
)
parser.add_argument(
'--list',
action='store_true',
help='Lista los permisos actuales en la BD agrupados por módulo.',
)
def handle(self, *args, **options):
from api.rbac.models import OrganizationRole, RolePermission
if options['list']:
self._list_permisos(RolePermission)
return
self._sync_catalogo(RolePermission)
if options['roles']:
self._sync_roles(RolePermission, OrganizationRole)
# ------------------------------------------------------------------
def _sync_catalogo(self, RolePermission):
creados = 0
existentes = 0
for codename, descripcion, modulo in PERMISSIONS_CATALOG:
_, created = RolePermission.objects.get_or_create(
codename=codename,
defaults={'descripcion': descripcion, 'modulo': modulo},
)
if created:
self.stdout.write(self.style.SUCCESS(f' [+] {codename} ({modulo})'))
creados += 1
else:
existentes += 1
self.stdout.write(
self.style.SUCCESS(f'\nCatálogo: {creados} permisos creados, {existentes} ya existían.')
)
def _sync_roles(self, RolePermission, OrganizationRole):
perms_map = {p.codename: p for p in RolePermission.objects.all()}
roles_actualizados = 0
permisos_agregados = 0
for org_role in OrganizationRole.objects.select_related('organizacion').prefetch_related('permissions'):
config = DEFAULT_ROLES.get(org_role.nombre)
if not config:
continue
esperados = {c: perms_map[c] for c in config['permissions'] if c in perms_map}
actuales = {p.codename for p in org_role.permissions.all()}
nuevos = {c: p for c, p in esperados.items() if c not in actuales}
if nuevos:
org_role.permissions.add(*nuevos.values())
roles_actualizados += 1
permisos_agregados += len(nuevos)
self.stdout.write(
f' Rol "{org_role.nombre}" en {org_role.organizacion}: '
f'+{len(nuevos)}{", ".join(nuevos.keys())}'
)
self.stdout.write(
self.style.SUCCESS(
f'\nRoles: {roles_actualizados} roles actualizados, {permisos_agregados} asignaciones nuevas.'
)
)
def _list_permisos(self, RolePermission):
modulo_actual = None
for perm in RolePermission.objects.order_by('modulo', 'codename'):
if perm.modulo != modulo_actual:
modulo_actual = perm.modulo
self.stdout.write(self.style.HTTP_INFO(f'\n {modulo_actual}'))
self.stdout.write(f' {perm.codename:<40} {perm.descripcion}')

View File

@@ -0,0 +1,116 @@
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('organization', '0003_organizacion_apply_auto_download'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='RolePermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('codename', models.CharField(max_length=100, unique=True)),
('descripcion', models.CharField(max_length=255)),
('modulo', models.CharField(max_length=50)),
],
options={
'verbose_name': 'Permiso',
'verbose_name_plural': 'Permisos',
'db_table': 'rbac_role_permission',
'ordering': ['modulo', 'codename'],
},
),
migrations.CreateModel(
name='OrganizationRole',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('nombre', models.CharField(max_length=100)),
('descripcion', models.CharField(blank=True, max_length=255)),
('is_admin_role', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('organizacion', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='roles',
to='organization.organizacion',
)),
('permissions', models.ManyToManyField(
blank=True,
related_name='roles',
to='rbac.rolepermission',
)),
],
options={
'verbose_name': 'Rol de Organización',
'verbose_name_plural': 'Roles de Organización',
'db_table': 'rbac_organization_role',
'ordering': ['nombre'],
},
),
migrations.AddConstraint(
model_name='organizationrole',
constraint=models.UniqueConstraint(fields=['organizacion', 'nombre'], name='unique_role_per_org'),
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='user_roles',
to=settings.AUTH_USER_MODEL,
)),
('role', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='user_roles',
to='rbac.organizationrole',
)),
],
options={
'verbose_name': 'Rol de Usuario',
'verbose_name_plural': 'Roles de Usuario',
'db_table': 'rbac_user_role',
},
),
migrations.AddConstraint(
model_name='userrole',
constraint=models.UniqueConstraint(fields=['user', 'role'], name='unique_user_role'),
),
migrations.CreateModel(
name='UserPermission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('granted', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='rbac_permissions',
to=settings.AUTH_USER_MODEL,
)),
('permission', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='user_overrides',
to='rbac.rolepermission',
)),
],
options={
'verbose_name': 'Permiso Singular',
'verbose_name_plural': 'Permisos Singulares',
'db_table': 'rbac_user_permission',
},
),
migrations.AddConstraint(
model_name='userpermission',
constraint=models.UniqueConstraint(fields=['user', 'permission'], name='unique_user_permission'),
),
]

View File

@@ -0,0 +1,88 @@
"""
Data migration que:
1. Crea el catálogo global de permisos (RolePermission).
2. Para cada Organizacion existente, crea los 5 roles por defecto con sus permisos.
3. Para cada CustomUser existente, mapea sus auth.Group actuales al UserRole equivalente.
Usa get_or_create en todos los pasos — segura de ejecutar múltiples veces.
"""
from django.db import migrations
# Importamos solo constantes (no modelos ni funciones con imports de Django)
# para que la migration sea estable ante futuros refactors del código de la app.
from api.rbac.roles import PERMISSIONS_CATALOG, DEFAULT_ROLES
def _crear_permisos(RolePermission):
perms_map = {}
for codename, descripcion, modulo in PERMISSIONS_CATALOG:
perm, _ = RolePermission.objects.get_or_create(
codename=codename,
defaults={'descripcion': descripcion, 'modulo': modulo},
)
perms_map[codename] = perm
return perms_map
def _crear_roles_org(OrganizationRole, org, perms_map):
for nombre, config in DEFAULT_ROLES.items():
role, created = OrganizationRole.objects.get_or_create(
organizacion=org,
nombre=nombre,
defaults={
'descripcion': config['descripcion'],
'is_admin_role': config.get('is_admin_role', False),
},
)
if created:
role_perms = [perms_map[c] for c in config['permissions'] if c in perms_map]
role.permissions.set(role_perms)
def seed_rbac_data(apps, schema_editor):
RolePermission = apps.get_model('rbac', 'RolePermission')
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
UserRole = apps.get_model('rbac', 'UserRole')
Organizacion = apps.get_model('organization', 'Organizacion')
CustomUser = apps.get_model('cuser', 'CustomUser')
# Paso 1 — Catálogo de permisos
perms_map = _crear_permisos(RolePermission)
# Paso 2 — Roles por defecto para cada organización existente
for org in Organizacion.objects.all():
_crear_roles_org(OrganizationRole, org, perms_map)
# Paso 3 — Mapeo de usuarios: auth.Group → UserRole
# Solo usuarios que tengan organización asignada y grupos asignados
for user in CustomUser.objects.filter(organizacion__isnull=False).prefetch_related('groups'):
for group in user.groups.all():
try:
role = OrganizationRole.objects.get(
organizacion=user.organizacion,
nombre=group.name,
)
UserRole.objects.get_or_create(user=user, role=role)
except OrganizationRole.DoesNotExist:
# El grupo no tiene equivalente en los roles por defecto — se ignora
pass
def reverse_seed(apps, schema_editor):
# Revertir borra todos los datos RBAC. Los auth.Group originales no se tocan.
apps.get_model('rbac', 'UserRole').objects.all().delete()
apps.get_model('rbac', 'OrganizationRole').objects.all().delete()
apps.get_model('rbac', 'RolePermission').objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('rbac', '0001_initial'),
('cuser', '0005_customuser_rfc_fk_to_m2m'),
('organization', '0003_organizacion_apply_auto_download'),
]
operations = [
migrations.RunPython(seed_rbac_data, reverse_code=reverse_seed),
]

View File

@@ -0,0 +1,56 @@
"""
Agrega el permiso notificaciones.receive al catálogo y lo asigna a todos los
OrganizationRole que correspondan a los 5 roles por defecto (en todas las orgs).
"""
from django.db import migrations
NUEVO_PERMISO = (
'notificaciones.receive',
'Recibir notificaciones automáticas de eventos',
'notificaciones',
)
# Todos los roles por defecto deben recibir notificaciones
ROLES_CON_PERMISO = ['admin', 'developer', 'Agente Aduanal', 'user', 'Importador']
def agregar_notificaciones_receive(apps, schema_editor):
RolePermission = apps.get_model('rbac', 'RolePermission')
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
codename, descripcion, modulo = NUEVO_PERMISO
perm, _ = RolePermission.objects.get_or_create(
codename=codename,
defaults={'descripcion': descripcion, 'modulo': modulo},
)
roles = OrganizationRole.objects.filter(nombre__in=ROLES_CON_PERMISO)
for role in roles:
role.permissions.add(perm)
def revertir(apps, schema_editor):
RolePermission = apps.get_model('rbac', 'RolePermission')
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
try:
perm = RolePermission.objects.get(codename='notificaciones.receive')
except RolePermission.DoesNotExist:
return
for role in OrganizationRole.objects.all():
role.permissions.remove(perm)
perm.delete()
class Migration(migrations.Migration):
dependencies = [
('rbac', '0002_data_permissions'),
]
operations = [
migrations.RunPython(agregar_notificaciones_receive, reverse_code=revertir),
]

View File

@@ -0,0 +1,57 @@
"""
Agrega los permisos auditoria.view y auditoria.process al catálogo y los asigna
a los roles admin, developer (ambos) y Agente Aduanal (solo view).
"""
from django.db import migrations
NUEVOS_PERMISOS = [
('auditoria.view', 'Ver estado y resultados de auditoría VUCEM', 'auditoria'),
('auditoria.process', 'Lanzar procesos de auditoría y reauditoría', 'auditoria'),
]
ROLES_AUDITORIA_FULL = ['admin', 'developer']
ROLES_AUDITORIA_VIEW = ['Agente Aduanal']
def agregar_auditoria(apps, schema_editor):
RolePermission = apps.get_model('rbac', 'RolePermission')
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
perms = {}
for codename, descripcion, modulo in NUEVOS_PERMISOS:
perm, _ = RolePermission.objects.get_or_create(
codename=codename,
defaults={'descripcion': descripcion, 'modulo': modulo},
)
perms[codename] = perm
for role in OrganizationRole.objects.filter(nombre__in=ROLES_AUDITORIA_FULL):
role.permissions.add(perms['auditoria.view'], perms['auditoria.process'])
for role in OrganizationRole.objects.filter(nombre__in=ROLES_AUDITORIA_VIEW):
role.permissions.add(perms['auditoria.view'])
def revertir(apps, schema_editor):
RolePermission = apps.get_model('rbac', 'RolePermission')
OrganizationRole = apps.get_model('rbac', 'OrganizationRole')
for codename, _, _ in NUEVOS_PERMISOS:
try:
perm = RolePermission.objects.get(codename=codename)
except RolePermission.DoesNotExist:
continue
for role in OrganizationRole.objects.all():
role.permissions.remove(perm)
perm.delete()
class Migration(migrations.Migration):
dependencies = [
('rbac', '0003_notificaciones_receive'),
]
operations = [
migrations.RunPython(agregar_auditoria, reverse_code=revertir),
]

View File

109
api/rbac/models.py Normal file
View File

@@ -0,0 +1,109 @@
import uuid
from django.conf import settings
from django.db import models
class RolePermission(models.Model):
"""Catálogo global de permisos de la aplicación. Se define una vez y es compartido por todas las orgs."""
codename = models.CharField(max_length=100, unique=True)
descripcion = models.CharField(max_length=255)
modulo = models.CharField(max_length=50)
def __str__(self):
return self.codename
class Meta:
db_table = 'rbac_role_permission'
ordering = ['modulo', 'codename']
verbose_name = 'Permiso'
verbose_name_plural = 'Permisos'
class OrganizationRole(models.Model):
"""Rol de una organización. Cada org tiene su propio conjunto de roles con sus permisos."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
organizacion = models.ForeignKey(
'organization.Organizacion',
on_delete=models.CASCADE,
related_name='roles',
)
nombre = models.CharField(max_length=100)
descripcion = models.CharField(max_length=255, blank=True)
# El rol admin maestro no puede ser removido del owner de la org
is_admin_role = models.BooleanField(default=False)
permissions = models.ManyToManyField(
RolePermission,
blank=True,
related_name='roles',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.nombre} ({self.organizacion})'
class Meta:
db_table = 'rbac_organization_role'
ordering = ['nombre']
verbose_name = 'Rol de Organización'
verbose_name_plural = 'Roles de Organización'
constraints = [
models.UniqueConstraint(fields=['organizacion', 'nombre'], name='unique_role_per_org'),
]
class UserRole(models.Model):
"""Asignación de un rol a un usuario dentro de su organización."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='user_roles',
)
role = models.ForeignKey(
OrganizationRole,
on_delete=models.CASCADE,
related_name='user_roles',
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user}{self.role.nombre}'
class Meta:
db_table = 'rbac_user_role'
verbose_name = 'Rol de Usuario'
verbose_name_plural = 'Roles de Usuario'
constraints = [
models.UniqueConstraint(fields=['user', 'role'], name='unique_user_role'),
]
class UserPermission(models.Model):
"""Permiso singular asignado directamente a un usuario, sin necesidad de rol.
granted=True otorga, granted=False deniega explícitamente (override sobre roles)."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='rbac_permissions',
)
permission = models.ForeignKey(
RolePermission,
on_delete=models.CASCADE,
related_name='user_overrides',
)
granted = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
estado = 'GRANT' if self.granted else 'DENY'
return f'{estado}: {self.user}{self.permission.codename}'
class Meta:
db_table = 'rbac_user_permission'
verbose_name = 'Permiso Singular'
verbose_name_plural = 'Permisos Singulares'
constraints = [
models.UniqueConstraint(fields=['user', 'permission'], name='unique_user_permission'),
]

176
api/rbac/roles.py Normal file
View File

@@ -0,0 +1,176 @@
# Catálogo de permisos y configuración de roles por defecto.
# Este módulo es importado tanto por la data migration como por el signal de Organizacion,
# por lo que NO debe importar modelos directamente al nivel de módulo.
# --- CATÁLOGO DE PERMISOS ---
# (codename, descripcion, modulo)
PERMISSIONS_CATALOG = [
# Usuarios
('usuarios.view', 'Ver usuarios de la organización', 'usuarios'),
('usuarios.create', 'Crear usuarios en la organización', 'usuarios'),
('usuarios.edit', 'Modificar usuarios de la organización', 'usuarios'),
('usuarios.delete', 'Eliminar usuarios de la organización', 'usuarios'),
('usuarios.manage_roles', 'Asignar y revocar roles a usuarios', 'usuarios'),
('usuarios.change_password', 'Cambiar contraseña de otro usuario', 'usuarios'),
# Pedimentos
('pedimentos.view', 'Ver pedimentos', 'pedimentos'),
('pedimentos.create', 'Crear e importar pedimentos', 'pedimentos'),
('pedimentos.edit', 'Modificar pedimentos', 'pedimentos'),
('pedimentos.delete', 'Eliminar pedimentos', 'pedimentos'),
('pedimentos.process', 'Procesar pedimentos contra VUCEM', 'pedimentos'),
# Importadores
('importadores.view', 'Ver importadores', 'importadores'),
('importadores.create', 'Crear importadores', 'importadores'),
('importadores.edit', 'Modificar importadores', 'importadores'),
('importadores.delete', 'Eliminar importadores', 'importadores'),
# Partidas
('partidas.view', 'Ver partidas', 'partidas'),
('partidas.create', 'Crear partidas', 'partidas'),
('partidas.edit', 'Modificar partidas', 'partidas'),
('partidas.delete', 'Eliminar partidas', 'partidas'),
# Remesas
('remesas.view', 'Ver remesas', 'remesas'),
# COVEs
('coves.view', 'Ver COVEs', 'coves'),
('coves.create', 'Crear COVEs', 'coves'),
('coves.edit', 'Modificar COVEs', 'coves'),
('coves.delete', 'Eliminar COVEs', 'coves'),
# E-Documents
('edocuments.view', 'Ver E-Documents', 'edocuments'),
('edocuments.create', 'Crear E-Documents', 'edocuments'),
('edocuments.edit', 'Modificar E-Documents', 'edocuments'),
('edocuments.delete', 'Eliminar E-Documents', 'edocuments'),
# Acuses
('acuses.view', 'Ver acuses', 'acuses'),
# Documentos (expediente)
('documentos.view', 'Ver documentos del expediente', 'documentos'),
('documentos.upload', 'Cargar documentos', 'documentos'),
('documentos.download', 'Descargar documentos y ZIPs', 'documentos'),
('documentos.delete', 'Eliminar documentos del expediente', 'documentos'),
# VUCEM
('vucem.view', 'Ver credenciales VUCEM', 'vucem'),
('vucem.manage', 'Gestionar credenciales VUCEM', 'vucem'),
# Reportes
('reportes.view', 'Ver reportes y dashboard', 'reportes'),
('reportes.export', 'Exportar reportes a CSV/Excel', 'reportes'),
# DataStage
('datastage.view', 'Ver DataStages', 'datastage'),
('datastage.create', 'Crear DataStages', 'datastage'),
('datastage.process', 'Procesar DataStages', 'datastage'),
('datastage.delete', 'Eliminar DataStages', 'datastage'),
# Organización
('organizacion.view', 'Ver datos de la organización', 'organizacion'),
('organizacion.edit', 'Modificar datos de la organización', 'organizacion'),
# Notificaciones
('notificaciones.view', 'Ver notificaciones propias', 'notificaciones'),
('notificaciones.receive', 'Recibir notificaciones automáticas de eventos', 'notificaciones'),
# Cards / Analytics
('cards.view', 'Ver dashboard y analytics', 'cards'),
# Auditoría
('auditoria.view', 'Ver estado y resultados de auditoría VUCEM', 'auditoria'),
('auditoria.process', 'Lanzar procesos de auditoría y reauditoría', 'auditoria'),
]
# Conjuntos reutilizables para armar la matriz de permisos por rol
_IMPORTADORES_FULL = ['importadores.view', 'importadores.create', 'importadores.edit', 'importadores.delete']
_PEDIMENTOS_FULL = ['pedimentos.view', 'pedimentos.create', 'pedimentos.edit', 'pedimentos.delete', 'pedimentos.process']
_PARTIDAS_FULL = ['partidas.view', 'partidas.create', 'partidas.edit', 'partidas.delete']
_COVES_FULL = ['coves.view', 'coves.create', 'coves.edit', 'coves.delete']
_EDOCUMENTS_FULL = ['edocuments.view', 'edocuments.create', 'edocuments.edit', 'edocuments.delete']
_DOCUMENTOS_FULL = ['documentos.view', 'documentos.upload', 'documentos.download', 'documentos.delete']
_VUCEM_FULL = ['vucem.view', 'vucem.manage']
_REPORTES_FULL = ['reportes.view', 'reportes.export']
_DATASTAGE_FULL = ['datastage.view', 'datastage.create', 'datastage.process']
# --- ROLES POR DEFECTO ---
# Cada entrada: nombre → { descripcion, is_admin_role, permissions }
DEFAULT_ROLES = {
'admin': {
'descripcion': 'Administrador de la organización',
'is_admin_role': True,
'permissions': [
'usuarios.view', 'usuarios.create', 'usuarios.edit', 'usuarios.delete',
'usuarios.manage_roles', 'usuarios.change_password',
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
*_DOCUMENTOS_FULL, *_VUCEM_FULL,
*_IMPORTADORES_FULL,
*_REPORTES_FULL, *_DATASTAGE_FULL,
'organizacion.view', 'organizacion.edit',
'notificaciones.view', 'notificaciones.receive', 'cards.view',
'auditoria.view', 'auditoria.process',
],
},
'developer': {
'descripcion': 'Desarrollador con acceso técnico avanzado',
'is_admin_role': False,
'permissions': [
'usuarios.view', 'usuarios.create',
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
*_DOCUMENTOS_FULL, *_VUCEM_FULL, *_IMPORTADORES_FULL,
*_REPORTES_FULL, *_DATASTAGE_FULL,
'organizacion.view',
'notificaciones.view', 'notificaciones.receive', 'cards.view',
'auditoria.view', 'auditoria.process',
],
},
'Agente Aduanal': {
'descripcion': 'Agente aduanal operativo',
'is_admin_role': False,
'permissions': [
*_PEDIMENTOS_FULL, *_PARTIDAS_FULL, 'remesas.view',
*_COVES_FULL, *_EDOCUMENTS_FULL, 'acuses.view',
*_DOCUMENTOS_FULL, *_VUCEM_FULL,
*_REPORTES_FULL,
'organizacion.view',
'notificaciones.view', 'notificaciones.receive', 'cards.view',
'auditoria.view',
],
},
'user': {
'descripcion': 'Usuario básico de la organización',
'is_admin_role': False,
'permissions': [
'pedimentos.view', 'pedimentos.process',
'partidas.view', 'remesas.view',
'coves.view', 'edocuments.view', 'acuses.view',
'documentos.view', 'documentos.upload', 'documentos.download',
'reportes.view', 'datastage.view',
'notificaciones.view', 'notificaciones.receive', 'cards.view',
],
},
'Importador': {
'descripcion': 'Importador con acceso filtrado por RFC',
'is_admin_role': False,
'permissions': [
'pedimentos.view', 'partidas.view', 'remesas.view',
'coves.view', 'edocuments.view', 'acuses.view',
'documentos.view', 'documentos.download',
'vucem.view', 'vucem.manage',
'reportes.view',
'notificaciones.view', 'notificaciones.receive', 'cards.view',
],
},
}
def crear_roles_para_organizacion(organizacion):
"""Crea los 5 roles por defecto para una organización, con sus permisos.
Usa get_or_create — seguro de ejecutar múltiples veces."""
from api.rbac.models import RolePermission, OrganizationRole
perms_map = {p.codename: p for p in RolePermission.objects.all()}
for nombre, config in DEFAULT_ROLES.items():
role, created = OrganizationRole.objects.get_or_create(
organizacion=organizacion,
nombre=nombre,
defaults={
'descripcion': config['descripcion'],
'is_admin_role': config.get('is_admin_role', False),
},
)
if created:
role_perms = [perms_map[c] for c in config['permissions'] if c in perms_map]
role.permissions.set(role_perms)

105
api/rbac/serializers.py Normal file
View File

@@ -0,0 +1,105 @@
from rest_framework import serializers
from api.rbac.models import OrganizationRole, RolePermission, UserPermission, UserRole
class RolePermissionSerializer(serializers.ModelSerializer):
class Meta:
model = RolePermission
fields = ['id', 'codename', 'descripcion', 'modulo']
class OrganizationRoleSerializer(serializers.ModelSerializer):
permissions = RolePermissionSerializer(many=True, read_only=True)
permission_ids = serializers.PrimaryKeyRelatedField(
queryset=RolePermission.objects.all(),
many=True,
write_only=True,
source='permissions',
required=False,
)
user_count = serializers.IntegerField(read_only=True)
class Meta:
model = OrganizationRole
fields = [
'id', 'nombre', 'descripcion', 'is_admin_role',
'permissions', 'permission_ids', 'user_count',
'created_at', 'updated_at',
]
read_only_fields = ['id', 'is_admin_role', 'created_at', 'updated_at']
class OrganizationRoleWriteSerializer(serializers.ModelSerializer):
"""Serializer para crear/editar roles — recibe lista de IDs de permisos."""
permission_ids = serializers.PrimaryKeyRelatedField(
queryset=RolePermission.objects.all(),
many=True,
source='permissions',
required=False,
)
class Meta:
model = OrganizationRole
fields = ['nombre', 'descripcion', 'permission_ids']
def create(self, validated_data):
perms = validated_data.pop('permissions', [])
role = OrganizationRole.objects.create(**validated_data)
role.permissions.set(perms)
return role
def update(self, instance, validated_data):
perms = validated_data.pop('permissions', None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
if perms is not None:
instance.permissions.set(perms)
return instance
class _UserMinimalSerializer(serializers.Serializer):
id = serializers.UUIDField()
username = serializers.CharField()
email = serializers.EmailField()
first_name = serializers.CharField()
last_name = serializers.CharField()
class _RoleMinimalSerializer(serializers.Serializer):
id = serializers.UUIDField()
nombre = serializers.CharField()
descripcion = serializers.CharField()
class UserRoleSerializer(serializers.ModelSerializer):
user = _UserMinimalSerializer(read_only=True)
role = _RoleMinimalSerializer(read_only=True)
# write
user_id = serializers.UUIDField(write_only=True, source='user')
role_id = serializers.UUIDField(write_only=True, source='role')
class Meta:
model = UserRole
fields = ['id', 'user', 'user_id', 'role', 'role_id', 'created_at']
read_only_fields = ['id', 'created_at']
class UserPermissionSerializer(serializers.ModelSerializer):
user = _UserMinimalSerializer(read_only=True)
permission = RolePermissionSerializer(read_only=True)
# write
user_id = serializers.UUIDField(write_only=True, source='user')
permission_id = serializers.IntegerField(write_only=True, source='permission')
class Meta:
model = UserPermission
fields = ['id', 'user', 'user_id', 'permission', 'permission_id', 'granted', 'created_at']
read_only_fields = ['id', 'created_at']
class MyPermissionsSerializer(serializers.Serializer):
"""Respuesta de /rbac/my-permissions/ — permisos efectivos del usuario autenticado."""
permissions = serializers.ListField(child=serializers.CharField())
roles = serializers.ListField(child=serializers.CharField())

23
api/rbac/urls.py Normal file
View File

@@ -0,0 +1,23 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from api.rbac.views import (
MyPermissionsView,
OrganizationRoleViewSet,
RolePermissionViewSet,
SwitchOrganizationView,
UserPermissionViewSet,
UserRoleViewSet,
)
router = DefaultRouter()
router.register(r'permissions', RolePermissionViewSet, basename='rbac-permission')
router.register(r'roles', OrganizationRoleViewSet, basename='rbac-role')
router.register(r'user-roles', UserRoleViewSet, basename='rbac-user-role')
router.register(r'user-permissions', UserPermissionViewSet, basename='rbac-user-permission')
urlpatterns = [
path('', include(router.urls)),
path('my-permissions/', MyPermissionsView.as_view(), name='rbac-my-permissions'),
path('switch-organization/', SwitchOrganizationView.as_view(), name='rbac-switch-org'),
]

412
api/rbac/views.py Normal file
View File

@@ -0,0 +1,412 @@
from django.db.models import Count
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from api.rbac.models import OrganizationRole, RolePermission, UserPermission, UserRole
from api.rbac.serializers import (
MyPermissionsSerializer,
OrganizationRoleSerializer,
OrganizationRoleWriteSerializer,
RolePermissionSerializer,
UserPermissionSerializer,
UserRoleSerializer,
)
from core.permissions import OrgScopedPermission, get_org_context, is_internal_service_request, require_permission, user_has_permission
def _require_manage_roles(user):
"""Retorna True si el usuario puede gestionar roles/permisos en su org."""
return user.is_superuser or user_has_permission(user, 'usuarios.manage_roles')
# ---------------------------------------------------------------------------
# Catálogo de permisos (lectura para todos los autenticados con org)
# ---------------------------------------------------------------------------
class RolePermissionViewSet(ReadOnlyModelViewSet):
"""Lista el catálogo global de permisos disponibles, agrupados por módulo."""
my_tags = ['RBAC']
serializer_class = RolePermissionSerializer
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
def get_queryset(self):
return RolePermission.objects.all().order_by('modulo', 'codename')
@action(detail=False, methods=['get'], url_path='by-module')
def by_module(self, request):
"""Devuelve el catálogo agrupado por módulo."""
perms = self.get_queryset()
result = {}
for p in perms:
result.setdefault(p.modulo, []).append(
RolePermissionSerializer(p).data
)
return Response(result)
# ---------------------------------------------------------------------------
# Roles de la organización
# ---------------------------------------------------------------------------
class OrganizationRoleViewSet(ModelViewSet):
"""
CRUD de roles de la organización activa.
Solo usuarios con usuarios.manage_roles pueden crear/editar/eliminar.
"""
my_tags = ['RBAC']
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
def get_queryset(self):
if is_internal_service_request(self.request):
return (
OrganizationRole.objects
.annotate(user_count=Count('user_roles'))
.prefetch_related('permissions')
.order_by('nombre')
)
org = get_org_context(self.request.user)
if not org:
return OrganizationRole.objects.none()
return (
OrganizationRole.objects
.filter(organizacion=org)
.annotate(user_count=Count('user_roles'))
.prefetch_related('permissions')
.order_by('nombre')
)
def get_serializer_class(self):
if self.action in ('create', 'update', 'partial_update'):
return OrganizationRoleWriteSerializer
return OrganizationRoleSerializer
def _check_manage_roles(self):
if not _require_manage_roles(self.request.user):
return Response(
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
status=status.HTTP_403_FORBIDDEN,
)
return None
def create(self, request, *args, **kwargs):
err = self._check_manage_roles()
if err:
return err
org = get_org_context(request.user)
if not org:
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(organizacion=org)
return Response(
OrganizationRoleSerializer(serializer.instance).data,
status=status.HTTP_201_CREATED,
)
def update(self, request, *args, **kwargs):
err = self._check_manage_roles()
if err:
return err
instance = self.get_object()
# No se puede cambiar nombre ni permisos de un rol is_admin_role
if instance.is_admin_role and not request.user.is_superuser:
return Response(
{'detail': 'No se puede modificar un rol de administrador.'},
status=status.HTTP_403_FORBIDDEN,
)
return super().update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
err = self._check_manage_roles()
if err:
return err
instance = self.get_object()
if instance.is_admin_role and not request.user.is_superuser:
return Response(
{'detail': 'No se puede eliminar un rol de administrador.'},
status=status.HTTP_403_FORBIDDEN,
)
if instance.user_roles.exists():
return Response(
{'detail': 'No se puede eliminar un rol con usuarios asignados.'},
status=status.HTTP_400_BAD_REQUEST,
)
return super().destroy(request, *args, **kwargs)
# ---------------------------------------------------------------------------
# Asignación de roles a usuarios
# ---------------------------------------------------------------------------
class UserRoleViewSet(ModelViewSet):
"""
Asigna y revoca roles de usuarios en la organización activa.
Solo usuarios con usuarios.manage_roles pueden modificar.
"""
my_tags = ['RBAC']
serializer_class = UserRoleSerializer
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
http_method_names = ['get', 'post', 'delete', 'head', 'options']
def get_queryset(self):
if is_internal_service_request(self.request):
qs = UserRole.objects.select_related('user', 'role')
user_id = self.request.query_params.get('user_id')
if user_id:
qs = qs.filter(user_id=user_id)
return qs
org = get_org_context(self.request.user)
if not org:
return UserRole.objects.none()
qs = (
UserRole.objects
.filter(role__organizacion=org)
.select_related('user', 'role')
)
user_id = self.request.query_params.get('user_id')
if user_id:
qs = qs.filter(user_id=user_id)
return qs
def create(self, request, *args, **kwargs):
if not _require_manage_roles(request.user):
return Response(
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
status=status.HTTP_403_FORBIDDEN,
)
org = get_org_context(request.user)
if not org:
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
user_id = request.data.get('user_id')
role_id = request.data.get('role_id')
if not user_id or not role_id:
return Response({'detail': 'user_id y role_id son requeridos.'}, status=status.HTTP_400_BAD_REQUEST)
# Verificar que el rol pertenece a la misma org
try:
role = OrganizationRole.objects.get(id=role_id, organizacion=org)
except OrganizationRole.DoesNotExist:
return Response({'detail': 'El rol no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
# Verificar que el usuario pertenece a la misma org
from api.cuser.models import CustomUser
try:
target_user = CustomUser.objects.get(id=user_id, organizacion=org)
except CustomUser.DoesNotExist:
return Response({'detail': 'El usuario no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
user_role, created = UserRole.objects.get_or_create(user=target_user, role=role)
serializer = self.get_serializer(user_role)
return Response(serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
def destroy(self, request, *args, **kwargs):
if not _require_manage_roles(request.user):
return Response(
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
status=status.HTTP_403_FORBIDDEN,
)
instance = self.get_object()
org = get_org_context(request.user)
# Proteger al owner de la org: no se le puede quitar el rol admin
if org and hasattr(org, 'owner') and org.owner and instance.user == org.owner:
if instance.role.is_admin_role:
return Response(
{'detail': 'No se puede revocar el rol de administrador al propietario de la organización.'},
status=status.HTTP_403_FORBIDDEN,
)
return super().destroy(request, *args, **kwargs)
# ---------------------------------------------------------------------------
# Permisos singulares (overrides por usuario)
# ---------------------------------------------------------------------------
class UserPermissionViewSet(ModelViewSet):
"""
Otorga o deniega permisos singulares a usuarios, sin necesidad de crear un rol.
granted=true → otorgar; granted=false → denegar explícitamente (override sobre roles).
Solo usuarios con usuarios.manage_roles pueden modificar.
"""
my_tags = ['RBAC']
serializer_class = UserPermissionSerializer
permission_classes = [IsAuthenticated, require_permission('usuarios.manage_roles')]
http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
def get_queryset(self):
if is_internal_service_request(self.request):
qs = UserPermission.objects.select_related('user', 'permission')
user_id = self.request.query_params.get('user_id')
if user_id:
qs = qs.filter(user_id=user_id)
return qs
org = get_org_context(self.request.user)
if not org:
return UserPermission.objects.none()
qs = (
UserPermission.objects
.filter(user__organizacion=org)
.select_related('user', 'permission')
)
user_id = self.request.query_params.get('user_id')
if user_id:
qs = qs.filter(user_id=user_id)
return qs
def _check(self):
if not _require_manage_roles(self.request.user):
return Response(
{'detail': 'Se requiere el permiso usuarios.manage_roles.'},
status=status.HTTP_403_FORBIDDEN,
)
return None
def create(self, request, *args, **kwargs):
err = self._check()
if err:
return err
org = get_org_context(request.user)
if not org:
return Response({'detail': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
user_id = request.data.get('user_id')
permission_id = request.data.get('permission_id')
granted = request.data.get('granted', True)
if not user_id or not permission_id:
return Response({'detail': 'user_id y permission_id son requeridos.'}, status=status.HTTP_400_BAD_REQUEST)
from api.cuser.models import CustomUser
try:
target_user = CustomUser.objects.get(id=user_id, organizacion=org)
except CustomUser.DoesNotExist:
return Response({'detail': 'El usuario no pertenece a esta organización.'}, status=status.HTTP_404_NOT_FOUND)
try:
perm = RolePermission.objects.get(id=permission_id)
except RolePermission.DoesNotExist:
return Response({'detail': 'Permiso no encontrado.'}, status=status.HTTP_404_NOT_FOUND)
override, created = UserPermission.objects.update_or_create(
user=target_user,
permission=perm,
defaults={'granted': granted},
)
serializer = self.get_serializer(override)
return Response(serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs):
err = self._check()
if err:
return err
return super().partial_update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
err = self._check()
if err:
return err
return super().destroy(request, *args, **kwargs)
# ---------------------------------------------------------------------------
# Mis permisos efectivos (para el frontend)
# ---------------------------------------------------------------------------
class MyPermissionsView(APIView):
"""
Retorna los permisos efectivos del usuario autenticado.
El frontend usa esto para decidir qué mostrar/ocultar.
"""
my_tags = ['RBAC']
permission_classes = [IsAuthenticated & OrgScopedPermission]
def get(self, request):
user = request.user
org = get_org_context(user)
if user.is_superuser:
all_perms = list(RolePermission.objects.values_list('codename', flat=True))
return Response({'permissions': all_perms, 'roles': ['superuser']})
if not org:
return Response({'permissions': [], 'roles': []})
# Roles del usuario en la org
roles = list(
UserRole.objects.filter(user=user, role__organizacion=org)
.values_list('role__nombre', flat=True)
)
# Permisos de roles
perms_set = set(
UserRole.objects.filter(user=user, role__organizacion=org)
.values_list('role__permissions__codename', flat=True)
)
perms_set.discard(None)
# Aplicar overrides singulares
for override in UserPermission.objects.filter(user=user).select_related('permission'):
if override.granted:
perms_set.add(override.permission.codename)
else:
perms_set.discard(override.permission.codename)
return Response({'permissions': sorted(perms_set), 'roles': roles})
# ---------------------------------------------------------------------------
# Switch de organización (solo superusuarios)
# ---------------------------------------------------------------------------
class SwitchOrganizationView(APIView):
"""
Permite a un superusuario cambiar su organización activa.
POST { "organization_id": "<uuid>" } → actualiza active_organization del superuser.
DELETE → limpia active_organization (el superuser queda sin contexto de org).
"""
my_tags = ['RBAC']
permission_classes = [IsAuthenticated]
def post(self, request):
if not request.user.is_superuser:
return Response(
{'detail': 'Solo superusuarios pueden cambiar de organización.'},
status=status.HTTP_403_FORBIDDEN,
)
org_id = request.data.get('organization_id')
if not org_id:
return Response({'detail': 'organization_id es requerido.'}, status=status.HTTP_400_BAD_REQUEST)
from api.organization.models import Organizacion
try:
import uuid as _uuid
org = Organizacion.objects.get(id=_uuid.UUID(str(org_id)))
except (Organizacion.DoesNotExist, ValueError):
return Response({'detail': 'Organización no encontrada.'}, status=status.HTTP_404_NOT_FOUND)
request.user.active_organization = org
request.user.save(update_fields=['active_organization'])
return Response({
'detail': f'Organización activa actualizada a: {org.nombre}',
'organization': {'id': str(org.id), 'nombre': org.nombre},
})
def delete(self, request):
if not request.user.is_superuser:
return Response(
{'detail': 'Solo superusuarios pueden limpiar la organización activa.'},
status=status.HTTP_403_FORBIDDEN,
)
request.user.active_organization = None
request.user.save(update_fields=['active_organization'])
return Response({'detail': 'Organización activa removida.'})

View File

@@ -26,11 +26,13 @@ from django.utils import timezone
from django.db.models import Q
from api.utils.storage_service import storage_service
from rest_framework.authentication import TokenAuthentication
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
get_org_context,
require_permission,
user_has_permission,
IsInternalService,
)
import logging
@@ -142,21 +144,47 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"""
ViewSet for Document model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
model = Document
pagination_class = CustomPagination
serializer_class = DocumentSerializer
# Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado)
filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento', 'created_at']
# filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento']
# Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero>
# Ejemplo: /api/record/documents/?pedimento_numero=12345678
my_tags = ['Documents']
def get_permissions(self):
# Service account (Token + superuser): acceso directo sin RBAC de org
if (self.request.user.is_authenticated and self.request.user.is_superuser and
isinstance(getattr(self.request, 'successful_authenticator', None), TokenAuthentication)):
return [IsAuthenticated(), IsInternalService()]
perms = {
'list': 'documentos.view',
'retrieve': 'documentos.view',
'create': 'documentos.upload',
'update': 'documentos.upload',
'partial_update': 'documentos.upload',
'destroy': 'documentos.delete',
'vu_documentos_errores': 'documentos.view',
'bulk_delete': 'documentos.delete',
'bulk_delete_partidas_vu': 'documentos.delete',
'bulk_delete_coves_vu': 'documentos.delete',
'bulk_delete_edocs_vu': 'documentos.delete',
'bulk_upload': 'documentos.upload',
'bulk_upload_vu': 'documentos.upload',
'create_vu_record': 'documentos.upload',
}
codename = perms.get(self.action, 'documentos.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
queryset = self.get_queryset_filtrado_por_organizacion()
user = self.request.user
if user.is_superuser and isinstance(
getattr(self.request, 'successful_authenticator', None), TokenAuthentication
):
queryset = Document.objects.all()
else:
if not user_has_permission(user, 'documentos.view'):
return Document.objects.none()
queryset = self.get_queryset_filtrado_por_organizacion()
modulo_efc = self.request.query_params.get('modulo')
if modulo_efc:
if modulo_efc == 'expedientes-detalle-pedimentos':
@@ -2017,7 +2045,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
serializer_class = DocumentSerializer
model = Document
my_tags = ['Documents']
@@ -2030,17 +2058,14 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
import os
from api.utils.storage_service import storage_service
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
raise Http404("Usuario no autenticado")
try:
doc = Document.objects.get(pk=pk)
except Document.DoesNotExist:
raise Http404("Documento no encontrado")
if not request.user.is_superuser:
if doc.organizacion != request.user.organizacion:
raise Http404("No autorizado")
org = get_org_context(request.user)
if doc.organizacion != org:
raise Http404("No autorizado")
if not doc.archivo:
raise Http404("Documento sin archivo asociado")
@@ -2064,7 +2089,7 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
return response
class BulkDownloadZipView(APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
my_tags = ['Documents']
def post(self, request):
@@ -2172,7 +2197,7 @@ class BulkDownloadZipView(APIView):
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
class GetFuenteView(APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
serializer_class = FuenteSerializer
my_tags = ['Fuente Documentos']
@@ -2187,7 +2212,7 @@ class GetFuenteView(APIView):
return Response(serializer.data, status=200)
class DocumentTypeView(APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
serializer_class = DocumentTypeSerializer
my_tags = ['Tipo de Documentos']
@@ -2204,7 +2229,7 @@ class DocumentTypeView(APIView):
return Response(serializer.data, status=200)
class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
my_tags = ['Documents']
def post(self, request):
@@ -2306,7 +2331,7 @@ class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
logger.warning(f"No se pudo eliminar archivo temporal {tmp_path}: {e}")
class MultiPedimentoZipDownloadView(APIView):
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper)]
permission_classes = [IsAuthenticated, require_permission('documentos.download')]
my_tags = ['Documents']
def post(self, request):
@@ -2375,7 +2400,7 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"""
ViewSet for Document model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
permission_classes = [IsAuthenticated, require_permission('documentos.view')]
model = Document
pagination_class = CustomPagination
@@ -2389,6 +2414,8 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
my_tags = ['Documents']
def get_queryset(self):
if not user_has_permission(self.request.user, 'documentos.view'):
return Document.objects.none()
queryset = self.get_queryset_filtrado_por_organizacion()
pedimento_id = self.request.query_params.get('pedimento')
@@ -2435,8 +2462,7 @@ class TriggerPedimentoCompletoView(APIView):
en el microservicio FastAPI. Reenvía el payload tal cual y devuelve
la respuesta del microservicio (normalmente un `task_id`).
"""
# permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
permission_classes = [IsAuthenticated, require_permission('pedimentos.process')]
my_tags = ['Microservice - Pedimento Completo']

View File

@@ -1,57 +1,31 @@
from warnings import filters
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from api.customs.models import Pedimento, Cove, EDocument, Partida
from api.record.models import Document
from api.organization.models import Organizacion
from django.db.models import Count, Q
# Registrar endpoint en urls.py:
# path('dashboard/summary/', dashboard_summary)
import csv
import io
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from .serializers import ExportModelSerializer
from rest_framework.response import Response
from django.http import HttpResponse
import openpyxl
from django.apps import apps
from rest_framework import status
from django.shortcuts import render
from rest_framework import viewsets
from .serializers import ExportModelSerializer
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from rest_framework.permissions import IsAuthenticated
import csv
import io
import openpyxl
from django.http import HttpResponse
from django.apps import apps
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework.permissions import IsAuthenticated
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from .serializers import ExportModelSerializer
import uuid
import datetime
import zipfile
import openpyxl
from django.apps import apps
from django.db import models
from django.db.models import Count, Q
from django.http import HttpResponse
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status, viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from api.customs.models import Cove, EDocument, Partida, Pedimento
from api.organization.models import Organizacion
from api.record.models import Document
from core.permissions import (
get_org_context,
require_permission,
user_has_permission,
)
from .serializers import ExportModelSerializer
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
@@ -110,7 +84,11 @@ def export_model_to_excel(request, model_name, fields, module='datastage', filte
class ExportDataStageView(APIView):
my_tags = ['Reportes-DataStage']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
def get_permissions(self):
if self.request.method == 'GET':
return [IsAuthenticated(), require_permission('reportes.view')()]
return [IsAuthenticated(), require_permission('reportes.export')()]
# Constantes para partición
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
@@ -136,20 +114,14 @@ class ExportDataStageView(APIView):
return str(value)
def get(self, request, *args, **kwargs):
"""Retorna RFCs distintos de Registro501 para la organización indicada. El parámetro organizacion es obligatorio."""
"""Retorna RFCs distintos de Registro501 para la organización activa del usuario."""
try:
Registro501 = apps.get_model('datastage', 'Registro501')
if not request.user.is_superuser:
qs = Registro501.objects.filter(organizacion=request.user.organizacion)
else:
org_id = request.query_params.get('organizacion')
if not org_id:
return Response({'error': 'El parámetro organizacion es obligatorio'}, status=status.HTTP_400_BAD_REQUEST)
try:
qs = Registro501.objects.filter(organizacion_id=uuid.UUID(org_id))
except (ValueError, AttributeError):
return Response({'error': 'UUID de organización inválido'}, status=status.HTTP_400_BAD_REQUEST)
org = get_org_context(request.user)
if not org:
return Response({'error': 'Sin organización activa'}, status=status.HTTP_403_FORBIDDEN)
qs = Registro501.objects.filter(organizacion=org)
rfcs = (
qs.exclude(rfc__isnull=True)
@@ -178,23 +150,19 @@ class ExportDataStageView(APIView):
def _resolve_org_filter(self, global_filters, user):
"""
Devuelve los global_filters asegurando que siempre haya una organización.
- Superuser sin org → error (no mezclar tenants).
- No-superuser sin org → se inyecta la org del usuario.
La org se obtiene de active_organization (superuser) o del campo organizacion (usuario normal).
Retorna (filters_dict, error_response_or_None).
"""
org_value = (global_filters or {}).get('organizacion', '')
if not org_value:
if user.is_superuser:
filters = dict(global_filters or {})
if not filters.get('organizacion'):
org = get_org_context(user)
if not org:
return None, Response(
{'error': 'El parámetro organizacion es obligatorio'},
status=status.HTTP_400_BAD_REQUEST
{'error': 'Sin organización activa'},
status=status.HTTP_403_FORBIDDEN,
)
# No-superuser: inyectar su propia org
if hasattr(user, 'organizacion') and user.organizacion:
filters = dict(global_filters or {})
filters['organizacion'] = str(user.organizacion.id)
return filters, None
return dict(global_filters or {}), None
filters['organizacion'] = str(org.id)
return filters, None
def handle_simple_export(self, request):
"""Maneja exportación simple de DataStage (un solo modelo)"""
@@ -1868,7 +1836,11 @@ class ExportDataStageView(APIView):
class ExportModelView(APIView):
my_tags = ['Reportes']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
def get_permissions(self):
if self.request.method == 'GET':
return [IsAuthenticated(), require_permission('reportes.view')()]
return [IsAuthenticated(), require_permission('reportes.export')()]
@swagger_auto_schema(
manual_parameters=[
@@ -1906,6 +1878,8 @@ class ExportModelView(APIView):
model_name = request.data.get('model')
fields = request.data.get('fields')
filters = request.data.get('filters', {})
org = get_org_context(request.user)
filters['organizacion__id'] = org.id if org else None
export_type = request.data.get('type', 'csv')
module = request.data.get('module', 'datastage')
@@ -1917,40 +1891,12 @@ class ExportModelView(APIView):
else:
return export_model_to_csv(request, model_name, fields, module, filters)
# Create your views here.
class ExportModelView(APIView):
my_tags = ['Reportes']
permission_classes = [IsAuthenticated & (
IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
@swagger_auto_schema(request_body=ExportModelSerializer, esponses={200: 'Archivo generado (Excel o CSV)'})
def post(self, request, *args, **kwargs):
model_name = request.data.get('model')
fields = request.data.get('fields')
filters = request.data.get('filters', {})
filters['organizacion__id'] = self.request.user.organizacion.id if hasattr(request.user, 'organizacion') and request.user.organizacion else None
export_type = request.data.get('type', 'csv')
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
module = request.data.get('module', 'datastage')
if export_type == 'excel':
return export_model_to_excel(request, model_name, fields, module, filters)
else:
return export_model_to_csv(request, model_name, fields, module, filters)
# Resumen general para dashboard
@api_view(['GET'])
@permission_classes([
IsAuthenticated
])
@permission_classes([IsAuthenticated, require_permission('reportes.view')])
def dashboard_summary(request):
org_id = request.query_params.get('organizacion_id')
filters = {}
user = request.user
@@ -1964,18 +1910,16 @@ def dashboard_summary(request):
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
contribuyente__rfc = request.query_params.get('contribuyente__rfc')
# Si no se especifica organización y el usuario tiene organización, usarla
if not org_id and hasattr(user, 'organizacion') and user.organizacion:
org_id = user.organizacion.id
# Si no es superusuario, filtrar por organización
if org_id and not getattr(user, 'is_superuser', False):
filters['organizacion_id'] = org_id
org = get_org_context(user)
if not org:
return Response({'error': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
filters['organizacion_id'] = org.id
# Si el usuario pertenece al grupo Importador, filtrar por RFC
if user.groups.filter(name='Importador').exists():
rfc = getattr(user, 'rfc', None)
if rfc:
filters['contribuyente__rfc'] = rfc
# Importador: filtrar solo por sus RFC asignados
if user.is_importador:
rfcs = list(user.rfc.values_list('rfc', flat=True))
if rfcs:
filters['contribuyente__rfc__in'] = rfcs
if pedimento_app:
filters['pedimento_app'] = pedimento_app

View File

@@ -1,53 +1,54 @@
from django.shortcuts import render
from rest_framework import viewsets, filters
from rest_framework.authentication import TokenAuthentication
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin
from core.permissions import require_permission, user_has_permission, IsInternalService
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
from .models import Task
from .serializers import TaskSerializer
from .filters import TaskFilter
from rest_framework.permissions import IsAuthenticated
# Create your views here.
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
class TaskPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet,OrganizacionFiltradaMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
class TaskViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
# Task se relaciona con pedimento, que tiene contribuyente
campo_contribuyente = 'pedimento__contribuyente'
queryset = Task.objects.select_related('pedimento', 'servicio').all()
serializer_class = TaskSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_class = TaskFilter
pagination_class = TaskPagination
ordering_fields = ['timestamp']
ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero
ordering = ['-timestamp']
my_tags = ['tasks']
def get_queryset(self):
def get_permissions(self):
# Escritura: exclusivo para microservicio interno (Token + superuser)
# Lectura: usuarios con pedimentos.view via JWT
if self.action in ('create', 'update', 'partial_update', 'destroy'):
return [IsAuthenticated(), IsInternalService()]
return [IsAuthenticated(), require_permission('pedimentos.view')()]
"""
Filtra las tareas según la organización del usuario.
Superusuarios pueden ver todas las tareas.
"""
queryset = self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
# if user.is_superuser:
# return self.queryset
# # return self.queryset.filter(organizacion_id=user.organizacion.id)
# else:
# return self.queryset.filter(organizacion_id=user.organizacion.id)
return queryset
def get_queryset(self):
user = self.request.user
# Service account (Token + superuser): sin filtro de org, accede a todas las tasks
if user.is_superuser and isinstance(
getattr(self.request, 'successful_authenticator', None), TokenAuthentication
):
return Task.objects.select_related('pedimento', 'servicio').all()
if not user_has_permission(user, 'pedimentos.view'):
return Task.objects.none()
return self.get_queryset_filtrado_por_organizacion()
from rest_framework.views import APIView
@@ -57,20 +58,82 @@ from celery.result import AsyncResult
class TaskStatusView(APIView):
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, require_permission('pedimentos.view')]
# Mapeo de status del microservicio → estados estándar
_STATUS_MAP = {
'failed': 'FAILURE',
'completed': 'SUCCESS',
'processing': 'STARTED',
'submitted': 'PENDING',
'pending': 'PENDING',
}
def get(self, request, task_id):
"""
Consulta el estado de una tarea Celery.
Consulta el estado de una tarea.
Fuente de verdad: registro Django Task (actualizado por el microservicio vía PUT).
Celery AsyncResult se usa como complemento para tareas de auditoría masiva (SUCCESS)
y como fallback cuando la tarea no está en la BD todavía.
Estados posibles:
PENDING — en cola, aún no inició
STARTED — worker la tomó y está ejecutando
SUCCESS — terminó correctamente, `result` contiene el resumen
FAILURE — lanzó una excepción no capturada, `error` describe el problema
PENDING — en cola o aún no registrada
STARTED — worker ejecutando
SUCCESS — completada sin errores
FAILURE — terminó con error
RETRY — el worker la está reintentando
"""
try:
# Prioridad 1: Django Task record (fuente de verdad del microservicio)
try:
django_task = Task.objects.get(task_id=task_id)
effective_state = self._STATUS_MAP.get(
django_task.status.lower(), django_task.status.upper()
)
is_terminal = effective_state in ('SUCCESS', 'FAILURE')
response_data = {
'task_id': task_id,
'status': effective_state,
'ready': is_terminal,
'successful': (effective_state == 'SUCCESS') if is_terminal else None,
'message': django_task.message,
}
if effective_state == 'FAILURE':
response_data['error'] = django_task.message
elif effective_state == 'SUCCESS':
# Para auditoría masiva, intentar enriquecer con resultado de Celery
try:
celery_result = AsyncResult(task_id)
if celery_result.ready() and celery_result.successful():
result = celery_result.result
response_data['result'] = result
if isinstance(result, dict) and 'total_pedimentos' in result:
total = result.get('total_pedimentos', 0)
completados = result.get('completados', 0)
con_pendientes = result.get('con_pendientes', 0)
con_errores = result.get('con_errores', 0)
if con_pendientes == 0 and con_errores == 0:
response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
else:
partes = []
if con_pendientes:
partes.append(f'{con_pendientes} con documentos pendientes')
if con_errores:
partes.append(f'{con_errores} con error')
response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
except Exception:
pass
return Response(response_data, status=status.HTTP_200_OK)
except Task.DoesNotExist:
pass
# Prioridad 2: Celery AsyncResult (tarea aún no registrada en BD)
task_result = AsyncResult(task_id)
state = task_result.state
@@ -84,25 +147,20 @@ class TaskStatusView(APIView):
if state == 'SUCCESS':
result = task_result.result
response_data['result'] = result
# Resumen legible cuando es auditoría masiva de organización
if isinstance(result, dict) and 'total_pedimentos' in result:
total = result.get('total_pedimentos', 0)
completados = result.get('completados', 0)
con_pendientes = result.get('con_pendientes', 0)
con_errores = result.get('con_errores', 0)
if con_pendientes == 0 and con_errores == 0:
mensaje = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes'
else:
partes = []
if con_pendientes:
partes.append(f'{con_pendientes} con documentos pendientes')
if con_errores:
partes.append(f'{con_errores} con error')
mensaje = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
response_data['mensaje'] = mensaje
response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}'
elif state == 'FAILURE':
response_data['error'] = str(task_result.info)

View File

@@ -25,15 +25,14 @@ class VucemUpdateSerializer(VucemSerializer):
class Meta(VucemSerializer.Meta):
fields = VucemSerializer.Meta.fields
from .models import Vucem, CredencialesImportador
from core.permissions import IsSameOrganizationDeveloper
from rest_framework import mixins
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser,
IsSameOrganizationAndInAllowedGroups
IsSameOrganizationAndInAllowedGroups,
get_org_context,
is_internal_service_request,
require_permission,
user_has_permission,
)
class CustomVucemPagination(PageNumberPagination):
@@ -53,8 +52,6 @@ class CustomVucemPagination(PageNumberPagination):
# Create your views here.
class VucemView(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated , (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
queryset = Vucem.objects.all()
pagination_class = CustomVucemPagination
filterset_fields = ['organizacion', 'patente', 'usuario', 'is_importador', 'acusecove', 'acuseedocument', 'is_active']
@@ -68,27 +65,45 @@ class VucemView(viewsets.ModelViewSet):
return VucemSerializer
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [IsAuthenticated(), IsSameOrganizationAndInAllowedGroups()]
return super().get_permissions()
perms = {
'list': 'vucem.view',
'retrieve': 'vucem.view',
'create': 'vucem.manage',
'update': 'vucem.manage',
'partial_update': 'vucem.manage',
'destroy': 'vucem.manage',
'download_cer': 'vucem.view',
'download_key': 'vucem.view',
}
codename = perms.get(self.action, 'vucem.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
# Verificar que el usuario esté autenticado y tenga organización
if not self.request.user.is_authenticated:
return self.queryset.none()
queryset = self.queryset
if is_internal_service_request(self.request):
queryset = self.queryset.all()
importador_rfc = self.request.query_params.get('importador')
if importador_rfc:
queryset = queryset.filter(usuarios_importadores__rfc__rfc=importador_rfc).distinct()
return queryset
if self.request.user.is_superuser:
queryset = queryset.all()
elif not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
return queryset.none()
elif self.request.user.groups.filter(name='Importador').exists():
queryset = queryset.filter(organizacion=self.request.user.organizacion, usuario__in=self.request.user.rfc.all())
if not user_has_permission(self.request.user, 'vucem.view'):
return self.queryset.none()
org = get_org_context(self.request.user)
if not org:
return self.queryset.none()
if self.request.user.is_importador:
queryset = self.queryset.filter(
organizacion=org,
usuario__in=self.request.user.rfc.all(),
)
else:
queryset = queryset.filter(organizacion=self.request.user.organizacion)
queryset = self.queryset.filter(organizacion=org)
# Filtro por importador (RFC)
importador_rfc = self.request.query_params.get('importador')
if importador_rfc:
queryset = queryset.filter(usuarios_importadores__rfc__rfc=importador_rfc).distinct()
@@ -96,54 +111,37 @@ class VucemView(viewsets.ModelViewSet):
return queryset
def perform_create(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
if self.request.user.is_superuser:
organizacion_id = self.request.data.get('organizacion_id')
if not organizacion_id:
raise ValueError("Los superusuarios deben especificar una organización")
try:
# Importa el modelo Organizacion
# from ..organization.models import Organizacion
organizacion = Organizacion.objects.get(id=organizacion_id)
except Organizacion.DoesNotExist:
raise ValueError({"organizacion": "Organización no encontrada"})
serializer.save(
organizacion=organizacion,
created_by=self.request.user,
updated_by=self.request.user
)
return
else:
serializer.save(
organizacion=self.request.user.organizacion,
created_by=self.request.user,
updated_by=self.request.user
)
return
if is_internal_service_request(self.request):
serializer.save(updated_by=self.request.user)
return
org = get_org_context(self.request.user)
if not org:
raise ValueError("El usuario debe tener una organización activa para crear credenciales VUCEM.")
serializer.save(
organizacion=org,
created_by=self.request.user,
updated_by=self.request.user,
)
def perform_update(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
if is_internal_service_request(self.request):
instance = self.get_object()
serializer.save(
created_by=instance.created_by,
updated_by=self.request.user,
)
return
org = get_org_context(self.request.user)
if not org:
raise ValueError("El usuario debe tener una organización activa para modificar credenciales VUCEM.")
instance = self.get_object()
if self.request.user.is_superuser:
serializer.save(
created_by=instance.created_by,
updated_by=self.request.user
)
return
else:
serializer.save(
organizacion=self.request.user.organizacion,
created_by=instance.created_by,
updated_by=self.request.user
)
return
serializer.save(
organizacion=org,
created_by=instance.created_by,
updated_by=self.request.user,
)
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
@action(detail=True, methods=["get"])
def download_cer(self, request, pk=None):
vucem = self.get_object()
if not vucem.cer:
@@ -164,7 +162,7 @@ class VucemView(viewsets.ModelViewSet):
return response
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
@action(detail=True, methods=["get"])
def download_key(self, request, pk=None):
vucem = self.get_object()
if not vucem.key:
@@ -194,7 +192,6 @@ class VucemView(viewsets.ModelViewSet):
class CredencialesImportadorViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = CredencialesImportador.objects.all()
serializer_class = CredencialesImportadorSimpleSerializer
filterset_fields = ['organizacion', 'vucem', 'rfc']
@@ -205,27 +202,34 @@ class CredencialesImportadorViewSet(viewsets.ModelViewSet):
my_tags = ['Credenciales por Importador']
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [IsAuthenticated()]
return super().get_permissions()
perms = {
'list': 'vucem.view',
'retrieve': 'vucem.view',
'create': 'vucem.manage',
'update': 'vucem.manage',
'partial_update': 'vucem.manage',
'destroy': 'vucem.manage',
}
codename = perms.get(self.action, 'vucem.view')
return [IsAuthenticated(), require_permission(codename)()]
def get_queryset(self):
if self.request.user.is_superuser:
# Si es superusuario, devolver todos los registros
return self.queryset.all()
# Verificar que el usuario esté autenticado y tenga organización
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
if not self.request.user.is_authenticated:
return self.queryset.none()
queryset = self.queryset.filter(organizacion=self.request.user.organizacion)
return queryset
if is_internal_service_request(self.request):
return self.queryset.all()
if not user_has_permission(self.request.user, 'vucem.view'):
return self.queryset.none()
org = get_org_context(self.request.user)
if not org:
return self.queryset.none()
return self.queryset.filter(organizacion=org)
def perform_create(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("El usuario debe estar autenticado y tener una organización asignada.")
serializer.save(organizacion=self.request.user.organizacion)
return
if is_internal_service_request(self.request):
serializer.save()
return
org = get_org_context(self.request.user)
if not org:
raise ValueError("El usuario debe tener una organización activa.")
serializer.save(organizacion=org)

View File

@@ -97,7 +97,8 @@ OWN_APPS = [
'api.record',
'api.organization',
'api.licence',
'api.cuser',
'api.cuser',
'api.rbac',
'api.datastage',
'api.vucem',
'api.logger',

View File

@@ -51,6 +51,7 @@ urlpatterns = [
path('api/v1/cards/', include('api.cards.urls')), # Cards app
path('api/v1/reports/', include('api.reports.urls')), # Reports app
path('api/v1/tasks/', include('api.tasks.urls')), # Tasks app
path('api/v1/rbac/', include('api.rbac.urls')), # RBAC app
]
# En producción, los archivos media son servidos por Nginx
if settings.DEBUG:

View File

@@ -1,100 +1,244 @@
# permissions.py
from rest_framework import permissions
from api.cuser.models import CustomUser
from rest_framework.exceptions import PermissionDenied
from rest_framework.authentication import TokenAuthentication
class IsSameOrganization(permissions.BasePermission):
"""
Permiso personalizado que solo permite acceder a usuarios de la misma organización
o a administradores/staff.
"""
def has_permission(self, request, view):
# Permite listar/crear solo si el usuario está autenticado
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Permite operaciones sobre un objeto específico solo si:
# - El objeto pertenece a la misma organización (acceso por usuario relacionado)
return (getattr(obj, 'dirigido', None) and obj.dirigido.organizacion == request.user.organizacion)
class IsSameOrganizationAndAdmin(permissions.BasePermission):
"""
Permiso personalizado que solo permite acceder a usuarios de la misma organización
o a administradores/staff.
"""
def has_permission(self, request, view):
# Permite listar/crear solo si el usuario está autenticado
return request.user.is_authenticated
# ---------------------------------------------------------------------------
# Helpers centrales — toda la lógica de RBAC pasa por aquí
# ---------------------------------------------------------------------------
def has_object_permission(self, request, view, obj):
# Permite operaciones solo si el usuario es admin, Agente Aduanal o user y la organización coincide
allowed_groups = ['admin', 'Agente Aduanal', 'user']
user_in_group = request.user.groups.filter(name__in=allowed_groups).exists()
if not user_in_group:
return False
if hasattr(obj, 'organizacion'):
return obj.organizacion == request.user.organizacion
def is_internal_service_request(request):
"""True si la petición proviene de un service account (Token auth + superuser).
Misma lógica que IsInternalService, útil en get_queryset() y perform_* methods."""
user = getattr(request, 'user', None)
if not user or not user.is_superuser:
return False
class IsSameOrganizationDeveloper(permissions.BasePermission):
"""
Permiso personalizado que solo permite acceder a usuarios de la misma organización
o a administradores/staff.
"""
def has_permission(self, request, view):
# Permite listar/crear solo si el usuario está autenticado
return request.user.is_authenticated
return isinstance(getattr(request, 'successful_authenticator', None), TokenAuthentication)
def has_object_permission(self, request, view, obj):
# Permite operaciones solo si el usuario es developer, Agente Aduanal o user y la organización coincide
allowed_groups = ['developer', 'Agente Aduanal', 'user']
user_in_group = request.user.groups.filter(name__in=allowed_groups).exists()
if not user_in_group:
return False
if hasattr(obj, 'organizacion'):
return obj.organizacion == request.user.organizacion
def get_org_context(user):
"""Retorna la organización activa para filtrado de datos.
Superusuarios usan active_organization; usuarios normales usan organizacion."""
if user.is_superuser:
return getattr(user, 'active_organization', None)
return getattr(user, 'organizacion', None)
def user_has_permission(user, codename):
"""Verifica si un usuario tiene un permiso RBAC por su codename.
Orden de evaluación:
1. is_superuser → True siempre
2. UserPermission deny explícito → False
3. UserPermission grant explícito → True
4. Algún UserRole en su org tiene el permiso → True
5. Denegar
"""
if user.is_superuser:
return True
org = getattr(user, 'organizacion', None)
if not org:
return False
class IsOwnerOrOrgAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (
obj == request.user or
request.user.is_staff or
request.user.groups.filter(name='admin').exists()
)
class IsSuperUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_superuser
class HasStoragePermission(permissions.BasePermission):
"""
Permiso personalizado que permite el acceso a los usuarios que tienen permisos de almacenamiento.
"""
from api.rbac.models import UserPermission, UserRole
try:
override = UserPermission.objects.get(user=user, permission__codename=codename)
return override.granted
except UserPermission.DoesNotExist:
pass
return UserRole.objects.filter(
user=user,
role__organizacion=org,
role__permissions__codename=codename,
).exists()
def user_has_role(user, role_name):
"""Verifica si un usuario tiene un rol por nombre dentro de su organización.
Función puente durante la transición — lee desde UserRole en lugar de auth.Group."""
from api.rbac.models import UserRole
org = getattr(user, 'organizacion', None)
if not org:
return False
return UserRole.objects.filter(
user=user,
role__nombre=role_name,
role__organizacion=org,
).exists()
# ---------------------------------------------------------------------------
# Base compartida — aplica el requisito de org activa a superusuarios
# ---------------------------------------------------------------------------
class OrgScopedPermission(permissions.BasePermission):
"""Base para todas las clases de permiso con scope de organización.
Superusuario sin active_organization recibe 403, EXCEPTO service accounts
(Token auth + superuser) que pasan sin restricción de org."""
message = 'No tienes permiso para realizar esta acción.'
def has_permission(self, request, view):
# Permite el acceso si el usuario tiene el permiso 'can_access_storage'
return request.user.has_perm('api.cuser.can_access_storage')
if not request.user.is_authenticated:
return False
if request.user.is_superuser:
from rest_framework.authentication import TokenAuthentication
# Service account interno: Token auth + superuser → siempre permitido
if isinstance(getattr(request, 'successful_authenticator', None), TokenAuthentication):
return True
# Superuser JWT: requiere active_organization
if not getattr(request.user, 'active_organization', None):
return False
return True
# ---------------------------------------------------------------------------
# Clases de permiso
# ---------------------------------------------------------------------------
class IsSameOrganization(OrgScopedPermission):
"""Usuario autenticado con org activa. Cualquier rol pasa (incluyendo Importador)."""
def has_object_permission(self, request, view, obj):
# Permite operaciones sobre un objeto específico si el usuario tiene el permiso
return request.user.has_perm('api.cuser.can_access_storage')
org = get_org_context(request.user)
if not org:
return False
dirigido = getattr(obj, 'dirigido', None)
if dirigido:
return getattr(dirigido, 'organizacion', None) == org
return getattr(obj, 'organizacion', None) == org
class IsSameOrganizationAndInAllowedGroups(permissions.BasePermission):
"""
Permite update/delete solo si el usuario está en TODOS los grupos permitidos
y pertenece a la misma organización que el registro, o es superuser.
"""
allowed_groups = ['admin', 'Agente Aduanal', 'user']
class IsSameOrganizationAndAdmin(OrgScopedPermission):
"""Usuario con rol admin, Agente Aduanal o user en su organización."""
def has_object_permission(self, request, view, obj):
user = request.user
if not user.is_authenticated:
return False
if user.is_superuser:
return True
if not hasattr(user, 'organizacion') or not user.organizacion:
org = get_org_context(user)
if not org:
return False
# Debe tener los tres grupos asignados
for group in self.allowed_groups:
if not user.groups.filter(name=group).exists():
tiene_rol = (
user_has_role(user, 'admin') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
)
if not tiene_rol:
return False
return getattr(obj, 'organizacion', None) == org
class IsSameOrganizationDeveloper(OrgScopedPermission):
"""Usuario con rol developer, Agente Aduanal o user en su organización."""
def has_object_permission(self, request, view, obj):
user = request.user
if user.is_superuser:
return True
org = get_org_context(user)
if not org:
return False
tiene_rol = (
user_has_role(user, 'developer') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
)
if not tiene_rol:
return False
return getattr(obj, 'organizacion', None) == org
class IsOwnerOrOrgAdmin(OrgScopedPermission):
"""El propio usuario, staff de Django o usuario con rol admin en la org."""
def has_object_permission(self, request, view, obj):
user = request.user
return (
obj == user or
user.is_staff or
user.is_superuser or
user_has_role(user, 'admin')
)
class IsSuperUser(permissions.BasePermission):
"""Solo superusuarios de Django. No requiere org activa (para endpoints de gestión global)."""
message = 'No tienes permiso para realizar esta acción.'
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_superuser
def has_object_permission(self, request, view, obj):
return request.user.is_superuser
class IsInternalService(permissions.BasePermission):
"""
Identifica llamadas internas de microservicio → backend.
Criterio: autenticación via Token (no JWT) + usuario superuser.
Esto garantiza que solo cuentas de servicio predefinidas pasan,
sin depender de flags manuales como is_staff que pueden no estar
configurados en producción.
"""
message = 'Acceso reservado para servicios internos.'
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
from rest_framework.authentication import TokenAuthentication
return (
isinstance(request.successful_authenticator, TokenAuthentication)
and request.user.is_superuser
)
class HasStoragePermission(OrgScopedPermission):
"""Usuarios con acceso a operaciones de almacenamiento (organizacion.view)."""
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
return user_has_permission(request.user, 'organizacion.view')
def has_object_permission(self, request, view, obj):
return user_has_permission(request.user, 'organizacion.view')
def require_permission(codename):
"""
Devuelve una clase de permiso DRF que exige el codename RBAC indicado.
Uso en permission_classes: require_permission('pedimentos.view')
Uso en get_permissions(): require_permission('pedimentos.create')()
"""
class _RbacPerm(OrgScopedPermission):
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
return obj.organizacion == user.organizacion
return user_has_permission(request.user, codename)
_RbacPerm.__name__ = f'HasPerm_{codename.replace(".", "_")}'
_RbacPerm.__qualname__ = _RbacPerm.__name__
return _RbacPerm
class IsSameOrganizationAndInAllowedGroups(OrgScopedPermission):
"""Usuario con permiso vucem.manage en su organización.
Reemplaza la lógica rota que requería 3 grupos simultáneamente."""
def has_object_permission(self, request, view, obj):
user = request.user
if user.is_superuser:
return True
org = get_org_context(user)
if not org:
return False
if not user_has_permission(user, 'vucem.manage'):
return False
return getattr(obj, 'organizacion', None) == org

View File

@@ -1,142 +1,179 @@
import logging
from core.permissions import get_org_context, user_has_role, is_internal_service_request
logger = logging.getLogger(__name__)
def _is_internal_service(request):
return is_internal_service_request(request)
class FiltroPorOrganizacionMixin:
model = None
campo_usuario = 'user'
campo_organizacion = 'organizacion'
campo_rfc = 'rfc__id'
campo_contribuyente = 'pedimento__contribuyente' # solo si aplica
campo_contribuyente = 'pedimento__contribuyente'
def get_queryset_filtrado(self):
user = self.request.user
if not user.is_authenticated or not hasattr(user, self.campo_organizacion):
if not user.is_authenticated:
return self.model.objects.none()
if user.is_superuser:
if _is_internal_service(self.request):
return self.model.objects.all()
if (user.groups.filter(name='admin').exists() or user.groups.filter(name='developer').exists()) and user.is_authenticated and user.groups.filter(name='Agente Aduanal').exists():
model_fields = [f.name for f in self.model._meta.get_fields()]
if self.campo_organizacion in model_fields:
filtro = {f"{self.campo_organizacion}": getattr(user, self.campo_organizacion)}
else:
return self.model.objects.none()
org = get_org_context(user)
if not org:
return self.model.objects.none()
filtro = {self.campo_organizacion: org}
# Superuser y usuarios con rol operativo ven todo lo de su org activa
if user.is_superuser:
return self.model.objects.filter(**filtro)
if user.groups.filter(name='Importador').exists() and getattr(user, 'is_importador', False):
filtro = {
f"{self.campo_contribuyente}__{self.campo_rfc}": getattr(user, self.campo_rfc),
}
if (
user_has_role(user, 'admin') or
user_has_role(user, 'developer') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
):
return self.model.objects.filter(**filtro)
# Importador: acceso filtrado por org + RFC como contribuyente
if user.is_importador:
filtro[f"{self.campo_contribuyente}__in"] = user.rfc.all()
return self.model.objects.filter(**filtro)
return self.model.objects.none()
# en core/mixins/organizacion.py o similar
class OrganizacionFiltradaMixin:
model = None # Puedes sobreescribir esto en la vista
model = None
campo_organizacion = 'organizacion'
campo_contribuyente = 'contribuyente' # solo si aplica
campo_contribuyente = 'contribuyente'
def get_queryset_filtrado_por_organizacion(self):
model = self.model or self.queryset.model
user = self.request.user
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
if not user.is_authenticated:
return model.objects.none()
if self.request.user.is_superuser:
if _is_internal_service(self.request):
return model.objects.all()
org = self.request.user.organizacion
org = get_org_context(user)
if not org:
return model.objects.none()
filtros_base = {
f"{self.campo_organizacion}": org,
f"{self.campo_organizacion}__is_active": True,
f"{self.campo_organizacion}__is_verified": True,
self.campo_organizacion: org,
f'{self.campo_organizacion}__is_active': True,
f'{self.campo_organizacion}__is_verified': True,
}
grupos = self.request.user.groups.values_list('name', flat=True)
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and (('admin' in grupos or 'developer' in grupos) and 'user' in grupos) :
if 'Agente Aduanal' in grupos:
return model.objects.filter(**filtros_base)
# if hasattr(model, self.campo_contribuyente):
if self.request.user.is_authenticated and 'Importador' in grupos:
filtros_base[f"{self.campo_contribuyente}__in"] = self.request.user.rfc.all()
if user.is_superuser:
return model.objects.filter(**filtros_base)
if (
user_has_role(user, 'admin') or
user_has_role(user, 'developer') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
):
return model.objects.filter(**filtros_base)
if user.is_importador:
filtros_base[f'{self.campo_contribuyente}__in'] = user.rfc.all()
return model.objects.filter(**filtros_base)
# Si no entra en los roles válidos
return model.objects.none()
class DocumentosFiltradosMixin:
model = None
campo_organizacion = 'organizacion'
campo_contribuyente = 'pedimento' # solo si aplica
campo_contribuyente = 'pedimento'
def get_queryset_filtrado_por_organizacion(self):
model = self.model or self.queryset.model
user = self.request.user
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
if not user.is_authenticated:
return model.objects.none()
if self.request.user.is_superuser:
if _is_internal_service(self.request):
return model.objects.all()
org = self.request.user.organizacion
org = get_org_context(user)
if not org:
return model.objects.none()
filtros_base = {
f"{self.campo_organizacion}": org.id,
f"{self.campo_organizacion}__is_active": True,
f"{self.campo_organizacion}__is_verified": True,
f'{self.campo_organizacion}': org.id,
f'{self.campo_organizacion}__is_active': True,
f'{self.campo_organizacion}__is_verified': True,
}
grupos = self.request.user.groups.values_list('name', flat=True)
if user.is_superuser:
return model.objects.filter(**filtros_base)
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and ('admin' in grupos or 'developer' in grupos or 'user' in grupos):
if 'Agente Aduanal' in grupos:
return model.objects.filter(**filtros_base)
if hasattr(model, self.campo_contribuyente):
if self.request.user.is_authenticated and 'Importador' in grupos and getattr(self.request.user, 'is_importador', False):
filtros_base[f"{self.campo_contribuyente}__contribuyente__in"] = self.request.user.rfc.all()
return model.objects.filter(**filtros_base)
if (
user_has_role(user, 'admin') or
user_has_role(user, 'developer') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
):
return model.objects.filter(**filtros_base)
if user.is_importador:
filtros_base[f'{self.campo_contribuyente}__contribuyente__in'] = user.rfc.all()
return model.objects.filter(**filtros_base)
# Si no entra en los roles válidos
return model.objects.none()
class ProcesosPorOrganizacionMixin:
model = None # Puedes sobreescribir esto en la vista
model = None
campo_organizacion = 'organizacion'
campo_pedimento = 'pedimento' # solo si aplica
campo_pedimento = 'pedimento'
def get_queryset_filtrado_por_organizacion(self):
model = self.model or self.queryset.model
user = self.request.user
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
if not user.is_authenticated:
return model.objects.none()
if self.request.user.is_superuser:
if _is_internal_service(self.request):
return model.objects.all()
org = self.request.user.organizacion
org = get_org_context(user)
if not org:
return model.objects.none()
filtros_base = {
f"{self.campo_organizacion}": org,
f"{self.campo_organizacion}__is_active": True,
f"{self.campo_organizacion}__is_verified": True,
self.campo_organizacion: org,
f'{self.campo_organizacion}__is_active': True,
f'{self.campo_organizacion}__is_verified': True,
}
grupos = self.request.user.groups.values_list('name', flat=True)
if user.is_superuser:
return model.objects.filter(**filtros_base)
if (
user_has_role(user, 'admin') or
user_has_role(user, 'developer') or
user_has_role(user, 'Agente Aduanal') or
user_has_role(user, 'user')
):
return model.objects.filter(**filtros_base)
if self.request.user.is_authenticated and 'Agente Aduanal' in grupos and ('admin' in grupos or 'developer' in grupos or 'user' in grupos) :
if 'Agente Aduanal' in grupos:
return model.objects.filter(**filtros_base)
if hasattr(model, self.campo_pedimento):
if self.request.user.is_authenticated and 'Importador' in grupos and getattr(self.request.user, 'is_importador', False):
filtros_base[f"{self.campo_pedimento}__contribuyente__in"] = self.request.user.rfc.all()
return model.objects.filter(**filtros_base)
if user.is_importador:
filtros_base[f'{self.campo_pedimento}__contribuyente__in'] = user.rfc.all()
return model.objects.filter(**filtros_base)
# Si no entra en los roles válidos
return model.objects.none()