Autorisations hiérarchiques dans une hiérarchie stockée dans une table


9

En supposant la structure de base de données suivante (modifiable si nécessaire) ...

entrez la description de l'image ici

Je cherche un bon moyen de déterminer les "autorisations effectives" pour un utilisateur donné sur une page donnée d'une manière qui me permet de renvoyer une ligne contenant la page et les autorisations effectives.

Je pense que la solution idéale peut inclure une fonction qui utilise un CTE pour effectuer la récursivité nécessaire pour évaluer les "autorisations effectives" pour une ligne de page donnée pour l'utilisateur actuel.

Contexte et détails de mise en œuvre

Le schéma ci-dessus représente un point de départ pour un système de gestion de contenu dans lequel les utilisateurs peuvent se voir accorder des autorisations en étant ajoutés et supprimés des rôles.

Les ressources du système (par exemple, les pages) sont associées à des rôles pour accorder au groupe d'utilisateurs liés à ce rôle les autorisations qu'il accorde.

L'idée est de pouvoir verrouiller facilement un utilisateur simplement en refusant tout rôle et en ajoutant la page de niveau racine dans l'arborescence à ce rôle, puis en ajoutant l'utilisateur à ce rôle.

Cela permettrait à la structure d'autorisations de rester en place lorsque (par exemple) un entrepreneur travaillant pour l'entreprise n'est pas disponible pendant de longues périodes, cela permettra également la même octroi de leurs autorisations d'origine en supprimant simplement l'utilisateur de ce rôle. .

Les autorisations sont basées sur des règles de type ACL typiques qui peuvent s'appliquer au système de fichiers en suivant ces règles.

Les autorisations CRUD doivent être des bits annulables, de sorte que les valeurs disponibles sont vraies, fausses, non définies dans les cas suivants:

  • faux + n'importe quoi = faux
  • vrai + non défini = vrai
  • vrai + vrai = vrai
  • non défini + non défini = non défini
Si l'une des autorisations est fausse -> fausse 
Sinon, s'il y en a un vrai -> vrai
Sinon (tous non définis) -> faux

En d'autres termes, vous n'obtenez aucune autorisation sur quoi que ce soit, sauf si vous leur êtes accordé via l'appartenance au rôle et qu'une règle de refus remplace une règle d'autorisation.

Le "jeu" d'autorisations auquel cela s'applique est toutes les autorisations appliquées à l'arborescence jusqu'à et y compris la page en cours, en d'autres termes: si un faux est dans n'importe quel rôle appliqué à n'importe quelle page de l'arborescence de cette page, alors le résultat est faux , mais si l’arbre entier jusqu’ici n’est pas défini, alors la page courante contient une vraie règle, le résultat est vrai ici mais serait faux pour le parent.

J'aimerais garder la structure de la base de données si possible, gardez également à l'esprit que mon objectif ici est de pouvoir faire quelque chose comme: select * from pages where effective permissions (read = true) and user = ?ainsi, toute solution devrait pouvoir me permettre d'avoir un ensemble interrogeable avec les autorisations effectives dedans dans d'une certaine manière (leur retour est facultatif tant que les critères peuvent être spécifiés).

En supposant que 2 pages existent où 1 est un enfant de l'autre et 2 rôles existent, un pour les utilisateurs admin et 1 pour les utilisateurs en lecture seule, les deux sont liés uniquement à la page de niveau racine, je m'attendrais à voir quelque chose comme ceci en tant que sortie attendue:

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

Une discussion plus approfondie autour de cette question peut être trouvée dans la salle de discussion du site principal commençant ici .

Réponses:


11

En utilisant ce modèle, j'ai trouvé un moyen d'interroger la table Pages de la manière suivante:

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

Le résultat de la fonction de valeur de table en ligne GetPermissionStatus peut être un ensemble vide ou une ligne à une seule colonne. Lorsque le jeu de résultats est vide, cela signifie qu'il n'y a aucune entrée non NULL pour la combinaison page / utilisateur / autorisation spécifiée. La ligne Pages correspondante est automatiquement filtrée.

Si la fonction renvoie une ligne, sa seule colonne ( IsAllowed ) contiendra 1 (signifiant vrai ) ou 0 (signifiant faux ). Le filtre WHERE vérifie en outre que la valeur doit être 1 pour que la ligne soit incluse dans la sortie.

Ce que fait la fonction:

  • parcourt la table Pages dans la hiérarchie pour collecter la page spécifiée et tous ses parents dans un ensemble de lignes;

  • crée un autre ensemble de lignes contenant tous les rôles dans lesquels l'utilisateur spécifié est inclus, ainsi qu'une des colonnes d'autorisation (mais uniquement des valeurs non NULL) - spécifiquement celle correspondant à l'autorisation spécifiée comme troisième argument;

  • enfin, joint le premier et le deuxième ensemble via la table RolePages pour trouver l'ensemble complet d'autorisations explicites correspondant à la page spécifiée ou à l'un de ses parents.

L'ensemble de lignes résultant est trié dans l'ordre croissant des valeurs d'autorisation et la valeur la plus élevée est renvoyée comme résultat de la fonction. Étant donné que les valeurs nulles sont filtrées à un stade antérieur, la liste peut contenir uniquement des 0 et des 1. Ainsi, s'il y a au moins un "refus" (0) dans la liste des permissions, ce sera le résultat de la fonction. Sinon, le résultat le plus élevé sera 1, à moins que les rôles correspondant aux pages sélectionnées n'aient aucun "autorise" explicite ou qu'il n'y ait tout simplement aucune entrée correspondante pour la page et l'utilisateur spécifiés, auquel cas le résultat sera vide ensemble de lignes.

Voici la fonction:

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

Cas de test

  • 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
  • Inserts de données:

    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

    Ainsi, un seul utilisateur est utilisé, mais il est affecté à deux rôles, avec différentes combinaisons de valeurs d'autorisation entre les deux rôles pour tester la logique de fusion sur les objets enfants.

    La hiérarchie des pages est très simple: un parent, deux enfants. Le parent est associé à un rôle, l'un des enfants à l'autre.

  • Script de test:

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

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

Résultats

  • pour Créer :

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

    Il y avait un vrai explicite pour Page 1.1seulement. La page a été renvoyée selon la logique "vrai + non défini". Les autres étaient "non définis" et "non définis + non définis" - donc exclus.

  • pour lire :

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

    Un vrai explicite a été trouvé dans les paramètres pour Page 1et pour Page 1.1. Ainsi, pour les premiers, ce n'était qu'un seul "vrai" tandis que pour les seconds "vrai + vrai". Il n'y avait aucune autorisation de lecture explicite pour Page 1.2, donc c'était un autre cas "vrai + non défini". Ainsi, les trois pages ont été retournées.

  • pour la mise à jour :

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

    Dans les paramètres, un vrai explicite a été renvoyé pour Page 1et un faux pour Page 1.1. Pour les pages qui sont entrées dans la sortie, la logique était la même qu'en cas de lecture . Pour la ligne exclue, à la fois faux et vrai ont été trouvés et donc la logique "faux + n'importe quoi" a fonctionné.

  • pour Supprimer , aucune ligne n'a été renvoyée. Le parent et l'un des enfants avaient des valeurs nulles explicites dans les paramètres et l'autre enfant n'avait rien.

Obtenez toutes les autorisations

Maintenant, si vous souhaitez simplement renvoyer toutes les autorisations effectives, vous pouvez adapter la fonction 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
);

La fonction renvoie quatre colonnes - les autorisations effectives pour la page et l'utilisateur spécifiés. Exemple d'utilisation:

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

Production:

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.