jinaga
Version:
Data management for web and mobile applications.
434 lines • 19.3 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.describeAuthorizationRules = exports.AuthorizationRules = exports.AuthorizationRuleSpecification = exports.AuthorizationRuleNone = exports.AuthorizationRuleAny = void 0;
const memory_store_1 = require("../memory/memory-store");
const user_1 = require("../model/user");
const description_1 = require("../specification/description");
const model_1 = require("../specification/model");
const specification_1 = require("../specification/specification");
const specification_parser_1 = require("../specification/specification-parser");
const storage_1 = require("../storage");
const fn_1 = require("../util/fn");
const trace_1 = require("../util/trace");
class FactGraph {
constructor(factRecords) {
this.factRecords = factRecords;
}
getField(reference, name) {
const record = this.findFact(reference);
if (record === null) {
throw new Error(`The fact ${reference.type}:${reference.hash} is not defined.`);
}
return record.fields[name];
}
executeSpecification(givenName, matches, label, fact) {
const references = {
[givenName]: {
type: fact.type,
hash: fact.hash
}
};
const results = this.executeMatches(references, matches);
return results.map(result => result[label]);
}
executeMatches(references, matches) {
const results = matches.reduce((tuples, match) => tuples.flatMap(tuple => this.executeMatch(tuple, match)), [references]);
return results;
}
executeMatch(references, match) {
let results = [];
if (match.conditions.length === 0) {
throw new Error("A match must have at least one condition.");
}
const firstCondition = match.conditions[0];
if (firstCondition.type === "path") {
const result = this.executePathCondition(references, match.unknown, firstCondition);
results = result.map(reference => (Object.assign(Object.assign({}, references), { [match.unknown.name]: {
type: reference.type,
hash: reference.hash
} })));
}
else {
throw new Error("The first condition must be a path condition.");
}
const remainingConditions = match.conditions.slice(1);
for (const condition of remainingConditions) {
results = this.filterByCondition(references, match.unknown, results, condition);
}
return results;
}
executePathCondition(references, unknown, pathCondition) {
if (!references.hasOwnProperty(pathCondition.labelRight)) {
throw new Error(`The label ${pathCondition.labelRight} is not defined.`);
}
const start = references[pathCondition.labelRight];
const predecessors = pathCondition.rolesRight.reduce((set, role) => this.executePredecessorStep(set, role.name, role.predecessorType), [start]);
if (pathCondition.rolesLeft.length > 0) {
throw new Error('Cannot execute successor steps on evidence.');
}
return predecessors;
}
executePredecessorStep(set, name, predecessorType) {
return (0, fn_1.flatten)(set, reference => {
const record = this.findFact(reference);
if (record === null) {
throw new Error(`The fact ${reference.type}:${reference.hash} is not defined.`);
}
const predecessors = (0, memory_store_1.getPredecessors)(record, name);
return predecessors.filter(predecessor => predecessor.type === predecessorType);
});
}
filterByCondition(references, unknown, results, condition) {
if (condition.type === "path") {
const otherResults = this.executePathCondition(references, unknown, condition);
return results.filter(result => otherResults.some((0, storage_1.factReferenceEquals)(result[unknown.name])));
}
else if (condition.type === "existential") {
const matchingReferences = results.filter(result => {
const matches = this.executeMatches(result, condition.matches);
return condition.exists ?
matches.length > 0 :
matches.length === 0;
});
return matchingReferences;
}
else {
const _exhaustiveCheck = condition;
throw new Error(`Unknown condition type: ${_exhaustiveCheck.type}`);
}
}
findFact(reference) {
var _a;
return (_a = this.factRecords.find((0, storage_1.factReferenceEquals)(reference))) !== null && _a !== void 0 ? _a : null;
}
}
class AuthorizationRuleAny {
describe(type) {
return ` any ${type}\n`;
}
isAuthorized(userFact, fact, graph, store) {
return Promise.resolve(true);
}
getAuthorizedPopulation(candidateKeys, fact, graph, store) {
return Promise.resolve({
quantifier: 'everyone'
});
}
}
exports.AuthorizationRuleAny = AuthorizationRuleAny;
class AuthorizationRuleNone {
describe(type) {
return ` no ${type}\n`;
}
isAuthorized(userFact, fact, graph, store) {
trace_1.Trace.warn(`No fact of type ${fact.type} is authorized.`);
return Promise.resolve(false);
}
getAuthorizedPopulation(candidateKeys, fact, graph, store) {
return Promise.resolve({
quantifier: 'none'
});
}
}
exports.AuthorizationRuleNone = AuthorizationRuleNone;
class AuthorizationRuleSpecification {
constructor(specification) {
this.specification = specification;
}
describe(type) {
const description = (0, description_1.describeSpecification)(this.specification, 1);
return description;
}
isAuthorized(userFact, fact, graph, store) {
return __awaiter(this, void 0, void 0, function* () {
if (!userFact) {
trace_1.Trace.warn(`No user is logged in while attempting to authorize ${fact.type}.`);
return false;
}
// The specification must be given a single fact.
if (this.specification.given.length !== 1) {
throw new Error('The specification must be given a single fact.');
}
// The projection must be a singular label.
if (this.specification.projection.type !== 'fact') {
throw new Error('The projection must be a singular label.');
}
const label = this.specification.projection.label;
// Split the specification.
// The head is deterministic, and can be run on the graph.
// The tail is non-deterministic, and must be run on the store.
const { head, tail } = (0, specification_1.splitBeforeFirstSuccessor)(this.specification);
// If there is no head, then the specification is unsatisfiable.
if (head === undefined) {
throw new Error('The specification must start with a predecessor join. Otherwise, it is unsatisfiable.');
}
// Execute the head on the graph.
if (head.projection.type !== 'fact') {
throw new Error('The head of the specification must project a fact.');
}
let results = graph.executeSpecification(head.given[0].label.name, head.matches, head.projection.label, fact);
// If there is a tail, execute it on the store.
if (tail !== undefined) {
if (tail.given.length !== 1) {
throw new Error('The tail of the specification must be given a single fact.');
}
const tailResults = [];
for (const result of results) {
const users = yield store.read([result], tail);
tailResults.push(...users.map(user => user.tuple[label]));
}
results = tailResults;
}
// If any of the results match the user, then the user is authorized.
const authorized = results.some((0, storage_1.factReferenceEquals)(userFact));
return authorized;
});
}
getAuthorizedPopulation(candidateKeys, fact, graph, store) {
return __awaiter(this, void 0, void 0, function* () {
if (candidateKeys.length === 0) {
trace_1.Trace.warn(`No candidate keys were given while attempting to authorize ${fact.type}.`);
return {
quantifier: 'none'
};
}
// The specification must be given a single fact.
if (this.specification.given.length !== 1) {
throw new Error('The specification must be given a single fact.');
}
// The projection must be a singular label.
if (this.specification.projection.type !== 'fact') {
throw new Error('The projection must be a singular label.');
}
// Split the specification.
// The head is deterministic, and can be run on the graph.
// The tail is non-deterministic, and must be run on the store.
const { head, tail } = (0, specification_1.splitBeforeFirstSuccessor)(this.specification);
// If there is no head, then the specification is unsatisfiable.
if (head === undefined) {
throw new Error('The specification must start with a predecessor join. Otherwise, it is unsatisfiable.');
}
// Execute the head on the graph.
if (head.projection.type !== 'fact') {
throw new Error('The head of the specification must project a fact.');
}
const results = graph.executeSpecification(head.given[0].label.name, head.matches, head.projection.label, fact);
const publicKeys = [];
// If there is a tail, execute it on the store.
if (tail !== undefined) {
if (tail.given.length !== 1) {
throw new Error('The tail of the specification must be given a single fact.');
}
for (const result of results) {
const users = yield store.read([result], tail);
publicKeys.push(...users.map(user => user.result.publicKey));
}
}
else {
publicKeys.push(...results.map(result => graph.getField(result, 'publicKey')));
}
// Find the intersection between the candidate keys and the public keys.
const authorizedKeys = candidateKeys.filter(key => publicKeys.some(publicKey => publicKey === key));
// If any are left, then those are the authorized keys.
if (authorizedKeys.length > 0) {
return {
quantifier: 'some',
authorizedKeys
};
}
else {
return {
quantifier: 'none'
};
}
});
}
}
exports.AuthorizationRuleSpecification = AuthorizationRuleSpecification;
class AuthorizationRules {
constructor(model) {
this.model = model;
this.rulesByType = {};
}
with(rules) {
return rules(this);
}
no(typeOrFactConstructor) {
const type = typeof (typeOrFactConstructor) === 'string' ?
typeOrFactConstructor :
typeOrFactConstructor.Type;
return this.withRule(type, new AuthorizationRuleNone());
}
any(typeOrFactConstructor) {
const type = typeof (typeOrFactConstructor) === 'string' ?
typeOrFactConstructor :
typeOrFactConstructor.Type;
return this.withRule(type, new AuthorizationRuleAny());
}
type(factConstructor, definitionOrPredecessorSelector) {
if (definitionOrPredecessorSelector.length === 2) {
return this.typeFromDefinition(factConstructor, definitionOrPredecessorSelector);
}
else {
return this.typeFromPredecessorSelector(factConstructor, definitionOrPredecessorSelector);
}
}
typeFromDefinition(factConstructor, definition) {
const type = factConstructor.Type;
if (this.model === undefined) {
throw new Error('The model must be given to define a rule using a specification.');
}
const specification = this.model.given(factConstructor).match(definition);
return this.withRule(type, new AuthorizationRuleSpecification(specification.specification));
}
typeFromPredecessorSelector(factConstructor, predecessorSelector) {
const type = factConstructor.Type;
if (this.model === undefined) {
throw new Error('The model must be given to define a rule using a specification.');
}
const specification = this.model.given(factConstructor).match((fact, facts) => {
const label = predecessorSelector(fact);
const payload = (0, model_1.getPayload)(label);
if (payload instanceof model_1.Traversal) {
const traversal = payload;
const projection = traversal.projection;
if (projection.type !== 'fact') {
throw new Error('Authorization rules must select facts.');
}
const label = projection.label;
const match = traversal.matches.find(m => m.unknown.name === label);
if (match === undefined) {
throw new Error(`The traversal must match the label ${label}.`);
}
if (match.unknown.type !== user_1.User.Type && match.unknown.type !== user_1.Device.Type) {
throw new Error(`The traversal must match a user or device.`);
}
return traversal;
}
if (payload.type !== 'fact') {
throw new Error('Authorization rules must select facts.');
}
if (payload.factType === user_1.User.Type) {
const userTraversal = facts.ofType(user_1.User)
.join(user => user, label);
return userTraversal;
}
else if (payload.factType === user_1.Device.Type) {
const deviceTraversal = facts.ofType(user_1.Device)
.join(device => device, label);
return deviceTraversal;
}
else {
throw new Error(`Authorization rules must select users or devices.`);
}
});
return this.withRule(type, new AuthorizationRuleSpecification(specification.specification));
}
merge(authorizationRules2) {
let result = new AuthorizationRules(this.model);
for (const type in this.rulesByType) {
const rules1 = this.rulesByType[type];
const rules2 = authorizationRules2.rulesByType[type];
if (rules2) {
const rules = [...rules1, ...rules2];
for (const rule of rules) {
result = result.withRule(type, rule);
}
}
else {
for (const rule of rules1) {
result = result.withRule(type, rule);
}
}
}
for (const type in authorizationRules2.rulesByType) {
if (!this.rulesByType[type]) {
const rules2 = authorizationRules2.rulesByType[type];
for (const rule of rules2) {
result = result.withRule(type, rule);
}
}
}
return result;
}
static combine(rules, type, rule) {
return rules.withRule(type, rule);
}
withRule(type, rule) {
const oldRules = this.rulesByType[type] || [];
const newRules = [...oldRules, rule];
const newRulesByType = Object.assign(Object.assign({}, this.rulesByType), { [type]: newRules });
const result = new AuthorizationRules(this.model);
result.rulesByType = newRulesByType;
return result;
}
hasRule(type) {
return !!this.rulesByType[type];
}
getAuthorizedPopulation(candidateKeys, fact, factRecords, store) {
return __awaiter(this, void 0, void 0, function* () {
const rules = this.rulesByType[fact.type];
if (!rules) {
return {
quantifier: 'none'
};
}
const graph = new FactGraph(factRecords);
let authorizedKeys = [];
for (const rule of rules) {
const population = yield rule.getAuthorizedPopulation(candidateKeys, fact, graph, store);
if (population.quantifier === 'everyone') {
return population;
}
else if (population.quantifier === 'some') {
authorizedKeys = [...authorizedKeys, ...population.authorizedKeys]
.filter(fn_1.distinct);
}
}
if (authorizedKeys.length > 0) {
return {
quantifier: 'some',
authorizedKeys
};
}
return {
quantifier: 'none'
};
});
}
saveToDescription() {
let description = 'authorization {\n';
for (const type in this.rulesByType) {
const rules = this.rulesByType[type];
for (const rule of rules) {
const ruleDescription = rule.describe(type);
description += ruleDescription;
}
}
description += '}\n';
return description;
}
static loadFromDescription(description) {
const parser = new specification_parser_1.SpecificationParser(description);
parser.skipWhitespace();
const authorizationRules = parser.parseAuthorizationRules();
return authorizationRules;
}
}
exports.AuthorizationRules = AuthorizationRules;
AuthorizationRules.empty = new AuthorizationRules(undefined);
function describeAuthorizationRules(model, authorization) {
const rules = authorization(new AuthorizationRules(model));
return rules.saveToDescription();
}
exports.describeAuthorizationRules = describeAuthorizationRules;
//# sourceMappingURL=authorizationRules.js.map