concise
Version:
A tool belt for concise schemas
189 lines (152 loc) • 6.39 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
/* eslint-disable no-prototype-builtins, no-eval */
// ====================================
// Main
// ====================================
class Authorizer {
constructor(rules) {
this.rules = rules;
} // Determine if a request for a certain operation can
// be authorised to the user (the viewer) or is rejected.
// Find the first matching rule and execute it.
// If not rule is found, the request is rejected.
async can(req) {
const rule = this.rules.find(o => matchesRule(req, o));
if (!rule) return false;
return executeRule(req, rule);
}
} // ====================================
// Rule filtering
// ====================================
// The request must match all of the rule's specified filters
// (concerning the who, what and where of potential requests)
const matchesRule = (req, rule) => {
if (!matchesFilter(req.viewerId, rule.viewerId)) return false;
if (!matchesFilter(req.roleNames, rule.roleNames, true)) return false;
if (!matchesFilter(req.operation, rule.operation)) return false;
if (!matchesFilter(req.baseId, rule.baseId)) return false;
if (!matchesFilter(req.baseType, rule.baseType)) return false;
if (!matchesFilter(req.targetName, rule.targetName)) return false;
if (!matchesFilter(req.targetId, rule.targetId)) return false;
if (!matchesFilter(req.targetType, rule.targetType)) return false;
if (!matchesFilter(req.targetBefore, rule.targetBefore)) return false;
if (!matchesFilter(req.targetAfter, rule.targetAfter)) return false;
if (!matchesFilter(req.isClientSide, rule.isClientSide)) return false;
return true;
};
const matchesFilter = (actualValue, filterSpec, isPlural) => {
if (filterSpec === undefined) return true;
return isPlural ? matchesPlural(actualValue, filterSpec) : matchesSingular(actualValue, filterSpec);
};
const matchesSingular = (actualValue, filterSpec) => {
if (filterSpec == null || typeof filterSpec !== 'object') {
return actualValue === filterSpec;
}
const operators = Object.keys(filterSpec);
for (let i = 0; i < operators.length; i++) {
const operator = operators[i];
const refValue = filterSpec[operator];
let matches = false;
if (operator === '$is') {
matches = actualValue === refValue;
} else if (operator === '$isnt') {
matches = actualValue !== refValue;
} else if (operator === '$in') {
matches = refValue.indexOf(actualValue) >= 0;
} else if (operator === '$notIn') {
matches = refValue.indexOf(actualValue) < 0;
} else {
throw new Error(`Unknown operator '${operator}' in auth filter (singular)`);
}
if (!matches) return false;
}
return true;
};
const matchesPlural = (actualValues, filterSpec) => {
if (filterSpec == null || typeof filterSpec !== 'object') {
throw new Error('Plural filter in auth rule must be an object');
}
const operators = Object.keys(filterSpec);
for (let i = 0; i < operators.length; i++) {
const operator = operators[i];
const refValue = filterSpec[operator];
let matches = false;
if (operator === '$include') {
matches = actualValues.indexOf(refValue) >= 0;
} else if (operator === '$dontInclude') {
matches = actualValues.indexOf(refValue) < 0;
} else if (operator === '$includeAny' || operator === '$dontIncludeAny') {
for (let k = 0; k < refValue.length; k++) {
if (actualValues.indexOf(refValue[k])) {
matches = true;
break;
}
}
if (operator === '$dontIncludeAny') matches = !matches;
} else {
throw new Error(`Unknown operator '${operator}' in auth filter (plural)`);
}
if (!matches) return false;
}
return true;
}; // ====================================
// Rule execution
// ====================================
const executeRule = async (req, rule) => {
const {
can
} = rule; // Many rules can have a simple, immediate YES/NO result
if (can === true || can === false) return can; // For others, some additional checks must pass first. Note that
// checks may result in a YES, a NO, or a DON'T KNOW (`null`).
// A DON'T KNOW may occur when we don't have enough information.
// In principle, it should be taken as a NO.
const checks = Array.isArray(can) ? can : [can];
for (let i = 1; i < checks.length; i++) {
const check = checks[i]; // Route-based checks
if (check.type === 'targetBefore->viewerId') {
if (!req.hasOwnProperty('targetBefore')) return null; // missing data
if (req.viewerId == null) throwNoViewer();
const result = await req.checkRoute(req.targetBefore, check.route, req.viewerId);
if (!result) return false;
} else if (check.type === 'targetAfter->viewerId') {
if (!req.hasOwnProperty('targetBefore')) return null; // missing data
if (!req.hasOwnProperty('targetAfter')) return null; // missing data
if (req.viewerId == null) throwNoViewer();
const result = await req.checkRoute(req.targetAfter, check.route, req.viewerId);
if (!result) return false;
} else if (check.type === 'root->viewerId') {
if (req.viewerId == null) throwNoViewer();
const result = await req.checkRoute(null, check.route, req.viewerId);
if (!result) return false;
} else if (check.type === 'viewer->targetId') {
if (req.viewer == null) throwNoViewer();
if (req.targetId == null) throwNoTarget();
const result = await req.checkRoute(req.viewer, check.route, req.targetId);
if (!result) return false; // Custom checks
} else if (check.type === 'satisfies') {
const {
fn: fnSpec
} = check;
const fn = typeof fnSpec === 'function' ? fnSpec : eval(fnSpec);
const result = await fn(req);
if (result !== true) return result; // can be false or null (missing data)
}
} // All checks passed
return true;
}; // ====================================
// Helpers
// ====================================
const throwNoViewer = () => {
throw new Error('Auth route failed: no viewer');
};
const throwNoTarget = () => {
throw new Error('Auth route failed: no target');
}; // ====================================
// Public
// ====================================
var _default = Authorizer;
exports.default = _default;