2
\$\begingroup\$

I come to see you to help me improve an express middleware that allows you to control access resources by checking the rights of the user. The following code is supposed to be as generic as possible so that it can easily be added to another project.

But I have doubts both on its genericity and the clarity of its prototype, I let you judge :)

acl.ts

export let config: ACLConfig = {
  userField: 'me',
  roleField: 'roles'
};

export function isAllowed(roles: String[], ...cbs: Function[]) {
  return async function (req, res, next) {
    let allowed = false;
    let user = req[config.userField];
    if (!user) throw new APIError('ACL module cannot find req.{{userField}}', APICodes.ACL_INTERNAL_ERROR);

    let userRoles = user[config.roleField] || '';
    userRoles = userRoles.split(',');

    if (roles && roles.length) {
      for (let role of roles) {
        if (userRoles.indexOf(role) !== -1) return next();
      }
    }

    if (cbs && cbs.length) {
      for (let cb of cbs) {
        if (cb(req) === true) return next();
      }
    }

    throw new APIError('You do not have the required privileges', APICodes.AUTH_ACCESS_FORBIDDEN, HttpStatus.UNAUTHORIZED, true);
  }
}

must be used like this

route('/...').all(verifyToken, isAllowed(['staff, admin'], policies.isMyArticle))

maybe this following prototype is better ?

isAllow(...args: String | Function)
isAllow('staff', policies.isMyArticle, 'staff');
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

The code you've provided is fairly thin and so there's not to much to pick at here, however, I think your checks on roles and cbs can be simplified using Array.isArray(), because the for of iterator cannot iterate an empty array.

if (roles && roles.length) {
  for (let role of roles) {
    if (userRoles.indexOf(role) !== -1) return next();
  }
}

can be transformed to:

if (Array.isArray(roles)) {
  for (let role of roles) {
    if (userRoles.indexOf(role) !== -1) return next();
  }
}

Although I think these checks would be better moved up and for you to return a 500 Server Error if roles or cbs aren't arrays.

In the same block of code you have indexOf(role) !== -1, which was simplified to .includes(role) in ES6.

The only other thing that comes to mind is the userRoles.split(',') call, because, if userRoles isn't a string, it'll raise an exception. However, that would depend on how malleable your database / validation is.

Additionally, your second suggested prototype has way more confusing usage than your implementation, I would stick with what you've created.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you for your advice, I will apply it. Another solution might be to create a completely generic allow (... cbs: Function []) middleware in a npm module. Then in a shared directory of the project create a function hasAnyRole (... roles). Perhaps better because the role management can be specific for a project (RBAC, ACL, etc ...) What do you think ? \$\endgroup\$
    – AntiLoxy
    Commented Nov 11, 2018 at 17:51
  • \$\begingroup\$ If every route has role checks it would be simpler to bake it in, but if not then that would work too \$\endgroup\$
    – Quill
    Commented Nov 11, 2018 at 20:08
0
\$\begingroup\$

After a few hours of research for a good logical and semantic prototype, i found a good solution.

router.put('/:articleId', AuthGuard([{ role: 'user', when: isOwner }, { role: 'admin' }]), async (req, res) => {

});

A predicate function can be added to a specific role for more flexibility. In this example, the user can only update an item "when" it owns it.

The little module :

import passport from 'passport';
import { Request, Response, NextFunction } from 'express';

export const AuthenticateGuard = passport.authenticate('jwt', { session: false });

export interface IAuthorizeItem {
  role: string;
  when?: (req: Request) => boolean;
};

export default function AuthGuard(items?: IAuthorizeItem[]) {
  return [AuthenticateGuard, AuthorizeGuard(items)];
};

export function AuthorizeGuard(items?: IAuthorizeItem[]) {
  return async function (req: Request, res: Response, next: NextFunction) {
    let user = req.user;
    if (!user) {
      return res.status(500).send('Access not allowed !');
    }

    if (typeof req.user.hasRole !== 'function') {
      throw new Error('User object must have hasRole method');
    }

    if (!items || items.length == 0) {
      return next();
    }

    for (let item of items) {
      if (req.user.hasRole(item.role)) {
        if (!item.when) {
          return next();
        }
        else {
          if (item.when(req)) {
            return next();
          }
        }
      }
    }

    res.status(500).send('Access not allowed !');
  }
};

Note: I avoid any dynamic types checking because i used typescript and this module must be used only with static object.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.