¿Cómo implementar permisos de lógica de negocios en PostgreSQL (o SQL en general)?

16

Supongamos que tengo una tabla de elementos:

CREATE TABLE items
(
    item serial PRIMARY KEY,
    ...
);

Ahora, quiero presentar el concepto de "permisos" para cada elemento (tenga en cuenta que no estoy hablando de permisos de acceso a la base de datos aquí, sino de permisos de lógica de negocios para ese elemento). Cada elemento tiene permisos predeterminados y también permisos por usuario que pueden anular los permisos predeterminados.

Traté de pensar en varias formas de implementar esto y se me ocurrieron las siguientes soluciones, pero no estoy seguro de cuál es la mejor y por qué:

1) La solución booleana

Use una columna booleana para cada permiso:

CREATE TABLE items
(
    item serial PRIMARY KEY,

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),

    PRIMARY KEY(item, user),

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

Ventajas : se nombra cada permiso.

Desventajas : hay docenas de permisos que aumentan significativamente el número de columnas y debe definirlas dos veces (una en cada tabla).

2) La solución entera

Use un número entero y trátelo como un campo de bits (es decir, el bit 0 es para can_change_description, el bit 1 es para can_change_price, y así sucesivamente, y use operaciones bit a bit para establecer o leer permisos).

CREATE DOMAIN permissions AS integer;

Ventajas : muy rápido.

Desventajas : debe realizar un seguimiento de qué bit representa qué permiso tanto en la base de datos como en la interfaz front-end.

3) La solución de Bitfield

Igual que 2), pero uso bit(n). Lo más probable es que tengan las mismas ventajas y desventajas, quizás un poco más lento.

4) La solución Enum

Use un tipo de enumeración para los permisos:

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

y luego cree una tabla adicional para los permisos predeterminados:

CREATE TABLE item_default_permissions
(
    item int NOT NULL REFERENCES items(item),
    perm permission NOT NULL,

    PRIMARY KEY(item, perm)
);

y cambie la tabla de definición por usuario a:

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),
    perm permission NOT NULL,

    PRIMARY KEY(item, user, perm)    
);

Ventajas : Fácil de nombrar permisos individuales (no tiene que manejar posiciones de bits).

Desventajas : incluso cuando solo se recuperan los permisos predeterminados, se requiere acceder a dos tablas adicionales: primero, la tabla de permisos predeterminada y, en segundo lugar, el catálogo del sistema que almacena los valores de enumeración.

Especialmente porque los permisos predeterminados deben recuperarse para cada vista de página de ese elemento , el impacto en el rendimiento de la última alternativa podría ser significativo.

5) La solución de matriz de enumeración

Igual que 4), pero use una matriz para mantener todos los permisos (predeterminados):

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Ventajas : Fácil de nombrar permisos individuales (no tiene que manejar posiciones de bits).

Desventajas : rompe la primera forma normal y es un poco feo. Ocupa una cantidad considerable de bytes seguidos si la cantidad de permisos es grande (aproximadamente 50).

¿Se te ocurren otras alternativas?

¿Qué enfoque se debe tomar y por qué?

Tenga en cuenta: esta es una versión modificada de una pregunta publicada anteriormente en Stackoverflow .

JohnCand
fuente
2
Con docenas de permisos diferentes, podría elegir uno (o más) bigintcampos (cada uno válido para 64 bits) o una cadena de bits. Escribí un par de respuestas relacionadas sobre SO que podrían ser de ayuda.
Erwin Brandstetter

Respuestas:

7

Sé que no está preguntando sobre la seguridad de la base de datos per se , pero puede hacer lo que quiera con la seguridad de la base de datos. Incluso puedes usar esto en una aplicación web. Si no desea utilizar la seguridad de la base de datos, los esquemas aún se aplican.

Desea seguridad a nivel de columna, seguridad a nivel de fila y probablemente administración de roles jerárquicos. La seguridad basada en roles es mucho más fácil de administrar que la seguridad basada en usuarios.

Este código de ejemplo es para PostgreSQL 9.4, que saldrá pronto. Puede hacerlo con 9.3, pero se requiere más trabajo manual.

Desea que todo sea indexable si le preocupa el rendimiento †, que debería ser. Esto significa que los campos de máscara de bits y matriz probablemente no serán una buena idea.

En este ejemplo, mantenemos las tablas de datos principales en el dataesquema y las vistas correspondientes en public.

create schema data; --main data tables
create schema security; --acls, security triggers, default privileges

create table data.thing (
  thing_id int primary key,
  subject text not null, --or whatever
  owner name not null
);

Ponga un disparador en data.thing para inserciones y actualizaciones que obliguen a que la columna del propietario sea current_user. Quizás permita que solo el propietario elimine sus propios registros (otro desencadenante).

Cree una WITH CHECK OPTIONvista, que es lo que los usuarios realmente usarán. Intenta realmente hacer que sea actualizable, de lo contrario necesitarás disparadores / reglas, que es más trabajo.

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner,
from data.thing
where
pg_has_role(owner, 'member') --only owner or roles "above" him can view his rows. 
WITH CHECK OPTION;

A continuación, cree una tabla de lista de control de acceso:

--privileges r=read, w=write

create table security.thing_acl (
  thing_id int,
  grantee name, --the role to whom your are granting the privilege
  privilege char(1) check (privilege in ('r','w') ),

  primary key (thing_id, grantee, privilege),

  foreign key (thing_id) references data.thing(thing_id) on delete cascade
);

Cambie su vista para tener en cuenta las ACL:

drop view public.thing;

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner
from data.thing a
where
pg_has_role(owner, 'member')
or exists (select 1 from security.thing_acl b where b.thing_id = a.thing_id and pg_has_role(grantee, 'member') and privilege='r')
with check option;

Cree una tabla de privilegios de fila predeterminada:

create table security.default_row_privileges (
  table_name name,
  role_name name,
  privilege char(1),

  primary key (table_name, role_name, privilege)
);

Ponga un disparador en insertar en data.thing para que copie privilegios de fila predeterminados a security.thing_acl.

  • Ajuste la seguridad a nivel de tabla de manera adecuada (evite las inserciones de usuarios no deseados). Nadie debería poder leer los datos o los esquemas de seguridad.
  • Ajuste la seguridad a nivel de columna adecuadamente (evite que algunos usuarios vean / editen algunas columnas). Puede usar has_column_privilege () para verificar que un usuario pueda ver una columna.
  • Probablemente desee una etiqueta de seguridad definida en su vista.
  • Considerar la adición grantory admin_optioncolumnas a ACL tablas para hacer un seguimiento que ha otorgado el privilegio, y si el concesionario puede gestionar los privilegios de esa fila.
  • Lotes de prueba

† En este caso, pg_has_role probablemente no sea indexable. Debería obtener una lista de todos los roles superiores de current_user y compararlos con el valor del propietario / concesionario.

Neil McGuigan
fuente
¿Viste la parte " No estoy hablando de los permisos de acceso a la base de datos aquí "?
a_horse_with_no_name
@a_horse_with_no_name sí, lo hice. Podría escribir su propio sistema RLS / ACL, o podría usar la seguridad incorporada de una base de datos para hacer lo que está pidiendo.
Neil McGuigan el
Gracias por tu respuesta detallada! Sin embargo, no creo que usar roles de base de datos sea la respuesta correcta aquí, ya que no solo el personal, sino también todos los usuarios pueden tener permisos. Los ejemplos serían 'can_view_item', 'can_bulk_order_item' o 'can_review_item'. Creo que mi elección original de nombres de permisos lo llevó a creer que solo se trata de permisos de personal, pero todos estos nombres fueron solo ejemplos para abstraer las complejidades. Como dije en la pregunta original, se trata de permisos por usuario , no por permisos del personal .
JohnCand
De todos modos, tener roles de base de datos separados para cada fila de usuarios en la tabla de usuarios parece ser excesivo y difícil de manejar. Sin embargo, creo que su respuesta es valiosa para los desarrolladores que implementan solo permisos de personal.
JohnCand
1
@JohnCand Realmente no veo cómo es más fácil administrar los permisos en otros lugares, ¡pero por favor indíquenos su solución una vez que la haya encontrado! :)
Neil McGuigan
4

¿Ha considerado usar la extensión PostgreSQL de Access Control List ?

Contiene el tipo de datos PostgreSQL nativo ACE y un conjunto de funciones que le permiten verificar si un usuario tiene permiso para acceder a los datos. Funciona con el sistema de roles PostgreSQL o con números abstractos (o UUID) que representan los ID de usuario / rol de su aplicación.

En su caso, simplemente agregue una columna ACL a sus tablas de datos y use una de las acl_check_accessfunciones para comparar a un usuario con una ACL.

CREATE TABLE items
(
    item serial PRIMARY KEY,
    acl ace[],
    ...
);

INSERT INTO items(acl, ...) VALUES ('{a//<user id>=r, a//<role id>=rwd, ...}');

SELECT * FROM items where acl_check_access(acl, 'r', <roles of the user>, false) = 'r'

El uso de ACL es una forma extremadamente flexible de tratar con los permisos de lógica de negocios. Además, es increíblemente rápido: la sobrecarga promedio es solo el 25% del tiempo necesario para leer un registro. La única limitación es que admite un máximo de 16 permisos personalizados por tipo de objeto.

Slonopotamus
fuente
1

Puedo pensar en otra posibilidad para codificar esto, la relacional

Si no necesita la permission_per_itemmesa, puede omitirla y conectarse Permissionsy Itemsdirectamente a la item_per_user_permissionsmesa.

ingrese la descripción de la imagen aquí

diagrama de leyenda

milagro173
fuente