Permisos jerárquicos en una tabla de jerarquía almacenada

9

Asumiendo la siguiente estructura de base de datos (modificable si es necesario) ...

ingrese la descripción de la imagen aquí

Estoy buscando una buena manera de determinar los "permisos efectivos" para un usuario determinado en una página determinada de manera que me permita devolver una fila que contenga la Página y los permisos efectivos.

Estoy pensando que la solución ideal puede incluir una función que use un CTE para realizar la recursión necesaria para evaluar los "permisos efectivos" para una fila de página dada para el usuario actual.

Antecedentes y detalles de implementación

El esquema anterior representa un punto de partida para un sistema de gestión de contenido en el que los usuarios pueden obtener permisos mediante la adición y eliminación de roles.

Los recursos en el sistema (p. Ej. Páginas) están asociados con roles para otorgar al grupo de usuarios vinculados a ese rol los permisos que otorga.

La idea es poder bloquear fácilmente a un usuario simplemente haciendo que se niegue todo el rol y agregando la página de nivel raíz en el árbol a ese rol y luego agregando al usuario a ese rol.

Esto permitiría que la estructura de permisos permanezca en su lugar cuando (por ejemplo) un contratista que trabaja para la empresa no esté disponible durante largos períodos, esto también permitirá la misma concesión de sus permisos originales simplemente quitando al usuario de ese rol .

Los permisos se basan en reglas de tipo ACL típicas que podrían aplicarse al sistema de archivos siguiendo estas reglas.

Los permisos CRUD deben ser bits anulables para que los valores disponibles sean verdaderos, falsos, no definidos donde lo siguiente es verdadero:

  • falso + cualquier cosa = falso
  • verdadero + no definido = verdadero
  • verdadero + verdadero = verdadero
  • no definido + no definido = no definido
Si alguno de los permisos es falso -> falso 
De lo contrario, si alguno es cierto -> verdadero
De lo contrario (todo no definido) -> falso

En otras palabras, no obtiene permisos sobre nada a menos que se les otorgue a través de la membresía de roles y una regla de denegación anule una regla de permiso.

El "conjunto" de permisos a los que esto se aplica es todos los permisos aplicados al árbol hasta la página actual, incluida la página actual, en otras palabras: si un falso tiene algún rol aplicado a cualquier página del árbol de esta página, entonces el resultado es falso , pero si el árbol completo hasta aquí no está definido, entonces la página actual contiene una regla verdadera, el resultado es verdadero aquí pero sería falso para el padre.

Me gustaría mantener libremente la estructura de db si es posible, también tenga en cuenta que mi objetivo aquí es poder hacer algo como: select * from pages where effective permissions (read = true) and user = ?entonces cualquier solución debería permitirme tener un conjunto consultable con los permisos efectivos en ellos de alguna manera (devolverlos es opcional siempre que se puedan especificar los criterios).

Suponiendo que existen 2 páginas donde 1 es hijo del otro y existen 2 roles, uno para usuarios administradores y 1 para usuarios de solo lectura, ambos están vinculados solo a la página de nivel raíz. Espero ver algo como esto como resultado esperado:

Admin user:
Id, Parent, Name, Create, Read, Update, Delete
1,  null,   Root, True  , True, True  , True 
2,  1,      Child,True  , True, True  , True 

Read only user:
Id, Parent, Name, Create, Read, Update, Delete
1,  null,   Root, False , True, False , False 
2,  1,      Child,False , True, False , False

Puede encontrar más discusión sobre esta pregunta en la sala de chat del sitio principal a partir de aquí .

Guerra
fuente

Respuestas:

11

Con este modelo, se me ocurrió una forma de consultar la tabla de páginas de la siguiente manera:

SELECT
  p.*
FROM
  dbo.Pages AS p
  CROSS APPLY dbo.GetPermissionStatus(p.Id, @CurrentUserId, @PermissionName) AS ps
WHERE
  ps.IsAllowed = 1
;

El resultado de la función con valores de tabla en línea GetPermissionStatus puede ser un conjunto vacío o una fila de una sola columna. Cuando el conjunto de resultados está vacío, eso significa que no hay entradas no NULL para la combinación de página / usuario / permiso especificada. La fila de páginas correspondiente se filtra automáticamente.

Si la función devuelve una fila, entonces su única columna ( IsAllowed ) contendrá 1 (que significa verdadero ) o 0 (que significa falso ). El filtro WHERE además verifica que el valor debe ser 1 para que la fila se incluya en la salida.

Qué hace la función:

  • recorre la tabla Páginas por la jerarquía para recopilar la página especificada y todos sus padres en un conjunto de filas;

  • crea otro conjunto de filas que contiene todos los roles en los que se incluye el usuario especificado, junto con una de las columnas de permisos (pero solo valores no NULL), específicamente el correspondiente al permiso especificado como el tercer argumento;

  • finalmente, se une al primer y segundo conjunto a través de la tabla RolePages para encontrar el conjunto completo de permisos explícitos que coinciden con la página especificada o con cualquiera de sus padres.

El conjunto de filas resultante se ordena en el orden ascendente de los valores de permiso y el valor superior se devuelve como resultado de la función. Como los valores nulos se filtran en una etapa anterior, la lista puede contener solo 0s y 1s. Por lo tanto, si hay al menos un "negar" (0) en la lista de permisos, ese será el resultado de la función. De lo contrario, el resultado superior será 1, a menos que los roles correspondientes a las páginas seleccionadas no tengan "permisos" explícitos o simplemente no haya entradas coincidentes para la página y el usuario especificados, en cuyo caso el resultado será un vacío conjunto de filas

Esta es la función:

CREATE FUNCTION dbo.GetPermissionStatus
(
  @PageId int,
  @UserId int,
  @PermissionName varchar(50)
)
RETURNS TABLE
AS
RETURN
(
  WITH
    Hierarchy AS
    (
      SELECT
        p.Id,
        p.ParentId
      FROM
        dbo.Pages AS p
      WHERE
        p.Id = @PageId

      UNION ALL

      SELECT
        p.Id,
        p.ParentId
      FROM
        dbo.Pages AS p
        INNER JOIN hierarchy AS h ON p.Id = h.ParentId
    ),
    Permissions AS
    (
      SELECT
        ur.Role_Id,
        x.IsAllowed
      FROM
        dbo.UserRoles AS ur
        INNER JOIN Roles AS r ON ur.Role_Id = r.Id
        CROSS APPLY
        (
          SELECT
            CASE @PermissionName
              WHEN 'Create' THEN [Create]
              WHEN 'Read'   THEN [Read]
              WHEN 'Update' THEN [Update]
              WHEN 'Delete' THEN [Delete]
            END
        ) AS x (IsAllowed)
      WHERE
        ur.User_Id = @UserId AND
        x.IsAllowed IS NOT NULL
    )
  SELECT TOP (1)
    perm.IsAllowed
  FROM
    Hierarchy AS h
    INNER JOIN dbo.RolePages AS rp ON h.Id = rp.Page_Id
    INNER JOIN Permissions AS perm ON rp.Role_Id = perm.Role_Id
  ORDER BY
    perm.IsAllowed ASC
);

Caso de prueba

  • DDL:

    CREATE TABLE dbo.Users (
      Id       int          PRIMARY KEY,
      Name     varchar(50)  NOT NULL,
      Email    varchar(100)
    );
    
    CREATE TABLE dbo.Roles (
      Id       int          PRIMARY KEY,
      Name     varchar(50)  NOT NULL,
      [Create] bit,
      [Read]   bit,
      [Update] bit,
      [Delete] bit
    );
    
    CREATE TABLE dbo.Pages (
      Id       int          PRIMARY KEY,
      ParentId int          FOREIGN KEY REFERENCES dbo.Pages (Id),
      Name     varchar(50)  NOT NULL
    );
    
    CREATE TABLE dbo.UserRoles (
      User_Id  int          NOT NULL  FOREIGN KEY REFERENCES dbo.Users (Id),
      Role_Id  int          NOT NULL  FOREIGN KEY REFERENCES dbo.Roles (Id),
      PRIMARY KEY (User_Id, Role_Id)
    );
    
    CREATE TABLE dbo.RolePages (
      Role_Id  int          NOT NULL  FOREIGN KEY REFERENCES dbo.Roles (Id),
      Page_Id  int          NOT NULL  FOREIGN KEY REFERENCES dbo.Pages (Id),
      PRIMARY KEY (Role_Id, Page_Id)
    );
    GO
  • Inserciones de datos:

    INSERT INTO
      dbo.Users (ID, Name)
    VALUES
      (1, 'User A')
    ;
    INSERT INTO
      dbo.Roles (ID, Name, [Create], [Read], [Update], [Delete])
    VALUES
      (1, 'Role R', NULL, 1, 1, NULL),
      (2, 'Role S', 1   , 1, 0, NULL)
    ;
    INSERT INTO
      dbo.Pages (Id, ParentId, Name)
    VALUES
      (1, NULL, 'Page 1'),
      (2, 1, 'Page 1.1'),
      (3, 1, 'Page 1.2')
    ;
    INSERT INTO
      dbo.UserRoles (User_Id, Role_Id)
    VALUES
      (1, 1),
      (1, 2)
    ;
    INSERT INTO
      dbo.RolePages (Role_Id, Page_Id)
    VALUES
      (1, 1),
      (2, 3)
    ;
    GO

    Por lo tanto, solo se usa un usuario, pero se asigna a dos roles, con varias combinaciones de valores de permisos entre los dos roles para probar la lógica de fusión en los objetos secundarios.

    La jerarquía de la página es muy simple: un padre, dos hijos. El padre está asociado con un rol, uno de los hijos con el otro rol.

  • Script de prueba:

    DECLARE @CurrentUserId int = 1;
    SELECT p.* FROM dbo.Pages AS p CROSS APPLY dbo.GetPermissionStatus(p.Id, @CurrentUserId, 'Create') AS perm WHERE perm.IsAllowed = 1;
    SELECT p.* FROM dbo.Pages AS p CROSS APPLY dbo.GetPermissionStatus(p.Id, @CurrentUserId, 'Read'  ) AS perm WHERE perm.IsAllowed = 1;
    SELECT p.* FROM dbo.Pages AS p CROSS APPLY dbo.GetPermissionStatus(p.Id, @CurrentUserId, 'Update') AS perm WHERE perm.IsAllowed = 1;
    SELECT p.* FROM dbo.Pages AS p CROSS APPLY dbo.GetPermissionStatus(p.Id, @CurrentUserId, 'Delete') AS perm WHERE perm.IsAllowed = 1;
  • Limpiar:

    DROP FUNCTION dbo.GetPermissionStatus;
    GO
    DROP TABLE dbo.UserRoles, dbo.RolePages, dbo.Users, dbo.Roles, dbo.Pages;
    GO

Resultados

  • para crear :

    Id  ParentId  Name
    --  --------  --------
    2   1         Page 1.1

    Había una verdad explícita por Page 1.1solo. La página se devolvió de acuerdo con la lógica "verdadero + no definido". Los otros estaban "no definidos" y "no definidos + no definidos", por lo tanto, excluidos.

  • para leer :

    Id  ParentId  Name
    --  --------  --------
    1   NULL      Page 1
    2   1         Page 1.1
    3   1         Page 1.2

    Se encontró un verdadero explícito en la configuración para Page 1y para Page 1.1. Por lo tanto, para el primero era solo un "verdadero", mientras que para el segundo "verdadero + verdadero". No había permisos de lectura explícitos para Page 1.2, por lo que era otro caso "verdadero + no definido". Entonces, las tres páginas fueron devueltas.

  • para la actualización :

    Id  ParentId  Name
    --  --------  --------
    1   NULL      Page 1
    3   1         Page 1.2

    Desde la configuración, se devolvió un verdadero explícito Page 1y un falso para Page 1.1. Para las páginas que llegaron a la salida, la lógica era la misma que en el caso de Leer . Para la fila excluida se encontraron tanto falso como verdadero, por lo que funcionó la lógica "falso + cualquier cosa".

  • para Eliminar no se devolvieron filas. El padre y uno de los niños tenían nulos explícitos en la configuración y el otro niño no tenía nada.

Obtenga todos los permisos

Ahora, si solo desea devolver todos los permisos efectivos, puede adaptar la función GetPermissionStatus :

CREATE FUNCTION dbo.GetPermissions(@PageId int, @UserId int)
RETURNS TABLE
AS
RETURN
(
  WITH
    Hierarchy AS
    (
      SELECT
        p.Id,
        p.ParentId
      FROM
        dbo.Pages AS p
      WHERE
        p.Id = @PageId

      UNION ALL

      SELECT
        p.Id,
        p.ParentId
      FROM
        dbo.Pages AS p
        INNER JOIN hierarchy AS h ON p.Id = h.ParentId
    ),
    Permissions AS
    (
      SELECT
        ur.Role_Id,
        r.[Create],
        r.[Read],
        r.[Update],
        r.[Delete]
      FROM
        dbo.UserRoles AS ur
        INNER JOIN Roles AS r ON ur.Role_Id = r.Id
      WHERE
        ur.User_Id = @UserId
    )
  SELECT
    [Create] = ISNULL(CAST(MIN(CAST([Create] AS int)) AS bit), 0),
    [Read]   = ISNULL(CAST(MIN(CAST([Read]   AS int)) AS bit), 0),
    [Update] = ISNULL(CAST(MIN(CAST([Update] AS int)) AS bit), 0),
    [Delete] = ISNULL(CAST(MIN(CAST([Delete] AS int)) AS bit), 0)
  FROM
    Hierarchy AS h
    INNER JOIN dbo.RolePages AS rp ON h.Id = rp.Page_Id
    INNER JOIN Permissions AS perm ON rp.Role_Id = perm.Role_Id
);

La función devuelve cuatro columnas: los permisos efectivos para la página y el usuario especificados. Ejemplo de uso:

DECLARE @CurrentUserId int = 1;
SELECT
  *
FROM
  dbo.Pages AS p
  CROSS APPLY dbo.GetPermissions(p.Id, @CurrentUserId) AS perm
;

Salida:

Id  ParentId  Name      Create Read  Update Delete
--  --------  --------  ------ ----- ------ ------
1   NULL      Page 1    0      1     1      0
2   1         Page 1.1  1      1     0      0
3   1         Page 1.2  0      1     1      0
Andriy M
fuente