Как хранить в БД права доступа?

Положим есть единицы данных «content» и есть пользователи «user».


Пользователи могут создавать content и настраивать права доступа для других пользователей по нескольким правилам:

— доступно всем

— доступно друзьям

— доступно конкретному другу/списку друзей


Правила складываются через «или». Т.е. доступно, если доступно хотя бы по одному из правил.

content [id, user_id, name]
user [id, name]
user_friend [user_id, friend_user_id]


Как организовать хранение таких правил в БД с тем, чтобы можно было составить не фулскановый запрос на выборку всех (via pagination) «content», доступных для конкретного пользователя?


Простейшая нормализованная схема хранения:

content_share_wide [content_id, type] // всем или друзьям, где type = 1 - всем, type = 2 - друзьям

content_share_user [content_id, user_id] // контент, расшаренный на user_id



При такой схеме получается невозможным составить запрос кроме как с помощью UNION, что очень негативно сказывается на производительности.


Как сделать лучше? (как вообще сделать, потому что такой union и за работающий вариант считать не хочется)
  • Вопрос задан
  • 5328 просмотров
Пригласить эксперта
Ответы на вопрос 5
@Vampiro
select count(content_id) from content_share where
user_id=-1 /* friends */ 
or
user_id=0 /* anyone */ 
or
user_id=?
Ответ написан
@dmitry_dvp Автор вопроса
Добавлю, что если применить вашу подсказку и склеить таблицы share в одну по предложенному методу, получится вот так:

select c.* from content c
join content_share cs on cs.content_id = c.id
where cs.user_id = 0 or cs.user_id = ? /* anyone or me */

union

select c.* from content c
join content_share cs on cs.content_id = c.id
join user_friend uf on c.user_id = uf.user_id
where cs.user_id = -1  /* friends */
  and uf.friend_user_id = ?


Ответ написан
@rPman
'Доступно всем' без вариантов нужно хранить в виде bolean у content, даже хотя бы в виде копии, заполняемой тригером у таблицы content_share.

'Доступно друзьям' и 'Доступно конкретному пользователю'… так ли важно разделять эти понятия. это бы имело смысл, если бы количество действий по созданию нового пользователя и добавлению прав было бы сравнимо с количеством запросов на права доступа, а это маловероятно, наверняка в вашей задаче количество запросов на чтение на порядок (или обычно это логарифм) больше изменений.
Может быть достаточно правила 'Доступно конкретному пользователю', а значит обойдетесь таблицей content_share_user {user_id,content_id}

Дальше, никогда не нужно надеяться на чистую реляционную модель. Делайте дополнительную копию на все, что читается чаще чем пишется в удобном для этого месте. Сериализованный список идентификаторов user_id в content.authorised_list (если это числа, то к примеру через ',' с обязательным ',' в конце), если их количество меньше определенного, удобен для запросов вида like '%12345,%', и ведь его можно заполнять не сразу, а периодически отдельным процессом и очищать по триггеру на изменении. Тогда основная нагрузка ляжет не на выполнение тригера, а на запросы только последних измененных данных, а их обычно не так много.
content
.authorised_list varchar = '123,234,345,' или null — для данных, которые нужно запросить из content_share_user
.authorised_all boolean
content_share_user {user_id,content_id}
Ответ написан
agathis
@agathis
Хм, несколько соображений:
1. — доступно друзьям
— доступно конкретному другу/списку друзей

по сути одно и то же. «доступно друзьям» = «Доступно списку друзей, совпадающему с френд-листом». возможно дешевле будет хранить две копии списка друзей, одна во френд-листе, одна в таких, денормализация — это вовсе не всегда плохо.

2. OR в запросе — прощай индекс. Почему не использовать юнион и два раза доступ по разным индексам? Один раз все публичные записи, второй — для списка друзей. Публичность хранить отдельным столбцом в content — не самое элегантное, но самое простое.

ИЛИ

создаем такую структуру:

table groups
(id_group number,
id_user number,
id_friend number);

table permissions
(id_permission number,
id_content number,
id_group number);

В таблице user указать «дефолтную» группу, совпадающую с френд-листом, соответственно ее обновлять.
Каждое новое разрешение «другу или списку» порождает новую группу и новый пермишен, разрешение «друзьям» — запись в permissions с указанием на дефолтную группу.

Запись в permissions без группы значит «доступно всем».
И делаем так:
SELECT * FROM content WHERE id IN (SELECT p.id_content FROM permissions p LEFT JOIN groups g ON p.group = g.id WHERE g.friend = #me#);
Ответ написан
librarian
@librarian
Права доступа для конкретного пользователя лучше всего хранить в виде 11000100101, то есть в int и делать проверку на право доступа тупо сдвигом на два.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы