@shield-acl/core
Version:
Sistema ACL (Access Control List) inteligente e granular com algoritmos de permissões
753 lines (749 loc) • 20.2 kB
JavaScript
"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