@fajarnugraha37/nope-iam
Version:
A highly extensible, type-safe IAM-like access control library for Node.js, inspired by AWS IAM. Deny by default, allow by vibes and less patience for your bad access patterns. Supports policies, roles, decorators, adapters, and rich evaluation context be
113 lines • 4.87 kB
JavaScript
import { defaultPolicyEvaluator } from "./defaultEvaluator.js";
import { DefaultLogger } from './logger.js';
export class IAM {
storage;
evaluator;
hooks;
logger;
config;
constructor(options) {
this.storage = options?.storage;
this.hooks = options?.hooks;
this.config = options?.config || {};
const { logger, logLevel } = this.config;
if (logger) {
this.logger = logger;
}
else {
this.logger = new DefaultLogger(logLevel || 'info');
}
this.evaluator = options && options?.evaluatorFunc && options.evaluatorFunc(this.logger);
}
async can(params) {
let result = undefined;
let error = undefined;
try {
this.logger.debug('IAM.can called', params);
if (this.hooks?.onBeforeDecision)
await this.hooks.onBeforeDecision(params);
if (!this.storage) {
this.logger.error('No storage adapter configured');
throw new Error("No storage adapter configured");
}
const user = params.user;
const callStorage = async (method, ...args) => {
this.logger.debug(`Storage access: ${String(method)}`, ...args);
if (this.hooks?.onStorageAccess)
await this.hooks.onStorageAccess(method, args);
const res = await this.storage[method](...args);
if (this.hooks?.onStorageAccess)
await this.hooks.onStorageAccess(method, args, res);
this.logger.debug(`Storage result: ${String(method)}`, res);
return res;
};
const [userPolicies, userRoles] = await Promise.all([
callStorage("getPolicies", user.policyIds),
callStorage("getRoles", user.roleIds),
]);
if (this.hooks?.onRoleNotFound) {
if (params.user.roleIds.length === 0) {
this.logger.warn('User has no roles', params.user.id);
await this.hooks.onRoleNotFound(null);
}
else if (userRoles == undefined || userRoles.length === 0) {
for (const rid of user.roleIds) {
this.logger.warn('Role not found', rid);
await this.hooks.onRoleNotFound(rid);
}
}
else if (userRoles.some((r) => !r)) {
for (const rid of user.roleIds) {
if (!userRoles.find((r) => r && r.id === rid)) {
this.logger.warn('Role not found', rid);
await this.hooks.onRoleNotFound(rid);
}
}
}
}
const rolePolicyIds = userRoles.flatMap((r) => r.policyIds);
const rolePolicies = await callStorage("getPolicies", rolePolicyIds);
const allPolicies = [...userPolicies, ...rolePolicies];
const evaluator = this.evaluator ?? defaultPolicyEvaluator(this.logger);
const operatorsRaw = (await import("./evaluator.js")).defaultConditionOperators;
const operators = { ...operatorsRaw };
if (this.hooks?.onConditionCheck) {
for (const [name, op] of Object.entries(operatorsRaw)) {
operators[name] = async (key, value, ctx) => {
const res = await op(key, value, ctx);
this.logger.debug(`ConditionCheck: ${name}`, key, value, ctx, res);
await this.hooks.onConditionCheck(name, key, value, ctx, res);
return res;
};
}
}
result = await evaluator(user, params.action, params.resource, params.context ?? {}, allPolicies, userRoles, operators);
this.logger.info('IAM decision', result);
if (this.hooks?.onDecision)
await this.hooks.onDecision(result);
return result;
}
catch (err) {
error = err;
this.logger.error('IAM error', err);
if (this.hooks?.onError)
await this.hooks.onError(err);
return {
decision: false,
trace: { checkedPolicies: [], reason: err.message },
context: params.context || {},
};
}
finally {
if (this.hooks?.onAfterDecision) {
try {
await this.hooks.onAfterDecision(result, error);
}
catch (err) {
console.error("Error in onAfterDecision hook:", err);
}
}
}
}
}
//# sourceMappingURL=iam.js.map