Permissões baseadas em funções em Express.js
Isso é algo que eu fiz algumas vezes, mas acho que parece um pouco sujeito a erros com tantas condições, e me pergunto se alguém pode me apontar na direção de um caminho mais limpo. Esta é uma rota PATCH para editar um usuário. Usuários superadministradores e administradores podem alterar outros usuários (com algumas limitações), enquanto outros tipos de usuários podem apenas editar a si próprios.
router.patch('/:userId', async (req, res) => {
const patcher = (req as AuthRequest).user;
const otherUser = await database.getUserById(req.params.userId);
const requestedUpdate = req.body;
// 404 if user is not found.
if (!otherUser) {
return sendCannotFind(res);
}
// Basic validation of requestedUpdate
if (requestedUpdate.userType && !isUserTypeValid(requestedUpdate.userType)) {
return sendInvalidUserType(res);
}
if (patcher.userType === 'superAdmin') {
// Super admin cannot demote self.
if (otherUser.id === patcher.id && requestedUpdate.userType) {
return sendCannotSetUserType(res);
}
// Super admin cannot edit other super admins
if (otherUser.userType === 'superAdmin' && otherUser.id !== patcher.id) {
return sendCannotEdit(res);
}
} else if (patcher.userType === 'admin') {
// Admin cannot edit super admins
if (otherUser.userType === 'superAdmin') {
return sendCannotEdit(res);
}
// Admin cannot edit other admins
if (otherUser.userType === 'admin' && otherUser.id !== patcher.id) {
return sendCannotEdit(res);
}
// Admin cannot promote or demote themselves
if (otherUser.id === patcher.id && requestedUpdate.userType) {
return sendCannotSetUserType(res);
}
// Admin cannot promote anyone to admin or superAdmin
if (requestedUpdate.userType === 'admin' || requestedUpdate.userType === 'superAdmin') {
return sendCannotSetUserType(res);
}
} else {
// Non-admins cannot edit anyone but themselves
if (otherUser.id !== patcher.id) {
return sendCannotEdit(res);
}
// Non-admins cannot promote or demote themselves
if (requestedUpdate.userType && requestedUpdate.userType !== otherUser.userType) {
return sendCannotSetUserType(res);
}
}
await doEdit(otherUser, requestedUpdate);
return res.json(otherUser);
});
```
Respostas
Acho que vim com uma maneira melhor e mais declarativa usando esquemas Joi em dicionários aninhados.
A ideia é que primeiro descobramos essas três coisas:
- O usuário está editando a si mesmo? (bool)
- Que tipo de usuário é o editor? (admin, etc)
- Que tipo de usuário está sendo editado? (admin, etc)
E com base nessas três coisas, podemos pesquisar o esquema Joi correto a ser usado.
Portanto, o dicionário de esquema se parece com isto para a rota PATCH:
// No one is allowed to edit their own user type, everyone can edit their own name.
const selfEditSchemas: { [key in UserType]?: Joi.Schema } = {
[UserType.superAdmin]: Joi.object({
userType: Joi.forbidden(),
name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(), }), [UserType.admin]: Joi.object({ userType: Joi.forbidden(), name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
}),
[UserType.user]: Joi.object({
userType: Joi.forbidden(),
name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(), }), }; // We only allow admins and super admins to edit others, and we use different schemas // depending on what type of user they are trying to edit. const otherEditSchemas: { [key in UserType]: { [key in UserType]?: Joi.Schema } } = { [UserType.superAdmin]: { [UserType.admin]: Joi.object({ // (superAdmin can edit admins according to this schema) userType: Joi.string().valid(...Object.values(UserType)).optional(), name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
}),
[UserType.user]: Joi.object({ // (superAdmin can edit regular users according to this chema)
userType: Joi.string().valid(...Object.values(UserType)).optional(),
name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(), }), }, [UserType.admin]: { [UserType.user]: Joi.object({ // (admin can edit regular users according to this schema) userType: Joi.forbidden(), // (admin cannot promote regular users) name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
}),
},
[UserType.user]: {}, // (user is not allowed to edit anyone else)
};
E agora a lógica para validar edições se torna muito mais simples e menos sujeita a erros IMO:
function validateUserEdit(req: express.Request, res: express.Response, next: express.NextFunction) {
const userReq = req as UserRequest;
const isSelf = userReq.user.id === userReq.otherUser.id;
const schema = isSelf
? selfEditSchemas[userReq.user.userType]
: otherEditSchemas[userReq.user.userType][userReq.otherUser.userType];
if (!schema) {
return res.status(403).json({
message: 'Not allowed to edit that user.',
code: ErrorCodes.FORBIDDEN_TO_EDIT_USER,
});
}
const validateRes = schema.validate(req.body, { stripUnknown: true });
if (validateRes.error) {
res.status(400).json({
message: `Invalid arguments: ${validateRes.error}`,
code: ErrorCodes.INVALID_ARGUMENTS,
});
} else {
req.body = validateRes.value;
next();
}
}