sql >> Base de Datos >  >> RDS >> Database

Administrar roles y estados en un sistema

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:

  1. No guardamos ningún historial (o datos futuros).

    Cuando cambiamos el estado o el rol, simplemente actualizamos status_id y role_id en la user_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.

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