Иерархические разрешения в таблице хранятся в иерархии

9

Предполагая следующую структуру базы данных (изменяемая при необходимости) ...

введите описание изображения здесь

Я ищу хороший способ определить «эффективные разрешения» для данного пользователя на данной странице таким образом, чтобы я мог вернуть строку, содержащую страницу и действующие разрешения.

Я думаю, что идеальное решение может включать в себя функцию, которая использует CTE для выполнения рекурсии, необходимой для оценки «эффективных разрешений» для данной строки страницы для текущего пользователя.

Предпосылки и детали реализации

Приведенная выше схема представляет собой отправную точку для системы управления контентом, в которой пользователям могут быть предоставлены разрешения путем добавления и удаления из ролей.

Ресурсы в системе (например, страницы) связаны с ролями, чтобы предоставить группе пользователей, связанных с этой ролью, разрешения, которые она предоставляет.

Идея состоит в том, чтобы иметь возможность легко заблокировать пользователя, просто запретив роль и добавив страницу корневого уровня в дереве к этой роли, а затем добавив пользователя в эту роль.

Это позволит структуре разрешений оставаться на месте, когда (например) подрядчик, работающий на компанию, недоступен в течение длительных периодов времени, и это также позволит обеспечить такое же предоставление исходных разрешений путем простого удаления пользователя из этой одной роли. ,

Разрешения основаны на типичных правилах типа ACL, которые могут применяться к файловой системе при соблюдении этих правил.

Разрешения CRUD должны быть обнуляемыми битами, поэтому доступные значения - это истина, ложь, не определены, если верно следующее:

  • ложь + что-нибудь = ложь
  • верно + не определено = верно
  • правда + правда = правда
  • не определено + не определено = не определено
Если какое-либо из разрешений ложно -> ложно 
Иначе, если есть правда -> правда
Остальное (все не определено) -> ложь

Другими словами, вы не получаете никаких разрешений ни на что, если вы не получили их через членство в роли, а правило запрета отменяет правило разрешения.

«Набор» разрешений, к которому это относится, - это все разрешения, применяемые к дереву, включая текущую страницу, иными словами, если ложно в любой роли, примененной к любой странице дерева в этой странице, то результатом является ложь. , но если целое дерево до этого не определено, то текущая страница содержит истинное правило, результат здесь истинен, но будет родительским.

Я хотел бы по возможности свободно сохранить структуру базы данных, а также помнить, что моя цель здесь заключается в том, чтобы иметь возможность делать что-то вроде этого: select * from pages where effective permissions (read = true) and user = ?поэтому любое решение должно позволить мне иметь набор запросов с действующими разрешениями в них. каким-либо образом (возвращать их необязательно, если критерии могут быть указаны).

Предполагая, что существуют 2 страницы, где 1 является дочерним по отношению к другому, и существуют 2 роли, одна для пользователей-администраторов и 1 для пользователей только для чтения, обе связаны только с страницей корневого уровня. Я ожидаю увидеть что-то вроде этого в качестве ожидаемого результата:

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

Дальнейшее обсуждение этого вопроса можно найти в главном чате на сайте, начиная с этой страницы .

война
источник

Ответы:

11

Используя эту модель, я нашел способ запроса таблицы страниц следующим образом:

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

В GetPermissionStatus результат инлайн таблица-функции может быть либо пустое множество , или один один столбец строки. Если результирующий набор пуст, это означает, что для указанной комбинации страница / пользователь / разрешение отсутствуют ненулевые записи. Соответствующая строка Страницы автоматически отфильтровывается.

Если функция возвращает строку, то ее единственный столбец ( IsAllowed ) будет содержать либо 1 (означает « истина» ), либо 0 (означает « ложь» ). Фильтр WHERE дополнительно проверяет, что значение должно быть равно 1, чтобы строка была включена в вывод.

Что делает функция:

  • перемещается по таблице страниц вверх по иерархии, чтобы собрать указанную страницу и всех ее родителей в один набор строк;

  • создает другой набор строк, содержащий все роли, в которые входит указанный пользователь, вместе с одним из столбцов разрешений (но только не NULL-значений), в частности, набор, соответствующий разрешению, указанному в качестве третьего аргумента;

  • наконец, объединяет первый и второй набор через таблицу RolePages, чтобы найти полный набор явных разрешений, соответствующих либо указанной странице, либо любому из ее родителей.

Результирующий набор строк сортируется в порядке возрастания значений разрешений, и самое верхнее значение возвращается как результат функции. Поскольку нулевые значения отфильтровываются на более ранней стадии, список может содержать только 0 и 1. Таким образом, если в списке разрешений есть хотя бы одно «deny» (0), это будет результатом функции. В противном случае самый верхний результат будет равен 1, если только у ролей, соответствующих выбранным страницам, нет явных «разрешений», либо просто нет соответствующих записей для указанной страницы и пользователя вообще, и в этом случае результат будет пустым набор строк.

Это функция:

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
);

Прецедент

  • 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
  • Вставка данных:

    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

    Таким образом, используется только один пользователь, но он назначается двум ролям с различными комбинациями значений разрешений между двумя ролями для проверки логики смешивания на дочерних объектах.

    Иерархия страниц очень проста: один родитель, два ребенка. Родитель связан с одной ролью, один из детей - с другой.

  • Тестовый скрипт:

    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;
  • Очистка:

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

Результаты

  • для создания :

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

    Была явная истинаPage 1.1 только для . Страница была возвращена в соответствии с логикой «правда + не определено». Другие были «не определены» и «не определены + не определены» - следовательно, исключены.

  • для чтения :

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

    Явное истина было найдено в настройках для Page 1и для Page 1.1. Таким образом, для первого это было всего лишь одно «правда», а для второго «правда + правда». Не было явных разрешений на чтение для Page 1.2, так что это был еще один случай «правда + не определено». Итак, все три страницы были возвращены.

  • для обновления :

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

    Из настроек было возвращено явное true для Page 1и false для Page 1.1. Для страниц, которые попали в вывод, логика была такой же, как в случае чтения . Для исключенной строки были найдены и ложь, и истина , поэтому сработала логика «ложь + все».

  • для Delete не было возвращено ни одной строки. Родитель и один из детей имели явные значения NULL в настройках, а другой ребенок не имел ничего.

Получить все разрешения

Теперь, если вы хотите просто вернуть все действующие разрешения, вы можете адаптировать функцию 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
);

Функция возвращает четыре столбца - действующие разрешения для указанной страницы и пользователя. Пример использования:

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

Вывод:

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
Андрей М
источник