@eang/core
Version:
eang - model driven enterprise event processing
271 lines • 10.1 kB
JavaScript
// 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