UNPKG

@shield-acl/core

Version:

Sistema ACL (Access Control List) inteligente e granular com algoritmos de permissões

753 lines (749 loc) 20.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { ACL: () => ACL, PermissionRuleBuilder: () => PermissionRuleBuilder, conditions: () => conditions, createACL: () => createACL, permission: () => permission, permissionPatterns: () => permissionPatterns, permissions: () => permissions, resourcePermissions: () => resourcePermissions, rolePresets: () => rolePresets }); module.exports = __toCommonJS(src_exports); // src/acl.ts var ACL = class { constructor(config = {}) { this.roles = /* @__PURE__ */ new Map(); this.cache = /* @__PURE__ */ new Map(); this.config = { wildcardPattern: "*", caseSensitive: false, cache: true, cacheMaxSize: 1e3, cacheTTL: 5 * 60 * 1e3, // 5 minutos defaultDeny: true, debug: false, ...config }; this.patternMatcher = this.createPatternMatcher(); } /** * Cria um matcher de patterns com suporte a wildcards */ createPatternMatcher() { const { wildcardPattern, caseSensitive } = this.config; return (pattern, value) => { if (!caseSensitive) { pattern = pattern.toLowerCase(); value = value.toLowerCase(); } if (pattern === wildcardPattern) return true; const regexPattern = pattern.split(wildcardPattern).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*"); const regex = new RegExp(`^${regexPattern}$`); return regex.test(value); }; } /** * Define uma role no sistema */ defineRole(role) { this.roles.set(role.name, role); this.clearCache(); } /** * Remove uma role do sistema */ removeRole(name) { this.roles.delete(name); this.clearCache(); } /** * Obtém uma role pelo nome */ getRole(name) { return this.roles.get(name); } /** * Verifica se usuário tem permissão */ can(user, action, resource, context) { const result = this.evaluate(user, action, resource, context); return result.allowed; } /** * Avalia permissão com detalhes */ evaluate(user, action, resource, context) { if (this.config.cache) { const cached = this.checkCache(user, action, resource, context); if (cached) return cached; } const evalContext = { user, resource: (context == null ? void 0 : context.resource) || resource, metadata: { action, resourceType: resource } }; const permissions2 = this.getUserPermissions(user); const sortedPermissions = this.sortPermissions(permissions2); for (const permission2 of sortedPermissions) { const matches = this.matchesPermission( permission2, action, evalContext, context, resource ); if (matches) { const result2 = { allowed: !permission2.deny, reason: permission2.deny ? `Explicitly denied by rule: ${this.formatRule(permission2)}` : `Allowed by rule: ${this.formatRule(permission2)}`, matchedRule: permission2 }; if (this.config.cache) { this.saveToCache(user, action, resource, result2, context); } return result2; } } const result = { allowed: !this.config.defaultDeny, reason: this.config.defaultDeny ? "No matching permission found (default deny)" : "No matching permission found (default allow)" }; if (this.config.cache) { this.saveToCache(user, action, resource, result, context); } return result; } /** * Obtém todas as permissões de um usuário (incluindo roles) */ getUserPermissions(user) { const permissions2 = []; if (user.permissions) { permissions2.push(...user.permissions); } const processedRoles = /* @__PURE__ */ new Set(); for (const roleName of user.roles) { this.collectRolePermissions(roleName, permissions2, processedRoles); } return permissions2; } /** * Coleta permissões de uma role recursivamente (herança) */ collectRolePermissions(roleName, permissions2, processedRoles) { if (processedRoles.has(roleName)) return; processedRoles.add(roleName); const role = this.roles.get(roleName); if (!role) return; permissions2.push(...role.permissions); if (role.inherits) { for (const inheritedRole of role.inherits) { this.collectRolePermissions(inheritedRole, permissions2, processedRoles); } } } /** * Obtém hierarquia completa de uma role */ getRoleHierarchy(roleName) { const hierarchy = []; const processedRoles = /* @__PURE__ */ new Set(); const collectHierarchy = (name) => { if (processedRoles.has(name)) return; processedRoles.add(name); const role = this.roles.get(name); if (!role) return; hierarchy.push(name); if (role.inherits) { for (const inheritedRole of role.inherits) { collectHierarchy(inheritedRole); } } }; collectHierarchy(roleName); return hierarchy; } /** * Verifica se uma permissão corresponde à ação/recurso */ matchesPermission(permission2, action, evalContext, context, resource) { const actions = Array.isArray(permission2.action) ? permission2.action : [permission2.action]; const actionMatches = actions.some((a) => this.patternMatcher(a, action)); if (!actionMatches) return false; if (permission2.resource) { if (!resource) return false; const resources = Array.isArray(permission2.resource) ? permission2.resource : [permission2.resource]; const resourceMatches = resources.some( (r) => this.patternMatcher(r, resource) ); if (!resourceMatches) return false; } if (permission2.conditions && permission2.conditions.length > 0) { const conditionContext = context !== void 0 ? context : { user: evalContext.user, resource: evalContext.resource, metadata: evalContext.metadata }; const allConditionsMet = permission2.conditions.every( (condition) => condition(conditionContext) ); if (!allConditionsMet) return false; } return true; } /** * Ordena permissões por prioridade e especificidade */ sortPermissions(permissions2) { return permissions2.sort((a, b) => { if (a.priority !== void 0 && b.priority !== void 0) { return b.priority - a.priority; } if (a.priority !== void 0) return -1; if (b.priority !== void 0) return 1; if (a.deny && !b.deny) return -1; if (!a.deny && b.deny) return 1; const aHasConditions = a.conditions && a.conditions.length > 0; const bHasConditions = b.conditions && b.conditions.length > 0; if (aHasConditions && !bHasConditions) return -1; if (!aHasConditions && bHasConditions) return 1; const aSpecificity = this.calculateSpecificity(a); const bSpecificity = this.calculateSpecificity(b); return bSpecificity - aSpecificity; }); } /** * Calcula especificidade de uma regra (mais alto = mais específico) */ calculateSpecificity(rule) { let score = 0; const actions = Array.isArray(rule.action) ? rule.action : [rule.action]; for (const action of actions) { if (!action.includes(this.config.wildcardPattern)) { score += 10; } } if (rule.resource) { const resources = Array.isArray(rule.resource) ? rule.resource : [rule.resource]; for (const resource of resources) { if (!resource.includes(this.config.wildcardPattern)) { score += 10; } } } if (rule.conditions && rule.conditions.length > 0) { score += rule.conditions.length * 20; } return score; } /** * Formata regra para mensagem */ formatRule(rule) { const parts = []; const actions = Array.isArray(rule.action) ? rule.action : [rule.action]; parts.push(`action=${actions.join(",")}`); if (rule.resource) { const resources = Array.isArray(rule.resource) ? rule.resource : [rule.resource]; parts.push(`resource=${resources.join(",")}`); } if (rule.conditions) { parts.push(`conditions=${rule.conditions.length}`); } return parts.join(", "); } /** * Verifica cache */ checkCache(user, action, resource, context) { const key = this.getCacheKey(user, action, resource, context); const entry = this.cache.get(key); if (!entry) return null; if (Date.now() - entry.timestamp > this.config.cacheTTL) { this.cache.delete(key); return null; } if (this.config.debug) { console.log(`[ACL] Cache hit: ${key}`); } return entry.result; } /** * Salva no cache */ saveToCache(user, action, resource, result, context) { if (this.cache.size >= this.config.cacheMaxSize) { const entriesToDelete = Math.floor(this.config.cacheMaxSize * 0.2); const sortedEntries = Array.from(this.cache.entries()).sort( (a, b) => a[1].timestamp - b[1].timestamp ); for (let i = 0; i < entriesToDelete; i++) { this.cache.delete(sortedEntries[i][0]); } } const key = this.getCacheKey(user, action, resource, context); this.cache.set(key, { key, result, timestamp: Date.now() }); } /** * Gera chave de cache */ getCacheKey(user, action, resource, context) { const parts = [ `user:${user.id}`, `action:${action}`, `resource:${resource || "none"}`, `roles:${user.roles.join(",")}` ]; if (context) { const contextHash = this.hashContext(context); parts.push(`context:${contextHash}`); } return parts.join("|"); } /** * Gera hash simples do contexto para cache */ hashContext(context) { try { return JSON.stringify(context); } catch (e) { return "unparseable"; } } /** * Limpa todo o cache */ clearCache() { this.cache.clear(); } }; function createACL(config) { return new ACL(config); } // src/utils/permission-builder.ts var PermissionRuleBuilder = class { constructor() { this.rule = {}; } action(action) { this.rule.action = action; return this; } resource(resource) { this.rule.resource = resource; return this; } when(condition) { if (!this.rule.conditions) { this.rule.conditions = []; } this.rule.conditions.push(condition); return this; } deny() { this.rule.deny = true; return this; } priority(value) { this.rule.priority = value; return this; } build() { if (!this.rule.action) { throw new Error("Permission must have an action"); } return this.rule; } }; function permission() { return new PermissionRuleBuilder(); } var permissions = { // CRUD básico crud: (resource) => [ { action: "create", resource }, { action: "read", resource }, { action: "update", resource }, { action: "delete", resource } ], // Todas as ações em um recurso all: (resource) => ({ action: "*", resource }), // Ações de leitura readonly: (resource) => [ { action: "read", resource }, { action: "list", resource }, { action: "view", resource } ], // Ações de escrita write: (resource) => [ { action: "create", resource }, { action: "update", resource }, { action: "delete", resource } ], // Gerenciamento completo manage: (resource) => ({ action: "manage", resource }) }; var conditions = { // Verifica se é proprietário isOwner: (userIdField = "userId") => { return (context) => { if (!context.user || !context.user.id) return false; if (!context.resource) return false; const userId = context.user.id; const resourceOwnerId = context.resource[userIdField] || context.resource.ownerId; if (!resourceOwnerId) return false; return userId === resourceOwnerId; }; }, // Verifica se recurso está em determinado status hasStatus: (...statuses) => { return (context) => { var _a; const resourceStatus = (_a = context.resource) == null ? void 0 : _a.status; return resourceStatus ? statuses.includes(resourceStatus) : false; }; }, // Verifica se usuário tem atributo específico userHasAttribute: (attribute, value) => { return (context) => { var _a, _b; const userAttribute = (_b = (_a = context.user) == null ? void 0 : _a.attributes) == null ? void 0 : _b[attribute]; return value !== void 0 ? userAttribute === value : userAttribute !== void 0; }; }, // Verifica se recurso foi criado há menos de X tempo createdWithin: (milliseconds) => { return (context) => { var _a; const createdAt = (_a = context.resource) == null ? void 0 : _a.createdAt; if (!createdAt) return false; const createdTime = createdAt instanceof Date ? createdAt.getTime() : new Date(createdAt).getTime(); return Date.now() - createdTime <= milliseconds; }; }, // Combina múltiplas condições com AND and: (...conditions2) => { return (context) => conditions2.every((condition) => condition(context)); }, // Combina múltiplas condições com OR or: (...conditions2) => { return (context) => conditions2.some((condition) => condition(context)); }, // Nega uma condição not: (condition) => { return (context) => !condition(context); } }; // src/utils/presets.ts var rolePresets = { /** * Super administrador - acesso total */ superAdmin: () => ({ name: "super-admin", permissions: [ { action: "*", resource: "*" } ] }), /** * Administrador - gerencia tudo exceto configurações críticas */ admin: () => ({ name: "admin", permissions: [ { action: "*", resource: "*" }, { action: "*", resource: "system.*", deny: true } ] }), /** * Moderador - pode moderar conteúdo */ moderator: () => ({ name: "moderator", permissions: [ permissions.all("posts"), permissions.all("comments"), permissions.all("users"), { action: "delete", resource: "users", deny: true // Não pode deletar usuários }, { action: "ban", resource: "users" }, { action: "unban", resource: "users" } ] }), /** * Editor - pode criar e editar conteúdo */ editor: () => ({ name: "editor", permissions: [ ...permissions.crud("posts"), ...permissions.crud("pages"), ...permissions.crud("media"), ...permissions.readonly("users"), ...permissions.readonly("settings") ] }), /** * Autor - pode gerenciar apenas seu próprio conteúdo */ author: () => ({ name: "author", permissions: [ { action: "create", resource: "posts" }, { action: ["read", "update", "delete"], resource: "posts", conditions: [conditions.isOwner("authorId")] }, ...permissions.readonly("posts"), // Pode ler todos os posts ...permissions.crud("media"), { action: "upload", resource: "media" } // Upload é sinônimo de create para media ] }), /** * Usuário autenticado básico */ user: () => ({ name: "user", permissions: [ // Perfil próprio { action: ["read", "update"], resource: "profile", conditions: [conditions.isOwner()] }, // Comentários { action: "create", resource: "comments" }, { action: ["update", "delete"], resource: "comments", conditions: [conditions.isOwner()] }, // Leitura geral { action: "read", resource: ["posts", "pages", "comments"] } ] }), /** * Visitante - acesso apenas leitura */ guest: () => ({ name: "guest", permissions: [ { action: "read", resource: ["posts", "pages", "comments"], conditions: [ // Apenas conteúdo público (context) => { var _a; return ((_a = context.resource) == null ? void 0 : _a.visibility) === "public"; } ] } ] }) }; var resourcePermissions = { /** * Blog/CMS */ blog: { posts: permissions.crud("posts"), pages: permissions.crud("pages"), comments: permissions.crud("comments"), media: permissions.crud("media"), categories: permissions.crud("categories"), tags: permissions.crud("tags") }, /** * E-commerce */ ecommerce: { products: permissions.crud("products"), orders: permissions.crud("orders"), customers: permissions.crud("customers"), inventory: permissions.crud("inventory"), coupons: permissions.crud("coupons"), shipping: permissions.crud("shipping") }, /** * SaaS */ saas: { organizations: permissions.crud("organizations"), projects: permissions.crud("projects"), teams: permissions.crud("teams"), members: permissions.crud("members"), billing: permissions.crud("billing"), subscriptions: permissions.crud("subscriptions") }, /** * Social */ social: { posts: permissions.crud("posts"), friends: permissions.crud("friends"), messages: permissions.crud("messages"), groups: permissions.crud("groups"), events: permissions.crud("events"), notifications: permissions.crud("notifications") } }; var permissionPatterns = { /** * Padrão de propriedade - usuário só pode modificar seus próprios recursos */ ownership: (resource, ownerField = "userId") => [ { action: "create", resource }, { action: ["read", "update", "delete"], resource, conditions: [conditions.isOwner(ownerField)] } ], /** * Padrão de colaboração - múltiplos níveis de acesso */ collaboration: (resource) => [ { action: "*", resource, conditions: [(context) => { var _a; return ((_a = context.resource) == null ? void 0 : _a.role) === "owner"; }] }, { action: ["read", "update"], resource, conditions: [(context) => { var _a; return ((_a = context.resource) == null ? void 0 : _a.role) === "editor"; }] }, { action: "read", resource, conditions: [(context) => { var _a; return ((_a = context.resource) == null ? void 0 : _a.role) === "viewer"; }] } ], /** * Padrão de workflow - permissões baseadas em status */ workflow: (resource, workflow) => { const rules = []; Object.entries(workflow).forEach(([status, allowedActions]) => { allowedActions.forEach((action) => { rules.push({ action, resource, conditions: [conditions.hasStatus(status)] }); }); }); return rules; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ACL, PermissionRuleBuilder, conditions, createACL, permission, permissionPatterns, permissions, resourcePermissions, rolePresets }); //# sourceMappingURL=index.js.map