feature/rbac permisos y roles implementados
This commit is contained in:
0
api/rbac/__init__.py
Normal file
0
api/rbac/__init__.py
Normal file
99
api/rbac/admin.py
Normal file
99
api/rbac/admin.py
Normal 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
8
api/rbac/apps.py
Normal 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'
|
||||
0
api/rbac/management/__init__.py
Normal file
0
api/rbac/management/__init__.py
Normal file
0
api/rbac/management/commands/__init__.py
Normal file
0
api/rbac/management/commands/__init__.py
Normal file
101
api/rbac/management/commands/sync_rbac.py
Normal file
101
api/rbac/management/commands/sync_rbac.py
Normal 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}')
|
||||
116
api/rbac/migrations/0001_initial.py
Normal file
116
api/rbac/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
88
api/rbac/migrations/0002_data_permissions.py
Normal file
88
api/rbac/migrations/0002_data_permissions.py
Normal 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),
|
||||
]
|
||||
56
api/rbac/migrations/0003_notificaciones_receive.py
Normal file
56
api/rbac/migrations/0003_notificaciones_receive.py
Normal 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),
|
||||
]
|
||||
57
api/rbac/migrations/0004_auditoria_permissions.py
Normal file
57
api/rbac/migrations/0004_auditoria_permissions.py
Normal 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),
|
||||
]
|
||||
0
api/rbac/migrations/__init__.py
Normal file
0
api/rbac/migrations/__init__.py
Normal file
109
api/rbac/models.py
Normal file
109
api/rbac/models.py
Normal 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
176
api/rbac/roles.py
Normal 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
105
api/rbac/serializers.py
Normal 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
23
api/rbac/urls.py
Normal 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
412
api/rbac/views.py
Normal 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.'})
|
||||
Reference in New Issue
Block a user