Ієрархічні дозволи в таблиці, що зберігається ієрархією


9

Припускаючи таку структуру бази даних (за потреби змінюється) ...

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

Я шукаю хороший спосіб визначити "ефективні дозволи" для певного користувача на певній сторінці таким чином, що дозволяє мені повернути рядок, що містить Сторінку та ефективні дозволи.

Я думаю, що ідеальне рішення може включати в себе функцію, яка використовує CTE для виконання рекурсії, необхідної для оцінки "ефективних дозволів" для заданого рядка сторінки для поточного користувача.

Основні відомості та відомості про реалізацію

Вищевказана схема являє собою початкову точку для системи управління вмістом, в якій користувачі можуть надавати дозволи, додаючи та вилучаючи з ролей.

Ресурси в системі (наприклад, сторінки) пов'язані з ролями, щоб надати групі користувачів, пов'язаних з цією ролю, дозволи, які вона надає.

Ідея полягає у тому, щоб мати можливість легко заблокувати користувача, просто відмовившись у всій ролі та додавши сторінку кореневого рівня до дерева до цієї ролі, а потім додати користувача до цієї ролі.

Це дозволило б структурі дозволів залишатися на місці, коли (наприклад) підрядник, який працює в компанії, не є доступним протягом тривалого періоду, це також дозволить отримати те саме надання їхніх оригінальних дозволів, просто видаливши користувача з тієї однієї ролі .

Дозволи мають базуватися на типових правилах типу ACL, які можуть застосовуватися до файлової системи, дотримуючись цих правил.

Права CRUD повинні бути нульовими бітами, тому доступні значення є істинними, хибними, не визначеними, де відповідає дійсності наступне:

  • false + нічого = хибно
  • true + не визначено = true
  • true + true = правда
  • не визначено + не визначено = не визначено
Якщо будь-який з дозволів хибний -> false 
Інше, якщо така є правда -> правда
Інше (все не визначено) -> хибно

Іншими словами, ви не отримуєте жодних дозволів на що-небудь, якщо вам не надано їх через членство в ролі, а правило заборони не перевищує допустиме правило.

"Набір" дозволів, до яких це стосується, є всіма дозволами, застосованими до дерева до та включаючи поточну сторінку, іншими словами: Якщо помилка в будь-якій ролі застосована до будь-якої сторінки в дереві на цій сторінці, результат є помилковим , але якщо ціле дерево до цього часу не визначене, то поточна сторінка містить справжнє правило, результат є вірним тут, але був би хибним для батьків.

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

Якщо припустити, що існують 2 сторінки, де 1 є дочірньою частиною інших, і дві ролі існують, одна для користувачів адміністратора та 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 може бути або порожній набір, або один рядок з одним стовпцем. Якщо набір результатів порожній, це означає, що для вказаної комбінації сторінки / користувача / дозволу немає записів, що не стосуються NULL. Відповідний рядок " Сторінки" автоматично відфільтровується.

Якщо функція повертає рядок, то її єдиний стовпець ( IsAllowed ) буде містити або 1 (що означає істину ), або 0 (що означає хибність ). Фільтр WHERE додатково перевіряє, чи має бути значення 1 для рядка, який буде включений у висновок.

Що виконує функція:

  • ходить по таблиці Сторінки ієрархією, щоб зібрати вказану сторінку та всіх її батьків в один набір рядків;

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

  • нарешті, приєднується до першого та другого набору через таблицю RolePages, щоб знайти повний набір явних дозволів, що відповідають або вказаній сторінці, або будь-якому з її батьків.

Отриманий набір рядків сортується у порядку зростання значень дозволів, а найвище значення повертається у результаті функції. Оскільки нулі відфільтровано на більш ранній стадії, список може містити лише 0 і 1. Таким чином, якщо в переліку дозволів є хоча б одне "заперечення" (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

    У налаштуваннях для і для знайдено явне значення true . Таким чином, для перших це було лише єдине "справжнє", а для другого - "істинне + істинне". Немає явних дозволів читання для , тому це був ще один випадок "істина + не визначено". Отже, всі три сторінки було повернуто.Page 1Page 1.1Page 1.2

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

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

    З налаштувань було повернено явне значення truePage 1 та false для Page 1.1. Для сторінок, які внесли його у вихід, логіка була такою ж, як і у випадку з Read . Для виключеного рядка було виявлено і хибне, і істинне , і тому працювала логіка "false + все".

  • для видалення рядків не повернуто. У батьків і одного з дітей були явні нулі в налаштуваннях, а у іншої дитини нічого не було.

Отримати всі дозволи

Тепер, якщо ви хочете просто повернути всі ефективні дозволи, ви можете адаптувати функцію 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.