Permissões hierárquicas em uma hierarquia armazenada em tabela

9

Assumindo a seguinte estrutura de banco de dados (modificável, se necessário) ...

insira a descrição da imagem aqui

Estou procurando uma boa maneira de determinar as "permissões efetivas" para um determinado usuário em uma determinada página, de maneira que eu possa retornar uma linha contendo a Página e as permissões efetivas.

Eu estou pensando que a solução ideal pode incluir uma função que usa um CTE para executar a recursão necessária para avaliar as "permissões efetivas" para uma determinada linha da página para o usuário atual.

Detalhes de plano de fundo e implementação

O esquema acima representa um ponto de partida para um sistema de gerenciamento de conteúdo no qual os usuários podem receber permissões adicionando e removendo funções.

Os recursos no sistema (por exemplo, páginas) estão associados a funções para conceder ao grupo de usuários vinculados a essa função as permissões que ele concede.

A idéia é poder bloquear facilmente um usuário, simplesmente negando todas as funções e adicionando a página de nível raiz na árvore a essa função e adicionando o usuário a essa função.

Isso permitiria que a estrutura de permissão permanecesse em vigor quando (por exemplo) um contratado que trabalha para a empresa não estiver disponível por longos períodos; isso também permitirá a mesma concessão de suas permissões originais, simplesmente removendo o usuário dessa função. .

As permissões são baseadas em regras típicas do tipo ACL que podem ser aplicadas ao sistema de arquivos seguindo essas regras.

As permissões CRUD devem ser bits anuláveis ​​para que os valores disponíveis sejam verdadeiros, falsos, não definidos onde o seguinte for verdadeiro:

  • falso + qualquer coisa = falso
  • verdadeiro + não definido = verdadeiro
  • verdadeiro + verdadeiro = verdadeiro
  • não definido + não definido = não definido
Se alguma das permissões for falsa -> false 
Caso contrário, se algum for verdadeiro -> verdadeiro
Outro (todos não definidos) -> false

Em outras palavras, você não obtém permissões em nada, a menos que seja concedido através da associação à função e uma regra de negação substitua uma regra de permissão.

O "conjunto" de permissões às quais isso se aplica é todas as permissões aplicadas à árvore até e inclusive a página atual, em outras palavras: Se um false estiver em qualquer função aplicada a qualquer página da árvore nesta página, o resultado será falso. , mas se a árvore inteira até aqui não estiver definida, a página atual conterá uma regra verdadeira, o resultado será verdadeiro aqui, mas seria falso para o pai.

Gostaria de manter vagamente a estrutura db, se possível, lembre-se também de que meu objetivo aqui é poder fazer algo como: select * from pages where effective permissions (read = true) and user = ?portanto, qualquer solução deve permitir que eu tenha um conjunto consultável com as permissões efetivas neles em de alguma forma (devolvê-los é opcional, desde que os critérios possam ser especificados).

Supondo que existem 2 páginas em que 1 é filho da outra e existem 2 funções, uma para usuários administrativos e uma para usuários somente leitura, ambas estão vinculadas apenas à página no nível raiz. Eu esperaria ver algo assim como a saída esperada:

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

Discussões adicionais sobre essa questão podem ser encontradas na sala de bate-papo do site principal, começando aqui .

Guerra
fonte

Respostas:

11

Usando esse modelo, criei uma maneira de consultar a tabela Pages da seguinte maneira:

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

O resultado da função com valor de tabela em linha GetPermissionStatus pode ser um conjunto vazio ou uma linha de coluna única. Quando o conjunto de resultados está vazio, isso significa que não há entradas não NULL para a combinação de página / usuário / permissão especificada. A linha Páginas correspondente é filtrada automaticamente.

Se a função retornar uma linha, sua única coluna ( IsAllowed ) conterá 1 (significando verdadeiro ) ou 0 (significando falso ). O filtro WHERE adicionalmente verifica se o valor deve ser 1 para a linha ser incluída na saída.

O que a função faz:

  • percorre a tabela Páginas na hierarquia para coletar a página especificada e todos os seus pais em um conjunto de linhas;

  • cria outro conjunto de linhas que contém todas as funções em que o usuário especificado está incluído, juntamente com uma das colunas de permissão (mas apenas valores que não são NULL) - especificamente aquele que corresponde à permissão especificada como terceiro argumento;

  • finalmente, une o primeiro e o segundo conjunto por meio da tabela RolePages para encontrar o conjunto completo de permissões explícitas que correspondem à página especificada ou a qualquer um de seus pais.

O conjunto de linhas resultante é classificado na ordem crescente dos valores de permissão e o valor mais alto é retornado como resultado da função. Como os nulos são filtrados em um estágio anterior, a lista pode conter apenas 0s e 1s. Portanto, se houver pelo menos um "negar" (0) na lista de permissões, esse será o resultado da função. Caso contrário, o resultado mais alto será 1, a menos que as funções correspondentes às páginas selecionadas não possuam "permissões" explícitas ou simplesmente não haja entradas correspondentes para a página e o usuário especificados. Nesse caso, o resultado será um vazio conjunto de linhas.

Esta é a função:

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 teste

  • 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
  • Inserções de dados:

    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

    Portanto, apenas um usuário é usado, mas é atribuído a duas funções, com várias combinações de valores de permissão entre as duas funções para testar a lógica de mesclagem em objetos filho.

    A hierarquia da página é muito simples: um pai, dois filhos. O pai está associado a uma função, um dos filhos à outra função.

  • Script de teste:

    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;
  • Limpar:

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

Resultados

  • para Criar :

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

    Havia uma verdade explícita Page 1.1apenas para . A página foi retornada de acordo com a lógica "verdadeiro + não definido". Os outros foram "não definidos" e "não definidos + não definidos" - portanto excluídos.

  • para Ler :

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

    Um verdadeiro explícito foi encontrado nas configurações para Page 1e para Page 1.1. Assim, para o primeiro, era apenas um "verdadeiro", enquanto que para o primeiro, "verdadeiro + verdadeiro". Não havia permissões de leitura explícitas para Page 1.2, portanto, era outro caso "verdadeiro + não definido". Portanto, todas as três páginas foram retornadas.

  • para atualização :

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

    Nas configurações, um true explícito foi retornado para Page 1e um false para Page 1.1. Para as páginas que entraram na saída, a lógica era a mesma que no caso de Read . Para a linha excluída, false e true foram encontrados e, portanto, a lógica "false + everything" funcionou.

  • para Excluir, não foram retornadas linhas. O pai e um dos filhos tinham nulos explícitos nas configurações e o outro filho não tinha nada.

Obter todas as permissões

Agora, se você quiser retornar apenas todas as permissões efetivas, poderá adaptar a função 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
);

A função retorna quatro colunas - as permissões efetivas para a página e o usuário especificados. Exemplo de uso:

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

Resultado:

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
fonte