Autorizzazioni gerarchiche in una gerarchia memorizzata in una tabella


9

Supponendo la seguente struttura del database (modificabile se necessario) ...

inserisci qui la descrizione dell'immagine

Sto cercando un modo carino per determinare le "autorizzazioni effettive" per un determinato utente su una determinata pagina in un modo che mi permetta di restituire una riga contenente la Pagina e le autorizzazioni effettive.

Sto pensando che la soluzione ideale possa includere una funzione che utilizza un CTE per eseguire la ricorsione necessaria per valutare le "autorizzazioni effettive" per una determinata riga di pagina per l'utente corrente.

Background e dettagli di implementazione

Lo schema sopra rappresenta un punto di partenza per un sistema di gestione dei contenuti in cui gli utenti possono ottenere le autorizzazioni aggiungendoli e rimuovendoli dai ruoli.

Le risorse nel sistema (ad es. Pagine) sono associate a ruoli per concedere al gruppo di utenti collegato a quel ruolo le autorizzazioni che concede.

L'idea è quella di essere in grado di bloccare facilmente un utente semplicemente negando tutto il ruolo e aggiungendo la pagina di livello radice nella struttura ad esso e quindi aggiungendo l'utente a quel ruolo.

Ciò consentirebbe di mantenere la struttura delle autorizzazioni quando (ad esempio) un appaltatore che lavora per l'azienda non è disponibile per lunghi periodi, ciò consentirà anche la stessa concessione delle autorizzazioni originali semplicemente rimuovendo l'utente da quel ruolo .

Le autorizzazioni si basano su regole tipiche del tipo ACL che potrebbero essere applicate al file system seguendo queste regole.

Le autorizzazioni CRUD devono essere bit nullable quindi i valori disponibili sono true, false, non definiti dove è vero quanto segue:

  • falso + nulla = falso
  • vero + non definito = vero
  • vero + vero = vero
  • non definito + non definito = non definito
Se una delle autorizzazioni è false -> false 
Altrimenti se uno è vero -> vero
Altrimenti (tutto non definito) -> false

In altre parole, non si ottengono autorizzazioni per nulla, a meno che non vengano concessi tramite l'appartenenza al ruolo e una regola di rifiuto sovrascrive una regola di autorizzazione.

Il "set" di autorizzazioni a cui si applica sono tutte le autorizzazioni applicate all'albero fino alla pagina corrente inclusa, compresa: se un falso è in qualsiasi ruolo applicato a qualsiasi pagina dell'albero in questa pagina, il risultato è falso , ma se l'intero albero fino a qui non è definito, la pagina corrente contiene una regola vera, il risultato è vero qui ma sarebbe falso per il genitore.

Mi piacerebbe mantenere vagamente la struttura del db se possibile, anche tenere presente che il mio obiettivo qui è quello di essere in grado di fare qualcosa del genere: select * from pages where effective permissions (read = true) and user = ?quindi qualsiasi soluzione dovrebbe essere in grado di permettermi di avere un set interrogabile con le autorizzazioni efficaci in essi contenute in qualche modo (la loro restituzione è facoltativa purché sia ​​possibile specificare i criteri).

Supponendo che esistano 2 pagine in cui 1 è figlio dell'altro ed esistono 2 ruoli, uno per gli utenti admin e 1 per gli utenti di sola lettura, entrambi sono collegati solo alla pagina di livello principale che mi aspetto di vedere qualcosa del genere come output previsto:

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

Ulteriori discussioni su questa domanda sono disponibili nella chat room del sito principale a partire da qui .

Risposte:


11

Utilizzando questo modello, ho trovato un modo per interrogare la tabella di Pages nel modo seguente:

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

Il risultato della funzione con valori di tabella inline GetPermissionStatus può essere un set vuoto o una riga a colonna singola. Quando il set di risultati è vuoto, significa che non ci sono voci non NULL per la combinazione pagina / utente / autorizzazione specificata. La riga Pagine corrispondente viene automaticamente filtrata.

Se la funzione restituisce una riga, la sua unica colonna ( IsAllowed ) conterrà 1 (che significa vero ) o 0 (che significa falso ). Il filtro WHERE verifica inoltre che il valore deve essere 1 affinché la riga sia inclusa nell'output.

Cosa fa la funzione:

  • guida la tabella Pagine nella gerarchia per raccogliere la pagina specificata e tutti i suoi genitori in un set di righe;

  • crea un altro set di righe contenente tutti i ruoli in cui è incluso l'utente specificato, insieme a una delle colonne di autorizzazione (ma solo valori non NULL), in particolare quella corrispondente all'autorizzazione specificata come terzo argomento;

  • infine, unisce il primo e il secondo set tramite la tabella RolePages per trovare il set completo di autorizzazioni esplicite corrispondenti alla pagina specificata o ad uno dei suoi genitori.

Il set di righe risultante viene ordinato in ordine crescente di valori di autorizzazione e il valore più alto viene restituito come risultato della funzione. Poiché i valori null vengono filtrati in una fase precedente, l'elenco può contenere solo 0 e 1. Pertanto, se esiste almeno un "rifiuto" (0) nell'elenco delle autorizzazioni, questo sarà il risultato della funzione. In caso contrario, il risultato più in alto sarà 1, a meno che i ruoli corrispondenti alle pagine selezionate non abbiano o meno "consenti" espliciti o non ci siano semplicemente voci corrispondenti per la pagina e l'utente specificati, nel qual caso il risultato sarà vuoto riga impostata.

Questa è la funzione:

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

Caso di prova

  • 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
    
  • Inserti di dati:

    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
    

    Pertanto, viene utilizzato solo un utente ma è assegnato a due ruoli, con varie combinazioni di valori di autorizzazione tra i due ruoli per testare la logica di fusione su oggetti figlio.

    La gerarchia di pagine è molto semplice: un genitore, due figli. Il genitore è associato con un ruolo, uno dei bambini con l'altro ruolo.

  • Script di prova:

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

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

risultati

  • per creare :

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

    C'era un vero esplicito Page 1.1solo per . La pagina è stata restituita secondo la logica "true + non definito". Gli altri erano "non definiti" e "non definiti + non definiti" - quindi esclusi.

  • per leggere :

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

    È stato trovato un true esplicito nelle impostazioni per Page 1e per Page 1.1. Quindi, per il primo era solo un "vero" mentre per il secondo "vero + vero". Non c'erano permessi di lettura espliciti per Page 1.2, quindi era un altro caso "vero + non definito". Quindi, tutte e tre le pagine sono state restituite.

  • per l' aggiornamento :

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

    Dalle impostazioni, è stato restituito un true esplicito per Page 1e un false per Page 1.1. Per le pagine che sono entrate nell'output la logica era la stessa di Read . Per la riga esclusa sono stati trovati sia falso che vero e quindi la logica "falso + nulla" ha funzionato.

  • per Elimina non sono state restituite righe. Il genitore e uno dei figli avevano nulli espliciti nelle impostazioni e l'altro figlio non aveva nulla.

Ottieni tutte le autorizzazioni

Ora, se vuoi solo restituire tutte le autorizzazioni efficaci, puoi adattare la funzione 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 funzione restituisce quattro colonne: le autorizzazioni effettive per la pagina e l'utente specificati. Esempio di utilizzo:

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

Produzione:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.