Как реализовать разрешения бизнес-логики в PostgreSQL (или SQL в целом)?

16

Предположим, у меня есть таблица предметов:

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

Теперь я хочу представить концепцию «разрешений» для каждого элемента (обратите внимание, я здесь говорю не о разрешениях доступа к базе данных, а о разрешениях бизнес-логики для этого элемента). Каждый элемент имеет разрешения по умолчанию, а также разрешения для каждого пользователя, которые могут переопределять разрешения по умолчанию.

Я попытался придумать несколько способов реализовать это и придумал следующие решения, но я не уверен, какой из них лучший и почему:

1) Булево решение

Используйте логический столбец для каждого разрешения:

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

Преимущества : Каждое разрешение названо.

Недостатки : существуют десятки разрешений, которые значительно увеличивают количество столбцов, и вам нужно определить их дважды (один раз в каждой таблице).

2) Целочисленное решение

Используйте целое число и обрабатывайте его как битовое поле (т. Е. Бит 0 для can_change_description, бит 1 для can_change_priceи т. Д. И используйте побитовые операции для установки или чтения разрешений).

CREATE DOMAIN permissions AS integer;

Преимущества : очень быстро.

Недостатки : необходимо отслеживать, какой бит обозначает какое разрешение, как в базе данных, так и во внешнем интерфейсе.

3) Решение битового поля

То же, что 2), но использовать bit(n). Скорее всего, те же преимущества и недостатки, возможно, немного медленнее.

4) Решение Enum

Используйте тип enum для разрешений:

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

а затем создайте дополнительную таблицу для разрешений по умолчанию:

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

    PRIMARY KEY(item, perm)
);

и измените таблицу определения для пользователя на:

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

Преимущества : легко назвать индивидуальные разрешения (вам не нужно обрабатывать битовые позиции).

Недостатки : даже при простом получении разрешений по умолчанию требуется доступ к двум дополнительным таблицам: во-первых, к таблице разрешений по умолчанию и, во-вторых, к системному каталогу, в котором хранятся значения перечисления.

Тем более, что разрешения по умолчанию должны быть получены для каждого отдельного просмотра страницы этого элемента , влияние последней альтернативы на производительность может быть значительным.

5) Решение Enum Array

То же, что 4), но использовать массив для хранения всех (по умолчанию) разрешений:

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Преимущества : легко назвать индивидуальные разрешения (вам не нужно обрабатывать битовые позиции).

Недостатки : ломает 1-ую нормальную форму и немного уродлив. Занимает значительное количество байтов подряд, если количество разрешений велико (около 50).

Можете ли вы придумать другие альтернативы?

Какой подход следует использовать и почему?

Обратите внимание: это измененная версия вопроса, опубликованного ранее в Stackoverflow .

JohnCand
источник
2
С десятками разных разрешений я мог бы выбрать одно (или более) bigintполей (каждое подходит для 64 бит) или битовую строку. Я написал пару связанных ответов на SO, которые могут помочь.
Эрвин Брандштеттер,

Ответы:

7

Я знаю, что вы не спрашиваете о безопасности базы данных как таковой , но вы можете делать то, что вы хотите, используя безопасность базы данных. Вы даже можете использовать это в веб-приложении. Если вы не хотите использовать защиту базы данных, схемы все еще применяются.

Требуется защита на уровне столбцов, защита на уровне строк и, возможно, иерархическое управление ролями. Ролевая безопасность намного проще в управлении, чем пользовательская безопасность.

Этот пример кода для PostgreSQL 9.4, который скоро выйдет. Вы можете сделать это с 9.3, но требуется больше ручного труда.

Вы хотите, чтобы все было индексируемым, если вы обеспокоены производительностью †, которой вы должны быть. Это означает, что битовая маска и поля массива, вероятно, не будут хорошей идеей.

В этом примере мы сохраняем основные таблицы данных в dataсхеме и соответствующие представления в 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
);

Поместите триггер data.thing для вставок и обновлений, следя за тем, чтобы столбец владельца был current_user. Возможно, разрешить только владельцу удалять свои записи (еще один триггер).

Создайте WITH CHECK OPTIONпредставление, которое пользователи будут использовать. Постарайтесь сделать его обновляемым, иначе вам понадобятся триггеры / правила, что требует больше усилий.

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;

Затем создайте таблицу со списком контроля доступа:

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

Измените свой вид на учетную запись для 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;

Создайте таблицу привилегий строки по умолчанию:

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

  primary key (table_name, role_name, privilege)
);

Установите триггер для вставки в data.thing так, чтобы он копировал привилегии строки по умолчанию в security.thing_acl.

  • Отрегулируйте безопасность на уровне таблицы соответствующим образом (предотвратите вставки от нежелательных пользователей). Никто не должен быть в состоянии прочитать данные или схемы безопасности.
  • Отрегулируйте безопасность на уровне столбцов соответствующим образом (не позволяйте некоторым пользователям видеть / редактировать некоторые столбцы). Вы можете использовать has_column_privilege (), чтобы проверить, что пользователь может видеть столбец.
  • Вероятно, вы хотите, чтобы тег безопасности определял ваш взгляд.
  • Рассмотрите возможность добавления grantorи admin_optionстолбцов в таблицы acl, чтобы отслеживать, кто предоставил привилегию, и может ли получатель гранта управлять привилегиями в этой строке.
  • Тестовые лоты

† В этом случае pg_has_role, вероятно, не индексируется. Вы должны получить список всех вышестоящих ролей для current_user и сравнить со значением владельца / получателя.

Нил Макгиган
источник
Вы видели часть " Я не говорю о правах доступа к базе данных здесь "?
a_horse_with_no_name
@a_horse_with_no_name да, я сделал. Он мог написать свою собственную систему RLS / ACL или использовать встроенную защиту базы данных, чтобы делать то, что он просит.
Нил Макгиган
Спасибо за ваш подробный ответ! Тем не менее, я не думаю, что использование ролей базы данных является правильным ответом, поскольку не только сотрудники, но и каждый пользователь может иметь разрешения. Примерами могут быть «can_view_item», «can_bulk_order_item» или «can_review_item». Я думаю, что мой первоначальный выбор имен разрешений заставил вас поверить, что речь идет только о разрешениях сотрудников, но все эти имена были просто примерами, позволяющими абстрагироваться от сложностей. Как я уже говорил в первоначальном вопросе, речь идет о разрешениях для каждого пользователя , а не для сотрудников .
JohnCand
В любом случае, наличие отдельных ролей базы данных для каждой пользовательской строки в таблице пользователей кажется излишним и трудно управляемым. Тем не менее, я думаю, что ваш ответ полезен для разработчиков, которые реализуют только разрешения сотрудников.
JohnCand
1
@JohnC, и я не понимаю, как проще управлять разрешениями в других местах, но, пожалуйста, укажите нам свое решение, как только вы его найдете! :)
Нил Макгиган
4

Рассматривали ли вы возможность использования расширения Access Control List PostgreSQL?

Он содержит собственный тип данных PostgreSQL ACE и набор функций, которые позволяют вам проверить, есть ли у пользователя разрешение на доступ к данным. Он работает либо с системой ролей PostgreSQL, либо с абстрактными числами (или UUID), представляющими идентификаторы пользователя / роли вашего приложения.

В вашем случае вы просто добавляете столбец ACL в свои таблицы данных и используете одну из acl_check_accessфункций, чтобы проверить пользователя по 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'

Использование ACL - это чрезвычайно гибкий способ работы с разрешениями бизнес-логики. Кроме того, это невероятно быстро - средняя нагрузка составляет всего 25% времени, необходимого для чтения записи. Единственным ограничением является то, что он поддерживает максимум 16 пользовательских разрешений для каждого типа объекта.

Slonopotamus
источник
1

Я могу думать о другой возможности закодировать это, реляционная

Если вам не нужен permission_per_itemtableyou может пропустить его и подключить Permissionsи Itemsнепосредственно к item_per_user_permissionsстолу.

введите описание изображения здесь

схема легенды

miracle173
источник