A conta desativada — o desligamento que hoje é implícito¶
Prioridade: alta — à frente da 061, decidida
pelo papel de Product Owner em 2026-09-09, a confirmar pela pessoa alocada.
Risco: em produção hoje, com dado real de uma organização.
Origem: levantado pela spec 061 como "item de backlog anterior a esta feature", e
medido com teste pelo papel de Security em 2026-09-09 —
H3, severidade Alta, subida de Média
justamente por ter sido medida.
Correção do meu próprio levantamento, registrada em vez de silenciada. A primeira versão deste documento argumentava que o desligamento de hoje funciona — que reiniciar a senha gira o token de sessão e corta o acesso — e que o risco era a indistinguibilidade dos estados na tela. A primeira metade está certa e a segunda continua de pé, mas as duas juntas subestimam o problema: eu inferi do código o caminho que quem administra deveria seguir, e não medi o que acontece no caminho que a tela oferece. O H3 mediu, e o resultado é pior. É a lição "limitação declarada sem olhar o dado" aplicada a mim: ler o código da tela não diz o que a tela deixa fazer.
Estado em 2026-09-10 — o protótipo veio, e as seis lacunas fecharam¶
A pessoa mantenedora pediu o protótipo e depois "pode implementar". O protótipo é
accounts-disable.html,
com o prompt que o gerou e as decisões em
PROMPT.md e
README.md. A tela foi
reimplementada contra a seção 3 do PROMPT, que é a régua do QA.
| O que faltava (tabela abaixo) | fechado como |
|---|---|
| a razão ao desativar | lista fechada e nota, com o vocabulário em access.account_lifecycle na base de conhecimento. FR-025 |
| o ator e a razão ao reativar | enable_user/4 — tenant, conta, ator e mapa de razão. FR-026 |
| o teste de que nada muda na pessoa | test "a pessoa, o elo e os escopos ficam — e os escopos ficam INERTES", com a guarda que impede o verde contra zero. Prova medida: injetei a revogação do elo dentro do desativar, e o teste reprovou |
| o texto do revoke | passou a dizer "Does not remove access", em dois lugares — na confirmação e no painel dos três atos, acima da tabela, para ser lido antes do ato. FR-029 |
| o teste que reprova se revogar o elo voltar a ser o único ato | test "revogar o elo NÃO é o único ato oferecido a quem desliga" |
| o protótipo aprovado | existe, e a tela é a dele |
A decisão 1 foi tomada pela recomendação, e não pela omissão¶
Esta página recomendava (b) relator próprio, e o código da v0.7.0 tomou (a) sozinho. A
implementação de 2026-09-10 migrou para (b): account_disablements, um episódio por
desativação, com índice único parcial garantindo um aberto por conta.
users.disabled_at fica — é a resposta rápida a "pode entrar?", lida a cada entrada —, e as
duas escritas acontecem na mesma transação. Não são duas fontes discordando: é denormalização
declarada, e o estado inválido (marca sem episódio) é impedido pela transação e nomeado pela
recusa :sem_episodio_aberto quando aparecer.
O que a forma (b) resolveu, e (a) não resolvia: o par de colunas cabia um episódio. Uma conta
desativada duas vezes perdia a primeira, e reativar_changeset/1 apagava a que havia.
O que continua aberto, e é do papel de Product Owner¶
A reclassificação do D06. Duas das três recusas foram consertadas depois da avaliação, e a terceira — "a tela mudou sem protótipo aprovado" — deixou de valer. Quem tem o papel reclassifica; não é decisão de quem implementou.
Conta desativada não autentica por token continua sendo critério que não se pode avaliar ainda: o token não existe (spec 061 sem código). A tela escreve isso como promessa e não controle, em vez de o omitir.
Estado em 2026-09-09T23:19Z — construída, em produção, e NÃO ACEITA¶
O #844 construiu isto e foi mergeado na
development às 20:24Z. O veredito de aceitação está em
docs/releases/v0.7.0.md, entregável D06:
sro.not_accepted_deliverable — quatro critérios não conformes, um sem evidência.
O que este item passa a ser: não é mais "o que construir". É o que falta para que o que já está em produção possa ser aceito e anunciado.
| O que falta | Critério desta página que fica aberto |
|---|---|
| a razão ao desativar | "o registro diz quem desativou, quando e por quê" — a migração dá disabled_at e disabled_by_user_id; o porquê não existe |
| o ator e a razão ao reativar | "reativar é ato registrado, com autor e razão" — enable_user/2 recebe tenant e user_id, e mais nada. Desativar deixa autor; reativar não deixa |
| o teste de que nada muda na pessoa | "o roster, as medidas e o histórico da pessoa não mudam ao desativar a conta" — sem evidência: nenhum teste compara medida antes e depois |
| o texto do revoke | Parte C — hoje diz "a pessoa deixa de entrar pelo username do GitHub; a história do elo fica", e não diz que não remove acesso |
| o teste que reprova se revogar o elo voltar a ser o único ato oferecido a quem desliga | Parte C — não existe |
o protótipo aprovado da superfície de /accounts |
a seção Tem tela, logo tem protótipo, abaixo, que eu escrevi antes do código e o código não seguiu |
As quatro decisões foram tomadas pelo código, e uma contraria a recomendação¶
Esta página diz, textualmente: "Não decomponho antes destas respostas — decidir depois de construir é o que produz retrabalho." As quatro foram decididas na implementação, sem registro de escolha da pessoa mantenedora. A que importa é a decisão 1:
| recomendação | (b) relator próprio, "porque um disabled_at solto repete o connected_tools.status da ADR 0004 D7, que já é dívida declarada" |
| implementado | (a) disabled_at + autor na própria users |
| consequência | a dívida da #178 ganha uma segunda ocorrência, e o campo de razão — que um relator carregaria naturalmente — é justamente o que ficou de fora |
Isto não é motivo para desfazer, e não é o que proponho: a migração é só-acréscimo, foi ensaiada contra dado real, e desfazê-la custaria mais do que a dívida. É motivo para a decisão ser tomada — confirmar (a) com a razão escrita, ou decidir migrar para (b) — em vez de ficar como está, que é a forma vencendo por omissão.
O que não está em causa¶
A migração 20260909180000 está em ordem, e a recusa não a alcança: colunas nulas, sem
backfill, sem CHECK, ensaiada contra cópia do banco real com 0 contas desativadas por
acidente —
docs/producao/ensaio-2026-09-09-migracao-conta-desativada.md.
E as regras de recusa que existem — não se desativa a si, nem conta de outro tenant, nem duas
vezes; reativar não devolve a senha — têm asserção nomeada.
O fato, verificado no esquema e não na prosa¶
Não existe estado de conta desativada nesta plataforma.
| Onde | O que tem | O que falta |
|---|---|---|
users (lib/the_band/tenants/user.ex:38) |
email, name, role, tenant_id, credencial, sessão, elo com a pessoa |
nenhum status, disabled_at ou equivalente |
tenants (lib/the_band/tenants/tenant.ex:19) |
status, com "active" por omissão |
— |
migrações que tocam users |
só acrescentam credencial (20260828160855) e o elo com a pessoa (20260827050000) |
nenhuma coluna de desativação |
/accounts (accounts_live/index.ex) |
criar, reset, associar, revogar_elo, buscar_pessoa |
não há desativar |
O person_revoked_at que existe revoga o elo com a pessoa observada, não a conta: quem
perde o elo continua entrando.
A lacuna tem três partes, e só uma é a coluna que falta¶
Medidas com teste pelo papel de Security, não inferidas:
| # | Parte | O que se mediu |
|---|---|---|
| A | users não tem estado de conta desativada |
a tabela nasce com email, name, role, tenant_id (20260809120000_create_tenants_and_users.exs:26-31); nenhuma migração posterior acrescenta situação |
| B | tenants.status existe e não é lido em lugar nenhum |
marcar um tenant como "suspended" e autenticar em seguida: as duas coisas funcionam, com assert suspenso.status == "suspended" antes, para o teste não passar por não ter suspendido nada. É uma coluna que parece um controle e não é um |
| C | revogar o elo — o ato que a tela oferece a quem desliga alguém — não remove acesso | com o elo revogado, pode_ver/3 recusa o painel da própria pessoa e Tenants.authenticate/2 continua devolvendo {:ok, user}; /people, /teams e /work/verifications/people continuam abrindo |
A parte C é a que muda a natureza do item, e é a que eu não tinha visto. O único ato que a tela oferece com cara de desligamento — revoke link — não desliga. Quem administra faz o que a interface sugere, acredita ter cortado o acesso, e a pessoa continua entrando com a senha que sabe.
E é pior em combinação: pelo H2 o veredito de acesso vale em 2 de 24 rotas autenticadas, então o que a pessoa continua lendo não é o painel dela — é a lista de pessoas, as equipes, os perfis derivados e o ranking nominal de quem integrou com a verificação vermelha.
O único ato que de facto corta o acesso, e por que isso não salva¶
Reiniciar a senha e não entregar a temporária funciona:
# lib/the_band/tenants/user.ex:137 — dentro de hash_password/2
|> put_change(:session_token, novo_token())
# "Token de sessão novo — girá-lo derruba as outras sessões (FR-015)."
Mas isto é conhecimento tribal, não caminho oferecido: nada na tela diz que reset é o desligamento e revoke link não é. O ato certo é o que não parece ser, e o ato que parece ser é o que não funciona.
E há um segundo risco, na mesma tela: o desligado é indistinguível do recém-criado¶
reset_password/3 grava must_change_password: true (é o temporary: true de
gravar_temporaria/1). E a lista de contas mostra exactamente três estados:
# lib/the_band_web/live/accounts_live/index.ex:327-333
<span :if={user.password_hash && !user.must_change_password}>definida</span>
<span :if={user.password_hash && user.must_change_password} class="text-warning">
temporária pendente
</span>
<span :if={!user.password_hash} class="opacity-60">sem senha — a entrada recusa</span>
Uma pessoa desligada aparece como temporária pendente. Uma pessoa acabada de criar, que
ainda não entrou, aparece como temporária pendente. O mesmo rótulo, a mesma cor, a mesma
linha — e as duas pedem ações opostas:
| Estado real | O que a tela diz | O que quem administra faz, corretamente |
|---|---|---|
| acabou de ser criada, nunca entrou | temporária pendente |
reset, e entregar a senha — é o caminho normal de entrada |
| foi desligada | temporária pendente |
nada, para sempre |
O ato correto para o primeiro caso reativa o segundo, em silêncio, sem nada na tela avisando. Não é hipótese remota: é o procedimento de rotina de quem administra, aplicado à linha errada, e a tela não oferece nenhuma forma de distinguir — não há data do desligamento, não há autor, não há razão, não há sequer um rótulo diferente.
E a plataforma não consegue responder "quem tem acesso hoje, quem foi desligado, por quem e quando", que é a pergunta que se faz num pedido de auditoria. Num produto cuja tese é proveniência, o ato mais sensível da administração é o único sem proveniência alguma.
É a mesma família de defeito que a casa já nomeou¶
Zero não é a mesma coisa que não conferido — a asserção que carrega a seção Problems now (060 FR-067). Aqui: desligada não é a mesma coisa que ainda não entrou, e as duas se apresentam como um rótulo só. Duas afirmações opostas renderizadas idênticas é o defeito que esta plataforma existe para não cometer.
O que a 061 acrescenta, e o que ela não causa¶
A spec 061 registra a limitação com precisão: "esse mecanismo para de funcionar no dia em que
existir token". Com token, a pessoa que saiu continua lendo os dados por API — o giro do
session_token não alcança um token de API, e quem administra não tem ato que resolva. É
acesso órfão, e passa por todas as travas porque o token é válido e a conta existe.
Mas a 061 agrava, não origina. O defeito é de hoje, sem token nenhum, e são as partes A, B e C acima. O critério de separação do papel de Security é o certo: existe caminho de exploração contra o código que está rodando hoje? Para a parte C, sim, medido.
A decisão de prioridade, e a razão¶
Entra à frente da 061. Quatro razões, em ordem de força:
-
É risco de hoje, em produção, com dado real — e medido, não inferido. Não depende da API existir. O ato que a tela oferece para desligar alguém não desliga, e o que a pessoa continua lendo é ampliado pelo H2.
-
Colocá-la à frente não custa nada no caminho crítico, porque a 061 está bloqueada. A spec 061 tem tela (a de tokens, US1 e US3) e não tem protótipo — não existe
specs/061-api-publica/prototipo/. Pela regra deste papel, US1 e US3 não são decompostas em tarefas antes de o Design desenhar e a pessoa mantenedora aprovar. Então "à frente da 061" é grátis: não há troca a fazer, e sequenciar na ordem inversa não aceleraria a 061 em um dia. -
Feita antes, a 061 não precisa publicar um buraco. Hoje a 061 depende da FR-074 como substituto, e obriga a limitação a "aparecer na documentação da API". Entregar a API primeiro significa publicar, no primeiro contrato público do produto, a declaração de que acesso órfão não tem remédio. Com
disabled_atpronto, a regra passa a ser a simples e verdadeira: conta desativada não autentica, nem por senha nem por token — e não há limitação a documentar. -
Não está bloqueada em recurso nenhum. Ao contrário do ensaio de restauração, que espera uma conta S3 que não existe, esta pede uma coluna, um ato na tela e uma regra nas duas portas de autenticação. Fecha com trabalho, e trabalho é o que se pode agendar.
O que a decisão não é: não é "a 061 pode esperar indefinidamente". A 061 é o que destrava a 062 (servidor MCP) e é proposta de 2026-09-08. A ordem proposta é primeiro esta, que é pequena e desbloqueada; a 061 em seguida, sem a limitação.
O que precisa ser decidido antes de haver tarefa¶
Não decomponho antes destas respostas — decidir depois de construir é o que produz retrabalho.
| # | Pergunta | Opções | Recomendação |
|---|---|---|---|
| 1 | desativar é marca ou é campo? | (a) disabled_at + autor + razão na própria users; (b) relator próprio, como as concessões e os escopos |
(b), por coerência: esta casa registra ato com autor, data e razão, e revogação sem apagar. Um disabled_at solto repete o connected_tools.status da ADR 0004 D7, que já é dívida declarada |
| 2 | o que aparece na lista de contas | (a) um quarto estado no mesmo lugar da senha; (b) coluna própria de situação da conta, separada da situação da senha | (b) — misturar situação da conta com situação da credencial é o que produziu a colisão de hoje. São dois fatos, e o rótulo único é a causa do defeito |
| 3 | reativar existe? | (a) sim, com autor e razão; (b) não — cria-se conta nova | (a), porque (b) perderia o histórico da pessoa e o elo com eo_people |
| 4 | a conta desativada aparece no roster e nas medidas? | — | sim, e sem mudança: é o mesmo princípio da saída declarada da US3 — quem saiu continua contando no período em que esteve. Desativar a conta não altera o que a pessoa fez |
Tem tela, logo tem protótipo¶
O ato vive em /accounts, e a situação da conta aparece na lista. Pela regra da casa, o
Design desenha antes: o quarto estado (ou a coluna nova), o diálogo que pede a razão, e como o
desligado se distingue do recém-criado sem depender de cor. É uma superfície pequena sobre
uma tela que já existe, e não um protótipo de tela nova.
Critérios que a spec vai precisar carregar¶
Escritos aqui para não nascerem no fim, quando já não se pode avaliá-los:
Parte A — a conta desativada
- conta desativada não autentica por senha e a recusa é a mensagem única, sem dizer que a conta está desativada (não vaza estado de conta para quem não entrou);
- conta desativada não autentica por token — a regra tem de valer nas duas portas, e a segunda ainda não existe;
- desativar gira o token de sessão, e a sessão viva morre no ato;
- o registro diz quem desativou, quando e por quê; nada é apagado;
- a lista de contas distingue desativada de temporária pendente de sem senha, e a distinção sobrevive sem cor;
- reativar é ato registrado, com autor e razão;
- o roster, as medidas e o histórico da pessoa não mudam ao desativar a conta.
Parte B — tenants.status passa a ser lido, ou deixa de existir
- tenant com
statusdiferente de"active"não autentica, e a recusa é a mensagem única; - se a decisão for que suspender tenant não é caso de uso, a coluna sai — coluna que parece controle e não é é pior que coluna ausente, porque quem administra confia nela.
Parte C — o ato da tela diz a verdade sobre o que faz
- revoke link não é apresentado como desligamento: o texto diz que desfaz a ligação entre a conta e a pessoa observada, e que não remove acesso;
- desligar acesso tem ato próprio, nomeado, e é o que aparece a quem procura desligar alguém;
- teste que reprova se revogar o elo passar a ser o único ato oferecido a quem desliga.