mozak.tech Engenharia de IA Corporativa 20%

Parte I — Operações e Infraestrutura Potencializadas por IA

1.6 — ChatOps com Aprovação Humana: Bots, Comandos e Controle de Acesso (ChatOps)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Construir um bot ChatOps com slash commands (/deploy, /scale, /rollback, /approve)

Implementar RBAC real baseado em roles de usuário

Implementar geração de diff de mudanças para preview antes de aprovação

Projetar um workflow de aprovação com expiração e múltiplos aprovadores

Completar os 2 TODOs: temPermissao com RBAC real e diff de versões

Por que isso importa

ChatOps é o padrão operacional de equipes de alto desempenho: operações críticas (deploy, rollback, scale) são executadas via Slack/Teams, com contexto visível para todo o time, trilha de auditoria e gate de aprovação integrado.

O problema com bots simples: qualquer um pode executar qualquer comando. RBAC (Role-Based Access Control) define que um developer pode fazer /deploy staging mas não /deploy prod. Um sre pode fazer tudo exceto certas operações administrativas. Um manager pode aprovar mas não executar.

Conceitos Fundamentais

Matriz de Permissões

const PERMISSOES: Record<string, string[]> = {

  developer:  ['/deploy staging', '/scale staging', '/logs', '/status'],

  sre:        ['/deploy staging', '/deploy prod', '/scale', '/rollback', '/logs', '/status', '/restart'],

  manager:    ['/status', '/logs', '/scale down'],

  admin:      ['*'],  // todas as permissões

};

A lógica de verificação precisa ser implementada:

TODO 1: temPermissao(usuario, comando)

function temPermissao(usuario: Usuario, comando: string): boolean {

  // Admin tem acesso a tudo

  if (usuario.roles.includes('admin')) return true;

  

  // Para cada role do usuário, verificar se o comando está permitido

  for (const role of usuario.roles) {

    const permissoesDaRole = PERMISSOES[role] || [];

    

    // Wildcard: role tem acesso a tudo

    if (permissoesDaRole.includes('*')) return true;

    

    // Match exato: /deploy prod

    if (permissoesDaRole.includes(comando)) return true;

    

    // Match por prefixo: /deploy permite /deploy staging E /deploy prod?

    // Não! Usar match exato para mais segurança

    // Mas podemos permitir prefixo sem ambiente:

    // '/scale' no sre permite '/scale staging' e '/scale prod'

    const base = comando.split(' ')[0]; // extrai '/scale' de '/scale prod'

    if (permissoesDaRole.includes(base)) return true;

  }

  

  return false;

}

Exemplo de comportamento:

const dev = { roles: ['developer'] }

temPermissao(dev, '/deploy staging')  // true — na lista

temPermissao(dev, '/deploy prod')     // false — não na lista

temPermissao(dev, '/rollback')        // false — não na lista



const sre = { roles: ['sre'] }

temPermissao(sre, '/scale prod')      // true — '/scale' está na lista do sre

TODO 2: Diff de Mudanças

Antes de requerer aprovação para /deploy prod, mostrar o que vai mudar:

async function gerarDiff(servico: string, versaoNova: string): Promise<string> {

  // Em produção: chamar API do GitHub/GitLab para comparar tags

  // GET /repos/{owner}/{repo}/compare/{versao_atual}...{versao_nova}

  

  // Mock realista para o lab

  const versaoAtual = '2.0.5';  // buscar do cluster/registry real

  

  const response = await client.messages.create({

    model: 'claude-haiku-4-5-20251001',

    max_tokens: 400,

    messages: [{

      role: 'user',

      content: `Gere um diff de changelog realista para deploy de ${servico} de ${versaoAtual} para ${versaoNova}.

      

Formato:

## Mudanças ${versaoAtual} → ${versaoNova}

### Features

- ...

### Fixes

- ...

### Breaking Changes (se houver)

- ...`

    }]

  })

  

  return response.content[0].text

}



// Na função executarDeploy, antes de criar a aprovação:

const diff = await gerarDiff(servico, versao)

const acao = criarAcaoAprovacao(

  `Deploy ${servico}:${versao} em ${ambiente}\n\n${diff}`,

  `/deploy ${args.join(' ')}`,

  usuario,

)

Workflow de Aprovação

interface AcaoAprovacao {

  id: string;

  descricao: string;

  autor: Usuario;

  aprovadores_necessarios: string[];  // roles que podem aprovar

  aprovacoes: string[];               // IDs dos usuários que já aprovaram

  status: 'pendente' | 'aprovado' | 'rejeitado';

  expirado_em: Date;                  // 30 min de validade

}

Fluxo completo:

/deploy api v2.1.0 prod (por dev Ana)

→ RBAC check: developer pode /deploy staging, não /deploy prod → BLOQUEADO



/deploy api v2.1.0 prod (por sre Carlos)

→ RBAC check: sre pode /deploy prod → OK

→ diff gerado → aprovação criada APR-001 (30 min)

→ mensagem: "🔐 Deploy requer aprovação! ID: APR-001. Aguardando: manager, sre"



/approve APR-001 (por mgr Paula)

→ RBAC check: Paula tem role 'manager' que está em aprovadores_necessarios

→ ação marcada como aprovada

→ comando executado: deploy iniciado

Aprofundamento Técnico

RBAC com Herança de Roles

Para sistemas complexos, roles herdam de outras:

const HIERARQUIA: Record<string, string[]> = {

  'sre': ['developer'],     // sre herda tudo de developer

  'admin': ['sre', 'manager'],  // admin herda tudo

}



function expandirRoles(roles: string[]): string[] {

  const todas = new Set(roles)

  for (const role of roles) {

    for (const herdada of HIERARQUIA[role] || []) {

      todas.add(herdada)

    }

  }

  return [...todas]

}



function temPermissao(usuario: Usuario, comando: string): boolean {

  const rolesExpandidas = expandirRoles(usuario.roles)

  // verificação com todas as roles herdadas

}

Expiração de Aprovações

// Na verificação de aprovação:

if (new Date() > acao.expirado_em) {

  aprovacoesPendentes.delete(acaoId)

  return { mensagem: `⏰ Aprovação ${acaoId} expirou. Crie um novo deploy.` }

}



// Limpeza periódica de aprovações expiradas:

setInterval(() => {

  const agora = new Date()

  for (const [id, acao] of aprovacoesPendentes) {

    if (agora > acao.expirado_em) {

      aprovacoesPendentes.delete(id)

      console.log(`[LIMPEZA] Aprovação ${id} expirou e foi removida`)

    }

  }

}, 60_000)  // a cada minuto

Exemplos Anotados

Exemplo 1: Sequência Completa com RBAC

async function demonstrarRBAC() {

  const usuarios = {

    dev: { id: 'u1', nome: 'Ana', roles: ['developer'] },

    sre: { id: 'u2', nome: 'Carlos', roles: ['sre'] },

    mgr: { id: 'u3', nome: 'Paula', roles: ['manager'] },

  }

  

  // 1. Dev tenta deploy em prod → bloqueado

  const r1 = await processarComando({

    comando: '/deploy', args: ['api', 'v2.1.0', 'prod'],

    usuario: usuarios.dev, canal: '#devops', timestamp: new Date(),

  })

  console.log(r1)  // "❌ Sem permissão para /deploy prod"

  

  // 2. SRE faz deploy em prod → cria aprovação

  const r2 = await processarComando({

    comando: '/deploy', args: ['api', 'v2.1.0', 'prod'],

    usuario: usuarios.sre, canal: '#devops', timestamp: new Date(),

  })

  console.log(r2)  // "🔐 Deploy em prod requer aprovação! ID: APR-1234..."

  

  // 3. Manager aprova

  const acaoId = extrairId(r2)  // extrai APR-1234 da mensagem

  const r3 = await processarComando({

    comando: '/approve', args: [acaoId],

    usuario: usuarios.mgr, canal: '#devops', timestamp: new Date(),

  })

  console.log(r3)  // "✅ APR-1234 aprovado por Paula! Executando: /deploy api v2.1.0 prod"

}

Exemplo 2: TODO 1 Completo

function temPermissao(usuario: Usuario, comando: string): boolean {

  if (usuario.roles.includes('admin')) return true;

  

  for (const role of usuario.roles) {

    const perms = PERMISSOES[role] || [];

    if (perms.includes('*')) return true;

    if (perms.includes(comando)) return true;

    // base command: '/scale prod' → base '/scale'

    const base = comando.split(' ')[0];

    if (perms.includes(base)) return true;

  }

  

  return false;

}



// Testes:

// temPermissao({roles: ['developer']}, '/logs')          → true

// temPermissao({roles: ['developer']}, '/deploy prod')   → false

// temPermissao({roles: ['sre']}, '/scale prod 5')        → true (base '/scale' está na lista)

// temPermissao({roles: ['admin']}, '/qualquer-coisa')    → true

Padrões e Armadilhas

Padrões

Padrão 1: Aprovação com diff visível

const acao = criarAcaoAprovacao(

  `Deploy ${servico}:${versao} em prod\n\nDIFF:\n${diff}`,

  ...

)

O aprovador precisa ver o que vai mudar. Aprovação sem contexto é teatro de segurança.

Padrão 2: ID único e imprevisível

id: `APR-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`

// APR-1703123456789-k2p9

Date.now() evita colisões, o sufixo aleatório previne enumeração.

Padrão 3: Feedback claro sobre o que falta para aprovar

return `👍 Aprovação registrada por ${usuario.nome}.

Ainda necessário: ${faltam} aprovações de ${rolesNecessarias}.

Use /approve ${acaoId} para aprovar.`

Armadilhas

⚠️ Armadilha 1: RBAC com return true como placeholder

// O starter tem isso como placeholder:

return true; // ← TODO não implementado!

Com esse placeholder, QUALQUER usuário passa no RBAC. Implemente antes de testar segurança.

⚠️ Armadilha 2: Aprovação sem verificação de expiração

// ERRADO: aprovação de 3 dias atrás ainda válida

if (!acao.aprovacoes.includes(usuario.id)) {

  acao.aprovacoes.push(usuario.id)  // ← não checa expiração!

}



// CORRETO: checar antes

if (new Date() > acao.expirado_em) return { mensagem: 'Aprovação expirada.' }

⚠️ Armadilha 3: Mesmo usuário aprovando a própria ação

// O autor não deveria aprovar sua própria ação

if (acao.autor.id === usuario.id) {

  return { mensagem: '❌ Você não pode aprovar sua própria ação.' }

}
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

2 TODOs no starter.ts:

TODO 1 — temPermissao(usuario, comando):

function temPermissao(usuario: Usuario, comando: string): boolean {

  if (usuario.roles.includes('admin')) return true;

  for (const role of usuario.roles) {

    const perms = PERMISSOES[role] || [];

    if (perms.includes('*') || perms.includes(comando)) return true;

    const base = comando.split(' ')[0];

    if (perms.includes(base)) return true;

  }

  return false;

}

TODO 2 — diff de mudanças em executarDeploy:

const diff = await client.messages.create({

  model: 'claude-haiku-4-5-20251001',

  max_tokens: 300,

  messages: [{ role: 'user', content: `Gere changelog resumido para deploy de ${servico} versão ${versao} em prod.` }]

})

const textoiff = diff.content[0].text

Rode com npx ts-node starter.ts e observe o fluxo: status → tentativa de deploy em prod → criação de aprovação → aprovação executada.

Agora você está pronto para o lab.