Armazenando itens de menu com permissões de usuário

11

Estou criando um sistema de menus em PHP e MySQL. Terei vários menus diferentes e cada menu terá um conjunto de itens de menu conectados a ele.

No site, eu também tenho permissões de usuário diferentes, alguns usuários podem ver todos os itens de menu e alguns itens estão ocultos para alguns usuários. Estou curioso para saber como lidar com as permissões de maneira limpa, permitindo que mais tipos de usuários no futuro sejam adicionados facilmente.

O que tenho até agora é algo como isto:

-------------------
|Menus
-------------------
|id| |display name|
-------------------

----------------------------------------------------------
|Menu items
----------------------------------------------------------
|id| |menu_id| |label| |link| |parent| |sort| |permission|
----------------------------------------------------------

Eu estou pensando que o permission coluna poderia ser uma seqüência separada por vírgula que eu possa corresponder ao ID de permissão do usuário atual. Também poderia ser uma referência a alguma outra tabela que define todas as combinações possíveis das permissões existentes no momento.

Uma solução também poderia ser simplesmente armazenar vários itens de menu em que a única diferença é a permissão, embora isso levasse a armazenamento duplicado e talvez um pouco de administração.

Eu adoraria ouvir algumas reflexões sobre como estruturar isso e o que poderia ser considerado limpo, dinâmico e ruim.

Obrigado.

período
fonte
Os usuários têm algo em comum? Normalmente, você agrupa itens de menu em grupos funcionais e atribui usuários a esses grupos (por exemplo: usuários administrativos, usuários de banco de dados, comerciantes etc.). Depois, basta gerenciar os agrupamentos, dependendo da escolha da tecnologia - isso pode ser gerenciado usando algo como o Active Directory.
Michael
1
Você está recebendo muitas boas respostas aqui, mas o que você pode querer ler é sobre ACL. pt.wikipedia.org/wiki/Access_control_list
Reactgular

Respostas:

17

Vou modelá-lo usando um diagrama de ER.

  • A PERMISSIONé o acesso concedido a ROLEum dado MENU_ITEM.
  • Um ROLE é um conjunto de permissões predefinidas que recebem um nome
  • A USERpode ter muitos ROLEs concedidos a ele.
  • Ter permissões atribuídas a funções, em vez de usuários, facilita muito a administração de permissões.

insira a descrição da imagem aqui

Em seguida, você pode criar uma exibição para não precisar gravar as junções sempre:

create or replace view v_user_permissions
select
    distinct mi.id, mi.menu_id. mi.label. mi.link, mi.parent, mi.sort, u.user_id
from
    user u
    join user_role ur on (u.user_id = ur.user_id)
    join role ro on (ur.role_id = role.role_id)
    join permission per on (ro.role_id = per.role_id)
    join menu_item mi on (per.metu_item_id = mi.metu_item_id)

Então, sempre que quiser saber a quais itens de menu um usuário tem acesso, você pode consultá-lo:

select * from v_user_permissions up where up.user_id = 12736 order by up.sort

EDITAR:

Como um usuário pode ter várias funções concedidas, as permissões das funções podem se sobrepor, ou seja, duas funções distintas podem ter acesso ao mesmo item de menu. Quando você define uma função, não sabe de antemão se ela terá algumas permissões em comum com outras funções. Mas como se trata da união de conjuntos, importa apenas se uma determinada permissão faz parte do conjunto ou não, quantas vezes ela aparece, daí a distinctcláusula na exibição.

Tulains Córdova
fonte
Incrível, obrigado. Não consigo ver por que eu não conseguia elaborar isso sozinho. Acho que foi bloqueado e não experiente :)
tempo de
Ack, eu pensei que tinha entendido, mas obviamente não. Como posso ter várias permissões para um único item de menu?
span
1
@span Como um usuário pode ter várias funções concedidas e as permissões das funções podem se sobrepor, ou seja, duas funções distintas podem ter acesso ao mesmo item de menu. Quando você define uma função, não sabe de antemão se ela será concedida juntamente com outras que tenham algumas permissões em comum. Mas como esse problema é sobre a união de conjuntos, importa apenas se uma determinada permissão faz parte do conjunto ou não, não quantas vezes ele aparece.
Tulains Córdova
Obrigado, continuarei lendo sua resposta até que eu a receba;). Acho que meu erro foi pensar que uma única permissão poderia ser usada junto com a função de separar os itens do menu. Parece que preciso de uma permissão para cada 'tipo' de item de menu. Mais uma vez, obrigado pela sua ajuda! Vou desenhar alguns diagramas de Venn e ver se consigo entendê-lo adequadamente \ o /
span
5

Ter uma lista separada por vírgula significa fazer uma comparação de substring cada vez que você faz uma consulta no menu. Isso é menos que o ideal.

Você precisa normalizar a tabela:

---------------------------------------------
|Menu items
---------------------------------------------
|id| |menu_id| |label| |link| |parent| |sort|
---------------------------------------------

+---------------------------+
| menu_permission           |
|---------------------------|
| |menu_permission_id| (pk) |
| |menu_id| (fk)            |
| |permission|              |
+---------------------------+

Se você ainda deseja que a lista separada por vírgula (por alguma razão), você pode retirá-lo com coisas como group_concatno mysql wm_concat no oracle ou funções similares em outras línguas.

A vantagem disso é múltipla.

Primeiro, há a praticidade da ligação. Fazer uma substring em uma string arbitrariamente grande (se você corrigir o tamanho, poderá ter problemas mais tarde ao preencher a string para começar a obter permissões como em avez de another_permission) significa digitalizar a string em cada linha. Isso não é algo para o qual os bancos de dados são otimizados.

Segundo, a consulta que você escreve se torna muito mais simples. Para determinar se a permissão 'foo' existe em uma lista separada por vírgulas, é necessário verificar se há 'foo'.

... permission like "%foo%" ...

No entanto, isso dará um falso positivo se você também tiver a permissão 'foobar'. Então agora você precisa fazer um teste como

... permission like "%,foo,%" ...

mas isso dará um falso negativo se 'foo' estiver no início ou no final da string. Isso leva a algo como

... (permission like "foo,%" 
  or permission like "%,foo,%" 
  or permission like "%,foo" ) ...

Você notará que é bem provável que você precise fazer várias varreduras da string. Este caminho leva à loucura.

Observe com tudo isso que você não tem a capacidade prática de fazer a ligação de parâmetros (ainda possível, apenas fica ainda mais feia).

A normalização do campo oferece uma flexibilidade e legibilidade muito maiores no seu banco de dados. Você não vai se arrepender.


fonte
Obrigado por sua ótima resposta, isso me proporcionou mais conhecimento e sou grato por isso, embora eu ache que a solução user61852 se adequará melhor por enquanto.
span
3

Essa abordagem clássica para isso é User -> UserGroupe depois é associada a Menu -> MenuItem -> UserGroup. Usando um valor inteiro para pesar o nível de permissão.

-------------------
|Menu
-------------------
|id| |display name|
-------------------

-------------------------------------------------------------
|Menu Item
-------------------------------------------------------------
|id| |menu_id| |label| |link| |parent| |sort| | min_option1
-------------------------------------------------------------

-------------------------------------------
| User
-------------------------------------------
|id| | username | password | user_group_id
-------------------------------------------

-------------------------------------------
| User Group
-------------------------------------------
|id| option1 | option2 | option2
-------------------------------------------

Quando você precisa exibir um menu para o usuário atual. Você pode consultar o banco de dados assim.

SELECT * FROM `MenuItem`
    LEFT JOIN `UserGroup` ON (`MenuItem`.`user_group_id` = `UserGroup`.`id`)
    LEFT JOIN `User` ON (`UserGroup`.`id` = `User`.`user_group_id` AND `User`.`id` = $current_user_id)
        WHERE `MenuItem`.`menu_id` = $menu_id AND `UserGroup`.`option1` >= `MenuItem`.`min_option1`;

Isso selecionará apenas os menus visíveis ao usuário atual com base na condição para option1.

Como alternativa, se você armazenar os detalhes do grupo do usuário atual na sessão atual, nenhuma associação será necessária.

SELECT * FROM `MenuItem` WHERE `MenuItem`.`menu_id` = $menu_id AND `MenuItem`.`min_option1` >= $user_group_option1;

Quando você fala sobre o desejo de armazenar várias permissões por item de menu. Tome cuidado para não confundir as funções do usuário e a lógica de negócios.

Reactgular
fonte
2
Esse design permitiria que cada MenuItem fosse vinculado a um único grupo de usuários, o que significa menus limitados ou duplicação de dados. Na realidade, você deseja uma tabela de links, idealmente. Além disso, sua escolha de DB nomeação mesa como plurais me deixa triste;)
Ed James
@ Edwoodcock oh muito bom ponto. Eu deveria ter ido com um nível de permissão (int) e comparado com o nível de grupo do usuário. Eu vou mudar isso. Observe que o hábito de nomes no plural é causado pelo uso do CakePHP. O que é estranho, porque a estrutura usa aliases singulares para tabelas em consultas.
Reactgular
@MatthewFoscarini Não se preocupe, eu não estou realmente incomodado enquanto uma base de código é consistente;)
Ed James
1
Resposta incrível. Vou manter isso em mente na próxima vez que fizer algo parecido com isso. Por enquanto, acho que a solução user61852 se adequará melhor, pois não requer tantas alterações no código existente. Obrigado!
span