El problema
Zorg Intelligence es software de administración para cuidadores autónomos y pequeños proveedores de cuidados en Países Bajos. Todas las organizaciones comparten una aplicación y una base de datos, con datos sensibles: expedientes, planes de cuidados y el número de identificación ciudadana (BSN) de clientes y empleados.
De ahí salen tres requisitos que no pueden depender del código de la aplicación. Una organización nunca debe ver filas de otra. Un BSN no puede estar en la base de datos como texto plano. Y hay que poder responder después a la pregunta “¿quién abrió este expediente?”.
El enfoque
La base es Next.js con Supabase (Postgres, Auth, Row Level Security y Storage). Las reglas están en la base de datos, así que un listado, una búsqueda, un informe y una exportación CSV pasan por la misma puerta.
Row Level Security. Todas las tablas del esquema público tienen RLS activado, y cada fila pertenece exactamente a una organización. Un usuario se vincula a una organización mediante una pertenencia con uno de cuatro roles: propietario, administrador, empleado o lector. Las políticas llaman a unas pocas funciones auxiliares (si el usuario es miembro, si tiene alguno de estos roles, si puede escribir) que leen las pertenencias del usuario conectado. Un lector no escribe nada.
Detalles importantes:
- Las claves foráneas hacia una tabla con columna de organización son compuestas, así que una referencia no puede apuntar a una fila de otra organización.
- En las políticas, la llamada que devuelve el usuario actual va dentro de una subconsulta. Postgres la calcula así una vez por consulta y no una vez por fila.
- Una organización puede activar la restricción por cartera de clientes. Empleados y lectores ven entonces solo los clientes que tienen asignados. Como vive en RLS, se aplica a cada pantalla y exportación sin código adicional.
- El operador de la plataforma (Roosterspecialisten) no ve datos asistenciales. El acceso de soporte solo es posible con el consentimiento temporal de la propia organización. Esa sesión es de solo lectura, limitada en el tiempo y registrada.
Para que esto no se erosione, el repositorio guarda una huella de todas las políticas y funciones con un test que compara contra ella, además de una prueba de aislamiento repetible con 15 escenarios.
BSN cifrado. El BSN se cifra por campo con pgcrypto. La clave viene de Supabase Vault y no está en el código ni en git. Un trigger cifra el valor al insertar y actualizar, conserva las cuatro últimas cifras para mostrarlo enmascarado y vacía la columna de texto plano. Para el número completo, la aplicación tiene que llamar a una función que comprueba la pertenencia, escribe primero la línea de acceso y solo entonces descifra.
Registro de accesos. La primera versión escribía una línea de “consulta” en el registro general de cambios. Ahora es una tabla propia con la hora (de la base de datos), la organización, la persona, el rol en ese momento, el cliente, el tipo de objeto, la acción, una cantidad en exportaciones y búsquedas, y la sesión. Nunca contiene texto libre, términos de búsqueda, nombres de archivo ni direcciones IP.
Solo hay una entrada: una función que comprueba si quien llama puede ver el registro y deduce la organización y el cliente del propio registro, no de sus parámetros. Las líneas no se pueden modificar ni borrar, tampoco con el rol de servicio. La única excepción es la limpieza tras 5 años de conservación. El registro falla en cerrado: si una línea no se puede escribir, la aplicación no muestra nada, y con un BSN, una descarga o una exportación el servidor solo devuelve datos cuando la línea ya existe.
Las concesiones
- La aplicación registra, la base de datos comprueba. Quien lea directamente la API de la base de datos con su propio token recibe solo las filas que RLS permite, pero no deja línea de acceso. El BSN es la excepción, porque siempre pasa por la función de la base de datos. La lista de clientes (los nombres) no se registra.
- Todavía no hay copia del registro fuera de la base de datos.
- Las funciones auxiliares se ejecutan con privilegios elevados; si no, una política no podría leer las pertenencias. Cada una es pequeña y figura en la huella.
- La verificación en dos pasos es opcional por defecto. Cualquier usuario puede activar una app de autenticación, y un propietario puede hacerla obligatoria. Cuando es obligatoria, las sesiones de soporte no son posibles.
- Sin certificado. Son medidas técnicas. El producto no tiene certificación NEN 7510 y falta una prueba de penetración externa.
- Las políticas en cada tabla cuestan tiempo en cada migración: cada tabla nueva necesita políticas, una ejecución de prueba y una huella actualizada.
Hay más sobre el producto en la página del proyecto Zorg Intelligence.