
En las aplicaciones modernas que admiten autenticación, a menudo queremos cambiar lo que es visible para el usuario, dependiendo de su función. Por ejemplo, un usuario invitado puede ver un artículo, pero solo un usuario registrado o administrador ve un botón para eliminar este artículo.
Gestionar esta visibilidad puede ser una pesadilla completa con roles crecientes. Probablemente haya escrito o visto código como este:
if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
Dicho código se distribuye en toda la aplicación y generalmente se convierte en un gran problema cuando el cliente cambia los requisitos o solicita agregar roles adicionales. Al final, debe revisar todos los if-s y agregar verificaciones adicionales.
En este artículo, mostraré una forma alternativa de implementar la administración de permisos en la API de Expressjs utilizando una biblioteca llamada CASL . Simplifica enormemente el control de acceso y le permite reescribir el ejemplo anterior a algo como esto:
if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
¿Nuevo en CASL? Recomiendo leer ¿Qué es CASL?
Nota: Este artículo se publicó originalmente en Medium.
Aplicación de demostración
Como aplicación de prueba, hice una API REST bastante simple para el blog . La aplicación consta de 3 entidades ( User
, Post
y Comment
) y 4 módulos (un módulo para cada entidad y otro para verificación de autorización). Todos los módulos se pueden encontrar en la carpeta src/modules
.
La aplicación utiliza modelos de mangosta , autenticación de pasaportes y autorización basada en CASL (o control de acceso). Usando la API, el usuario puede:
- lea todos los artículos y comentarios
- crear un usuario (es decir, registrarse)
- administrar sus propios artículos (crear, editar, eliminar), si está autorizado
- actualizar información personal si está autorizado
- gestione sus propios comentarios si está autorizado
Para instalar esta aplicación, simplemente clónela desde github y ejecute npm install
y npm start
. También debe iniciar el servidor MongoDB, la aplicación se conecta a mongodb://localhost:27017/blog
. Después de que todo esté listo, puedes jugar un poco y, para hacerlo más divertido, importa los datos básicos de la carpeta db/
:
mongorestore ./db
Alternativamente, puede seguir las instrucciones en el archivo del proyecto README o usar mi colección Postman .
Cual es el truco
En primer lugar, la gran ventaja de CASL es que le permite determinar los derechos de acceso en un solo lugar, ¡para todos los usuarios! En segundo lugar, CASL no se centra en quién es el usuario, sino en lo que puede hacer, es decir, en sus capacidades. Esto le permite distribuir estas capacidades a diferentes roles o grupos de usuarios sin un esfuerzo innecesario. Esto significa que podemos registrar derechos de acceso para usuarios autorizados y no autorizados:
const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() }
Analicemos ahora el código escrito anteriormente. Se AbilityBuilder
una instancia de AbilityBuilder
-a en la función defineAbilitiesFor(user)
, su método de extracción divide este objeto en 2 funciones simples que can
y cannot
y una matriz de rules
( cannot
pueden usarse en este código). Luego, usando las llamadas a la función can
, determinamos lo que el usuario puede hacer: el primer argumento pasa la acción (o una matriz de acciones), el segundo argumento es el tipo de objeto sobre el cual se realiza la acción (o una matriz de tipos), y el objeto de condición se puede pasar como el tercer argumento opcional. El objeto de condición se usa cuando se verifican los derechos de acceso en una instancia de la clase, es decir comprueba si la propiedad del author
del objeto de post
y user._id
son iguales, si lo son, se devolverá true
, de lo contrario false
. Para mayor claridad, daré un ejemplo:
// Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) // post.author === user._id, true
Luego, usando if (user)
determinamos los derechos de acceso para el usuario autorizado (si el usuario no está autorizado, no sabemos quién es y no tenemos un objeto con información sobre el usuario). Al final, devolvemos una instancia de la clase Ability
, con la ayuda de la cual verificaremos los derechos de acceso.
A continuación, creamos la constante ANONYMOUS_ABILITY
, es una instancia de la clase Ability
para usuarios no autorizados. Al final, exportamos middleware express, que es responsable de crear una instancia de Ability
para un usuario específico.
API de prueba
Probemos lo que pasó con Postman . Primero debe obtener accessToken, para esto envíe una solicitud:
POST /session { "session": { "email": "casl@medium.com", "password": "password" } }
Obtienes algo como esto en respuesta:
{ "accessToken": "...." }
Este token debe insertarse en el Authorization header
y enviarse con todas las solicitudes posteriores.
Ahora intentemos actualizar el artículo.
PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } }
Todo funciona bien Pero, ¿qué pasa si actualizamos el artículo de otra persona?
PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" }
Tengo un error! Como se esperaba :)
Ahora imaginemos que para los autores de nuestro blog queremos crear una página donde puedan ver todas las publicaciones que pueden actualizar. Desde el punto de vista de la lógica específica, esto no es difícil, solo necesita seleccionar todos los artículos en los que autor es igual a user._id
. Pero ya registramos dicha lógica con la ayuda de CASL, sería muy conveniente obtener todos esos artículos de la base de datos sin escribir solicitudes adicionales, y si los derechos cambian, entonces tendrá que cambiar la solicitud: trabajo adicional :).
Afortunadamente, CASL tiene un paquete npm adicional: @ casl / mongoose . ¡Este paquete le permite consultar registros de MongoDB de acuerdo con permisos específicos! Para mangosta, este paquete proporciona un complemento que agrega el método accessibleBy(ability, action)
al modelo. Usando este método, también solicitaremos registros de la base de datos (lea más sobre esto en la documentación de CASL y el archivo del paquete README ).
Así es exactamente cómo /posts
implementa el handler
para /posts
(también agregué la capacidad de especificar para qué acción necesita verificar los permisos):
Post.accessibleBy(req.ability, req.query.action)
Entonces, para resolver el problema descrito anteriormente, simplemente agregue el parámetro action=update
:
GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] }
En conclusión
Gracias a CASL, tenemos una muy buena manera de administrar los derechos de acceso. Estoy más que seguro de que el tipo de construcción
if (ability.can('read', post)) ...
mucho más claro y fácil que
if (user.role === ADMIN || user.auth && todo.author === user.id) ...
Con CASL, podemos ser más claros sobre lo que hace nuestro código. Además, dichas comprobaciones ciertamente se utilizarán en otras partes de nuestra aplicación, y es aquí donde CASL ayudará a evitar la duplicación de código.
Espero que haya estado tan interesado en leer sobre CASL como yo estaba interesado en crearlo. CASL tiene bastante buena documentación , seguramente encontrará mucha información útil allí, pero no dude en hacer preguntas si en el chat de gitter y agrega un asterisco en el github ;)