web-dev-qa-db-ja.com

テーブルに格納された階層内の階層的な権限

次のデータベース構造を想定します(必要に応じて変更可能)...

enter image description here

ページと有効なアクセス許可を含む行を返すことができるように、特定のページで特定のユーザーの「有効なアクセス許可」を決定するための素晴らしい方法を探しています。

理想的なソリューションには、現在のユーザーの特定のページ行の「有効な権限」を評価するために必要な再帰を実行するためにCTEを使用する関数が含まれると考えています。

背景と実装の詳細

上記のスキーマは、コンテンツ管理システムの開始点を表しており、ロールにユーザーを追加したりロールからユーザーを削除したりすることで、ユーザーに権限を付与できます。

システム内のリソース(ページなど)はロールに関連付けられており、そのロールにリンクされているユーザーのグループに、システムが付与する権限を付与しています。

アイデアは、すべてのロールを拒否し、ツリーのルートレベルページをそのロールに追加して、ユーザーをそのロールに追加するだけで、ユーザーを簡単にロックできるようにすることです。

これにより、(たとえば)会社で働いている請負業者が長期間利用できない場合でも権限構造をそのままにしておくことができます。これにより、その1つのロールからユーザーを削除するだけで、元の権限を同じように付与することもできます。 。

アクセス許可は、これらのルールに従うことによってファイルシステムに適用される可能性がある一般的なACLタイプのルールに基づいています。

CRUD許可はヌル可能ビットにする必要があるため、使用可能な値はtrue、falseであり、以下の場合は定義されていません。

  • false +何か= false
  • true +未定義= true
  • true + true = true
  • 未定義+未定義=未定義
いずれかの権限がfalseの場合-> false 
その他の場合true-> true 
その他(すべて未定義)-> false 

言い換えると、ロールメンバーシップを介して権限が付与され、拒否ルールが許可ルールをオーバーライドしない限り、何も許可されません。

これが適用される権限の「セット」は、現在のページまでのツリーに適用されるすべての権限です。つまり、このページのツリー内のページに適用されるロールにfalseがある場合、結果はfalseになります。ただし、ここまでのツリー全体が定義されていない場合、現在のページには真のルールが含まれ、結果はここでは真になりますが、親では偽になります。

可能な場合は、db構造を緩やかに保持したいと思います。ここでの目標は、select * from pages where effective permissions (read = true) and user = ?のようなことを実行できるようにすることですので、あらゆるソリューションでクエリ可能なセットを使用できるようにする必要があります何らかの方法でそれらに有効なアクセス許可を設定します(基準を指定できる限り、それらを返すことはオプションです)。

2つのページが存在し、1つが他の子であり、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

この質問に関するさらなる議論は、メインサイトのチャットルーム ここから開始 で見つけることができます。

9
War

このモデルを使用して、次の方法でPagesテーブルを照会する方法を考え出しました。

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

GetPermissionStatusインラインテーブル値関数の結果は、空のセットまたは1つの単一列の行になります。結果セットが空の場合は、指定されたページ/ユーザー/権限の組み合わせにNULL以外のエントリがないことを意味します。対応するPages行は自動的に除外されます。

関数が行を返す場合、その唯一の列(IsAllowed)には1(true)または0(false)のいずれかが含まれます。 。さらに、WHEREフィルターは、行が出力に含まれるためには値が1でなければならないことをチェックします。

関数の機能:

  • Pagesテーブルを階層的にたどって、指定されたページとそのすべての親を1つの行セットに収集します。

  • 指定されたユーザーが含まれているすべてのロールを含む別の行セットを作成し、許可列の1つ(ただし、NULL以外の値のみ)を作成します。具体的には、3番目の引数として指定された許可に対応するものです。

  • 最後に、RolePagesテーブルを介して最初と2番目のセットを結合し、指定されたページまたはその親のいずれかに一致する明示的な権限の完全なセットを見つけます。

結果の行セットは許可値の昇順でソートされ、最上位の値が関数の結果として返されます。 nullは早い段階で除外されるため、リストには0と1のみを含めることができます。したがって、権限のリストに少なくとも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
    

    したがって、1人のユーザーのみが使用されますが、2つのロールに割り当てられた2つのロール間の許可値のさまざまな組み合わせにより、子オブジェクトの混合ロジックをテストします。

    ページ階層は非常に単純です。1つの親と2つの子です。親は1つの役割に関連付けられ、子の1つは他の役割に関連付けられます。

  • テストスクリプト:

    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
    

    明示的なtruePage 1.1のみにありました。 「true +未定義」ロジックに従ってページが返されました。その他は「未定義」および「未定義+未定義」であったため、除外されました。

  • Readの場合:

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

    Page 1およびPage 1.1の設定で、明示的なtrueが見つかりました。したがって、前者の場合は単一の「true」でしたが、後者の場合は「true + true」でした。 Page 1.2には明示的な読み取り権限がなかったため、「true +未定義」のケースもありました。したがって、3ページすべてが返されました。

  • pdateの場合:

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

    設定から、Page 1に対して明示的なtrueが返され、Page 1.1に対してfalseが返されました。出力にしたページのロジックはReadの場合と同じでした。除外された行では、falsetrueの両方が検出されたため、「false + Anything」ロジックが機能しました。

  • Deleteの場合、行は返されませんでした。親と子の1つは設定に明示的な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
);

この関数は4つの列を返します–指定されたページとユーザーの有効な権限。使用例:

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
11
Andriy M