Rollenbasierte Berechtigungen in Express.js

Aug 28 2020

Dies habe ich einige Male getan, aber ich habe festgestellt, dass es sich bei so vielen Bedingungen etwas fehleranfällig anfühlt, und ich frage mich, ob mich jemand in Richtung eines saubereren Weges weisen kann. Dies ist eine PATCH-Route zum Bearbeiten eines Benutzers. Superadministrator- und Administratorbenutzer können beide Benutzer (mit einigen Einschränkungen) ändern, während andere Benutzertypen nur sich selbst bearbeiten können.

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);
});
```

Antworten

Aurast Sep 08 2020 at 23:25

Ich glaube, ich habe eine bessere und aussagekräftigere Methode gefunden, um Joi-Schemata in verschachtelten Wörterbüchern zu verwenden.

Die Idee ist, dass wir zuerst diese drei Dinge herausfinden:

  1. Bearbeitet sich der Benutzer selbst? (bool)
  2. Welcher Benutzertyp ist der Editor? (admin usw.)
  3. Welcher Benutzertyp wird bearbeitet? (admin usw.)

Und basierend auf diesen drei Dingen können wir das richtige Joi-Schema nachschlagen, das verwendet werden soll.

Das Schema-Wörterbuch sieht also für die PATCH-Route folgendermaßen aus:

// 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)
};

Und jetzt wird die Logik zum Validieren von Änderungen viel einfacher und weniger fehleranfällig 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();
  }
}