Hay muchas formas de resolver un problema, y ese es el caso de la administración de roles y estados de usuario en los sistemas de software. En este artículo encontrará una evolución simple de esa idea, así como algunos consejos útiles y ejemplos de código.
Idea básica
En la mayoría de los sistemas, suele ser necesario tener roles y estados de usuario .
Los roles están relacionados con derechos que los usuarios tienen mientras usan un sistema después de iniciar sesión correctamente. Ejemplos de roles son "empleado del centro de llamadas", "gerente del centro de llamadas", "empleado de la oficina administrativa", "gerente de la oficina administrativa" o "gerente". En general, eso significa que un usuario tendrá acceso a alguna funcionalidad si tiene el rol apropiado. Es prudente suponer que un usuario puede tener varios roles al mismo tiempo.
Los estados son mucho más estrictos y determinan si el usuario tiene derechos para iniciar sesión en el sistema o no. Un usuario solo puede tener un estado a la vez Ejemplos de estados serían:"trabajando", "de vacaciones", "de baja por enfermedad", "contrato finalizado".
Cuando cambiamos el estado de un usuario, aún podemos mantener todos los roles relacionados con ese usuario sin cambios. Eso es muy útil porque la mayoría de las veces queremos cambiar solo el estado del usuario. Si un usuario que trabaja como empleado del centro de llamadas se va de vacaciones, simplemente podemos cambiar su estado a "de vacaciones" y devolverlo al estado "trabajando" cuando regrese.
Probar roles y estados durante el inicio de sesión nos permite decidir qué sucederá. Por ejemplo, tal vez queramos prohibir el inicio de sesión incluso si el nombre de usuario y la contraseña son correctos. Podríamos hacerlo si el estado actual del usuario no implica que esté trabajando o si el usuario no tiene ningún rol en el sistema.
En todos los modelos que figuran a continuación, las tablas status
y role
son iguales.
Tabla status
tiene los campos id
y status_name
y el atributo is_active
. Si el atributo is_active
está configurado en "Verdadero", eso significa que el usuario que tiene ese estado está trabajando actualmente. Por ejemplo, el estado “en funcionamiento” tendría el atributo is_active
con un valor de True, mientras que otros ("de vacaciones", "de baja por enfermedad", "contrato terminado") tendrían un valor de False.
La tabla de roles tiene solo dos campos:id
y role_name
.
La user_account
la tabla es la misma que la user_account
tabla presentada en este artículo. Solo en el primer modelo la user_account
la tabla contiene dos atributos adicionales (role_id
y status_id
).
Se presentarán algunos modelos. Todos ellos funcionan y se pueden utilizar, pero tienen sus ventajas y desventajas.
Modelo sencillo
La primera idea podría ser que simplemente agreguemos relaciones de clave externa a la user_account
tabla, haciendo referencia en las tablas status
y role
. Ambos role_id
y status_id
son obligatorios.
Esto es bastante simple de diseñar y también para manejar datos con consultas, pero tiene algunas desventajas:
-
No guardamos ningún historial (o datos futuros).
Cuando cambiamos el estado o el rol, simplemente actualizamos
status_id
yrole_id
en lauser_account
mesa. Eso funcionará bien por ahora, así que cuando hagamos un cambio se reflejará en el sistema. Esto está bien si no necesitamos saber cómo los estados y roles han cambiado históricamente. También existe el problema de que no podemos agregar futuro rol o estado sin agregar tablas adicionales a este modelo. Una situación en la que probablemente nos gustaría tener esa opción es cuando sabemos que alguien estará de vacaciones a partir del próximo lunes. Otro ejemplo es cuando tenemos un nuevo empleado; tal vez queramos ingresar a su estado y función ahora y que sea válido en algún momento en el futuro.También hay una complicación en caso de que tengamos eventos programados que usan roles y estados. Los eventos que preparan los datos para el siguiente día hábil generalmente se ejecutan mientras la mayoría de los usuarios no usan el sistema (por ejemplo, durante la noche). Entonces, si alguien no trabajará mañana, tendremos que esperar hasta el final del día actual y luego cambiar sus roles y estado según corresponda. Por ejemplo, si tenemos empleados que actualmente trabajan y tienen el rol de "empleado del centro de llamadas", obtendrán una lista de clientes a los que deben llamar. Si alguien por error tuviera ese estatus y rol también conseguirá sus clientes y tendremos que dedicar tiempo a corregirlo.
-
El usuario solo puede tener un rol a la vez.
Por lo general, los usuarios deberían poder tener más de un rol en el sistema. Tal vez en el momento en que estás diseñando la base de datos no hay necesidad de algo así. Tenga en cuenta que podrían ocurrir cambios en el flujo de trabajo/proceso. Por ejemplo, en algún momento el cliente podría decidir fusionar dos roles en uno. Una posible solución es crear un nuevo rol y asignarle todas las funcionalidades de los roles anteriores. La otra solución (si los usuarios pueden tener más de un rol) sería que el cliente simplemente asigne ambos roles a los usuarios que los necesitan. Por supuesto, esa segunda solución es más práctica y le brinda al cliente la capacidad de ajustar el sistema a sus necesidades más rápido (lo cual no es compatible con este modelo).
Por otro lado, este modelo también tiene una gran ventaja sobre los demás. Es simple, por lo que las consultas para cambiar estados y roles también serían simples. Además, una consulta que verifica si el usuario tiene derechos para iniciar sesión en el sistema es mucho más simple que en otros casos:
select user_account.id, user_account.role_id from user_account left join status on user_account.status_id = status.id where status.is_user_working = True and user_account.user_name = @user_name and user_account.password_hash_algorithm = @password;
@user_name y @password son variables de un formulario de entrada, mientras que la consulta devuelve el ID del usuario y el role_id que tiene. En los casos en que el nombre de usuario o la contraseña no son válidos, el par de nombre de usuario y contraseña no existe, o el usuario tiene un estado asignado que no está activo, la consulta no arrojará ningún resultado. De esa manera podemos prohibir el inicio de sesión.
Este modelo podría utilizarse en los casos en que:
- estamos seguros de que no habrá cambios en el proceso que requieran que los usuarios tengan más de un rol
- no necesitamos realizar un seguimiento de los roles/cambios de estado en el historial
- No esperamos tener mucha administración de roles/estados.
Componente de tiempo agregado
Si necesitamos realizar un seguimiento de la función y el historial de estado de un usuario, debemos agregar muchas a muchas relaciones entre la user_account
y role
y la user_account
y status
. Por supuesto, eliminaremos role_id
y status_id
desde la user_account
mesa. Las nuevas tablas en el modelo son user_has_role
y user_has_status
y todos los campos en ellos, excepto las horas de finalización, son obligatorios.
La tabla user_has_role
contiene datos sobre todos los roles que los usuarios alguna vez tuvieron en el sistema. La clave alternativa es (user_account_id
, role_id
, role_start_time
) porque no tiene sentido asignar el mismo rol al mismo tiempo a un usuario más de una vez.
La tabla user_has_status
contiene datos sobre todos los estados que los usuarios alguna vez tuvieron en el sistema. La clave alternativa aquí es (user_account_id
, status_start_time
) porque un usuario no puede tener dos estados que comiencen exactamente al mismo tiempo.
La hora de inicio no puede ser nula porque cuando insertamos un nuevo rol/estado, sabemos el momento desde el que comenzará. La hora de finalización puede ser nula en caso de que no sepamos cuándo finalizará el rol/estado (por ejemplo, el rol es válido desde mañana hasta que suceda algo en el futuro).
Además de tener un historial completo, ahora podemos agregar estados y roles en el futuro. Pero esto crea complicaciones porque tenemos que comprobar si hay superposición cuando hacemos una inserción o una actualización.
Por ejemplo, el usuario solo puede tener un estado a la vez. Antes de insertar un nuevo estado, debemos comparar la hora de inicio y la hora de finalización de un nuevo estado con todos los estados existentes para ese usuario en la base de datos. Podemos usar una consulta como esta:
select * from user_has_status where user_has_status.user_account_id = @user_account_id and ( # test if @start_time included in interval of some previous status (user_has_status.status_start_time <= @start_time and ifnull(user_has_status.status_end_time, "2200-01-01") >= @start_time) or # test if @end_time included in interval of some previous status (user_has_status.status_start_time <= @end_time and ifnull(user_has_status.status_end_time, "2200-01-01") >= ifnull(@end_time, "2199-12-31")) or # if @end_time is null we cannot have any statuses after @start_time (@end_time is null and user_has_status.status_start_time >= @start_time) or # new status "includes" old satus (@start_time <= user_has_status.status_start_time <= @end_time) (user_has_status.status_start_time >= @start_time and user_has_status.status_start_time <= ifnull(@end_time, "2199-12-31")) )
@start_time
y @end_time
son variables que contienen la hora de inicio y la hora de finalización de un estado que queremos insertar y @user_account_id
es el ID de usuario para el que lo insertamos. @end_time
puede ser nulo y debemos manejarlo en la consulta. Para este propósito, los valores nulos se prueban con ifnull()
función. Si el valor es nulo, se asigna un valor de fecha alto (lo suficientemente alto como para que cuando alguien note un error en la consulta nos hayamos ido :). La consulta comprueba todas las combinaciones de hora de inicio y hora de finalización para un nuevo estado en comparación con la hora de inicio y la hora de finalización de los estados existentes. Si la consulta devuelve algún registro, entonces tenemos una superposición con los estados existentes y deberíamos prohibir la inserción del nuevo estado. También sería bueno generar un error personalizado.
Si queremos verificar la lista de roles y estados actuales (derechos de usuario), simplemente probamos usando la hora de inicio y la hora de finalización.
select user_account.id, user_has_role.id from user_account left join user_has_role on user_has_role.user_account_id = user_account.id left join user_has_status on user_account.id = user_has_status.user_account_id left join status on user_has_status.status_id = status.id where user_account.user_name = @user_name and user_account.password_hash_algorithm = @password and user_has_role.role_start_time <= @time and ifnull(user_has_role.role_end_time,"2200-01-01") >= @time and user_has_status.status_start_time <= @time and ifnull(user_has_status.status_end_time,"2200-01-01") >= @time and status.is_user_working = True
@user_name
y @password
son variables del formulario de entrada mientras que @time
podría establecerse en Now(). Cuando un usuario intenta iniciar sesión, queremos verificar sus derechos en ese momento. El resultado es una lista de todos los roles que tiene un usuario en el sistema en caso de que el nombre de usuario y la contraseña coincidan y el usuario tenga actualmente un estado activo. Si el usuario tiene un estado activo pero no tiene roles asignados, la consulta no devolverá nada.
Esta consulta es más sencilla que la del apartado 3 y este modelo nos permite tener un historial de estados y roles. Además, podemos administrar estados y roles para el futuro y todo funcionará bien.
Modelo Final
Esto es solo una idea de cómo se podría cambiar el modelo anterior si quisiéramos mejorar el rendimiento. Dado que un usuario solo puede tener un estado activo a la vez, podríamos agregar status_id
en la user_account
tabla (current_status_id
). De esa forma, podemos probar el valor de ese atributo y no tendremos que unirnos al user_has_status
mesa. La consulta modificada se vería así:
select user_account.id, user_has_role.id from user_account left join user_has_role on user_has_role.user_account_id = user_account.id left join status on user_account.current_status_id = status.id where user_account.user_name = @user_name and user_account.password_hash_algorithm = @password and user_has_role.role_start_time <= @time and ifnull(user_has_role.role_end_time,"2200-01-01") >= @time and status.is_user_working = True
Obviamente, esto simplifica la consulta y conduce a un mejor rendimiento, pero hay un problema mayor que debería resolverse. El current_status_id
en la user_account
La tabla debe revisarse y cambiarse si es necesario en las siguientes situaciones:
- en cada inserción/actualización/eliminación en
user_has_status
mesa - todos los días en un evento programado, debemos verificar si el estado de alguien cambió (el estado activo actual expiró o algún estado futuro se volvió activo) y actualizarlo en consecuencia
Sería prudente guardar los valores que las consultas utilizarán con frecuencia. Así evitaremos hacer las mismas comprobaciones una y otra vez y dividir el trabajo. Aquí evitaremos unirnos al user_has_status
tabla y haremos cambios en current_status_id
solo cuando suceden (insertar/actualizar/eliminar) o cuando el sistema no se usa demasiado (los eventos programados generalmente se ejecutan cuando la mayoría de los usuarios no usan el sistema). Quizás en este caso no ganaríamos mucho con current_status_id
pero mira esto como una idea que puede ayudar en situaciones similares.