UNPKG

@resin/pinejs

Version:

Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make

1,127 lines (1,123 loc) • 43.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = exports.config = exports.addPermissions = exports.checkPermissionsMiddleware = exports.checkPermissions = exports.apiKeyMiddleware = exports.customApiKeyMiddleware = exports.authorizationMiddleware = exports.customAuthorizationMiddleware = exports.getApiKeyPermissions = exports.getUserPermissions = exports.checkPassword = exports.nestedCheck = exports.rootRead = exports.root = exports.PermissionParsingError = exports.PermissionError = void 0; const odata_to_abstract_sql_1 = require("@resin/odata-to-abstract-sql"); const ODataParser = require("@balena/odata-parser"); const Bluebird = require("bluebird"); const _ = require("lodash"); const memoize = require("memoizee"); const randomstring = require("randomstring"); const env = require("../config-loader/env"); const sbvrUtils = require("../sbvr-api/sbvr-utils"); const errors_1 = require("./errors"); Object.defineProperty(exports, "PermissionError", { enumerable: true, get: function () { return errors_1.PermissionError; } }); Object.defineProperty(exports, "PermissionParsingError", { enumerable: true, get: function () { return errors_1.PermissionParsingError; } }); const uri_parser_1 = require("./uri-parser"); const memoizeWeak = require("memoizee/weak"); const userModel = require('./user.sbvr'); const DEFAULT_ACTOR_BIND = '@__ACTOR_ID'; const DEFAULT_ACTOR_BIND_REGEX = new RegExp(_.escapeRegExp(DEFAULT_ACTOR_BIND), 'g'); exports.root = { user: { id: 0, actor: 0, permissions: ['resource.all'], }, }; exports.rootRead = { user: { id: 0, actor: 0, permissions: ['resource.get'], }, }; const methodPermissions = { GET: { or: ['get', 'read'], }, PUT: { or: [ 'set', { and: ['create', 'update'], }, ], }, POST: { or: ['set', 'create'], }, PATCH: { or: ['set', 'update'], }, MERGE: { or: ['set', 'update'], }, DELETE: 'delete', }; const $parsePermissions = memoize((filter) => { const { tree, binds } = ODataParser.parse(filter, { startRule: 'ProcessRule', rule: 'FilterByExpression', }); return { tree, extraBinds: binds, }; }, { primitive: true, max: env.cache.parsePermissions.max, }); const rewriteBinds = ({ tree, extraBinds }, odataBinds) => { const bindsLength = odataBinds.length; odataBinds.push(...extraBinds); return _.cloneDeepWith(tree, (value) => { if (value != null) { const bind = value.bind; if (Number.isInteger(bind)) { return { bind: value.bind + bindsLength }; } } }); }; const parsePermissions = (filter, odataBinds) => { const odata = $parsePermissions(filter); return rewriteBinds(odata, odataBinds); }; const isAnd = (x) => _.isObject(x) && 'and' in x; const isOr = (x) => typeof x === 'object' && 'or' in x; function nestedCheck(check, stringCallback) { if (typeof check === 'string') { return stringCallback(check); } if (typeof check === 'boolean') { return check; } if (Array.isArray(check)) { let results = []; for (const subcheck of check) { const result = nestedCheck(subcheck, stringCallback); if (typeof result === 'boolean') { if (result === false) { return false; } } else if (isAnd(result)) { results = results.concat(result.and); } else { results.push(result); } } if (results.length === 1) { return results[0]; } if (results.length > 1) { return { and: _.uniq(results), }; } return true; } if (typeof check === 'object') { const checkTypes = Object.keys(check); if (checkTypes.length > 1) { throw new Error('More than one check type: ' + checkTypes); } const checkType = checkTypes[0]; switch (checkType.toUpperCase()) { case 'AND': const and = check[checkType]; return nestedCheck(and, stringCallback); case 'OR': const or = check[checkType]; let results = []; for (const subcheck of or) { const result = nestedCheck(subcheck, stringCallback); if (typeof result === 'boolean') { if (result === true) { return true; } } else if (isOr(result)) { results = results.concat(result.or); } else { results.push(result); } } if (results.length === 1) { return results[0]; } if (results.length > 1) { return { or: _.uniq(results), }; } return false; default: throw new Error('Cannot parse required checking logic: ' + checkType); } } throw new Error('Cannot parse required checks: ' + check); } exports.nestedCheck = nestedCheck; const collapsePermissionFilters = (v) => { if (Array.isArray(v)) { return collapsePermissionFilters({ or: v }); } if (typeof v === 'object') { if ('filter' in v) { return v.filter; } if ('and' in v) { return ['and', ...v.and.map(collapsePermissionFilters)]; } if ('or' in v) { return ['or', ...v.or.map(collapsePermissionFilters)]; } throw new Error('Permission filter objects must have `filter` or `and` or `or` keys'); } return v; }; const namespaceRelationships = (relationships, alias) => { _.forEach(relationships, (relationship, key) => { if (key === '$') { return; } let mapping = relationship.$; if (mapping != null && mapping.length === 2) { mapping = _.cloneDeep(mapping); mapping[1][0] = `${mapping[1][0]}$${alias}`; relationships[`${key}$${alias}`] = { $: mapping, }; } namespaceRelationships(relationship, alias); }); }; const getPermissionsLookup = memoize((permissions) => { const permissionsLookup = {}; for (const permission of permissions) { const [target, condition] = permission.split('?'); if (condition == null) { permissionsLookup[target] = true; } else if (permissionsLookup[target] !== true) { if (permissionsLookup[target] == null) { permissionsLookup[target] = []; } permissionsLookup[target].push(condition); } } return permissionsLookup; }, { primitive: true, max: env.cache.permissionsLookup.max, }); const $checkPermissions = (permissionsLookup, actionList, vocabulary, resourceName) => { const checkObject = { or: ['all', actionList], }; return nestedCheck(checkObject, (permissionCheck) => { const resourcePermission = permissionsLookup['resource.' + permissionCheck]; let vocabularyPermission; let vocabularyResourcePermission; if (resourcePermission === true) { return true; } if (vocabulary != null) { const maybeVocabularyPermission = permissionsLookup[vocabulary + '.' + permissionCheck]; if (maybeVocabularyPermission === true) { return true; } vocabularyPermission = maybeVocabularyPermission; if (resourceName != null) { const maybeVocabularyResourcePermission = permissionsLookup[vocabulary + '.' + resourceName + '.' + permissionCheck]; if (maybeVocabularyResourcePermission === true) { return true; } vocabularyResourcePermission = maybeVocabularyResourcePermission; } } const conditionalPermissions = _.union(resourcePermission, vocabularyPermission, vocabularyResourcePermission); if (conditionalPermissions.length === 1) { return conditionalPermissions[0]; } if (conditionalPermissions.length > 1) { return { or: conditionalPermissions, }; } return false; }); }; const convertToLambda = (filter, identifier) => { const replaceObject = (object) => { if (typeof object === 'string') { return; } if (Array.isArray(object)) { object.forEach((element) => { replaceObject(element); }); } if (object.hasOwnProperty('name')) { object.property = { ...object }; object.name = identifier; delete object.lambda; } }; replaceObject(filter); }; const rewriteSubPermissionBindings = (filter, counter) => { const rewrite = (object) => { if (object == null) { return; } if (typeof object.bind === 'number') { object.bind = counter + object.bind; } if (Array.isArray(object) || _.isObject(object)) { _.forEach(object, (v) => { rewrite(v); }); } }; rewrite(filter); }; const buildODataPermission = (permissionsLookup, actionList, vocabulary, resourceName, odata) => { const conditionalPerms = $checkPermissions(permissionsLookup, actionList, vocabulary, resourceName); if (conditionalPerms === false) { throw constrainedPermissionError; } if (conditionalPerms === true) { return false; } const permissionFilters = nestedCheck(conditionalPerms, (permissionCheck) => { try { return { filter: parsePermissions(permissionCheck, odata.binds), }; } catch (e) { console.warn('Failed to parse conditional permissions: ', permissionCheck); throw new errors_1.PermissionParsingError(e); } }); const collapsedPermissionFilters = collapsePermissionFilters(permissionFilters); return collapsedPermissionFilters; }; const constrainedPermissionError = new errors_1.PermissionError(); const generateConstrainedAbstractSql = (permissionsLookup, actionList, vocabulary, resourceName) => { const abstractSQLModel = sbvrUtils.getAbstractSqlModel({ vocabulary, }); const odata = uri_parser_1.memoizedParseOdata(`/${resourceName}`); const collapsedPermissionFilters = buildODataPermission(permissionsLookup, actionList, vocabulary, resourceName, odata); _.set(odata, ['tree', 'options', '$filter'], collapsedPermissionFilters); const lambdaAlias = randomstring.generate(20); let inc = 0; const canAccessTrace = [resourceName]; const canAccessFunction = function (property) { delete property.method; if (!this.defaultResource) { throw new Error(`No resource selected in AST.`); } const targetResource = this.NavigateResources(this.defaultResource, property.name); const targetResourceName = odata_to_abstract_sql_1.sqlNameToODataName(targetResource.resource.name); if (canAccessTrace.includes(targetResourceName)) { throw new errors_1.PermissionError(`Permissions for ${resourceName} form a circle by the following path: ${canAccessTrace.join(' -> ')} -> ${targetResourceName}`); } const parentOdata = uri_parser_1.memoizedParseOdata(`/${targetResourceName}`); const collapsedParentPermissionFilters = buildODataPermission(permissionsLookup, actionList, vocabulary, targetResourceName, parentOdata); if (collapsedParentPermissionFilters === false) { throw constrainedPermissionError; } const lambdaId = `${lambdaAlias}+${inc}`; inc = inc + 1; rewriteSubPermissionBindings(collapsedParentPermissionFilters, this.bindVarsLength + this.extraBindVars.length); convertToLambda(collapsedParentPermissionFilters, lambdaId); property.lambda = { method: 'any', identifier: lambdaId, expression: collapsedParentPermissionFilters, }; this.extraBindVars.push(...parentOdata.binds); canAccessTrace.push(targetResourceName); try { return this.Property(property); } finally { canAccessTrace.pop(); } }; const odata2AbstractSQL = new odata_to_abstract_sql_1.OData2AbstractSQL(abstractSQLModel, { canAccess: canAccessFunction, }); const { tree, extraBindVars } = odata2AbstractSQL.match(odata.tree, 'GET', [], odata.binds.length); odata.binds.push(...extraBindVars); const odataBinds = odata.binds; const abstractSqlQuery = [...tree]; const selectIndex = abstractSqlQuery.findIndex((v) => v[0] === 'Select'); const select = (abstractSqlQuery[selectIndex] = [ ...abstractSqlQuery[selectIndex], ]); select[1] = select[1].map((selectField) => { if (selectField[0] === 'Alias') { const maybeField = selectField[1]; const fieldType = maybeField[0]; if (fieldType === 'ReferencedField' || fieldType === 'Field') { return maybeField; } return [ 'Alias', maybeField, odata_to_abstract_sql_1.odataNameToSqlName(selectField[2]), ]; } if (selectField.length === 2 && Array.isArray(selectField[0])) { return selectField[0]; } return selectField; }); return { extraBinds: odataBinds, abstractSqlQuery }; }; const onceGetter = (obj, propName, fn) => { let nullableFn = fn; let thrownErr; Object.defineProperty(obj, propName, { enumerable: true, configurable: true, get() { if (thrownErr != null) { throw thrownErr; } try { const result = nullableFn(); delete this[propName]; return (this[propName] = result); } catch (e) { thrownErr = e; throw thrownErr; } finally { nullableFn = undefined; } }, }); }; const deepFreezeExceptDefinition = (obj) => { Object.freeze(obj); Object.getOwnPropertyNames(obj).forEach((prop) => { if (prop !== 'definition' && obj.hasOwnProperty(prop) && obj[prop] !== null && !['object', 'function'].includes(typeof obj[prop])) { deepFreezeExceptDefinition(obj); } }); }; const createBypassDefinition = (definition) => _.cloneDeepWith(definition, (abstractSql) => { if (Array.isArray(abstractSql) && abstractSql[0] === 'Resource' && !abstractSql[1].endsWith('$bypass')) { return ['Resource', `${abstractSql[1]}$bypass`]; } }); const getAlias = (name) => { if (name.endsWith('$bypass')) { return 'bypass'; } const [, permissionsJSON] = name.split('permissions'); if (!permissionsJSON) { return; } return `permissions${permissionsJSON}`; }; const rewriteRelationship = memoizeWeak((value, name, abstractSqlModel, permissionsLookup, vocabulary) => { let escapedName = odata_to_abstract_sql_1.sqlNameToODataName(name); if (abstractSqlModel.tables[name]) { escapedName = odata_to_abstract_sql_1.sqlNameToODataName(abstractSqlModel.tables[name].name); } const originalAbstractSQLModel = sbvrUtils.getAbstractSqlModel({ vocabulary, }); const rewrite = (object) => { var _a, _b; if ('$' in object && Array.isArray(object.$)) { const mapping = object.$; if (mapping.length === 2 && Array.isArray(mapping[1]) && mapping[1].length === 2 && typeof mapping[1][0] === 'string') { const possibleTargetResourceName = mapping[1][0]; if (possibleTargetResourceName.endsWith('$bypass')) { return; } const targetResourceEscaped = odata_to_abstract_sql_1.sqlNameToODataName((_b = (_a = abstractSqlModel.tables[possibleTargetResourceName]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : possibleTargetResourceName); if (targetResourceEscaped.includes('$')) { return; } let foundCanAccessLink = false; try { const odata = uri_parser_1.memoizedParseOdata(`/${targetResourceEscaped}`); const collapsedPermissionFilters = buildODataPermission(permissionsLookup, methodPermissions.GET, vocabulary, targetResourceEscaped, odata); _.set(odata, ['tree', 'options', '$filter'], collapsedPermissionFilters); const canAccessFunction = function (property) { delete property.method; if (!this.defaultResource) { throw new Error(`No resource selected in AST.`); } const targetResourceAST = this.NavigateResources(this.defaultResource, property.name); const targetResourceName = odata_to_abstract_sql_1.sqlNameToODataName(targetResourceAST.resource.name); const currentResourceName = odata_to_abstract_sql_1.sqlNameToODataName(this.defaultResource.name); if (currentResourceName === targetResourceEscaped && targetResourceName === escapedName) { foundCanAccessLink = true; } return ['Equals', ['Boolean', true], ['Boolean', true]]; }; const odata2AbstractSQL = new odata_to_abstract_sql_1.OData2AbstractSQL(originalAbstractSQLModel, { canAccess: canAccessFunction, }); try { odata2AbstractSQL.match(odata.tree, 'GET', [], odata.binds.length); } catch (e) { throw new ODataParser.SyntaxError(e); } if (foundCanAccessLink) { mapping[1][0] = `${possibleTargetResourceName}$bypass`; } } catch (e) { if (e === constrainedPermissionError) { return; } if (e instanceof ODataParser.SyntaxError) { return; } throw e; } } } if (Array.isArray(object) || _.isObject(object)) { _.forEach(object, (v) => { if (typeof v !== 'string') { rewrite(v); } }); } }; rewrite(value); }); const rewriteRelationships = (abstractSqlModel, relationships, permissionsLookup, vocabulary) => { const newRelationships = _.cloneDeep(relationships); _.forOwn(newRelationships, (value, name) => rewriteRelationship(value, name, abstractSqlModel, permissionsLookup, vocabulary)); return newRelationships; }; const stringifiedGetPermissions = JSON.stringify(methodPermissions.GET); const getBoundConstrainedMemoizer = memoizeWeak((abstractSqlModel) => memoizeWeak((permissionsLookup, vocabulary) => { const constrainedAbstractSqlModel = _.cloneDeep(abstractSqlModel); const origSynonyms = Object.keys(constrainedAbstractSqlModel.synonyms); constrainedAbstractSqlModel.synonyms = new Proxy(constrainedAbstractSqlModel.synonyms, { get: (synonyms, permissionSynonym) => { if (synonyms[permissionSynonym]) { return synonyms[permissionSynonym]; } const alias = getAlias(permissionSynonym); if (!alias) { return; } origSynonyms.forEach((canonicalForm, synonym) => { synonyms[`${synonym}$${alias}`] = `${canonicalForm}$${alias}`; }); return synonyms[permissionSynonym]; }, }); const origRelationships = Object.keys(constrainedAbstractSqlModel.relationships); _.forEach(constrainedAbstractSqlModel.tables, (table, resourceName) => { const bypassResourceName = `${resourceName}$bypass`; constrainedAbstractSqlModel.tables[bypassResourceName] = { ...table, }; constrainedAbstractSqlModel.tables[bypassResourceName].resourceName = bypassResourceName; if (table.definition) { constrainedAbstractSqlModel.tables[bypassResourceName].definition = createBypassDefinition(table.definition); } else { onceGetter(table, 'definition', () => constrainedAbstractSqlModel.tables[`${resourceName}$permissions${stringifiedGetPermissions}`].definition); } }); constrainedAbstractSqlModel.tables = new Proxy(constrainedAbstractSqlModel.tables, { get: (tables, permissionResourceName) => { if (tables[permissionResourceName]) { return tables[permissionResourceName]; } const [resourceName, permissionsJSON,] = permissionResourceName.split('$permissions'); if (!permissionsJSON) { return; } const permissions = JSON.parse(permissionsJSON); const table = tables[`${resourceName}$bypass`]; const permissionsTable = (tables[permissionResourceName] = { ...table, }); permissionsTable.resourceName = permissionResourceName; onceGetter(permissionsTable, 'definition', () => generateConstrainedAbstractSql(permissionsLookup, permissions, vocabulary, odata_to_abstract_sql_1.sqlNameToODataName(permissionsTable.name))); return permissionsTable; }, }); constrainedAbstractSqlModel.relationships = rewriteRelationships(constrainedAbstractSqlModel, constrainedAbstractSqlModel.relationships, permissionsLookup, vocabulary); constrainedAbstractSqlModel.relationships = new Proxy(constrainedAbstractSqlModel.relationships, { get: (relationships, permissionResourceName) => { if (relationships[permissionResourceName]) { return relationships[permissionResourceName]; } const alias = getAlias(permissionResourceName); if (!alias) { return; } for (const relationship of origRelationships) { relationships[`${relationship}$${alias}`] = relationships[relationship]; namespaceRelationships(relationships[relationship], alias); } return relationships[permissionResourceName]; }, }); deepFreezeExceptDefinition(constrainedAbstractSqlModel); return constrainedAbstractSqlModel; }, { primitive: true, })); const memoizedGetConstrainedModel = (abstractSqlModel, permissionsLookup, vocabulary) => getBoundConstrainedMemoizer(abstractSqlModel)(permissionsLookup, vocabulary); const getCheckPasswordQuery = _.once(() => sbvrUtils.api.Auth.prepare({ resource: 'user', passthrough: { req: exports.rootRead, }, options: { $select: ['id', 'actor', 'password'], $filter: { username: { '@': 'username' }, }, }, })); exports.checkPassword = Bluebird.method(async (username, password) => { const [user] = (await getCheckPasswordQuery()({ username, })); if (user == null) { throw new Error('User not found'); } const hash = user.password; const userId = user.id; const actorId = user.actor; const res = await sbvrUtils.sbvrTypes.Hashed.compare(password, hash); if (!res) { throw new Error('Passwords do not match'); } const permissions = await exports.getUserPermissions(userId); return { id: userId, actor: actorId, username, permissions, }; }); const getUserPermissionsQuery = _.once(() => sbvrUtils.api.Auth.prepare({ resource: 'permission', passthrough: { req: exports.rootRead, }, options: { $select: 'name', $filter: { $or: { is_of__user: { $any: { $alias: 'uhp', $expr: { uhp: { user: { '@': 'userId' } }, $or: [ { uhp: { expiry_date: null }, }, { uhp: { expiry_date: { $gt: { $now: null } }, }, }, ], }, }, }, is_of__role: { $any: { $alias: 'rhp', $expr: { rhp: { role: { $any: { $alias: 'r', $expr: { r: { is_of__user: { $any: { $alias: 'uhr', $expr: { uhr: { user: { '@': 'userId' } }, $or: [ { uhr: { expiry_date: null }, }, { uhr: { expiry_date: { $gt: { $now: null } }, }, }, ], }, }, }, }, }, }, }, }, }, }, }, }, }, $orderby: { name: 'asc', }, }, })); exports.getUserPermissions = Bluebird.method(async (userId) => { if (typeof userId === 'string') { userId = parseInt(userId, 10); } if (!Number.isFinite(userId)) { throw new Error(`User ID has to be numeric, got: ${typeof userId}`); } try { const permissions = (await getUserPermissionsQuery()({ userId, })); return permissions.map((permission) => permission.name); } catch (err) { sbvrUtils.api.Auth.logger.error('Error loading user permissions', err); throw err; } }); const getApiKeyPermissionsQuery = _.once(() => sbvrUtils.api.Auth.prepare({ resource: 'permission', passthrough: { req: exports.rootRead, }, options: { $select: 'name', $filter: { $or: { is_of__api_key: { $any: { $alias: 'khp', $expr: { khp: { api_key: { $any: { $alias: 'k', $expr: { k: { key: { '@': 'apiKey' } }, }, }, }, }, }, }, }, is_of__role: { $any: { $alias: 'rhp', $expr: { rhp: { role: { $any: { $alias: 'r', $expr: { r: { is_of__api_key: { $any: { $alias: 'khr', $expr: { khr: { api_key: { $any: { $alias: 'k', $expr: { k: { key: { '@': 'apiKey' } }, }, }, }, }, }, }, }, }, }, }, }, }, }, }, }, }, }, $orderby: { name: 'asc', }, }, })); const $getApiKeyPermissions = memoize(async (apiKey) => { try { const permissions = (await getApiKeyPermissionsQuery()({ apiKey, })); return permissions.map((permission) => permission.name); } catch (err) { sbvrUtils.api.Auth.logger.error('Error loading api key permissions', err); throw err; } }, { primitive: true, promise: true, max: env.cache.apiKeys.max, maxAge: env.cache.apiKeys.maxAge, }); exports.getApiKeyPermissions = Bluebird.method((apiKey) => { if (typeof apiKey !== 'string') { throw new Error('API key has to be a string, got: ' + typeof apiKey); } return $getApiKeyPermissions(apiKey); }); const getApiKeyActorIdQuery = _.once(() => sbvrUtils.api.Auth.prepare({ resource: 'api_key', passthrough: { req: exports.rootRead, }, options: { $select: 'is_of__actor', $filter: { key: { '@': 'apiKey' }, }, }, })); const apiActorPermissionError = new errors_1.PermissionError(); const getApiKeyActorId = memoize(async (apiKey) => { const apiKeys = (await getApiKeyActorIdQuery()({ apiKey, })); if (apiKeys.length === 0) { throw apiActorPermissionError; } const apiKeyActorID = apiKeys[0].is_of__actor.__id; if (apiKeyActorID == null) { throw new Error('API key is not linked to a actor?!'); } return apiKeyActorID; }, { primitive: true, promise: true, maxAge: env.cache.apiKeys.maxAge, }); const checkApiKey = Bluebird.method(async (req, apiKey) => { if (apiKey == null || req.apiKey != null) { return; } let permissions; try { permissions = await exports.getApiKeyPermissions(apiKey); } catch (err) { console.warn('Error with API key:', err); permissions = []; } let actor; if (permissions.length > 0) { actor = await getApiKeyActorId(apiKey); } req.apiKey = { key: apiKey, permissions, }; if (actor != null) { req.apiKey.actor = actor; } }); exports.customAuthorizationMiddleware = (expectedScheme = 'Bearer') => { expectedScheme = expectedScheme.toLowerCase(); return Bluebird.method(async (req, _res, next) => { try { const auth = req.header('Authorization'); if (!auth) { return; } const parts = auth.split(' '); if (parts.length !== 2) { return; } const [scheme, apiKey] = parts; if (scheme.toLowerCase() !== expectedScheme) { return; } await checkApiKey(req, apiKey); } finally { next === null || next === void 0 ? void 0 : next(); } }); }; exports.authorizationMiddleware = exports.customAuthorizationMiddleware(); exports.customApiKeyMiddleware = (paramName = 'apikey') => { if (paramName == null) { paramName = 'apikey'; } return Bluebird.method(async (req, _res, next) => { try { const apiKey = req.params[paramName] != null ? req.params[paramName] : req.body[paramName] != null ? req.body[paramName] : req.query[paramName]; await checkApiKey(req, apiKey); } finally { next === null || next === void 0 ? void 0 : next(); } }); }; exports.apiKeyMiddleware = exports.customApiKeyMiddleware(); exports.checkPermissions = Bluebird.method(async (req, actionList, resourceName, vocabulary) => { const permissionsLookup = await getReqPermissions(req); return $checkPermissions(permissionsLookup, actionList, vocabulary, resourceName); }); exports.checkPermissionsMiddleware = (action) => Bluebird.method((async (req, res, next) => { try { const allowed = await exports.checkPermissions(req, action); switch (allowed) { case false: res.sendStatus(401); return; case true: next(); return; default: throw new Error('checkPermissionsMiddleware returned a conditional permission'); } } catch (err) { sbvrUtils.api.Auth.logger.error('Error checking permissions', err, err.stack); res.sendStatus(503); } })); const getGuestPermissions = memoize(async () => { const result = (await sbvrUtils.api.Auth.get({ resource: 'user', passthrough: { req: exports.rootRead, }, options: { $select: 'id', $filter: { username: 'guest', }, }, })); if (result.length === 0) { throw new Error('No guest user'); } return _.uniq(await exports.getUserPermissions(result[0].id)); }, { promise: true }); const getReqPermissions = async (req, odataBinds = []) => { const [guestPermissions] = await Promise.all([ getGuestPermissions(), (async () => { if (req.apiKey != null && req.apiKey.actor == null && req.apiKey.permissions != null && req.apiKey.permissions.length > 0) { const actorId = await getApiKeyActorId(req.apiKey.key); req.apiKey.actor = actorId; } })(), ]); if (guestPermissions.some((p) => DEFAULT_ACTOR_BIND_REGEX.test(p))) { throw new Error('Guest permissions cannot reference actors'); } let permissions = guestPermissions; let actorIndex = 0; const addActorPermissions = (actorId, actorPermissions) => { let actorBind = DEFAULT_ACTOR_BIND; if (actorIndex > 0) { actorBind += actorIndex; actorPermissions = actorPermissions.map((actorPermission) => actorPermission.replace(DEFAULT_ACTOR_BIND_REGEX, actorBind)); } odataBinds[actorBind] = ['Real', actorId]; actorIndex++; permissions = permissions.concat(actorPermissions); }; if (req.user != null && req.user.permissions != null) { addActorPermissions(req.user.actor, req.user.permissions); } else if (req.apiKey != null && req.apiKey.permissions != null) { addActorPermissions(req.apiKey.actor, req.apiKey.permissions); } permissions = _.uniq(permissions); return getPermissionsLookup(permissions); }; exports.addPermissions = Bluebird.method(async (req, request) => { const { vocabulary, resourceName, odataQuery, odataBinds } = request; let { method } = request; let abstractSqlModel = sbvrUtils.getAbstractSqlModel(request); method = method.toUpperCase(); const isMetadataEndpoint = uri_parser_1.metadataEndpoints.includes(resourceName) || method === 'OPTIONS'; let permissionType; if (request.permissionType != null) { permissionType = request.permissionType; } else if (isMetadataEndpoint) { permissionType = 'model'; } else { const methodPermission = methodPermissions[method]; if (methodPermission != null) { permissionType = methodPermission; } else { console.warn('Unknown method for permissions type check: ', method); permissionType = 'all'; } } let permissions = req.user == null ? [] : req.user.permissions || []; permissions = permissions.concat(req.apiKey == null ? [] : req.apiKey.permissions || []); if (permissions.length > 0 && $checkPermissions(getPermissionsLookup(permissions), permissionType, vocabulary) === true) { return; } const permissionsLookup = await getReqPermissions(req, odataBinds); request.abstractSqlModel = abstractSqlModel = memoizedGetConstrainedModel(abstractSqlModel, permissionsLookup, vocabulary); if (!_.isEqual(permissionType, methodPermissions.GET)) { const sqlName = sbvrUtils.resolveSynonym(request); odataQuery.resource = `${sqlName}$permissions${JSON.stringify(permissionType)}`; } }); exports.config = { models: [ { apiRoot: 'Auth', modelText: userModel, customServerCode: exports, migrations: { '11.0.0-modified-at': ` ALTER TABLE "actor" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "api key" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "api key-has-permission" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "api key-has-role" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "permission" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "role" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "user-has-role" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE "user-has-permission" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; `, '11.0.1-modified-at': ` ALTER TABLE "role-has-permission" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; `, }, }, ], }; exports.setup = () => { sbvrUtils.addPureHook('all', 'all', 'all', { PREPARSE: ({ req }) => exports.apiKeyMiddleware(req), POSTPARSE: ({ req, request, }) => { if (request.abstractSqlQuery != null) { return; } if (request.method === 'POST' && request.odataQuery.property != null && request.odataQuery.property.resource === 'canAccess') { if (request.odataQuery.key == null) { throw new errors_1.BadRequestError(); } const { action, method } = request.values; if ((method == null) === (action == null)) { throw new errors_1.BadRequestError(); } if (method != null) { const permissions = methodPermissions[method]; if (permissions == null) { throw new errors_1.BadRequestError(); } request.permissionType = permissions; } else { request.permissionType = action; } const abstractSqlModel = sbvrUtils.getAbstractSqlModel(request); request.resourceName = request.resourceName.slice(0, -'#canAccess'.length); const resourceName = sbvrUtils.resolveSynonym(request); const resourceTable = abstractSqlModel.tables[resourceName]; if (resourceTable == null) { throw new Error('Unknown resource: ' + request.resourceName); } const idField = resourceTable.idField; request.odataQuery.options = { $select: { properties: [{ name: idField }] }, $top: 1, }; request.odataQuery.resource = request.resourceName; delete request.odataQuery.property; request.method = 'GET'; request.custom.isAction = 'canAccess'; } return exports.addPermissions(req, request); }, PRERESPOND: ({ request, data }) => { if (request.custom.isAction === 'canAccess' && _.isEmpty(data)) { throw new errors_1.PermissionError(); } }, }); sbvrUtils.addPureHook('POST', 'Auth', 'user', { POSTPARSE: async ({ request, api }) => { const result = (await api.post({ resource: 'actor', options: { returnResource: false }, })); request.values.actor = result.id; }, }); sbvrUtils.addPureHook('DELETE', 'Auth', 'user', { POSTRUN: ({ request, api }) => api.delete({ resource: 'actor', id: request.values.actor, }), }); }; //# sourceMappingURL=permissions.js.map