UNPKG

wkr-util

Version:
275 lines (219 loc) 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CheckExistsUuid = exports.CheckExists = exports.CheckPermissionAny = exports.CheckPermission = exports.allowedAll = exports.allowedAny = exports.CheckJwt = exports.GroupsRolesCache = void 0; var _express = require("express"); var _urlJoin = _interopRequireDefault(require("url-join")); var _nodeFetch = _interopRequireDefault(require("node-fetch")); var _f = require("@cullylarson/f"); var _p = require("@cullylarson/p"); var _expressJwt = _interopRequireDefault(require("express-jwt")); var _jwksRsa = require("jwks-rsa"); var _isUuid = _interopRequireDefault(require("is-uuid")); var _ResponseError = _interopRequireDefault(require("./errors/ResponseError")); var _ = require("./"); var _log = require("./log"); var _validateMustExist = _interopRequireDefault(require("./validators/validateMustExist")); var _ForbiddenError = _interopRequireDefault(require("./errors/ForbiddenError")); var _PageNotFoundError = _interopRequireDefault(require("./errors/PageNotFoundError")); var _CannotContinueError = _interopRequireDefault(require("./errors/CannotContinueError")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const fetchAllGroupsRolesDefault = (authApiUrl, appAuth) => { return appAuth.getToken().then(token => (0, _nodeFetch.default)((0, _urlJoin.default)(authApiUrl, '/api/v1/.well-known/all-roles-groups'), { method: 'get', headers: { Authorization: `Bearer ${token}` } })).then(_.responseData).then(({ response, data }) => { if (!response.ok) { throw new _ResponseError.default(response.status, data, `Failed to get groups and roles. Got status: ${response.status}`); } return data; }); }; // fetches all groups and roles from the auth API and caches them const GroupsRolesCache = ({ authApiUrl, appAuth, cacheFor = 1200, // in seconds. 1200 seconds === 20 minutes maxNumRetries = 3, // if we fail, try again; number of times to try again fetchAllGroupsRoles = fetchAllGroupsRolesDefault // a function that will resolve to what is returned by the auth .well-known/all-roles-groups endpoint }) => { fetchAllGroupsRoles = (0, _p.retry)(maxNumRetries, fetchAllGroupsRoles); let lastResult; let lastResultFetched; return { getAllGroupsRoles: async () => { const now = (0, _.nowStamp)(); if (!lastResult || !lastResultFetched || now - lastResultFetched > cacheFor) { lastResult = await fetchAllGroupsRoles(authApiUrl, appAuth); lastResultFetched = (0, _.nowStamp)(); } return lastResult; } }; }; // checks that a jwt is provided and is valid. if so, will add the decoded jwt to jwtDecoded and the list of permissions to jwtPermissions, on the request. // // if appAuth and authApiUrl, or a non-default fetchAllGroupsRoles, are provided, will fetch all groups and roles (and their associated permissions) from the auth API and use them when doing permission checks. this allows JWTs to only list the groups, roles, and permissions a user has rather than exahustively list all of them, since we can use the list from the auth API to see if a certain permission is in the roles and groups the user has. exports.GroupsRolesCache = GroupsRolesCache; const CheckJwt = (jwksUri, audience, issuer, jwtClaimsNamespace, // namespace for custom claims (expects to find permissions, roles, groups keys) { authApiUrl, // if this and appAuth are provided, will ignore fetchAllGroupsRoles appAuth, fetchAllGroupsRoles = fetchAllGroupsRolesDefault, // will use this function to fetch all of the groups and roles cacheFor = 1200, // in seconds. 1200 seconds === 20 minutes logRepo = null } = {}) => { const check = (0, _expressJwt.default)({ // Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint. secret: (0, _jwksRsa.expressJwtSecret)({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri }), audience, issuer, algorithms: ['RS256'], requestProperty: 'jwtDecoded' // the request object will get the decoded jwt at this key }); const augJwtAccount = (req, res, next) => { req.jwtAccount = (0, _f.get)(['jwtDecoded', jwtClaimsNamespace, 'account'], null, req); return next(); }; const augJwtPermissions = (req, res, next) => { const jwtPermissionsKey = [jwtClaimsNamespace, 'permissions']; const jwtRolesKey = [jwtClaimsNamespace, 'roles']; const jwtGroupsKey = [jwtClaimsNamespace, 'groups']; const permissions = jwtPermissionsKey ? (0, _f.compose)((0, _f.get)(jwtPermissionsKey, []), (0, _f.get)('jwtDecoded', {}))(req) : []; const roles = jwtRolesKey ? (0, _f.compose)((0, _f.get)(jwtRolesKey, []), (0, _f.get)('jwtDecoded', {}))(req) : []; const groups = jwtGroupsKey ? (0, _f.compose)((0, _f.get)(jwtGroupsKey, []), (0, _f.get)('jwtDecoded', {}))(req) : []; req.jwtPermissions = Array.isArray(permissions) ? permissions : []; req.jwtRoles = Array.isArray(roles) ? roles : []; req.jwtGroups = Array.isArray(groups) ? groups : []; return next(); }; // will add all of the permissions associated with the JWT's roles and groups to req.jwtPermissions const AugPermissionsFromGroupsAndRoles = groupsRolesCache => async (req, res, next) => { if (!groupsRolesCache) return next(); let allGroupsRoles; try { allGroupsRoles = await groupsRolesCache.getAllGroupsRoles(); } catch (err) { logRepo && logRepo.add('check-jwt-get-all-groups-roles-fail', _log.logLevels.ERROR, 'Exception while fetching all groups and roles.', { data: { authApiUrl, cacheFor } }, req, err); return next(new _CannotContinueError.default('permission-fetch-fail', 'Permissions could not be fetched in order to authorize the request.')); } const permissionsSet = new Set(req.jwtPermissions); req.jwtRoles.forEach(roleName => { const rolePermissions = (0, _f.get)(['roles', roleName.toLowerCase(), 'permissions'], [], allGroupsRoles); rolePermissions.forEach(permission => permissionsSet.add(permission)); }); req.jwtGroups.forEach(groupName => { const groupPermissions = (0, _f.get)(['groups', groupName.toLowerCase(), 'permissions'], [], allGroupsRoles); groupPermissions.forEach(permission => permissionsSet.add(permission)); }); req.jwtPermissions = Array.from(permissionsSet); next(); }; const groupsRolesCache = authApiUrl && appAuth || fetchAllGroupsRoles !== fetchAllGroupsRolesDefault ? GroupsRolesCache({ authApiUrl, appAuth, fetchAllGroupsRoles, cacheFor }) : null; // compose the middlewarres return (0, _express.Router)().use(check, augJwtAccount, augJwtPermissions, AugPermissionsFromGroupsAndRoles(groupsRolesCache)); }; // at least one permission passed must match. // permissionsOrRequest can be an array of permissions, or the request object. if the // request object, will grab permissions from it. exports.CheckJwt = CheckJwt; const allowedAny = (permissionsOrRequest, checkPermissions) => { checkPermissions = (0, _f.liftA)(checkPermissions).filter(x => !!x); const permissions = (0, _f.compose)((0, _f.filter)(x => !!x), _f.liftA, x => Array.isArray(x) ? x : (0, _f.get)('jwtPermissions', [], x) // the request object )(permissionsOrRequest); return (0, _f.compose)((0, _f.reduce)((acc, x) => { // already matched if (acc) return acc; // ends with a :, so can match a category of permissions if (/:$/.test(x)) { const regex = new RegExp(`^${x}`); return permissions.filter(regex.test.bind(regex)).length > 0; } // must matched a permission exactly else { return permissions.includes(x); } }, false), (0, _f.filter)(x => !!x), _f.liftA)(checkPermissions); }; // all permissions passed must match. // // permissionsOrRequest can be an array of permissions, or the request object. if the // request object, will grab permissions from it. exports.allowedAny = allowedAny; const allowedAll = (permissionsOrRequest, checkPermissions) => { checkPermissions = (0, _f.liftA)(checkPermissions).filter(x => !!x); const permissions = (0, _f.compose)((0, _f.filter)(x => !!x), _f.liftA, x => Array.isArray(x) ? x : (0, _f.get)('jwtPermissions', [], x) // the request object )(permissionsOrRequest); return (0, _f.compose)((0, _f.reduce)((acc, x) => { // already found one that doesn't match if (!acc) return acc; // ends with a :, so can match a category of permissions if (/:$/.test(x)) { const regex = new RegExp(`^${x}`); return permissions.filter(regex.test.bind(regex)).length > 0; } // must matched a permission exactly else { return permissions.includes(x); } }, true), (0, _f.filter)(x => !!x), _f.liftA)(checkPermissions); }; // checks that the current account has all of the provided permissions. // relies on jwtPermissions being present on the request (so use CheckJwt first). exports.allowedAll = allowedAll; const CheckPermission = permissionsToCheck => (req, res, next) => { if (allowedAll(req, permissionsToCheck)) { next(); } else { next(new _ForbiddenError.default('not-permitted', { message: 'Not permitted to access this resource.' })); } }; // checks that the current account has any of the provided permissions. // relies on jwtPermissions being present on the request (so use CheckJwt first). exports.CheckPermission = CheckPermission; const CheckPermissionAny = permissionsToCheck => (req, res, next) => { if (allowedAny(req, permissionsToCheck)) { next(); } else { next(new _ForbiddenError.default('not-permitted', { message: 'Not permitted to access this resource.' })); } }; exports.CheckPermissionAny = CheckPermissionAny; const CheckExists = (pool, tableName, paramNameToColumnName) => (req, res, next) => { (0, _validateMustExist.default)(pool, tableName, paramNameToColumnName, undefined, req.params).then(x => { x.isValid ? next() : next(new _PageNotFoundError.default()); }).catch(err => { next(err); }); }; // makes sure a row in the database exists based on a uuid provided as a request param. if // the id param is not a valid uuid, will not hit the database (passing a non-valid uuid to // mysql's UUID_TO_BIN function will throw an error). // if record not found, will next with a PageNotFoundError. exports.CheckExists = CheckExists; const CheckExistsUuid = (pool, tableName, idParamName, idColumnName = undefined) => (req, res, next) => { idColumnName = idColumnName || idParamName; const idParam = (0, _f.get)(['params', idParamName], undefined, req); if (!idParam || !_isUuid.default.v1(idParam)) return next(new _PageNotFoundError.default()); CheckExists(pool, tableName, { [idParamName]: [idColumnName, 'UUID_TO_BIN(?)'] })(req, res, next); }; exports.CheckExistsUuid = CheckExistsUuid;