Como implementar permissões de lógica de negócios no PostgreSQL (ou SQL em geral)?


16

Vamos supor que eu tenha uma tabela de itens:

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

Agora, quero introduzir o conceito de "permissões" para cada item (observe, aqui não estou falando de permissões de acesso ao banco de dados, mas de permissões de lógica de negócios para esse item). Cada item tem permissões padrão e também permissões por usuário que podem substituir as permissões padrão.

Tentei pensar em várias maneiras de implementar isso e vi as seguintes soluções, mas não tenho certeza sobre qual é o melhor e por que:

1) A solução booleana

Use uma coluna booleana para cada permissão:

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

Vantagens : Cada permissão é nomeada.

Desvantagens : Existem dezenas de permissões que aumentam significativamente o número de colunas e você deve defini-las duas vezes (uma vez em cada tabela).

2) A Solução Inteira

Use um número inteiro e trate-o como um campo de bits (ou seja, o bit 0 é para can_change_description, o bit 1 é para can_change_pricee assim por diante, e use operações bit a bit para definir ou ler permissões).

CREATE DOMAIN permissions AS integer;

Vantagens : muito rápido.

Desvantagens : Você deve acompanhar qual bit representa a permissão no banco de dados e na interface front-end.

3) A solução Bitfield

O mesmo que 2), mas use bit(n). Provavelmente as mesmas vantagens e desvantagens, talvez um pouco mais lentas.

4) A solução Enum

Use um tipo de enum para as permissões:

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

e crie uma tabela extra para permissões padrão:

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

    PRIMARY KEY(item, perm)
);

e altere a tabela de definição por usuário para:

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

Vantagens : Fácil de nomear permissões individuais (você não precisa lidar com posições de bits).

Desvantagens : Mesmo ao recuperar as permissões padrão, é necessário acessar duas tabelas adicionais: primeiro, a tabela de permissões padrão e, segundo, o catálogo do sistema que armazena os valores de enumeração.

Especialmente porque as permissões padrão devem ser recuperadas para cada visualização de página única desse item , o impacto no desempenho da última alternativa pode ser significativo.

5) A solução de matriz Enum

Igual a 4), mas use uma matriz para armazenar todas as permissões (padrão):

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

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Vantagens : Fácil de nomear permissões individuais (você não precisa lidar com posições de bits).

Desvantagens : quebra a 1ª forma normal e é um pouco feio. Ocupa um número considerável de bytes em uma linha se o número de permissões for grande (cerca de 50).

Você consegue pensar em outras alternativas?

Qual abordagem deve ser adotada e por quê?

Observe: esta é uma versão modificada de uma pergunta postada anteriormente no Stackoverflow .


2
Com dezenas de permissões diferentes, posso escolher um (ou mais) bigintcampos (cada um bom para 64 bits) ou uma sequência de bits. Escrevi algumas respostas relacionadas ao SO que podem ser úteis.
amigos estão dizendo sobre erwin brandstetter

Respostas:


7

Sei que você não está perguntando sobre a segurança do banco de dados em si , mas pode fazer o que quiser usando a segurança do banco de dados. Você pode até usar isso em um aplicativo da web. Se você não deseja usar a segurança do banco de dados, os esquemas ainda se aplicam.

Você deseja segurança no nível da coluna, segurança no nível da linha e, provavelmente, gerenciamento de funções hierárquico. A segurança baseada em funções é muito mais fácil de gerenciar do que a segurança baseada no usuário.

Este código de exemplo é para o PostgreSQL 9.4, que será lançado em breve. Você pode fazer isso com a 9.3, mas há mais trabalho manual necessário.

Você quer que tudo seja indexável se estiver preocupado com o desempenho †, o que deveria ser. Isso significa que os campos de máscara de bits e matriz provavelmente não serão uma boa ideia.

Neste exemplo, mantemos as principais tabelas de dados no dataesquema e as visualizações correspondentes em 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
);

Coloque um gatilho em data.thing para inserções e atualizações, impondo que a coluna do proprietário seja o current_user. Talvez permita apenas que o proprietário exclua seus próprios registros (outro gatilho).

Crie uma WITH CHECK OPTIONvisualização, que é o que os usuários realmente usarão. Tente realmente atualizá-lo, caso contrário você precisará de gatilhos / regras, o que é mais trabalhoso.

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;

Em seguida, crie uma tabela da lista de controle de acesso:

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

Altere sua visualização para considerar ACLs:

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;

Crie uma tabela de privilégios de linha padrão:

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

  primary key (table_name, role_name, privilege)
);

Coloque um gatilho na inserção em data.thing para que ele copie os privilégios de linha padrão para security.thing_acl.

  • Ajuste a segurança no nível da tabela adequadamente (evite inserções de usuários indesejados). Ninguém deve conseguir ler os dados ou esquemas de segurança.
  • Ajuste a segurança no nível da coluna adequadamente (impeça que alguns usuários vejam / editem algumas colunas). Você pode usar has_column_privilege () para verificar se um usuário pode ver uma coluna.
  • Provavelmente, você quer a etiqueta definidora de segurança na sua exibição.
  • Considere adicionar grantore admin_optioncolunas às tabelas acl para rastrear quem concedeu o privilégio e se o donatário pode gerenciar privilégios nessa linha.
  • Lotes de teste

† Nesse caso, pg_has_role provavelmente não é indexável. Você precisaria obter uma lista de todas as funções superiores ao current_user e comparar com o valor do proprietário / donatário.


Você viu a parte " Não estou falando sobre permissões de acesso ao banco de dados aqui "?
A_horse_with_no_name

@a_horse_with_no_name sim eu fiz. Ele poderia escrever seu próprio sistema RLS / ACL ou poderia usar a segurança interna de um banco de dados para fazer o que estava solicitando.
Neil McGuigan

Obrigado pela sua resposta detalhada! No entanto, não acho que o uso de funções de banco de dados seja a resposta certa, pois não apenas a equipe, mas também todos os usuários podem ter permissões. Os exemplos seriam 'can_view_item', 'can_bulk_order_item' ou 'can_review_item'. Acho que minha escolha original de nomes de permissão levou você a acreditar que se trata apenas de permissões da equipe, mas todos esses nomes foram apenas exemplos para abstrair as complexidades. Como eu disse na pergunta original, trata-se de permissões por usuário , não por funcionários .
JohnCand

De qualquer forma, ter que ter funções de banco de dados separadas para cada linha de usuário na tabela de usuários parece ser um exagero e dificilmente gerenciável. No entanto, acho que sua resposta é valiosa para desenvolvedores que implementam apenas permissões da equipe.
JohnCand

11
@JohnCand Eu realmente não vejo como é mais fácil gerenciar permissões em outros lugares, mas aponte-nos para a sua solução assim que a encontrar! :)
Neil McGuigan

4

Você já pensou em usar a extensão PostgreSQL da Lista de controle de acesso ?

Ele contém o tipo de dados ACE nativo do PostgreSQL e um conjunto de funções que permitem verificar se um usuário tem permissão para acessar dados. Ele funciona com o sistema de funções do PostgreSQL ou com números abstratos (ou UUIDs) que representam os IDs de usuário / função do aplicativo.

No seu caso, basta adicionar uma coluna ACL às suas tabelas de dados e usar uma das acl_check_accessfunções para verificar um usuário em relação a uma 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'

O uso de ACLs é uma maneira extremamente flexível de lidar com permissões de lógica de negócios. Além disso, é incrivelmente rápido - a sobrecarga média é de apenas 25% do tempo necessário para ler um registro. A única limitação é que ele suporta no máximo 16 permissões personalizadas por tipo de objeto.


1

Eu posso pensar em outra possibilidade de codificar isso, a relacional

Se você não precisar da permission_per_itemmesa, pode ignorá-la, conectar-se Permissionse Itemsdiretamente à item_per_user_permissionsmesa.

insira a descrição da imagem aqui

diagrama de legenda

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.