Hierarchische Berechtigungen in einer in einer Tabelle gespeicherten Hierarchie

9

Angenommen, die folgende Datenbankstruktur (kann bei Bedarf geändert werden) ...

Geben Sie hier die Bildbeschreibung ein

Ich suche nach einer guten Möglichkeit, die "effektiven Berechtigungen" für einen bestimmten Benutzer auf einer bestimmten Seite so zu ermitteln, dass ich eine Zeile mit der Seite und den effektiven Berechtigungen zurückgeben kann.

Ich denke, dass die ideale Lösung eine Funktion enthalten kann, die einen CTE verwendet, um die Rekursion durchzuführen, die erforderlich ist, um die "effektiven Berechtigungen" für eine bestimmte Seitenzeile für den aktuellen Benutzer zu bewerten.

Hintergrund und Implementierungsdetails

Das obige Schema stellt einen Startpunkt für ein Content-Management-System dar, in dem Benutzern Berechtigungen erteilt werden können, indem sie Rollen hinzugefügt und daraus entfernt werden.

Ressourcen im System (z. B. Seiten) sind Rollen zugeordnet, um der mit dieser Rolle verknüpften Benutzergruppe die von ihr gewährten Berechtigungen zu erteilen.

Die Idee ist, einen Benutzer einfach sperren zu können, indem einfach alle Rollen verweigert werden und die Stammebene im Baum zu dieser Rolle hinzugefügt wird und der Benutzer dann zu dieser Rolle hinzugefügt wird.

Dies würde es ermöglichen, dass die Berechtigungsstruktur erhalten bleibt, wenn (zum Beispiel) ein Auftragnehmer, der für das Unternehmen arbeitet, für längere Zeit nicht verfügbar ist. Dies ermöglicht dann auch die gleiche Erteilung seiner ursprünglichen Berechtigungen, indem der Benutzer einfach aus dieser einen Rolle entfernt wird .

Berechtigungen basieren auf typischen ACL-Regeln, die möglicherweise für das Dateisystem gelten, indem diese Regeln befolgt werden.

Die CRUD-Berechtigungen müssen nullfähige Bits sein, damit die verfügbaren Werte wahr, falsch und nicht definiert sind, wenn Folgendes wahr ist:

  • falsch + irgendetwas = falsch
  • wahr + nicht definiert = wahr
  • wahr + wahr = wahr
  • nicht definiert + nicht definiert = nicht definiert
Wenn eine der Berechtigungen falsch ist -> falsch 
Sonst, wenn einer wahr ist -> wahr
Sonst (alle nicht definiert) -> false

Mit anderen Worten, Sie erhalten keine Berechtigungen für irgendetwas, es sei denn, Sie erhalten sie durch Rollenmitgliedschaft und eine Verweigerungsregel überschreibt eine Zulassungsregel.

Der "Satz" von Berechtigungen, für den dies gilt, sind alle Berechtigungen, die auf den Baum bis einschließlich der aktuellen Seite angewendet werden, mit anderen Worten: Wenn ein Falsch in einer Rolle auf eine Seite im Baum dieser Seite angewendet wird, ist das Ergebnis falsch Wenn jedoch der gesamte Baum bis hierher nicht definiert ist, enthält die aktuelle Seite eine wahre Regel. Das Ergebnis ist hier wahr, für das übergeordnete Element jedoch falsch.

Ich möchte die Datenbankstruktur möglichst locker beibehalten und auch bedenken, dass mein Ziel hier darin besteht, in der Lage zu sein, Folgendes zu tun: select * from pages where effective permissions (read = true) and user = ?Jede Lösung sollte es mir ermöglichen, einen abfragbaren Satz mit den darin enthaltenen effektiven Berechtigungen zu haben auf irgendeine Weise (die Rückgabe ist optional, solange die Kriterien angegeben werden können).

Angenommen, es gibt 2 Seiten, auf denen 1 ein untergeordnetes Element der anderen ist und 2 Rollen vorhanden sind, eine für Administratorbenutzer und eine für schreibgeschützte Benutzer. Beide sind nur mit der Seite auf Stammebene verknüpft, von der ich erwarten würde, dass so etwas als erwartete Ausgabe angezeigt wird:

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

Weitere Diskussionen zu dieser Frage finden Sie ab hier im Chatroom der Hauptseite .

Krieg
quelle

Antworten:

11

Mit diesem Modell habe ich eine Möglichkeit gefunden, die Pages- Tabelle folgendermaßen abzufragen :

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

Das Ergebnis der GetPermissionStatus- Inline-Tabellenwertfunktion kann entweder eine leere Menge oder eine einspaltige Zeile sein. Wenn die Ergebnismenge leer ist, bedeutet dies, dass für die angegebene Kombination aus Seite, Benutzer und Berechtigung keine Nicht-NULL-Einträge vorhanden sind. Die entsprechende Seitenzeile wird automatisch herausgefiltert.

Wenn die Funktion eine Zeile zurückgibt , enthält ihre einzige Spalte ( IsAllowed ) entweder 1 (bedeutet wahr ) oder 0 (bedeutet falsch ). Der WHERE-Filter prüft zusätzlich, ob der Wert 1 sein muss, damit die Zeile in die Ausgabe aufgenommen wird.

Was die Funktion macht:

  • geht die Seiten Tisch der Hierarchie der angegebenen Seite und alle seine Eltern in einer Reihe Satz zu sammeln;

  • Erstellt einen weiteren Zeilensatz, der alle Rollen enthält, in denen der angegebene Benutzer enthalten ist, sowie eine der Berechtigungsspalten (jedoch nur Nicht-NULL-Werte) - insbesondere diejenige, die der als drittes Argument angegebenen Berechtigung entspricht.

  • Verbindet schließlich den ersten und den zweiten Satz über die RolePages- Tabelle, um den vollständigen Satz expliziter Berechtigungen zu finden, die entweder der angegebenen Seite oder einer der übergeordneten Seiten entsprechen.

Der resultierende Zeilensatz wird in aufsteigender Reihenfolge der Berechtigungswerte sortiert und der oberste Wert wird als Ergebnis der Funktion zurückgegeben. Da Nullen zu einem früheren Zeitpunkt herausgefiltert werden, kann die Liste nur Nullen und Einsen enthalten. Wenn also mindestens eine "Verweigerung" (0) in der Liste der Berechtigungen vorhanden ist, ist dies das Ergebnis der Funktion. Andernfalls ist das oberste Ergebnis 1, es sei denn, die Rollen, die den ausgewählten Seiten entsprechen, haben entweder keine expliziten "Zulassungen" oder es gibt nur keine übereinstimmenden Einträge für die angegebene Seite und den angegebenen Benutzer. In diesem Fall ist das Ergebnis leer Zeilensatz.

Dies ist die Funktion:

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

Testfall

  • 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
  • Dateneinfügungen:

    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

    Es wird also nur ein Benutzer verwendet, der jedoch zwei Rollen mit verschiedenen Kombinationen von Berechtigungswerten zwischen den beiden Rollen zugewiesen ist, um die Mischlogik für untergeordnete Objekte zu testen.

    Die Seitenhierarchie ist sehr einfach: ein Elternteil, zwei Kinder. Der Elternteil ist einer Rolle zugeordnet, eines der Kinder der anderen Rolle.

  • Testskript:

    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;
  • Aufräumen:

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

Ergebnisse

  • zum Erstellen :

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

    Es gab eine explizite wahr für Page 1.1nur. Die Seite wurde gemäß der Logik "true + not defined" zurückgegeben. Die anderen waren "nicht definiert" und "nicht definiert + nicht definiert" - daher ausgeschlossen.

  • zum Lesen :

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

    In den Einstellungen für und für wurde ein explizites true gefunden . Für das erstere war es also nur ein einziges "wahr", während für das letztere "wahr + wahr". Es gab keine expliziten Leseberechtigungen für , daher war es ein weiterer "wahr + nicht definiert" Fall. Also wurden alle drei Seiten zurückgegeben.Page 1Page 1.1Page 1.2

  • zum Update :

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

    In den Einstellungen wurde ein explizites true für Page 1und ein false für zurückgegeben Page 1.1. Für die Seiten, die es in die Ausgabe schafften, war die Logik dieselbe wie im Fall von Lesen . Für die ausgeschlossene Zeile wurden sowohl falsch als auch wahr gefunden, und so funktionierte die Logik "falsch + alles".

  • Für Löschen wurden keine Zeilen zurückgegeben. Der Elternteil und eines der Kinder hatten explizite Nullen in den Einstellungen und das andere Kind hatte nichts.

Holen Sie sich alle Berechtigungen

Wenn Sie nur alle effektiven Berechtigungen zurückgeben möchten, können Sie die GetPermissionStatus- Funktion anpassen :

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

Die Funktion gibt vier Spalten zurück - die effektiven Berechtigungen für die angegebene Seite und den angegebenen Benutzer. Anwendungsbeispiel:

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

Ausgabe:

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.
quelle