@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
JavaScript
"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