Almacenar elementos de menú con permisos de usuario

11

Estoy creando un sistema de menús en PHP y MySQL. Tendré varios menús diferentes y cada menú tendrá un conjunto de elementos de menú conectados.

En el sitio, también tengo diferentes permisos de usuario, algunos usuarios pueden ver todos los elementos del menú y algunos elementos están ocultos para algunos usuarios. Tengo curiosidad por saber cómo podría manejar los permisos de una manera limpia que permita agregar fácilmente más tipos de usuarios en el futuro.

Lo que tengo hasta ahora es algo como esto:

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

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

Estoy pensando que la permissioncolumna podría ser una cadena separada por comas que puedo comparar con la identificación de permiso del usuario actual. También podría ser una referencia a alguna otra tabla que defina todas las combinaciones posibles de los permisos existentes actualmente.

Una solución también podría ser simplemente almacenar múltiples elementos de menú donde la única diferencia es el permiso, aunque esto conduciría a un almacenamiento duplicado y quizás una molestia de administrar.

Me encantaría escuchar algunas ideas sobre cómo estructurar esto y lo que podría considerarse limpio, dinámico y horrible.

Gracias.

lapso
fuente
¿Los usuarios tienen algo en común? Por lo general, agruparía los elementos del menú en grupos funcionales y asignaría usuarios a esos grupos (por ejemplo: usuarios administradores, usuarios de bases de datos, comerciantes, etc.). Luego, solo administra las agrupaciones, dependiendo de la elección de la tecnología; esto se puede administrar usando algo como Active Directory.
Michael
1
Aquí está obteniendo muchas buenas respuestas, pero lo que tal vez quiera leer es ACL. en.wikipedia.org/wiki/Access_control_list
Reactgular

Respuestas:

17

Lo modelaré usando un diagrama ER.

  • A PERMISSIONes el acceso otorgado a un ROLEen un determinado MENU_ITEM.
  • Un PAPEL es un conjunto de permisos predefinidos al que se le asigna un nombre.
  • A se le USERpueden otorgar muchos ROLES.
  • Tener permisos asignados a roles en lugar de usuarios, hace que la administración de permisos sea mucho más fácil.

ingrese la descripción de la imagen aquí

Luego, puede crear una vista para no tener que escribir las combinaciones cada vez:

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)

Luego, cada vez que desee saber a qué elementos de menú tiene acceso un usuario, puede consultarlo:

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

EDITAR:

Dado que un usuario puede tener múltiples roles otorgados, los permisos de los roles pueden superponerse, es decir, dos roles distintos pueden tener acceso al mismo elemento del menú. Cuando define un rol que no sabe de antemano si tendrá algunos permisos en común con otros roles. Pero dado que se trata de la unión de conjuntos, solo importa si un permiso dado es parte del conjunto, no cuántas veces aparece, de ahí la distinctcláusula en la vista.

Tulains Córdova
fuente
Genial gracias. No puedo ver por qué no pude dibujar eso yo mismo. Creo que estaba bloqueado y no experimentado :)
lapso
Ack, pensé que entendía pero obviamente no. ¿Cómo puedo tener múltiples permisos para un solo elemento del menú?
abarcan el
1
@span Debido a que un usuario puede tener múltiples roles otorgados y los permisos de los roles pueden superponerse, es decir, dos roles distintos pueden tener acceso al mismo elemento del menú. Cuando define un rol que no sabe de antemano si el rol se otorgará junto con otro que tenga algunos permisos en común. Pero dado que este problema se trata de la unión de conjuntos, solo importa si un permiso dado es parte del conjunto o no, no cuántas veces aparece.
Tulains Córdova
Gracias, seguiré leyendo tu respuesta hasta que la reciba;). Creo que mi error fue pensar que un solo permiso podría usarse junto con el rol para separar los elementos del menú. Parece que necesito un permiso para cada 'tipo' de elemento de menú. ¡De nuevo, gracias por tu ayuda! Dibujaré algunos diagramas de Venn y veré si puedo entenderlo correctamente \ / /
abarcado el
5

Tener una lista separada por comas significa hacer una comparación de subcadenas cada vez que realiza una consulta en el menú. Esto es menos que ideal.

Necesitas normalizar la tabla:

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

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

Si aún desea la lista separada por comas (por alguna razón), se puede tirar hacia fuera con cosas tales como group_concaten MySQL wm_concat en Oracle o funciones similares en otros idiomas.

La ventaja de esto es múltiple.

Primero, está la practicidad de la llamada. Hacer una subcadena contra una cadena arbitrariamente grande (si arregla el tamaño, puede tener problemas más adelante al llenar la cadena para que comience a obtener permisos como en alugar de another_permission) significa escanear la cadena en cada fila. Esto no es algo para lo que las bases de datos estén optimizadas.

Segundo, la consulta que escribes se vuelve mucho más simple. Para determinar si el permiso 'foo' existe en una lista separada por comas, debe verificar 'foo'.

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

Sin embargo, esto dará un falso positivo si también tiene el permiso 'foobar'. Entonces ahora necesitas hacerte una prueba como

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

pero eso dará un falso negativo si 'foo' está al principio o al final de la cadena. Eso lleva a algo como

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

Notarás que es muy probable que necesites hacer múltiples escaneos de la cadena. Este camino lleva a la locura.

Tenga en cuenta que con todo esto le falta la capacidad práctica para hacer el enlace de parámetros (aún posible, simplemente se pone aún más feo).

La normalización del campo le brinda mucha más flexibilidad y legibilidad en su base de datos. No te arrepentirás.


fuente
Gracias por su excelente respuesta, me ha dado más conocimiento y estoy agradecido por eso, aunque creo que la solución user61852 se adaptará mejor por ahora.
abarcan el
3

Ese enfoque clásico de esto es User -> UserGroupy luego asociado a Menu -> MenuItem -> UserGroup. Usando un valor entero para pesar el nivel de permiso.

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

Cuando necesita mostrar un menú para el usuario actual. Puede consultar la base de datos de esta manera.

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

Eso solo seleccionará los menús que son visibles para el usuario actual en función de la condición para option1.

Alternativamente, si almacena los detalles del grupo del usuario actual en la sesión actual, no es necesario unirse.

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

Cuando habla de querer almacenar múltiples permisos por elemento de menú. Tendría cuidado de no confundir los roles de usuario y la lógica empresarial.

Reactgular
fuente
2
Este diseño solo permitiría que cada MenuItem se vincule a un solo UserGroup, lo que significa menús limitados o duplicación de datos. En realidad quieres una tabla de enlaces, idealmente. Además, su elección de nombres de tablas DB como plurales me pone triste;)
Ed James
@EdWoodcock oh muy buen punto. Debería haber ido con un nivel de permiso (int) y luego compararlo con el nivel de grupo del usuario. Voy a cambiar eso Tenga en cuenta, el hábito de nombres en plural causado por mi uso de CakePHP. Lo cual es extraño, porque el marco utiliza alias singulares para tablas en consultas.
Reactgular
@MatthewFoscarini No se preocupe, no estoy realmente molesto siempre que una base de código sea consistente;)
Ed James
1
Impresionante respuesta. Voy a tener esto en cuenta la próxima vez que haga algo parecido a esto. Por ahora, creo que la solución user61852 se adaptará mejor, ya que no requiere tantos cambios en el código existente. ¡Gracias!
abarca el