Izin hierarki dalam tabel disimpan hierarki

9

Dengan asumsi struktur database berikut (dapat dimodifikasi jika perlu) ...

masukkan deskripsi gambar di sini

Saya mencari cara yang bagus untuk menentukan "izin efektif" untuk pengguna yang diberikan pada halaman tertentu dengan cara yang memungkinkan saya mengembalikan baris yang berisi Halaman dan izin efektif.

Saya berpikir bahwa solusi ideal dapat mencakup fungsi yang menggunakan CTE untuk melakukan rekursi yang diperlukan untuk mengevaluasi "izin efektif" untuk baris halaman yang diberikan untuk pengguna saat ini.

Latar belakang dan detail implementasi

Skema di atas merupakan titik awal untuk sistem manajemen konten di mana pengguna dapat diberikan izin dengan ditambahkan dan dihapus dari peran.

Sumber daya dalam sistem (misalnya halaman) dikaitkan dengan peran untuk memberikan kelompok pengguna yang ditautkan ke peran itu, izin yang diberikannya.

Idenya adalah untuk dapat dengan mudah mengunci pengguna hanya dengan menyangkal semua peran dan menambahkan halaman level root di pohon ke peran itu dan kemudian menambahkan pengguna ke peran itu.

Ini akan memungkinkan struktur izin tetap di tempat ketika (misalnya) kontraktor yang bekerja untuk perusahaan tidak tersedia untuk jangka waktu lama, ini juga akan memungkinkan pemberian izin yang sama dengan izin asli mereka hanya dengan menghapus pengguna dari satu peran itu .

Izin didasarkan pada aturan tipe ACL khas yang mungkin berlaku untuk sistem file dengan mengikuti aturan ini.

Izin CRUD harus bit nullable sehingga nilai yang tersedia benar, salah, tidak didefinisikan di mana yang berikut ini benar:

  • false + anything = false
  • true + not defined = true
  • true + true = benar
  • tidak didefinisikan + tidak didefinisikan = tidak didefinisikan
Jika salah satu izin salah -> salah 
Jika ada yang benar -> benar
Lain (semua tidak didefinisikan) -> false

Dengan kata lain Anda tidak mendapatkan izin apa pun kecuali Anda diberikan mereka melalui keanggotaan peran dan aturan penolakan menimpa aturan izin.

"Set" izin yang berlaku untuk ini adalah semua izin yang diterapkan ke pohon hingga dan termasuk halaman saat ini, dengan kata lain: Jika salah dalam peran apa pun diterapkan ke halaman mana saja di pohon ke halaman ini maka hasilnya salah , tetapi jika keseluruhan pohon hingga di sini tidak ditentukan maka halaman saat ini berisi aturan yang benar hasilnya adalah benar di sini tetapi akan menjadi salah untuk induknya.

Saya ingin secara longgar menjaga struktur db jika memungkinkan, juga perlu diingat bahwa tujuan saya di sini adalah untuk dapat melakukan sesuatu seperti: select * from pages where effective permissions (read = true) and user = ?jadi solusi apa pun harus dapat membuat saya memiliki seperangkat yang dapat dicari dengan izin efektif di dalamnya di beberapa cara (mengembalikannya adalah opsional selama kriteria dapat ditentukan).

Dengan asumsi 2 halaman ada di mana 1 adalah anak dari yang lain dan 2 peran ada, satu untuk pengguna admin dan 1 untuk pengguna hanya baca, keduanya terkait dengan hanya halaman tingkat root saya akan berharap untuk melihat sesuatu seperti ini sebagai output yang diharapkan:

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

Diskusi lebih lanjut seputar pertanyaan ini dapat ditemukan di ruang obrolan situs utama mulai dari sini .

Perang
sumber

Jawaban:

11

Menggunakan model ini, saya telah menemukan cara untuk menanyakan tabel Halaman dengan cara berikut:

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

Hasil fungsi bernilai-nilai dari tabel GetPermissionStatus dapat berupa set kosong atau satu baris kolom tunggal. Ketika set hasil kosong, itu berarti bahwa tidak ada entri NULL untuk kombinasi halaman / pengguna / izin yang ditentukan. Baris Halaman terkait disaring secara otomatis.

Jika fungsi mengembalikan baris, maka satu-satunya kolom ( IsAllowed ) akan berisi 1 (berarti benar ) atau 0 (berarti salah ). Filter WHERE juga memeriksa bahwa nilai harus 1 untuk baris yang akan disertakan dalam output.

Apa fungsi ini:

  • berjalan tabel Pages naik hierarki untuk mengumpulkan halaman yang ditentukan dan semua orang tuanya ke dalam satu set baris;

  • membuat kumpulan baris lain yang berisi semua peran yang dimasukkan pengguna, bersama dengan salah satu kolom izin (tetapi hanya nilai-nilai NULL) - khususnya yang terkait dengan izin yang ditentukan sebagai argumen ketiga;

  • akhirnya, bergabung dengan set pertama dan kedua melalui tabel RolePages untuk menemukan set lengkap izin eksplisit yang cocok dengan halaman yang ditentukan atau salah satu dari orang tuanya.

Set baris yang dihasilkan diurutkan dalam urutan nilai izin yang menanjak dan nilai paling atas dikembalikan sebagai hasil dari fungsi. Karena nol disaring pada tahap sebelumnya, daftar dapat berisi hanya 0 dan 1. Jadi, jika ada setidaknya satu "deny" (0) dalam daftar izin, itu akan menjadi hasil dari fungsi. Kalau tidak, hasil paling atas akan menjadi 1, kecuali peran yang sesuai dengan halaman yang dipilih kebetulan tidak memiliki "memungkinkan" baik atau tidak ada entri yang cocok untuk halaman dan pengguna yang ditentukan sama sekali, dalam hal ini hasilnya akan menjadi kosong set baris.

Ini fungsinya:

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

Kasus cobaan

  • 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
    
  • Sisipan data:

    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
    

    Jadi, hanya satu pengguna yang digunakan tetapi ditugaskan untuk dua peran, dengan berbagai kombinasi nilai izin antara dua peran untuk menguji blending logic pada objek anak.

    Hirarki halaman sangat sederhana: satu orangtua, dua anak. Orang tua dikaitkan dengan satu peran, satu anak dengan peran lainnya.

  • Skrip uji:

    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;
    
  • Membersihkan:

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

Hasil

  • untuk Buat :

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

    Ada eksplisit benar untuk Page 1.1saja. Halaman dikembalikan sesuai dengan logika "true + not defined". Yang lain "tidak didefinisikan" dan "tidak didefinisikan + tidak didefinisikan" - karenanya dikecualikan.

  • untuk Baca :

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

    Benar eksplisit ditemukan di pengaturan untuk Page 1dan untuk Page 1.1. Jadi, untuk yang pertama itu hanya satu "benar" sedangkan untuk yang terakhir "benar + benar". Tidak ada izin baca eksplisit untuk Page 1.2, jadi itu adalah kasus "benar + tidak didefinisikan" yang lain. Jadi, ketiga halaman dikembalikan.

  • untuk Pembaruan :

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

    Dari pengaturan, true true dikembalikan untuk Page 1dan false for Page 1.1. Untuk halaman yang membuatnya menjadi output, logikanya sama seperti dalam kasus Read . Untuk baris yang dikecualikan, false dan true ditemukan dan logika "false + anything" bekerja.

  • untuk Hapus tidak ada baris yang dikembalikan. Orang tua dan salah satu dari anak-anak memiliki null eksplisit dalam pengaturan dan anak lainnya tidak punya apa-apa.

Dapatkan semua izin

Sekarang jika Anda ingin mengembalikan semua izin yang efektif, Anda dapat menyesuaikan fungsi 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
);

Fungsi mengembalikan empat kolom - izin efektif untuk halaman dan pengguna yang ditentukan. Contoh penggunaan:

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

Keluaran:

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