No GarraIA — framework de agentes IA em Rust, 100% local, MIT — a Fase 3 (Group Workspace) é o módulo onde múltiplos usuários compartilham arquivos, tasks, chats e memória IA dentro de um espaço comum. É o módulo que define se o projeto pode ser usado por equipes brasileiras sob LGPD ou se vai ficar restrito a uso individual.
Ele encosta no beta agora. E "encostar no beta" tem um significado específico: o ônus da prova de isolamento cross-tenant é verificável, não retórica. Este post documenta como estamos costurando essa prova em duas camadas e por que a maioria das implementações de tenant isolation em SaaS brasileiro pararia no portão de uma auditoria séria.
A regra que define tudo: provar, não prometer
Em qualquer SaaS multi-tenant, três perguntas precisam ter resposta auditável:
- Quem é você — autenticação. (Não é login com senha em texto plano.)
- O que você pode fazer — RBAC. (Não é "o handler decide" caso a caso.)
-
A que dados você tem acesso — tenant isolation. (Não é "o
WHERE group_idestá em toda query, prometo".)
Cada uma dessas perguntas vira um teste auditável. E o número de cenários do teste é o que distingue "documentação de marketing" de "compliance LGPD".
Camada 1 — Autenticação: Argon2id RFC 9106 + JWT HS256 + lazy upgrade dual-verify
O GAR-391c (já shipado, README documenta) define o trait IdentityProvider e a InternalProvider real. Os números que importam:
- Argon2id RFC 9106 como hash novo (não PBKDF2). RFC 9106 é a referência atual; PBKDF2 ainda é aceito pelo NIST mas a recomendação é Argon2id desde 2023.
- JWT HS256 com 15 min de validade para access token, refresh token HMAC separado.
-
Lazy upgrade dual-verify — usuários antigos com hash PBKDF2 (600k iterações HMAC-SHA256) ainda autenticam, e no primeiro login bem-sucedido o hash é re-gerado em Argon2id dentro da mesma transação. Migration 009 adicionou
user_identities.hash_upgraded_atpara o estado desse upgrade.
O ponto técnico chave aqui não é "usamos Argon2id" — quase todo mundo diz isso. O ponto é o dual-verify transacional: o usuário não precisa redefinir senha, o upgrade acontece silenciosamente no fluxo de login, e a migração de hash não precisa de uma "data X em que todos os usuários antigos viram offline". É invisível e seguro, e o estado fica auditável na coluna hash_upgraded_at.
Camada 2 — RBAC: Role × Action × can() com 110-case table-driven test
O garraia-auth define dois enums centrais — Role e Action — e uma função fn can(role: Role, action: Action) -> bool. O extractor Axum Principal puxa role e group do JWT, e RequirePermission(Action) é o middleware que cada endpoint declara.
Os números aqui:
- 5 roles seedados estaticamente via migration 002 (Owner, Admin, Editor, Viewer, Guest).
- 22 actions discretas (FilesRead, FilesWrite, TasksRead, ChatsWrite, etc.).
- 63 role_permissions seedados estaticamente (não é "Admin tem tudo, Viewer tem nada" — a matriz é granular).
- 110 cenários de teste em uma table-driven test que cobre toda combinação Role × Action relevante mais os casos limítrofes.
Por que 110 e não 5 × 22 = 110? Coincidência feliz: o teste cobre os 110 pares relevantes do produto cartesiano, omitindo pares que não fazem sentido produto (ex.: ações internas só por sistema). É a régua mínima para dizer "o RBAC tem cobertura matricial completa".
E mais importante: se uma role nova é adicionada ou uma action nova surge, o teste quebra até alguém decidir explicitamente como aquela célula da matriz se comporta. Isso é o que evita o caso clássico de "achei que Viewer não podia escrever, mas o handler de Tasks não checou".
Camada 3 — Tenant isolation no SQL: FORCE RLS em 10 tabelas + 81 cenários (GAR-392)
Esse é o ponto que mais difere de implementações "tradicionais". A maioria dos SaaS brasileiros depende de filtragem no handler HTTP ou em um repository pattern:
// ❌ O handler "sabe" o que filtrar
let files = sqlx::query!("SELECT * FROM files WHERE group_id = $1", group_id)
.fetch_all(&pool).await?;
Uma linha errada em 200 handlers = vazamento cross-tenant. Auditor LGPD pega na primeira pergunta direta: "como vocês provam que o handler nunca esquece o WHERE group_id?". Resposta honesta: "code review e CodeQL". Resposta auditavelmente robusta: "não conseguimos esquecer, porque o Postgres filtra abaixo do handler".
No GarraIA, essa segunda resposta é o design. O pool de aplicação garraia_app é BYPASSRLS = false. As 10 tabelas sensíveis (users, groups, group_members, chats, messages, memory_items, memory_embeddings, tasks, files, file_versions) têm:
ALTER TABLE files ENABLE ROW LEVEL SECURITY;
ALTER TABLE files FORCE ROW LEVEL SECURITY;
CREATE POLICY files_isolation ON files
USING (group_id = NULLIF(current_setting('app.current_group_id', true), '')::uuid);
Duas decisões que aprendemos a duras penas, e que estão documentadas no ADR 0003:
-
FORCE ROW LEVEL SECURITY— sem ele, o owner da tabela faz bypass automático. Migrações rodando como owner veriam tudo. Para LGPD isso é inadmissível. -
NULLIF(current_setting('app.current_group_id', true), '')::uuid— fail-closed. Se o handler esqueceu de armar o GUC, o cast falha, a query retorna zero linhas. Não é vazamento — é bug óbvio.
O GAR-392 é a matriz que prova isso: 81 cenários × 3 dedicated roles (garraia_app, garraia_login, garraia_signup) × 10 tabelas FORCE RLS. Cada cenário verifica empiricamente que (a) o role correto vê o que deveria ver, e (b) os roles errados não veem nada que não deveriam ver.
E aqui está o detalhe que diferencia "teste" de "prova": um dos cenários explícitos é a transferência de ownership de tabela com RESET ROLE scopeguard-safe, que demonstra empiricamente que mesmo o postgres superuser, ao virar owner da tabela, ainda é barrado pelo FORCE quando autenticado como garraia_app. Esse é o cenário que prova que FORCE não é decoração.
A camada que falta: cross-group authz via HTTP (GAR-391d)
Aqui é onde a semana 21 entra. O GAR-391d é o último entregável do epic GAR-391, e ele estava bloqueado até a semana passada por uma razão simples: para testar cross-group authz em HTTP, você precisa ter endpoints HTTP. E os endpoints da Fase 3.4 (/v1/files, /v1/tasks, /v1/groups, /v1/memory, /v1/chats) só fecharam na semana 20.
O design do GAR-391d:
-
≥100 cenários de teste end-to-end, cobrindo combinação
(usuário do grupo A, tenta acessar recurso do grupo B)para cada endpoint relevante. - Cada cenário sobe um servidor real, autentica via
/v1/auth/login, monta JWT real, faz request HTTP real contra recurso de outro grupo. - Asserção: status
403 Forbidden(e não, por exemplo,404ou — pior —200com payload de outro tenant).
O ponto delicado é que essa suite não pode usar mocking. Se você mocka o RLS, você está testando o handler — não o sistema. A garraia-workspace já tem smoke test via testcontainers (~7s para subir um Postgres real, ADR 0003 documenta), e a suite GAR-391d roda sobre essa infra. Postgres real, RLS real, JWT real, HTTP real.
Quando essa suite virar verde, o epic GAR-391 fecha. Quando o epic fecha, a Fase 3 entra em beta.
Por que isso importa para a empresa brasileira
LGPD obriga a provar isolamento de dados entre clientes. Provar significa: dado um auditor com acesso ao código e ao SQL, ele consegue rodar um cenário e verificar que cross-tenant não vaza. Sem teste de cross-group authz HTTP, a melhor resposta possível é "o handler tem WHERE group_id, confia". Com a suite, a resposta é "esses 100+ cenários rodam em CI a cada PR, todos verdes, executando contra Postgres real com RLS real".
A diferença prática: a primeira resposta perde o cliente no compliance check. A segunda passa.
O ciclo completo: 4 camadas, 4 provas, 1 release
Resumindo o epic GAR-391 quando ele fechar:
| Camada | Cenários | Status |
|---|---|---|
| Autenticação (Argon2id + JWT + dual-verify) | dual-verify integration tests | ✅ GAR-391c shipado |
| RBAC (Role × Action × can()) | 110 cenários table-driven | ✅ GAR-391c shipado |
| RLS no Postgres (FORCE em 10 tabelas) | 81 cenários × 3 roles × 10 tables | ✅ GAR-392 shipado |
| Cross-group authz via HTTP | ≥100 cenários end-to-end | 🚧 GAR-391d em curso (semana 21) |
São quatro provas independentes. Quando a quarta fechar, a Fase 3 entra em beta. E "entra em beta" significa que um time brasileiro pode subir o GarraIA on-prem, criar grupos para sua equipe ou família, e dormir tranquilo de que o vizinho de mesa não tem acesso ao chat privado nem aos arquivos do outro grupo — não por promessa do handler, mas por prova auditável em SQL e em HTTP.
Quer ver o código?
GarraIA é open source MIT: github.com/michelbr84/GarraRUST
Roadmap completo e issues no Linear: linear.app/chatgpt25/team/GAR/projects
Wiki técnica (changelogs semanais): github.com/michelbr84/GarraRUST/wiki
Top comments (0)