Autorisations basées sur les rôles dans Express.js
C'est quelque chose que j'ai fait à quelques reprises, mais je l'ai trouvé un peu sujet aux erreurs avec tant de conditions, et je me demande si quelqu'un peut me diriger vers une voie plus propre. C'est une route PATCH pour éditer un utilisateur. Les utilisateurs super-administrateurs et administrateurs peuvent tous deux changer d'autres utilisateurs (avec certaines limitations) tandis que les autres types d'utilisateurs ne peuvent se modifier qu'eux-mêmes.
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);
});
```
Réponses
Je pense que j'ai trouvé une manière meilleure et plus déclarative en utilisant des schémas Joi dans des dictionnaires imbriqués.
L'idée est que nous découvrons d'abord ces trois choses:
- L'utilisateur se modifie-t-il lui-même? (booléen)
- Quel type d'utilisateur est l'éditeur? (administrateur, etc.)
- Quel type d'utilisateur est en cours de modification? (administrateur, etc.)
Et sur la base de ces trois éléments, nous pouvons rechercher le schéma Joi correct à utiliser.
Ainsi, le dictionnaire de schéma ressemble à ceci pour la route 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)
};
Et maintenant, la logique de validation des modifications devient beaucoup plus simple et moins sujette aux erreurs 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();
}
}