Como projetar um sistema de permissões escalável

1. Fundamentos de um sistema de permissões escalável

Um sistema de permissões escalável precisa suportar milhões de usuários, bilhões de recursos e milhares de regras sem degradar a latência. A escalabilidade horizontal — adicionar mais servidores — é preferível à vertical (aumentar recursos de uma única máquina), pois permite crescimento elástico.

Os principais desafios incluem:
- Latência: decisões de autorização precisam ser tomadas em milissegundos para não impactar a experiência do usuário
- Consistência: em sistemas distribuídos, mudanças nas permissões devem se propagar rapidamente sem causar falhas de segurança
- Volume de regras: com centenas de milhares de políticas, a avaliação eficiente se torna crítica

É essencial diferenciar três conceitos:
- Autenticação: verifica "quem é você" (login, tokens JWT)
- Autorização: determina "o que você pode fazer" (permissões)
- Auditoria: registra "o que foi feito" (logs de decisões)

text
// Exemplo de estrutura de decisão de autorização
{
  "subject": "user_123",
  "action": "read",
  "resource": "document_456",
  "context": {
    "ip": "192.168.1.1",
    "time": "2024-01-15T10:30:00Z"
  },
  "decision": "allow"
}

2. Modelos de permissões: RBAC, ABAC e ReBAC

RBAC (Role-Based Access Control)

Usa papéis hierárquicos com herança de permissões. Simples e eficiente, mas pode se tornar rígido.

text
// Exemplo de RBAC com hierarquia
roles:
  admin:
    permissions: [read, write, delete, manage_users]
  editor:
    permissions: [read, write]
    inherits: [viewer]
  viewer:
    permissions: [read]

user_roles:
  alice: [admin]
  bob: [editor]
  charlie: [viewer]

ABAC (Attribute-Based Access Control)

Avalia permissões com base em atributos do usuário, recurso, ação e ambiente. Mais flexível, mas complexo de gerenciar.

text
// Política ABAC exemplo
policy:
  rule: "allow_if"
  conditions:
    - attribute: "user.department"
      operator: "equals"
      value: "engineering"
    - attribute: "resource.type"
      operator: "equals"
      value: "code_repository"
    - attribute: "action"
      operator: "in"
      value: ["read", "write"]

ReBAC (Relationship-Based Access Control)

Baseado em grafos de relacionamento. Ideal para redes sociais, sistemas colaborativos e organizações hierárquicas.

text
// Grafo ReBAC
users:
  - alice: { manager_of: [bob, charlie] }
  - bob: { member_of: [team_alpha] }
  - charlie: { member_of: [team_beta] }

resources:
  - project_x: { owned_by: [alice], shared_with: [team_alpha] }

// Avaliação: alice pode ler project_x? Sim (owner)
// bob pode ler project_x? Sim (membro do team_alpha)
// charlie pode ler project_x? Não (não tem relacionamento)

3. Arquitetura de dados para permissões em larga escala

A escolha do armazenamento impacta diretamente a escalabilidade:

  • Bancos relacionais (PostgreSQL): bom para RBAC com joins simples, mas limitado para consultas complexas
  • NoSQL (DynamoDB, Cassandra): melhor para alta disponibilidade e consultas por chave, porém sem joins nativos
  • Bancos de grafos (Neo4j, Dgraph): ideais para ReBAC, com consultas eficientes em relacionamentos profundos

Estratégias de cache distribuído com Redis ou Memcached reduzem a latência:

text
// Estrutura de cache de permissões
cache_key: "permissions:user_123:resource_456"
cache_value: {
  "can_read": true,
  "can_write": false,
  "can_delete": false,
  "expires_at": 1705314000
}

// Tempo médio de resposta com cache: 2ms
// Tempo médio de resposta sem cache: 50ms

Para multi-tenancy, o sharding por tenant isola regras e melhora a performance:

text
// Sharding por tenant_id
shard_1: tenants [A-M]
shard_2: tenants [N-Z]

// Roteamento baseado em hash do tenant_id
function getShard(tenantId) {
  return hash(tenantId) % numberOfShards;
}

4. Motor de avaliação de permissões (Policy Engine)

Abordagens de implantação

  • Centralizada: um serviço dedicado avalia todas as permissões (ex: OPA, Casbin Server)
  • Embarcada (sidecar): o motor roda junto com a aplicação (ex: Casbin library, Cedar)

Implementação de algoritmos de decisão

Árvores de decisão e avaliação lazy permitem processar apenas as regras necessárias:

text
// Algoritmo de avaliação lazy
function evaluatePolicy(user, action, resource, policies):
  applicablePolicies = []

  for policy in policies:
    if matchesSubject(policy, user) 
       and matchesAction(policy, action)
       and matchesResource(policy, resource):
      applicablePolicies.append(policy)

  // Early exit: primeira regra "deny" bloqueia
  for policy in applicablePolicies:
    if policy.effect == "deny":
      return "deny"

  // Se nenhuma regra "allow" explícita, negar por padrão
  for policy in applicablePolicies:
    if policy.effect == "allow":
      return "allow"

  return "deny"  // default deny

Linguagens de política populares

  • Open Policy Agent (OPA): usa Rego, poderoso para ABAC complexo
  • Casbin: suporta RBAC, ABAC e múltiplos backends de armazenamento
  • Cedar (AWS): focado em desempenho e segurança, usado no AWS Verified Permissions
text
// Exemplo de política em Rego (OPA)
package authz

default allow = false

allow {
  input.action == "read"
  input.resource.type == "document"
  data.roles[input.subject.role].permissions[_] == "read"
}

allow {
  input.action == "write"
  input.resource.owner == input.subject.id
}

5. Sincronização e consistência em sistemas distribuídos

Em sistemas distribuídos, mudanças de permissões precisam ser propagadas rapidamente. A consistência eventual é aceitável para muitos cenários, mas algumas operações críticas (como revogação de acesso) exigem consistência forte.

Uso de event sourcing para rastrear alterações:

text
// Eventos de mudança de permissão
event_1: {
  "type": "role_assigned",
  "user": "user_123",
  "role": "admin",
  "timestamp": 1705314000
}

event_2: {
  "type": "permission_revoked",
  "user": "user_456",
  "resource": "document_789",
  "action": "delete",
  "timestamp": 1705314100
}

Estratégias de invalidação de cache:
- TTL (Time-to-Live): expiração automática após tempo definido
- Invalidation por evento: quando uma permissão muda, publica-se um evento para limpar cache
- Write-through: atualiza cache e banco simultaneamente

text
// Invalidação de cache baseada em eventos
function onPermissionChange(userId, resourceId) {
  cacheKey = `permissions:${userId}:${resourceId}`;
  redis.del(cacheKey);

  // Publicar evento para outros serviços
  messageQueue.publish("permission.changed", {
    userId, resourceId, timestamp: Date.now()
  });
}

6. Otimizações de desempenho e redução de latência

Pré-computação de permissões

Para consultas frequentes, materialize as permissões em tabelas ou caches:

text
// Tabela materializada de permissões
user_id | resource_id | can_read | can_write | can_delete
user_123| doc_456    | true     | true      | false
user_456| doc_456    | true     | false     | false

Indexação eficiente

Índices compostos por usuário, recurso e ação aceleram consultas:

text
-- Índices para consultas de permissão
CREATE INDEX idx_permissions_user_resource 
ON permissions(user_id, resource_id);

CREATE INDEX idx_permissions_action 
ON permissions(action);

Batching e paralelismo

Para avaliar múltiplas permissões simultaneamente, use processamento em lote:

text
// Batching de consultas de permissão
function batchCheckPermissions(userId, resourceIds, action) {
  // Consulta única com múltiplos recursos
  const results = db.query(`
    SELECT resource_id, 
           EXISTS(SELECT 1 FROM permissions 
                  WHERE user_id = $1 
                    AND resource_id = ANY($2) 
                    AND action = $3) as allowed
    FROM unnest($2) as resource_id
  `, [userId, resourceIds, action]);

  return results;
}

7. Monitoramento, auditoria e evolução do sistema

Logs de decisão

Cada decisão de autorização deve ser registrada para compliance e debugging:

text
// Log de decisão de autorização
{
  "decision_id": "dec_789012",
  "timestamp": "2024-01-15T10:30:00.123Z",
  "subject": "user_123",
  "action": "delete",
  "resource": "document_456",
  "decision": "deny",
  "reason": "user_not_owner",
  "policies_evaluated": 5,
  "evaluation_time_ms": 3.2
}

Métricas de performance

Monitore continuamente:
- Tempo médio de avaliação (alvo: <10ms)
- Taxa de acerto de cache (alvo: >95%)
- Número de regras avaliadas por decisão
- Latência P99 das consultas

Versionamento de políticas

Permite rollback seguro em caso de erros:

text
// Versionamento de política
policy_version_1:
  created_at: "2024-01-01T00:00:00Z"
  rules: [/* regras antigas */]

policy_version_2:
  created_at: "2024-01-15T00:00:00Z"
  rules: [/* regras novas */]

// Rollback para versão anterior
function rollbackPolicy(policyId, targetVersion) {
  const policy = getPolicyVersion(policyId, targetVersion);
  activatePolicy(policyId, policy);
  logRollback(policyId, targetVersion);
}

Projetar um sistema de permissões escalável exige equilibrar flexibilidade, desempenho e segurança. Comece com RBAC para simplicidade, evolua para ABAC ou ReBAC conforme a complexidade aumentar. Invista em cache, indexação e monitoramento desde o início para garantir que o sistema cresça sem comprometer a experiência do usuário.

Referências