Wie implementiere ich Geschäftslogik-Berechtigungen in PostgreSQL (oder allgemein in SQL)?

16

Nehmen wir an, ich habe eine Artikeltabelle:

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

Nun möchte ich das Konzept der "Berechtigungen" für jedes Element einführen (bitte beachten Sie, dass es sich hier nicht um Datenbankzugriffsberechtigungen handelt, sondern um Geschäftslogikberechtigungen für dieses Element). Jedes Element verfügt über Standardberechtigungen und Benutzerberechtigungen, die die Standardberechtigungen überschreiben können.

Ich habe versucht, dies auf verschiedene Arten umzusetzen, und habe die folgenden Lösungen gefunden, bin mir aber nicht sicher, welche die beste ist und warum:

1) Die Boolesche Lösung

Verwenden Sie für jede Berechtigung eine boolesche Spalte:

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

Vorteile : Jede Berechtigung ist benannt.

Nachteile : Es gibt Dutzende von Berechtigungen, die die Anzahl der Spalten erheblich erhöhen, und Sie müssen sie zweimal definieren (einmal in jeder Tabelle).

2) Die ganzzahlige Lösung

Verwenden Sie eine Ganzzahl und behandeln Sie sie als Bitfeld (dh Bit 0 ist für can_change_description, Bit 1 ist für can_change_priceusw. und verwenden Sie bitweise Operationen, um Berechtigungen festzulegen oder zu lesen).

CREATE DOMAIN permissions AS integer;

Vorteile : sehr schnell.

Nachteile : Sie müssen sowohl in der Datenbank als auch in der Front-End-Schnittstelle nachverfolgen , welches Bit für welche Berechtigung steht.

3) Die Bitfield-Lösung

Wie 2), jedoch verwenden bit(n). Wahrscheinlich die gleichen Vor- und Nachteile, vielleicht etwas langsamer.

4) Die Enum-Lösung

Verwenden Sie einen Aufzählungstyp für die Berechtigungen:

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

Erstellen Sie dann eine zusätzliche Tabelle für Standardberechtigungen:

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

    PRIMARY KEY(item, perm)
);

und ändern Sie die Definitionstabelle pro Benutzer 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)    
);

Vorteile : Einfache Benennung einzelner Berechtigungen (Bitpositionen müssen nicht bearbeitet werden).

Nachteile : Auch wenn Sie nur die Standardberechtigungen abrufen, müssen Sie auf zwei zusätzliche Tabellen zugreifen: erstens auf die Standardberechtigungstabelle und zweitens auf den Systemkatalog, in dem die Enum-Werte gespeichert sind.

Insbesondere, weil die Standardberechtigungen für jede einzelne Seitenansicht dieses Elements abgerufen werden müssen , kann die Leistung der letzten Alternative erheblich beeinträchtigt werden.

5) Die Enum Array Lösung

Entspricht 4), aber verwenden Sie ein Array, um alle (Standard-) Berechtigungen zu speichern:

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Vorteile : Einfache Benennung einzelner Berechtigungen (Bitpositionen müssen nicht bearbeitet werden).

Nachteile : Bricht die 1. Normalform und ist etwas hässlich. Nimmt eine beträchtliche Anzahl von Bytes in Folge ein, wenn die Anzahl der Berechtigungen groß ist (ungefähr 50).

Können Sie sich andere Alternativen vorstellen?

Welcher Ansatz sollte gewählt werden und warum?

Bitte beachten Sie: Dies ist eine modifizierte Version einer Frage, die zuvor bei Stackoverflow gepostet wurde .

JohnCand
quelle
2
Mit Dutzenden verschiedener Berechtigungen kann ich ein (oder mehrere) bigintFelder (jedes für 64 Bit) oder eine Bitfolge auswählen. Ich habe ein paar verwandte Antworten auf SO geschrieben, die hilfreich sein könnten.
Erwin Brandstetter

Antworten:

7

Ich weiß, dass Sie nicht nach der Datenbanksicherheit per se fragen , sondern mit der Datenbanksicherheit tun können, was Sie wollen. Sie können dies sogar in einer Web-App verwenden. Wenn Sie die Datenbanksicherheit nicht verwenden möchten, gelten die Schemas weiterhin.

Sie möchten Sicherheit auf Spaltenebene, Sicherheit auf Zeilenebene und wahrscheinlich hierarchische Rollenverwaltung. Rollenbasierte Sicherheit ist viel einfacher zu verwalten als benutzerbasierte Sicherheit.

Dieser Beispielcode ist für PostgreSQL 9.4, das bald herauskommt. Sie können es mit 9.3 tun, aber es ist mehr Handarbeit erforderlich.

Sie möchten, dass alles indexierbar ist, wenn Sie sich mit der Leistung befassen †, die Sie sein sollten. Dies bedeutet, dass Bitmasken- und Array-Felder wahrscheinlich keine gute Idee sind.

In diesem Beispiel behalten wir die Hauptdatentabellen im dataSchema und die entsprechenden Ansichten 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
);

Setzen Sie einen Auslöser für data.thing für Einfügungen und Aktualisierungen, die erzwingen, dass die Eignerspalte der aktuelle_Benutzer ist. Möglicherweise darf nur der Eigentümer seine eigenen Datensätze löschen (ein weiterer Auslöser).

Erstellen Sie eine WITH CHECK OPTIONAnsicht, die die Benutzer tatsächlich verwenden. Versuchen Sie wirklich, es aktualisierbar zu machen, sonst benötigen Sie Trigger / Regeln, was mehr Arbeit ist.

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;

Erstellen Sie als Nächstes eine Zugriffssteuerungslistentabelle:

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

Ändern Sie Ihre Ansicht, um ACLs zu berücksichtigen:

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;

Erstellen Sie eine Standardreihenberechtigungstabelle:

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

  primary key (table_name, role_name, privilege)
);

Setzen Sie einen Trigger auf insert on data.thing, damit die Standardzeilenberechtigungen in security.thing_acl kopiert werden.

  • Passen Sie die Sicherheit auf Tabellenebene entsprechend an (verhindern Sie Einfügungen unerwünschter Benutzer). Niemand sollte in der Lage sein, die Daten oder Sicherheitsschemata zu lesen.
  • Passen Sie die Sicherheit auf Spaltenebene entsprechend an (verhindern Sie, dass einige Benutzer einige Spalten sehen / bearbeiten). Sie können has_column_privilege () verwenden, um zu überprüfen, ob ein Benutzer eine Spalte sehen kann.
  • Möchte wahrscheinlich ein Sicherheits-Definer-Tag in Ihrer Ansicht haben.
  • Erwägen Sie, acl-Tabellen grantorund admin_optionSpalten hinzuzufügen , um nachzuverfolgen, wer die Berechtigung erteilt hat und ob der Berechtigte Berechtigungen für diese Zeile verwalten kann.
  • Lose testen

† In diesem Fall ist pg_has_role wahrscheinlich nicht indizierbar. Sie müssten eine Liste aller übergeordneten Rollen für current_user abrufen und stattdessen mit dem Eigentümer- / Berechtigungswert vergleichen.

Neil McGuigan
quelle
Haben Sie den Teil " Ich spreche hier nicht über Datenbankzugriffsberechtigungen " gesehen?
a_horse_with_no_name
@a_horse_with_no_name ja habe ich gemacht. Er könnte sein eigenes RLS / ACL-System schreiben oder die integrierte Sicherheit einer Datenbank verwenden, um das zu tun, wonach er fragt.
Neil McGuigan
Vielen Dank für Ihre ausführliche Antwort! Ich denke jedoch nicht, dass die Verwendung von Datenbankrollen die richtige Antwort ist, da nicht nur Mitarbeiter, sondern auch jeder einzelne Benutzer Berechtigungen haben kann. Beispiele wären 'can_view_item', 'can_bulk_order_item' oder 'can_review_item'. Ich glaube, meine ursprüngliche Wahl der Berechtigungsnamen hat Sie zu der Annahme veranlasst, dass es sich nur um Mitarbeiterberechtigungen handelt, aber all diese Namen waren nur Beispiele, um die Komplexität zu beseitigen. Wie ich in der ursprünglichen Frage sagte, geht es um pro Benutzer Berechtigungen, nicht pro Mitarbeiter Berechtigungen.
JohnCand
Auf jeden Fall scheint es übertrieben und kaum verwaltbar zu sein, für jede Benutzerzeile in der Benutzertabelle separate Datenbankrollen zu haben. Ich denke jedoch, dass Ihre Antwort für Entwickler wertvoll ist, die nur Personalberechtigungen implementieren.
JohnCand
1
@JohnCand Ich verstehe nicht wirklich, wie es einfacher ist, Berechtigungen an anderer Stelle zu verwalten, aber bitte verweisen Sie uns auf Ihre Lösung, sobald Sie sie gefunden haben! :)
Neil McGuigan
4

Haben Sie darüber nachgedacht, die PostgreSQL-Erweiterung für die Zugriffssteuerungsliste zu verwenden ?

Es enthält den nativen PostgreSQL-Datentyp ACE und eine Reihe von Funktionen, mit denen Sie überprüfen können, ob ein Benutzer über die Berechtigung zum Zugriff auf Daten verfügt. Es funktioniert entweder mit dem PostgreSQL-Rollensystem oder mit abstrakten Nummern (oder UUIDs), die die Benutzer- / Rollen-IDs Ihrer Anwendung darstellen.

In Ihrem Fall fügen Sie einfach eine ACL-Spalte zu Ihren Datentabellen hinzu und verwenden eine der acl_check_accessFunktionen, um einen Benutzer gegen eine ACL zu prüfen.

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'

Die Verwendung von ACLs ist eine äußerst flexible Möglichkeit, mit Geschäftslogikberechtigungen umzugehen. Darüber hinaus ist es unglaublich schnell - der durchschnittliche Overhead beträgt nur 25% der Zeit, die zum Lesen eines Datensatzes benötigt wird. Die einzige Einschränkung besteht darin, dass maximal 16 benutzerdefinierte Berechtigungen pro Objekttyp unterstützt werden.

Slonopotamus
quelle
1

Ich kann mir eine andere Möglichkeit vorstellen, dies zu kodieren, die relationale

Wenn Sie das nicht brauchen permission_per_itemkann tableyou es überspringen und verbinden Permissionsund Itemsdirekt an den item_per_user_permissionsTisch.

Bildbeschreibung hier eingeben

Legende
Diagramm

miracle173
quelle