表存储层次结构中的层次结构权限

War*_*War 9 database-design sql-server hierarchy

假设以下数据库结构(如果需要可以修改)......

在此处输入图片说明

我正在寻找一种很好的方法来确定给定页面上给定用户的“有效权限”,这种方式允许我返回包含页面和有效权限的行。

我认为理想的解决方案可能包括一个函数,该函数使用 CTE 来执行评估当前用户给定页面行的“有效权限”所需的递归。

背景和实施细节

上面的架构代表了内容管理系统的起点,在该系统中,可以通过添加和删除角色来授予用户权限。

系统中的资源(例如页面)与角色相关联,以向链接到该角色的用户组授予其授予的权限。

这个想法是通过简单地拥有一个拒绝所有角色并将树中的根级别页面添加到该角色,然后将用户添加到该角色,从而能够轻松锁定用户。

这将允许权限结构在(例如)为公司工作的承包商长期不可用时保持原状,然后这也将允许通过简单地从该角色中删除用户来授予他们原始权限.

权限基于典型的 ACL 类型规则,这些规则可能通过遵循这些规则应用于文件系统。

CRUD 权限是可以为空的位,因此可用值是 true、false,在以下情况下未定义:

  • 假 + 任何东西 = 假
  • 真 + 未定义 = 真
  • 真 + 真 = 真
  • 未定义 + 未定义 = 未定义
如果任何权限为 false -> false 
否则,如果有的话 -> true
否则(全部未定义)-> false

换句话说,除非通过角色成员资格授予您任何权限,并且拒绝规则覆盖允许规则,否则您无法获得任何权限。

这适用于的“权限集”是应用于树直到并包括当前页面的所有权限,换句话说:如果应用于树中任何页面的任何角色为假,则结果为假,但是如果到这里的整个树都没有定义,那么当前页面包含一个真正的规则,结果在这里是真的,但对于父级来说是假的。

如果可能,我想松散地保留 db 结构,还请记住,我在这里的目标是能够执行以下操作: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
Run Code Online (Sandbox Code Playgroud)

关于这个问题的进一步讨论可以从这里开始的主站点聊天室中找到

And*_*y M 11

使用这个模型,我想出了一种以下列方式查询Pages表的方法:

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

所述GetPermissionStatus联表值函数的结果可以是一个空集或者一个单一列的行。当结果集为空时,这意味着指定的页面/用户/权限组合没有非 NULL 条目。相应的页面行会被自动过滤掉。

如果该函数确实返回一行,则其唯一的列 ( IsAllowed ) 将包含 1 (表示true ) 或 0 (表示false )。WHERE 过滤器另外检查值必须为 1 才能包含在输出中的行。

函数的作用:

  • Pages表沿层次结构向上移动,以将指定页面及其所有父页面收集到一个行集中;

  • 构建另一个行集,其中包含指定用户所在的所有角色,以及其中一个权限列(但只有非 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
);
Run Code Online (Sandbox Code Playgroud)

测试用例

  • 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
    
    Run Code Online (Sandbox Code Playgroud)
  • 数据插入:

    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
    
    Run Code Online (Sandbox Code Playgroud)

    因此,只使用了一个用户,但将其分配给了两个角色,在两个角色之间使用各种权限值组合来测试子对象上的混合逻辑。

    页面层次结构非常简单:一个父级,两个子级。父级与一个角色相关联,其中一个子级与另一个角色相关联。

  • 测试脚本:

    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;
    
    Run Code Online (Sandbox Code Playgroud)
  • 清理:

    DROP FUNCTION dbo.GetPermissionStatus;
    GO
    DROP TABLE dbo.UserRoles, dbo.RolePages, dbo.Users, dbo.Roles, dbo.Pages;
    GO
    
    Run Code Online (Sandbox Code Playgroud)

结果

  • 对于创建

    Id  ParentId  Name
    --  --------  --------
    2   1         Page 1.1
    
    Run Code Online (Sandbox Code Playgroud)

    有一个明确的true for Page 1.1only。该页面是根据“true + not defined”逻辑返回的。其他的是“未定义”和“未定义+未定义”——因此被排除在外。

  • 对于阅读

    Id  ParentId  Name
    --  --------  --------
    1   NULL      Page 1
    2   1         Page 1.1
    3   1         Page 1.2
    
    Run Code Online (Sandbox Code Playgroud)

    在 for和 for的设置中发现了一个明确的true。因此,对于前者,它只是一个单一的“真”,而对于后者则是“真 + 真”。没有明确的读取权限,所以这是另一个“真实+未定义”的情况。因此,所有三个页面都返回了。Page 1Page 1.1Page 1.2

  • 用于更新

    Id  ParentId  Name
    --  --------  --------
    1   NULL      Page 1
    3   1         Page 1.2
    
    Run Code Online (Sandbox Code Playgroud)

    从设置,明确真正得到返回的Page 1虚假Page 1.1。对于使其成为输出的页面,逻辑与Read 的情况相同。对于排除的行,发现了falsetrue,因此“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
);
Run Code Online (Sandbox Code Playgroud)

该函数返回四列 - 指定页面和用户的有效权限。用法示例:

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

输出:

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
Run Code Online (Sandbox Code Playgroud)