• saas
  • multi-tenancy
  • postgres
  • seguridad a nivel de fila
  • rls
  • función de disparador
  • arquitectura multiusuario
  • arquitectura de un solo usuario

Implementación de multiarquitectura con PostgreSQL: Aprende a través de un simple ejemplo del mundo real

Aprende cómo implementar arquitectura multiusuario con Seguridad a Nivel de Fila (RLS) de PostgreSQL y roles de base de datos a través de un ejemplo del mundo real para el aislamiento seguro de datos entre usuarios.

Yijun
Yijun
Developer

En algunos de nuestros artículos anteriores, profundizamos en el concepto de multiarquitectura y sus aplicaciones en productos y escenarios de negocios del mundo real.

En este artículo, exploraremos cómo implementar una arquitectura multiusuario para tu aplicación usando PostgreSQL desde una perspectiva técnica.

¿Qué es la arquitectura de un solo usuario?

La arquitectura de un solo usuario se refiere a una arquitectura de software donde cada cliente tiene su propia instancia dedicada de la aplicación y la base de datos.

En esta arquitectura, los datos y recursos de cada usuario están completamente aislados de otros usuarios.

Tenencia única

¿Qué es la arquitectura multiusuario?

La arquitectura multiusuario es una arquitectura de software donde múltiples clientes (usuarios) comparten la misma instancia de aplicación e infraestructura manteniendo el aislamiento de datos. En esta arquitectura, una sola instancia del software atiende a múltiples usuarios, manteniendo los datos de cada usuario separados de otros mediante varios mecanismos de aislamiento.

Multiusuario

Arquitectura de un solo usuario vs arquitectura multiusuario

La arquitectura de un solo usuario y la arquitectura multiusuario difieren en aspectos tales como el aislamiento de datos, utilización de recursos, escalabilidad, gestión y mantenimiento, y seguridad.

En la arquitectura de un solo usuario, cada cliente tiene un espacio de datos independiente, lo que conduce a una menor utilización de recursos pero relativamente más simple para la personalización. Típicamente, el software de un solo usuario se adapta a las necesidades específicas del cliente, como sistemas de inventario para un proveedor de tela en particular o una aplicación web de blog personal. La característica común entre ellos es que cada cliente ocupa una instancia separada del servicio de la aplicación, lo que facilita la personalización para cumplir con los requisitos específicos.

En una arquitectura multiusuario, múltiples usuarios comparten los mismos recursos subyacentes, resultando en una mayor utilización de recursos. Sin embargo, es crucial asegurar el aislamiento de datos y la seguridad.

La arquitectura multiusuario es a menudo la arquitectura de software preferida cuando los proveedores de servicios ofrecen servicios estandarizados a diferentes clientes. Estos servicios generalmente tienen bajos niveles de personalización, y todos los clientes comparten la misma instancia de aplicación. Cuando una aplicación requiere una actualización, actualizar una instancia de aplicación equivale a actualizar la aplicación para todos los clientes. Por ejemplo, CRM (Gestión de Relación con Clientes) es un requerimiento estandarizado. Estos sistemas suelen usar una arquitectura multiusuario para proporcionar el mismo servicio a todos los usuarios.

Estrategias de aislamiento de datos de usuarios en arquitectura multiusuario

En una arquitectura multiusuario, todos los usuarios comparten los mismos recursos subyacentes, haciendo que el aislamiento de recursos entre usuarios sea crucial. Este aislamiento no necesariamente necesita ser físico; simplemente requiere garantizar que los recursos de un usuario no sean visibles para otros.

En el diseño de la arquitectura, se pueden lograr varios grados de aislamiento de recursos entre usuarios:

Aislado a compartido

En general, cuántos más recursos se compartan entre usuarios, menor será el costo de iteración y mantenimiento del sistema. A la inversa, cuantos menos recursos se compartan, mayor será el costo.

Comenzando la implementación multiusuario con un ejemplo del mundo real

En este artículo, utilizaremos un sistema CRM como ejemplo para introducir una arquitectura multiusuario simple pero práctica.

Reconocemos que todos los usuarios utilizan los mismos servicios estándar, por lo que decidimos que todos los usuarios compartan los mismos recursos básicos, e implementaremos el aislamiento de datos entre diferentes usuarios a nivel de base de datos usando la Seguridad a Nivel de Fila de PostgreSQL.

Además, crearemos una conexión de datos separada para cada usuario para facilitar una mejor gestión de permisos de los usuarios.

A continuación, introduciremos cómo implementar esta arquitectura multiusuario.

Cómo implementar la arquitectura multiusuario con PostgreSQL

Añadir identificador de usuario para todos los recursos

En un sistema CRM, tendremos muchos recursos y estos se almacenan en diferentes tablas. Por ejemplo, la información del cliente se almacena en la tabla customers.

Antes de implementar la multiarquitectura, estos recursos no están asociados con ningún usuario:

Para diferenciar los usuarios que poseen diferentes recursos, introducimos una tabla tenants para almacenar información de usuario (donde db_user y db_user_password se utilizan para almacenar la información de conexión a base de datos para cada usuario, serán detallados a continuación). Además, añadimos un campo tenant_id a cada recurso para identificar a qué usuario pertenece:

Ahora, cada recurso está asociado con un tenant_id, lo que nos permite teóricamente añadir una cláusula where a todas las consultas para restringir el acceso a los recursos para cada usuario:

A primera vista, esto parece sencillo y factible. Sin embargo, tendrá los siguientes problemas:

  • Casi todas las consultas incluirán esta cláusula where, lo que desordenará el código y lo hará más difícil de mantener, especialmente al escribir declaraciones de unión complejas.
  • Los recién llegados al código pueden fácilmente olvidar añadir esta cláusula where.
  • Los datos entre diferentes usuarios no están realmente aislados, ya que cada usuario aún tiene permisos para acceder a datos que pertenecen a otros usuarios.

Por lo tanto, no adoptaremos este enfoque. En su lugar, usaremos la Seguridad a Nivel de Fila de PostgreSQL para abordar estas inquietudes. Sin embargo, antes de proceder, crearemos una cuenta de base de datos dedicada para que cada usuario acceda a esta base de datos compartida.

Configurar roles de base de datos para usuarios

Es una buena práctica asignar un rol de base de datos a cada usuario que puede conectarse a la base de datos. Esto permite un mejor control del acceso de cada usuario a la base de datos, facilitando el aislamiento de operaciones entre diferentes usuarios y mejorando la estabilidad y seguridad del sistema.

Como todos los usuarios tienen los mismos permisos de operación de base de datos, podemos crear un rol base para gestionar estos permisos:

Luego, para diferenciar cada rol de usuario, se asigna un rol heredado del rol base a cada usuario al momento de la creación:

A continuación, la información de conexión de base de datos para cada usuario se almacenará en la tabla tenants:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Este mecanismo proporciona a cada usuario su propio rol de base de datos, y estos roles comparten los permisos otorgados al rol crm_tenant.

Luego podemos definir el alcance de los permisos para los usuarios usando el rol crm_tenant:

  • Los usuarios deben tener acceso CRUD a todas las tablas de recursos del sistema CRM.
  • Las tablas no relacionadas con los recursos del sistema CRM deben ser invisibles para los usuarios (asumiendo solo la tabla systems).
  • Los usuarios no deben poder modificar la tabla tenants, y solo los campos id y db_user deben ser visibles para ellos al consultar su propio id de usuario al realizar operaciones de base de datos.

Una vez configurados los roles para los usuarios, cuando un usuario solicite acceso al servicio, podemos interactuar con la base de datos utilizando el rol de base de datos que representa a ese usuario:

Asegurar los datos del usuario usando Seguridad a Nivel de Fila de PostgreSQL

Hasta ahora, hemos establecido roles de base de datos correspondientes para los usuarios, pero esto no restringe el acceso a datos entre usuarios. A continuación, utilizaremos la función de Seguridad a Nivel de Fila de PostgreSQL para limitar el acceso de cada usuario solo a sus propios datos.

En PostgreSQL, las tablas pueden tener políticas de seguridad de filas que controlan qué filas pueden ser accedidas por consultas o modificadas por comandos de manipulación de datos. Esta característica también se conoce como RLS (Seguridad a Nivel de Fila).

Por defecto, las tablas no tienen políticas de seguridad de filas. Para utilizar RLS, necesitas habilitarla para la tabla y crear políticas de seguridad que se ejecuten cada vez que la tabla sea accedida.

Tomando la tabla customers en el sistema CRM como ejemplo, habilitaremos RLS y crearemos una política de seguridad para restringir que cada usuario solo tenga acceso a los datos de sus propios clientes:

En la declaración que crea la política de seguridad:

  • for all (opcional) indica que esta política de acceso se utilizará para operaciones select, insert, update, y delete en la tabla. Puedes especificar una política de acceso para operaciones específicas utilizando for seguido de la palabra clave del comando.
  • to crm_tenant indica que esta política se aplica a usuarios con el rol de base de datos crm_tenant, es decir, todos los usuarios.
  • as restrictive especifica el modo de aplicación de la política, indicando que el acceso debe estar estrictamente limitado. Por defecto, una tabla puede tener múltiples políticas, múltiples políticas permissive serán combinadas con una relación OR. En este escenario, declaramos esta política como restrictive porque deseamos que este chequeo de política sea obligatorio para los usuarios que pertenecen a usuarios del sistema CRM.
  • La expresión using define las condiciones para el acceso real, restringiendo al usuario de base de datos que realiza la consulta actualmente a solo ver datos que pertenecen a su respectivo usuario. Esta restricción se aplica a las filas seleccionadas por un comando (select, update, o delete).
  • La expresión with check define la restricción necesaria al modificar las filas de datos (insert o update), garantizando que los usuarios solo pueden añadir o actualizar registros para ellos mismos.

Usar RLS para restringir el acceso de usuarios a nuestras tablas de recursos ofrece varios beneficios:

  • Esta política efectivamente añade where tenant_id = (select id from tenants where db_user = current_user) a todas las operaciones de consulta (select, update, o delete). Por ejemplo, cuando ejecutas select * from customers, es equivalente a ejecutar select * from customers where tenant_id = (select id from tenants where db_user = current_user). Esto elimina la necesidad de añadir explícitamente condiciones where en el código de la aplicación, simplificándolo y reduciendo la probabilidad de errores.
  • Controla centralmente los permisos de acceso a datos entre diferentes usuarios a nivel de base de datos, mitigando el riesgo de vulnerabilidades o inconsistencias en la aplicación, mejorando así la seguridad del sistema.

Sin embargo, hay algunos puntos a considerar:

  • Las políticas RLS se ejecutan para cada fila de datos. Si las condiciones de consulta dentro de la política RLS son demasiado complejas, podría impactar significativamente en el rendimiento del sistema. Afortunadamente, nuestra verificación de datos de usuario es lo suficientemente simple y no afectará el rendimiento. Si planeas implementar otras funcionalidades utilizando RLS más adelante, puedes seguir las recomendaciones de rendimiento de Seguridad a Nivel de Fila de Supabase para optimizar el rendimiento de RLS.
  • Las políticas RLS no completan automáticamente tenant_id durante las operaciones insert. Solo restringen a los usuarios a insertar sus propios datos. Esto significa que al insertar datos, todavía necesitamos proporcionar el ID de usuario, lo cual es inconsistente con el proceso de consulta y puede llevar a confusiones durante el desarrollo, aumentando la probabilidad de errores (esto se abordará en los pasos siguientes).

Además de la tabla customers, necesitamos aplicar las mismas operaciones a todas las tablas de recursos del sistema CRM (este proceso puede ser un poco tedioso, pero podemos escribir un programa para configurarlo durante la inicialización de las tablas), aislando así los datos de diferentes usuarios.

Crear función de disparador para inserción de datos

Como se mencionó anteriormente, RLS (Seguridad a Nivel de Fila) nos permite ejecutar consultas sin preocuparnos por la existencia de tenant_id, ya que la base de datos se encarga de ello automáticamente. Sin embargo, para operaciones insert, todavía necesitamos especificar manualmente el tenant_id correspondiente.

Para lograr una conveniencia similar a RLS para la inserción de datos, necesitamos que la base de datos pueda manejar tenant_id automáticamente durante la inserción de datos.

Esto tiene un beneficio claro: a nivel de desarrollo de aplicaciones, ya no necesitamos considerar a qué usuario pertenecen los datos, reduciendo la probabilidad de errores y facilitando nuestra carga mental cuando desarrollamos aplicaciones multiusuario.

Afortunadamente, PostgreSQL proporciona una potente funcionalidad de disparadores.

Los disparadores son funciones especiales asociadas con tablas que ejecutan automáticamente acciones específicas (como insert, update, o delete) cuando se realizan en la tabla. Estas acciones pueden ser activadas a nivel de fila (para cada fila) o a nivel de declaración (para toda la declaración). Con los disparadores, podemos ejecutar lógica personalizada antes o después de operaciones específicas de base de datos, permitiéndonos lograr fácilmente nuestro objetivo.

Primero, creemos una función de disparador set_tenant_id que se ejecutará antes de cada inserción de datos:

A continuación, asociamos esta función de disparador con la tabla customers para las operaciones de inserción (similar a habilitar RLS para una tabla, esta función de disparador necesita ser asociada con todas las tablas relevantes):

Este disparador asegura que los datos insertados contengan el tenant_id correcto. Si los nuevos datos ya incluyen un tenant_id, la función de disparador no hace nada. De lo contrario, llena automáticamente el campo tenant_id basado en la información del usuario actual en la tabla tenants.

De esta manera, logramos el manejo automático del tenant_id a nivel de base de datos durante la inserción de datos por los usuarios.

Resumen

En este artículo, profundizamos en la aplicación práctica de la arquitectura multiusuario, usando un sistema CRM como ejemplo para demostrar una solución práctica utilizando la base de datos PostgreSQL.

Discutimos la gestión de roles de base de datos, el control de acceso, y la característica de Seguridad a Nivel de Fila de PostgreSQL para asegurar el aislamiento de datos entre usuarios. Además, utilizamos funciones disparadoras para reducir la carga cognitiva de los desarrolladores en la gestión de diferentes usuarios.

Eso es todo por este artículo. Si deseas mejorar aún más tu aplicación multiusuario con la gestión de acceso de usuarios, puedes referirte a Una guía fácil para empezar con organizaciones Logto - para construir una aplicación multiusuario para más ideas.