Jak wdrożyć uprawnienia logiki biznesowej w PostgreSQL (lub SQL ogólnie)?

16

Załóżmy, że mam tabelę przedmiotów:

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

Teraz chcę wprowadzić pojęcie „uprawnień” dla każdego elementu (proszę zauważyć, że jestem nie mówię tu o uprawnieniach dostępu do bazy danych, ale o uprawnieniach logiki biznesowej dla tego elementu). Każdy element ma uprawnienia domyślne, a także uprawnienia na użytkownika, które mogą zastąpić uprawnienia domyślne.

Próbowałem wymyślić kilka sposobów na wdrożenie tego i wymyśliłem następujące rozwiązania, ale nie jestem pewien, który jest najlepszy i dlaczego:

1) Rozwiązanie logiczne

Użyj kolumny boolowskiej dla każdego pozwolenia:

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

Zalety : Każde uprawnienie ma nazwę.

Niedogodności : Istnieją dziesiątki uprawnień, które znacznie zwiększają liczbę kolumn i trzeba je zdefiniować dwukrotnie (raz w każdej tabeli).

2) Rozwiązanie Integer

Użyj liczby całkowitej i traktuj ją jak pole bitowe (tj. Bit 0 oznacza can_change_description, bit 1 oznacza can_change_priceitd. I użyj operacji bitowych, aby ustawić lub odczytać uprawnienia).

CREATE DOMAIN permissions AS integer;

Zalety : bardzo szybko.

Wady : Musisz śledzić, który bit oznacza, które uprawnienie zarówno w bazie danych, jak i interfejsie użytkownika.

3) Rozwiązanie Bitfield

Taki sam jak 2), ale użyj bit(n). Najprawdopodobniej te same zalety i wady, może nieco wolniejsze.

4) Rozwiązanie Enum

Użyj typu wyliczenia dla uprawnień:

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

a następnie utwórz dodatkową tabelę dla domyślnych uprawnień:

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

    PRIMARY KEY(item, perm)
);

i zmień tabelę definicji użytkownika na:

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

Zalety : Łatwo nazwać poszczególne uprawnienia (nie musisz obsługiwać pozycji bitów).

Niedogodności : Nawet przy pobieraniu domyślnych uprawnień wymaga dostępu do dwóch dodatkowych tabel: po pierwsze, do domyślnej tabeli uprawnień, a po drugie, do katalogu systemowego przechowującego wartości wyliczeniowe.

Zwłaszcza, że ​​należy przywrócić domyślne uprawnienia dla każdego pojedynczego wyświetlenia strony tego elementu , wpływ ostatniej alternatywy na wydajność może być znaczący.

5) Rozwiązanie Enum Array

Taki sam jak 4), ale użyj tablicy do przechowywania wszystkich (domyślnych) uprawnień:

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Zalety : Łatwo nazwać poszczególne uprawnienia (nie musisz obsługiwać pozycji bitów).

Wady : Łamie 1. normalną formę i jest trochę brzydka. Zajmuje znaczną liczbę bajtów z rzędu, jeśli liczba uprawnień jest duża (około 50).

Czy możesz wymyślić inne alternatywy?

Jakie podejście należy zastosować i dlaczego?

Uwaga: jest to zmodyfikowana wersja pytania opublikowanego wcześniej na Stackoverflow .

JohnCand
źródło
2
Mając dziesiątki różnych uprawnień, mogę wybrać jedno (lub więcej) bigintpól (każde dobre dla 64 bitów) lub ciąg znaków. Napisałem kilka powiązanych odpowiedzi na temat SO, które mogą być pomocne.
Erwin Brandstetter,

Odpowiedzi:

7

Wiem, że nie pytasz o bezpieczeństwo bazy danych per se , ale możesz robić, co chcesz, korzystając z zabezpieczeń bazy danych. Możesz nawet użyć tego w aplikacji internetowej. Jeśli nie chcesz używać zabezpieczeń bazy danych, schematy nadal obowiązują.

Potrzebujesz zabezpieczeń na poziomie kolumny, zabezpieczeń na poziomie wierszy i prawdopodobnie hierarchicznego zarządzania rolami. Bezpieczeństwo oparte na rolach jest znacznie łatwiejsze do zarządzania niż bezpieczeństwo oparte na użytkownikach.

Ten przykładowy kod dotyczy PostgreSQL 9.4, który wkrótce się pojawi. Możesz to zrobić za pomocą 9.3, ale potrzeba więcej pracy fizycznej.

Chcesz, aby wszystko było indeksowane, jeśli martwisz się wydajnością †, którą powinieneś być. Oznacza to, że pola maski bitowej i tablicy prawdopodobnie nie będą dobrym pomysłem.

W tym przykładzie trzymamy główne tabele danych w dataschemacie, a odpowiednie widoki w 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
);

Ustaw wyzwalacz dla data.thing dla wstawek i aktualizacji wymuszających, że kolumna właściciela jest bieżącym użytkownikiem. Być może tylko właściciel może usunąć własne rekordy (inny wyzwalacz).

Utwórz WITH CHECK OPTIONwidok, z którego będą korzystać użytkownicy. Naprawdę staraj się, aby można go było aktualizować, w przeciwnym razie będziesz potrzebować wyzwalaczy / reguł, co jest więcej pracy.

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;

Następnie utwórz tabelę listy kontroli dostępu:

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

Zmień widok na konta 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;

Utwórz domyślną tabelę uprawnień do wiersza:

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

  primary key (table_name, role_name, privilege)
);

Ustaw wyzwalacz wstawiania na data.thing, aby skopiował domyślne uprawnienia do wiersza do security.thing_acl.

  • Odpowiednio dostosuj zabezpieczenia na poziomie tabeli (zapobiegaj wstawianiu przez niechcianych użytkowników). Nikt nie powinien być w stanie odczytać danych ani schematów bezpieczeństwa.
  • Odpowiednio dostosuj zabezpieczenia na poziomie kolumny (uniemożliwić niektórym użytkownikom wyświetlanie / edytowanie niektórych kolumn). Możesz użyć has_column_privilege (), aby sprawdzić, czy użytkownik widzi kolumnę.
  • Prawdopodobnie chcesz mieć znacznik definiujący zabezpieczenia w swoim widoku.
  • Zastanów się nad dodaniem grantori admin_optionkolumnami do tabel acl, aby śledzić, kto przyznał uprawnienie i czy beneficjent może zarządzać uprawnieniami w tym wierszu.
  • Testuj partie

† W tym przypadku pg_has_role prawdopodobnie nie jest indeksowalny. Będziesz musiał uzyskać listę wszystkich ról nadrzędnych względem bieżącego_użytkownika i porównać z wartością właściciela / beneficjenta.

Neil McGuigan
źródło
Czy widziałeś część „ Nie mówię tutaj o uprawnieniach dostępu do bazy danych ”?
a_horse_w_no_name
@ a_horse_with_no_name tak zrobiłem. Mógłby napisać własny system RLS / ACL lub użyć wbudowanych zabezpieczeń bazy danych, aby zrobić to, o co prosi.
Neil McGuigan,
Dziękujemy za szczegółową odpowiedź! Jednak nie sądzę, aby używanie ról bazy danych było tutaj właściwą odpowiedzią, ponieważ nie tylko pracownicy, ale także każdy użytkownik może mieć uprawnienia. Przykładami mogą być „can_view_item”, „can_bulk_order_item” lub „can_review_item”. Myślę, że mój pierwotny wybór nazw uprawnień doprowadził cię do przekonania, że ​​chodzi tylko o uprawnienia pracowników, ale wszystkie te nazwiska były tylko przykładami, które pomogłyby pozbyć się złożoności. Jak powiedziałem w pierwotnym pytaniu, chodzi o uprawnienia na użytkownika , a nie na uprawnienia personelu .
JohnCand
W każdym razie konieczność posiadania osobnych ról bazy danych dla każdego wiersza użytkownika w tabeli użytkowników wydaje się przesadna i trudna do zarządzania. Uważam jednak, że twoja odpowiedź jest cenna dla programistów wdrażających tylko uprawnienia personelu.
JohnCand
1
@JohnCand Naprawdę nie widzę, jak łatwiej jest zarządzać uprawnieniami gdzie indziej, ale proszę, wskaż nam swoje rozwiązanie, gdy je znajdziesz! :)
Neil McGuigan
4

Czy rozważałeś użycie rozszerzenia PostgreSQL na liście kontroli dostępu ?

Zawiera natywny typ danych PostgreSQL ACE oraz zestaw funkcji, które pozwalają sprawdzić, czy użytkownik ma uprawnienia do dostępu do danych. Działa z systemem ról PostgreSQL lub z liczbami abstrakcyjnymi (lub UUID) reprezentującymi identyfikatory użytkownika / roli aplikacji.

W twoim przypadku po prostu dodajesz kolumnę ACL do swoich tabel danych i używasz jednej z acl_check_accessfunkcji, aby sprawdzić użytkownika na liście 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'

Korzystanie z list ACL jest niezwykle elastycznym sposobem radzenia sobie z uprawnieniami logiki biznesowej. Ponadto jest niesamowicie szybki - przeciętny koszt to tylko 25% czasu potrzebnego na odczytanie zapisu. Jedynym ograniczeniem jest to, że obsługuje maksymalnie 16 uprawnień niestandardowych na typ obiektu.

Slonopotamus
źródło
1

Mogę wymyślić inną możliwość zakodowania tego, relacyjną

Jeśli nie potrzebne permission_per_itemmoże tableyou pominąć go i podłącz Permissionsi Itemsbezpośrednio do item_per_user_permissionsstołu.

wprowadź opis zdjęcia tutaj

schemat legendy

cud173
źródło