Pular para o conteúdo

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:

  1. Risco LGPD: clínica não autorizou consultor, mas dado vaza
  2. Risco comercial: clínica reluta em entrar na plataforma se “todo mundo pode ver tudo”
  3. 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 BOOLEAN
LANGUAGE 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_admin
CREATE OR REPLACE FUNCTION public.platform_can_aggregate_clinic(p_clinic_id UUID)
RETURNS BOOLEAN
LANGUAGE 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.

-- F1
data_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/DELETE
REVOKE 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-clinic insere com granted + linha em data_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.

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.md
  • docs/clinic/configuracoes-privacidade.md
  • docs/consultancy/configuracoes-dados-agregados.md

Referências

  • ADR 0001 (multi-consultancy)
  • ADR 0002 (tri-lens)
  • AGENTS.md §5.3
  • BLUEPRINT.md §3