UNPKG

@eang/core

Version:

eang - model driven enterprise event processing

271 lines 10.1 kB
// Uncomment and update imports as needed import { MultiDirectedGraph } from 'graphology'; import { Cnx } from './entity.js'; import { createObj } from './objects.js'; export const RuleEffect = { allow: 'allow', deny: 'deny' }; export class Authorizer { graph; constructor(graph = new MultiDirectedGraph()) { this.graph = graph; } getPolicyRuleChain(entity) { const collectedPolicies = []; // Start from the entity and traverse up following "child_of" edges. // If the entity is a Cnx, start from its "child_of" target. let currentNodeId; if (entity instanceof Cnx) { currentNodeId = entity.childOf; } else { currentNodeId = entity.id; } // Traverse up the childOf chain while (currentNodeId && this.graph.hasNode(currentNodeId)) { // Collect SecurityPolicy nodes that have child_of edges TO current node this.graph.forEachInEdge(currentNodeId, (_edgeId, attributes, source, _target) => { if (attributes.entity.typeOf === 'child_of') { const sourceEntity = this.graph.getNodeAttribute(source, 'entity'); if (sourceEntity.typeOf === 'SecurityPolicy') { collectedPolicies.push(source); } } }); // Move to parent // TODO: This should be a DAG and cycles should not exist. Re-evaluate. const currentEntity = this.graph.getNodeAttribute(currentNodeId, 'entity'); if (currentEntity.childOf === undefined) { break; } else { currentNodeId = currentEntity.childOf; } } // Collect policy rules from all collected policies const policyRuleChain = []; collectedPolicies.reverse().forEach((policyId) => { this.graph.forEachInEdge(policyId, (_edgeId, attributes, source, _target) => { if (attributes.entity.typeOf === 'rule_for') { const ruleEntity = this.graph.getNodeAttribute(source, 'entity'); if (ruleEntity.typeOf === 'PolicyRule') { policyRuleChain.push(createObj(ruleEntity)); } } }); }); return policyRuleChain; } authorize(authzRequest) { const { entity, action } = authzRequest; // If a new entity is being created, use the childOf entity as a starting point let policyStartEntity = entity; if (action === 'create') { policyStartEntity = this.graph.getNodeAttribute(entity.childOf, 'entity'); } const policyRuleChain = this.getPolicyRuleChain(policyStartEntity); if (!policyRuleChain || policyRuleChain.length === 0) { return { effect: RuleEffect.deny, entity }; } const response = this.applyChain(authzRequest, policyRuleChain); return response.effect === RuleEffect.deny ? this.applyChain(authzRequest, policyRuleChain) : response; } hasConnectionTo(fromId, toId, connectionType) { // Check if a directed edge exists from fromId to toId with matching type let found = false; this.graph.forEachEdge(fromId, toId, (_edgeId, attributes) => { if (attributes.entity.typeOf === connectionType) { found = true; } }); return found; } /** * Resolves a property path from the authorization request context * @param path - Property path like "entity.id", "subject.id", "entity.fromObjId", "action" * @param authzRequest - The authorization request context * @returns The resolved value or undefined if not found */ resolvePropertyPath(path, authzRequest) { const parts = path.split('.'); let current = authzRequest; for (const part of parts) { if (current === undefined || current === null) { return undefined; } current = current[part]; } return current; } /** * Evaluates a single condition against the authorization request * @param condition - The condition to evaluate * @param authzRequest - The authorization request context * @returns true if the condition matches, false otherwise */ evaluateCondition(condition, authzRequest) { // Each condition object has one property path as the key const entries = Object.entries(condition); if (entries.length !== 1) { return false; } const entry = entries[0]; if (!entry) { return false; } const [propertyPath, conditionDef] = entry; if (!conditionDef) { return false; } const actualValue = this.resolvePropertyPath(propertyPath, authzRequest); const { op, value, ref, typeOf, negate } = conditionDef; let result = false; switch (op) { case 'equals': { const compareValue = ref ? this.resolvePropertyPath(ref, authzRequest) : value; result = actualValue === compareValue; break; } case 'in': { if (Array.isArray(value)) { result = value.includes(actualValue); } break; } case 'hasConnectionTo': { // For hasConnectionTo, the propertyPath should be 'subject' or similar // and we need to check if subject has a connection to the target const subjectId = actualValue?.id; const targetId = value; if (subjectId && targetId && typeOf && Array.isArray(typeOf)) { // Check if any of the connection types match result = typeOf.some((connectionType) => this.hasConnectionTo(subjectId, targetId, connectionType)); } break; } default: result = false; } // Apply negation if specified return negate ? !result : result; } /** * Evaluates an array of rules against the authorization request * All conditions within a rule must match (AND logic) * Any rule matching returns true (OR logic between rules) * @param rules - Array of rules to evaluate * @param authzRequest - The authorization request context * @returns true if any rule matches, false otherwise */ evaluateRules(rules, authzRequest) { // OR logic between rules - if any rule matches, return true for (const rule of rules) { // AND logic between conditions - all must match for the rule to match const allConditionsMatch = rule.conditions.every((condition) => this.evaluateCondition(condition, authzRequest)); if (allConditionsMatch) { return true; } } return false; } applyChain(accessRequest, policyRuleChain) { const response = { effect: RuleEffect.deny }; const filteredPolicyRules = policyRuleChain.filter((rule) => { const actionMatch = !rule.applicableActions || rule.applicableActions.includes(accessRequest.action); if (!actionMatch) return false; const resourceTypeMatch = !rule.applicableEntityTypes || rule.applicableEntityTypes.includes(accessRequest.entity.typeOf); return actionMatch && resourceTypeMatch; }); for (const rule of filteredPolicyRules) { // Check if the rule has a declarative rules array const matched = rule.rules ? this.evaluateRules(rule.rules, accessRequest) : false; if (matched) { response.effect = rule.effect; response.matchedRule = rule; response.entity = accessRequest.entity; if (rule.overrideable === false) { break; } } } return response; } } export class Environment { name; description; // Time-based attributes timeOfDay; timeRange; dayOfWeek; date; isHoliday; // Location-based attributes ipAddress; geographicLocation; networkZone; connectionType; // System/Device attributes deviceType; securityPosture; operatingSystem; osVersion; browserType; browserVersion; certificateStatus; // Risk-based attributes threatLevel; anomalyScore; previousAuthFailures; sessionCharacteristics; // Organizational context businessProcess; // (disaster recovery mode) emergencyStatus; // (restrict non-essential access during high load) systemLoad; systemHealth; maintenanceWindow; // Compliance context dataClassificationLevel; regulatoryJurisdiction; underAudit; constructor(name, description, attributes) { this.name = name; this.description = description; // Apply any provided attributes if (attributes) { Object.assign(this, attributes); } } } // export class AuthzAction { // private constructor( // public type: AuthzActionType, // public data: Record<string, unknown> = {} // ) {} // static create(data: Record<string, unknown>): AuthzAction { // return new AuthzAction(EangEventType.create, data) // } // static update(data: Record<string, unknown>): AuthzAction { // return new AuthzAction(EangEventType.update, data) // } // static delete(): AuthzAction { // return new AuthzAction(EangEventType.delete) // } // static start(): AuthzAction { // return new AuthzAction(EangEventType.start) // } // static stop(): AuthzAction { // return new AuthzAction(EangEventType.stop) // } // static read(): AuthzAction { // return new AuthzAction('read') // } // } //# sourceMappingURL=authorizer.js.map