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


9

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

ここに画像の説明を入力してください

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

理想的なソリューションには、現在のユーザーの特定のページ行の「有効な権限」を評価するために必要な再帰を実行するために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

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

回答:


11

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

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(trueを意味する)または0(falseを意味する)のいずれかが含まれます。さらに、WHEREフィルターは、行が出力に含まれるためには値が1でなければならないことをチェックします。

関数の機能:

  • 歩くページの 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

    明示的に認められた真のためにPage 1.1のみ。「true +未定義」ロジックに従ってページが返されました。その他は「未定義」と「未定義+未定義」でした–したがって除外されました。

  • 以下のために読みます

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

    およびの設定で明示的なtrueが見つかりました。したがって、前者の場合は単一の「true」でしたが、後者の場合は「true + true」でした。には明示的な読み取り権限がなかったため、「true +未定義」のケースもありました。したがって、3ページすべてが返されました。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の場合と同じでした。除外された行では、falsetrueの両方が検出されたため、「false + any」ロジックが機能しました。

  • 以下のために削除返された行はありませんでした。親と子の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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.