UNPKG

jinaga

Version:

Data management for web and mobile applications.

434 lines 19.3 kB
"use strict"; 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