Come implementare le autorizzazioni per la logica di business in PostgreSQL (o SQL in generale)?


16

Supponiamo di avere una tabella di articoli:

CREATE TABLE items
(
    item serial PRIMARY KEY,
    ...
);

Ora, voglio introdurre il concetto di "autorizzazioni" per ciascun elemento (si noti che non sto parlando di autorizzazioni di accesso al database qui, ma autorizzazioni di business logic per quell'elemento). Ogni elemento ha autorizzazioni predefinite e anche autorizzazioni per utente che possono sovrascrivere le autorizzazioni predefinite.

Ho provato a pensare a diversi modi per implementarlo e ho trovato le seguenti soluzioni, ma non sono sicuro su quale sia il migliore e perché:

1) La soluzione booleana

Utilizzare una colonna booleana per ogni autorizzazione:

CREATE TABLE items
(
    item serial PRIMARY KEY,

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),

    PRIMARY KEY(item, user),

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

Vantaggi : ogni autorizzazione è denominata.

Svantaggi : ci sono dozzine di autorizzazioni che aumentano significativamente il numero di colonne e devi definirle due volte (una volta in ogni tabella).

2) La soluzione integer

Utilizzare un numero intero e trattarlo come un campo di bit (ovvero il bit 0 è per can_change_description, il bit 1 è per can_change_pricee così via e utilizzare le operazioni bit a bit per impostare o leggere le autorizzazioni).

CREATE DOMAIN permissions AS integer;

Vantaggi : molto veloce.

Svantaggi : è necessario tenere traccia di quale bit rappresenta l'autorizzazione sia nel database che nell'interfaccia front-end.

3) La soluzione Bitfield

Come 2), ma usa bit(n). Molto probabilmente gli stessi vantaggi e svantaggi, forse leggermente più lenti.

4) La soluzione Enum

Utilizzare un tipo enum per le autorizzazioni:

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

e quindi creare una tabella aggiuntiva per le autorizzazioni predefinite:

CREATE TABLE item_default_permissions
(
    item int NOT NULL REFERENCES items(item),
    perm permission NOT NULL,

    PRIMARY KEY(item, perm)
);

e modifica la tabella delle definizioni per utente in:

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),
    perm permission NOT NULL,

    PRIMARY KEY(item, user, perm)    
);

Vantaggi : facile nominare le singole autorizzazioni (non è necessario gestire le posizioni dei bit).

Svantaggi : anche quando si recuperano solo le autorizzazioni predefinite, è necessario accedere a due tabelle aggiuntive: prima, la tabella delle autorizzazioni predefinite e, in secondo luogo, il catalogo di sistema che memorizza i valori enum.

Soprattutto perché le autorizzazioni predefinite devono essere recuperate per ogni singola visualizzazione di pagina di quell'elemento , l'impatto sulle prestazioni dell'ultima alternativa potrebbe essere significativo.

5) La soluzione di matrice Enum

Come 4), ma usa un array per contenere tutte le autorizzazioni (predefinite):

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Vantaggi : facile nominare le singole autorizzazioni (non è necessario gestire le posizioni dei bit).

Svantaggi : rompe la prima forma normale ed è un po 'brutta. Occupa un numero considerevole di byte consecutivi se il numero di autorizzazioni è elevato (circa 50).

Riesci a pensare ad altre alternative?

Quale approccio dovrebbe essere adottato e perché?

Nota: questa è una versione modificata di una domanda pubblicata in precedenza su StackOverflow .


2
Con dozzine di permessi diversi potrei scegliere uno (o più) bigintcampi (ciascuno buono per 64 bit) o ​​una stringa di bit. Ho scritto un paio di risposte correlate su SO che potrebbero essere di aiuto.
Erwin Brandstetter,

Risposte:


7

So che non stai chiedendo di per sé la sicurezza del database , ma puoi fare quello che vuoi usando la sicurezza del database. Puoi persino usarlo in un'app Web. Se non si desidera utilizzare la sicurezza del database, si applicano comunque gli schemi.

Volete sicurezza a livello di colonna, sicurezza a livello di riga e probabilmente gestione gerarchica dei ruoli. La sicurezza basata sui ruoli è molto più semplice da gestire rispetto alla sicurezza basata sugli utenti.

Questo codice di esempio è per PostgreSQL 9.4, che uscirà presto. Puoi farlo con 9.3, ma è richiesto più lavoro manuale.

Vuoi che tutto sia indicizzabile se ti preoccupi delle prestazioni †, come dovresti essere. Ciò significa che i campi maschera di bit e array probabilmente non saranno una buona idea.

In questo esempio, conserviamo le principali tabelle di dati nello dataschema e le corrispondenti viste in public.

create schema data; --main data tables
create schema security; --acls, security triggers, default privileges

create table data.thing (
  thing_id int primary key,
  subject text not null, --or whatever
  owner name not null
);

Metti un trigger su data.thing per inserimenti e aggiornamenti imponendo che la colonna del proprietario sia current_user. Forse consentire solo al proprietario di eliminare i propri record (un altro trigger).

Crea una WITH CHECK OPTIONvista, che è ciò che gli utenti useranno effettivamente. Fai del tuo meglio per renderlo aggiornabile, altrimenti avrai bisogno di trigger / regole, che è più lavoro.

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner,
from data.thing
where
pg_has_role(owner, 'member') --only owner or roles "above" him can view his rows. 
WITH CHECK OPTION;

Quindi, creare una tabella di elenco di controllo di accesso:

--privileges r=read, w=write

create table security.thing_acl (
  thing_id int,
  grantee name, --the role to whom your are granting the privilege
  privilege char(1) check (privilege in ('r','w') ),

  primary key (thing_id, grantee, privilege),

  foreign key (thing_id) references data.thing(thing_id) on delete cascade
);

Cambia la tua vista per tenere conto degli ACL:

drop view public.thing;

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner
from data.thing a
where
pg_has_role(owner, 'member')
or exists (select 1 from security.thing_acl b where b.thing_id = a.thing_id and pg_has_role(grantee, 'member') and privilege='r')
with check option;

Creare una tabella dei privilegi di riga predefinita:

create table security.default_row_privileges (
  table_name name,
  role_name name,
  privilege char(1),

  primary key (table_name, role_name, privilege)
);

Metti un trigger su insert su data.thing in modo che copi i privilegi di riga predefiniti in security.thing_acl.

  • Regola la sicurezza a livello di tabella in modo appropriato (impedisce inserimenti da utenti indesiderati). Nessuno dovrebbe essere in grado di leggere i dati o gli schemi di sicurezza.
  • Regola la sicurezza a livello di colonna in modo appropriato (impedisci ad alcuni utenti di vedere / modificare alcune colonne). È possibile utilizzare has_column_privilege () per verificare che un utente possa vedere una colonna.
  • Probabilmente vuoi tag di definizione della sicurezza a tuo avviso.
  • Prendi in considerazione l'aggiunta grantore le admin_optioncolonne di tabelle acl per tenere traccia di chi ha concesso il privilegio e se l'utente autorizzato può gestire i privilegi su quella riga.
  • Lotti di prova

† In questo caso probabilmente pg_has_role non è indicizzabile. Dovresti ottenere un elenco di tutti i ruoli superiori da current_user e confrontarlo con il valore del proprietario / beneficiario.


Hai visto la parte " Non sto parlando delle autorizzazioni di accesso al database qui "?
a_horse_with_no_name

@a_horse_with_no_name sì, l'ho fatto. Poteva scrivere il proprio sistema RLS / ACL, oppure poteva usare la sicurezza integrata di un database per fare ciò che chiedeva.
Neil McGuigan,

Grazie per la tua risposta dettagliata! Tuttavia, non credo che l'utilizzo dei ruoli di database sia la risposta giusta qui, poiché non solo il personale, ma anche ogni singolo utente può disporre delle autorizzazioni. Esempi potrebbero essere "can_view_item", "can_bulk_order_item" o "can_review_item". Penso che la mia scelta originale dei nomi dei permessi ti abbia portato a credere che riguardasse solo i permessi del personale, ma tutti questi nomi erano solo esempi per sottrarre le complessità. Come ho detto nella domanda originale, si tratta delle autorizzazioni per utente , non per le autorizzazioni del personale .
JohnCand

Ad ogni modo, dover avere ruoli di database separati per ogni riga dell'utente nella tabella degli utenti sembra eccessivo e difficilmente gestibile. Tuttavia, penso che la tua risposta sia preziosa per gli sviluppatori che implementano solo le autorizzazioni del personale.
JohnCand

1
@JohnCand Non vedo davvero come sia più semplice gestire le autorizzazioni altrove, ma ti preghiamo di indicarci la tua soluzione una volta trovata! :)
Neil McGuigan

4

Hai preso in considerazione l'utilizzo dell'estensione PostgreSQL dell'elenco di controllo degli accessi ?

Contiene il tipo di dati ACE PostgreSQL nativo e una serie di funzioni che consentono di verificare se un utente dispone dell'autorizzazione per accedere ai dati. Funziona con il sistema di ruoli PostgreSQL o con numeri astratti (o UUID) che rappresentano gli ID utente / ruolo dell'applicazione.

Nel tuo caso, devi solo aggiungere una colonna ACL alle tue tabelle di dati e utilizzare una delle acl_check_accessfunzioni per verificare un utente rispetto a un ACL.

CREATE TABLE items
(
    item serial PRIMARY KEY,
    acl ace[],
    ...
);

INSERT INTO items(acl, ...) VALUES ('{a//<user id>=r, a//<role id>=rwd, ...}');

SELECT * FROM items where acl_check_access(acl, 'r', <roles of the user>, false) = 'r'

L'uso degli ACL è un modo estremamente flessibile di gestire le autorizzazioni della logica aziendale. Inoltre, è incredibilmente veloce: l'overhead medio è solo il 25% del tempo necessario per leggere un record. L'unica limitazione è che supporta un massimo di 16 autorizzazioni personalizzate per tipo di oggetto.


1

Mi viene in mente un'altra possibilità per codificare questo, quello relazionale

Se non hai bisogno del permission_per_itemtavolo, puoi saltarlo, collegarti Permissionse Itemsdirettamente al item_per_user_permissionstavolo.

inserisci qui la descrizione dell'immagine

diagramma della legenda

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.