Roles, RLS y superadmin
Superadmin no vive en memberships, y su política de
lectura de socios nunca ganó una excepción para él — es la decisión
de seguridad más importante del proyecto.
Cómo se resuelve un rol
-
memberships(user_id, society_id, role) resuelvesociety_admin/society_staff/member. -
superadminvive enauth.jwt() -> app_metadata -> platform_role, escribible solo porservice_role— a propósito, para que nadie pueda auto-ascenderse con un simpleUPDATE. -
Funciones en
private(security definer,stable,set search_path=''):is_superadmin(),has_society_role(sid, roles[]),is_society_member(sid),my_society_ids()— siempre envueltas en(select ...)dentro de una política (evaluación única por consulta vía InitPlan, no por fila). -
Dato clave: la política
members_selectno incluyeis_superadmin(). Un superadmin autenticado como sí mismo no puede leer ninguna ficha de socio. La única vía es el modo soporte.
Dos puertas de entrada
/login (sociedad) y
/admin/login (superadmin) son Server Actions y
componentes completamente separados (iniciarSesion /
iniciarSesionAdmin), con el mismo mensaje genérico de
error en ambos. El middleware redirige a quien ya tiene sesión lejos
de la puerta equivocada, nunca hacia el destino de la otra.
2FA / MFA
-
TOTP vía Supabase Auth MFA, como módulo activable
(
mfa) con doble interruptor (global + por sociedad) — ver módulos. -
Middleware exige AAL2 para
society_admin/society_staffsi el módulo resuelve activo; para/admin/*(superadmin) es incondicional, sin interruptor de módulo. -
Códigos de recuperación: tabla
mfa_recovery_codes, 10 códigos de 8 caracteres (40 bits), guardados como SHA-256, consumo conFOR UPDATE SKIP LOCKEDpara evitar doble uso concurrente — cubre una carencia real de Supabase Auth, que no da recovery codes nativos.
Modo soporte: cómo funciona de verdad
No es un flag sobre la sesión del superadmin — es una
suplantación auténtica. Se elige así precisamente porque
members_select nunca gana una rama para
is_superadmin().
Generar el enlace
supabase.auth.admin.generateLink() (API
service_role, aislada en
src/lib/server/admin/impersonacion.ts).
Canjear al instante
verifyOtp() planta en el navegador las cookies de
la persona objetivo — de ahí en adelante, cada acción se ejecuta
bajo su RLS, no la del superadmin.
Se registra la sesión
Tabla support_sessions (motivo, inicio, caducidad).
registrar_auditoria() la consulta en cada escritura
y rellena
support_mode/reason/impersonated_by.
Caduca sola
El middleware comprueba la caducidad en cada petición; si ya
expiró, hace signOut() inmediato en vez de dejar la
sesión atrapada en un MFA imposible de completar (el superadmin
no tiene el TOTP del objetivo).
Volver a la cuenta propia
El refresh token original del superadmin viaja cifrado (mismo
AES-256-GCM que IBAN/BIC) en la cookie
sb-impersonation-return, restaurado al pulsar
"Volver a mi cuenta".
banda-modo-soporte.tsx), con motivo, hora de caducidad
y botón de retorno visibles siempre.
Superadmin: qué gestiona
/admin/sociedades
Alta de sociedades, administradores, modo soporte. No muestra fichas de socios a propósito.
/admin/usuarios
Vista unificada de gestores de todas las sociedades. Cambios de
email/nombre van por cola (job_queue), nunca en
vivo con service_role.
/admin/copias-seguridad
JSON autocontenido por sociedad, generado en segundo plano. Sin restauración todavía, solo generación y descarga.
/admin/estilos
Tipografía y colores del listado/menú por sociedad, con suelo mínimo de legibilidad validado en servidor.