0009 — Modelo de Consentimento de Dados em Camadas
Status
Accepted · 2026-04-26 · Autor: Alexandre + Claude.
Contexto
Multi-tenancy expõe dados de tenant inferior pra tenant superior. Sem regra clara de consent:
- Risco LGPD: clínica não autorizou consultor, mas dado vaza
- Risco comercial: clínica reluta em entrar na plataforma se “todo mundo pode ver tudo”
- Risco operacional: relação consultor↔clínica precisa de dados pra funcionar (status, contrato, etc)
Equilíbrio: respeitar privacidade + viabilizar relação comercial.
Decisão
1. Cascata estrita default deny
2 flags com cascata:
Clínica decide → "consultoria pode ver meus dados clínicos?" flag A em clinics.data_sharing_with_consultancy Default: 'denied'
Consultoria decide → "plataforma pode ver agregados meus?" flag B em consultancies.data_sharing_with_platform Default: 'denied'Cascata estrita: para um dado de clínica entrar em métricas agregadas vistas pelo platform_admin, AMBOS A e B precisam ser granted. Se A=denied, dado da clínica nem entra no agregado mesmo se B=granted.
Razão: tecnicamente platform_admin estaria tratando dado da clínica quando agrega. LGPD exige consent pra qualquer tratamento, mesmo anonimizado. Sem A, qualquer fluxo pra cima é violação.
2. Helpers RLS
-- Pergunta operacional: consultor pode ver dados clínicos desta clínica?CREATE OR REPLACE FUNCTION public.consultancy_has_data_access_to(p_clinic_id UUID)RETURNS BOOLEANLANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS ( SELECT 1 FROM public.clinics c WHERE c.id = p_clinic_id AND c.deleted_at IS NULL AND public.consultancy_can_view_clinic(public.jwt_current_consultancy_id(), c.id) AND ( -- Caso 1: consent explícito c.data_sharing_with_consultancy = 'granted' OR -- Caso 2: pacote avulso ativo (consent escopado por pacote — Ideia 1.J) EXISTS ( SELECT 1 FROM public.session_package_purchases spp WHERE spp.clinic_id = c.id AND spp.consultancy_id = public.jwt_current_consultancy_id() AND NOW() < spp.data_access_consent_until AND spp.status IN ('active', 'consumed') ) ) );$$;
-- Pra agregação pelo platform_adminCREATE OR REPLACE FUNCTION public.platform_can_aggregate_clinic(p_clinic_id UUID)RETURNS BOOLEANLANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS ( SELECT 1 FROM public.clinics c INNER JOIN public.consultancies cs ON c.consultancy_id = cs.id WHERE c.id = p_clinic_id AND c.deleted_at IS NULL AND c.data_sharing_with_consultancy = 'granted' -- flag A AND cs.data_sharing_with_platform = 'granted' -- flag B );$$;3. Boundary mínimo (sempre visível)
Dados operacionais necessários pra relação comercial existir — visíveis independente do consent:
Consultoria → Clínica (mesmo se A=denied)
- Status da subscription B2C (active, past_due, suspended)
- Tier do plano contratado
- Ciclo de cobrança
- Data de provisão / churn
- Razão social e CNPJ
- Status do contrato
- Identidade do
clinic_owner(nome, email — pra comunicação operacional)
Plataforma → Consultoria (mesmo se B=denied)
- Quantidade de clínicas ativas (pra calcular tier B2B)
- Status subscription B2B
- MRR
- Audit logs (compliance — você é controlador LGPD)
- Razão social + CNPJ + contato
consultancy_owner
Implementação: tabelas de boundary têm policies separadas que não dependem de data_sharing_*. Apenas is_consultancy_member ou is_platform_admin.
4. Granularidade
Em F1: tudo-ou-nada por clínica.
-- F1data_sharing_with_consultancy TEXT CHECK IN ('granted', 'denied')Em F2 (preparação): granular por categoria via data_sharing_categories jsonb:
{ "patients": "granted", "appointments": "granted", "financial": "denied", "medical_records": "granted", "operational_metrics": "granted"}Schema F2-ready desde F1 (campo jsonb opcional). Implementação operacional ativa em F2.
5. Auditoria de toggles
Tabela imutável data_access_consents:
CREATE TABLE public.data_access_consents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), scope TEXT NOT NULL CHECK (scope IN ('clinic_to_consultancy', 'consultancy_to_platform')), granter_clinic_id UUID REFERENCES public.clinics(id), granter_consultancy_id UUID REFERENCES public.consultancies(id), action TEXT NOT NULL CHECK (action IN ('granted', 'revoked')), granted_by_user_id UUID NOT NULL REFERENCES auth.users(id), granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reason TEXT, ip_address INET, user_agent TEXT, CHECK ( (scope = 'clinic_to_consultancy' AND granter_clinic_id IS NOT NULL AND granter_consultancy_id IS NULL) OR (scope = 'consultancy_to_platform' AND granter_clinic_id IS NULL AND granter_consultancy_id IS NOT NULL) ));
-- Imutável: sem UPDATE/DELETEREVOKE UPDATE, DELETE ON public.data_access_consents FROM authenticated, anon;Cada toggle gera linha. Histórico imutável.
6. Modelo de revogação
Quando clínica revoga (A: granted → denied):
- Imediato no acesso: na próxima query, consultor vê apenas boundary mínimo
- Notificação ao consultor: email “Cliente X revogou seu acesso pleno aos dados; mantenha contato pra resolver”
- Sem janela de tolerância: revogação é direito do titular LGPD; respeitar imediatamente
7. Default no provisionamento
Quando clínica é provisionada (ADR 0006 §8):
- Default:
data_sharing_with_consultancy = 'denied' - Cláusula contratual explícita no contrato comercial: “autorizo compartilhamento de dados pra fins de consultoria”
- Checkbox específico no fluxo
/contrato/:token(rota de aprovação pública) - Se checkbox marcado:
provision-clinicinsere comgranted+ linha emdata_access_consents
Sem checkbox marcado, clínica entra denied mesmo se contrato menciona consultoria. Decisão consciente pra LGPD-strict.
8. UI implicada
Pra clínica:
/clinic/configuracoes/privacidade— toggle A + audit trail (quem do consultor acessou meus dados, quando, qual recurso)- Mostra: “Você está compartilhando dados com [Consultor]” / “Não está compartilhando”
- Botão de revogar
Pra consultoria:
/consultancy/configuracoes/dados-agregados— toggle B + lista das clínicas que autorizaram (% de cobertura)- Mostra: “X de Y clínicas autorizaram acesso individual”
- “Você autorizou plataforma agregar: Sim/Não”
Consequências
Positivas
- LGPD-strong: consent explícito + imutável + audit
- Confiança comercial: clínica controla sua privacidade
- Defensável juridicamente: trilha de audit imutável
- Boundary mínimo permite operação: cobrança e contrato funcionam sem expor dados clínicos
Negativas
- Complexidade RLS: 2 helpers diferentes para tabelas operacionais e boundary mínimo
- Métricas agregadas filtradas: platform_admin pode ver “60% de cobertura” — desejável (transparência) mas reduz dataset
Riscos
- Bug em helper expõe dado bypassando consent: pior cenário possível
- Mitigação: tests RLS extensivos com 2 consultorias seed; cobertura de boundary mínimo
- Clínica revogar e consultor reclamar: operacional
- Mitigação: comunicação clara via email automatizado; consultor pode escalar pra plataforma se suspeitar má-fé
Alternativas consideradas
1. Default allow (consultor vê tudo a menos que explícito)
- Prós: simples
- Contras: viola LGPD; consent precisa ser explícito por lei
- Rejeitado.
2. Consent ortogonal (não cascata)
Flag A controla individual; flag B controla agregação independente.
- Prós: mais flexível
- Contras: clínica que negou consultor poderia ter dado em agregado; defensável se anônimo, mas LGPD vê com olho desconfiado
- Rejeitado — cascata estrita é mais conservador e LGPD-safe.
3. Granularidade por categoria desde F1
- Prós: maior controle
- Contras: UX complexa; muitos toggles confundem; YAGNI até F1.5+ ter demanda real
- Aceito como preparação mas não ativo em F1.
Atualizações de documentos
- AGENTS.md §5.3 (Consent & LGPD)
- BLUEPRINT.md §3.5 (Camadas de segurança)
docs/SECURITY.mddocs/clinic/configuracoes-privacidade.mddocs/consultancy/configuracoes-dados-agregados.md
Referências
- ADR 0001 (multi-consultancy)
- ADR 0002 (tri-lens)
- AGENTS.md §5.3
- BLUEPRINT.md §3