Lab 69 — Governança do lake: permissão por coluna
O problema, e a empresa que o tem
A Cadência é o mesmo marketplace do L65 — 40 lojistas parceiros, catálogo Glue vivo, alimentado todo dia pelo crawler que aquele laboratório deixou rodando. Theo, o analista que uma vez levou um COLUMN_NOT_FOUND por confiar numa DDL escrita à mão, aprendeu a lição: hoje ele confia no catálogo. E é exatamente essa confiança que abriu o próximo buraco.
Nos últimos dois meses, dois times mexeram no schema de prata_pedidos sem conversar entre si. Produto adicionou cpf_cliente, nome_cliente e telefone_cliente — exigência da nota fiscal eletrônica. Financeiro adicionou margem_custo_centavos, calculada por pedido para fechar repasse com cada lojista. O crawler do L65, com UPDATE_IN_DATABASE, aplicou as duas mudanças automaticamente — é literalmente o que aquela política foi desenhada para fazer.
Theo roda uma consulta de rotina para montar o painel semanal de atendimento — SELECT * FROM prata_pedidos LIMIT 20 — e as quatro colunas novas vêm juntas com o resto, sem aviso, sem erro, sem nada que distinga CPF do cliente de status do pedido. Ninguém decidiu isso — é o mesmo padrão do CPF que apareceu no log da Cadência (L49), só que uma camada abaixo: lá era o logger que serializava o objeto inteiro; aqui é o IAM que só sabe conceder a tabela inteira, sem noção nenhuma de coluna.
O que este laboratório NÃO é
Não é sobre como o catálogo passou a existir — isso é o L65, pré-requisito direto, e este módulo assume prata_pedidos já vivo e alimentado pelo crawler. Não é sobre como minimizar ou mascarar um campo no CÓDIGO da aplicação — isso é o L49, e aqui reaproveitamos só o padrão (campo derivado mascarado quando há caso de uso real), não a implementação nem a empresa. E não é conformidade legal: controle de acesso por coluna é princípio técnico de minimização, presente na LGPD e no GDPR, mas não substitui análise jurídica de base legal e finalidade — a mesma ressalva do L49 vale aqui.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que IAM sozinho (glue:GetTable + s3:GetObject) não sustenta permissão por coluna, e por que o Lake Formation precisa estar no caminho da consulta.
- Registrar o data lake location do L65 no Lake Formation e migrar do modelo de IAM direto para o modelo de permissão do LF.
- Aplicar tags LF-TBAC (sensibilidade=pii, sensibilidade=financeiro) às colunas do catálogo, e conceder acesso por tag em vez de nomear analista.
- Configurar um data filter de linha restringindo um lojista parceiro à própria loja, na mesma concessão que já filtra coluna.
- Diferenciar exclusão de coluna (ela simplesmente não existe no schema efetivo) de negação explícita (a consulta nomeada falha com erro do Lake Formation).
- Diagnosticar uma concessão do LF que não surte efeito porque uma policy IAM antiga ainda concede acesso direto ao recurso.
- Provar, com número, que uma consulta fora do papel de um principal realmente nega — não apenas assumir que nega.
- Auditar quem consultou qual coluna, via eventos do Lake Formation no CloudTrail.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Modelo de permissão do Lake Formation | SAP-C02 | registrar o bucket como Data Lake Location muda quem decide o acesso | que ativar o LF não substitui o IAM — os dois precisam concordar, é relação de E, não de OU |
| LF-TBAC (tag-based access control) | SAP-C02 | tag de sensibilidade na coluna, concessão por expressão de tag | que tag escala com número de coluna e papel, e GRANT nomeado por usuário não |
| Column-level e row-level (data filters) | SAP-C02 | um único data cells filter combina coluna incluída e predicado de linha | que os dois cabem na MESMA concessão, sem duplicar mecanismo |
| Underlying data access vs acesso nomeado | SAP-C02, SAA-C03 | Data Lake Location registrado; credencial de S3 emitida pelo próprio LF | por que a policy IAM do analista não tem mais s3:GetObject direto |
| Auditoria de acesso a dado sensível | SAP-C02 | eventos do Lake Formation no CloudTrail, por principal e por coluna | que 'quem viu qual coluna' é resposta rastreável, não suposição |
| Negação de coluna vs mascaramento em aplicação (L49) | SAP-C02 | LF nega a coluna inteira para quem não tem a tag; L49 altera o VALOR antes de existir no log | que são camadas diferentes: uma impede VER, a outra impede que o dado bruto EXISTA no destino |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um administrador que revoga a permissão de um analista só no Lake Formation e pergunta se isso basta para tirar o acesso dele. A resposta certa é que não se pode concluir isso sem checar o IAM: se uma policy antiga ainda concede glue:GetTable e s3:GetObject diretamente na tabela, o analista continua lendo tudo, porque o IAM decidiu antes do LF entrar em cena — revogar só um lado da relação de E deixa o outro lado inteiro de pé.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Atendimento não pode ver CPF, nome nem telefone do cliente | obrigatório | tag sensibilidade=pii nas 3 colunas + concessão que exclui essas colunas do papel de atendimento |
| Financeiro precisa de margem de custo, mas nunca de dado pessoal | obrigatório | duas tags distintas (pii, financeiro) concedidas a papéis diferentes na MESMA tabela — exige TBAC, não um único GRANT |
| Lojista parceiro só pode ver pedido da própria loja | obrigatório, cláusula do contrato de parceria | exige data filter de LINHA (predicado loja_id), não só exclusão de coluna |
| Consulta legítima de métrica agregada não pode quebrar | contínuo | concessão por tag preserva as 8 colunas não sensíveis para todo papel — só exclui o que é sensível |
| Auditoria trimestral exige prova de que o acesso negado nega de verdade | recorrente | teste automatizado que roda a consulta como cada papel e verifica a contagem de coluna e o erro esperado |
| Coluna nova do pipeline do L65 não pode herdar acesso amplo por padrão | recorrente, já que o crawler atualiza sozinho | tag associada no mesmo processo que o L65 usa para atualizar o schema, não como tarefa manual depois do fato |
Arquitetura mínima: o IAM só sabe conceder a tabela inteira
Este é o desenho que a Cadência tem hoje, e ele é o ponto de partida honesto: o catálogo do L65 funciona, a consulta funciona, e o defeito é invisível até alguém procurar por ele — que é exatamente o que este laboratório começa fazendo.
- → SELECT * FROM prata_pedidos, a mesma consulta de sempre
- → assume o papel único de leitura analytics
- → glue:GetTable e glue:GetPartitions sem restrição de coluna
- → resolve as 12 colunas, todas visíveis, nenhuma marcada
- → aponta para os mesmos arquivos Parquet do L65
- → lê todas as colunas do Parquet, inclusive as 4 sensíveis
- Fora da AWS
- Analytics
- Segurança e identidade
- Armazenamento
Este é o desenho que a Cadência tem hoje, herdado do L65: o catálogo funciona, a consulta funciona, e o defeito é invisível até alguém procurar por ele. Percorra os passos e repare que o IAM nunca teve a granularidade que este problema pede — ele concede glue:GetTable numa tabela, não numa coluna.
- Theo confia no catálogo, e é exatamente por isso que não desconfia. Depois do L65, Theo aprendeu a consultar o catálogo em vez de escrever DDL à mão. A lição resolveu o problema de schema desatualizado — e criou uma falsa sensação de que tudo que o catálogo mostra é seguro de mostrar.
- O papel de leitura é amplo por desenho: tabela inteira, sem coluna. O papel único de analytics foi criado no L65 para simplificar a consulta — e simplificar demais significa não distinguir coluna nenhuma. Ele concede acesso binário: toda a tabela, ou nada.
- IAM concede a tabela sem saber que 4 das 12 colunas são sensíveis. glue:GetTable e s3:GetObject não têm parâmetro de coluna. O IAM decide no nível do RECURSO — a tabela — e é estruturalmente incapaz de expressar 'esta coluna sim, aquela não'.
- O catálogo do L65 publica qualquer coluna nova, marcada ou não. A política UPDATE_IN_DATABASE do crawler — decisão correta do L65 para manter o schema atual — não tem opinião sobre sensibilidade. Ela aplica a mudança de schema automaticamente, sensível ou não.
- Athena lê o Parquet inteiro, sem seletividade nenhuma. Sem uma camada que decida por coluna, a leitura colunar do Parquet traz de volta exatamente o que foi pedido: todas as colunas que o SELECT * enxerga no catálogo.
- O painel de atendimento chega com CPF e margem de custo juntos. O resultado final mistura dado pessoal do cliente com dado financeiro interno da Cadência no mesmo relatório de atendimento — dois motivos de restrição diferentes, nenhum dos dois respeitado.
Antes de corrigir qualquer coisa, prove que o problema existe de verdade — com o schema real, não com a suposição de que 'deve ter algo sensível ali'.
# Rode ANTES de qualquer correcao — o numero que sai daqui e a linha de base.
aws athena start-query-execution \
--query-string "SELECT * FROM cadencia_lake.prata_pedidos LIMIT 5" \
--query-execution-context Database=cadencia_lake \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
aws athena get-query-results --query-execution-id "$(terraform output -raw ultima_execucao)" \
--query 'ResultSet.ResultSetMetadata.ColumnInfo[].Name'
# Esperado hoje: 12 colunas, incluindo cpf_cliente, nome_cliente, telefone_cliente e
# margem_custo_centavos — nenhuma marcada, nenhuma excluida. E a linha de base que
# a secao de prova, mais adiante, compara com o numero depois da correcao.Dado pessoal e margem de custo, no mesmo relatório de atendimento
Um painel pensado para localizar pedido e status de entrega passou a carregar CPF, nome e telefone do cliente — e a margem de custo que a Cadência embolsa por pedido, um dado que nenhum lojista parceiro deveria ver e que a maioria dos analistas internos também não precisa. Não é um vazamento externo, mas é exposição real: qualquer pessoa com o papel de atendimento, hoje, já pode ter lido os dois.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A troca não é 'acrescentar uma caixa' — é trocar quem decide o acesso.
- → SELECT * FROM prata_pedidos, mesma consulta de sempre
- → SELECT margem_custo_centavos, loja_id FROM prata_pedidos
- → consulta no portal, autenticada como a própria loja
- → tag de sensibilidade associada a cada coluna do catálogo
- → toda consulta passa pela concessão do Lake Formation antes do S3
- → a concessão referencia a TAG da coluna, nunca o nome do analista
- → resolve o schema efetivo por principal: 8, 9 ou 4 colunas
- → aponta para os mesmos arquivos Parquet do L65
- → libera leitura física só do que a concessão aprovou
- → todo GRANT avaliado vira evento auditável
- → evento de acesso alcança a regra de monitoramento
- → tentativa fora da matriz esperada dispara notificação
- Fora da AWS
- Analytics
- Armazenamento
- Gestão e governança
- Integração de apps
A troca não é 'acrescentar Lake Formation por cima do que já existe' — é substituir 'IAM concede a tabela inteira' por 'LF resolve, por principal, o schema efetivo'. Três papéis, uma tabela física só, e cada um sai da mesma consulta com uma contagem de coluna diferente. Percorra os passos: cada peça nova rastreia a um requisito da seção anterior.
- Três papéis, a mesma tabela, a mesma consulta de sempre. Nada muda do lado de quem pergunta — Theo, Ana e Rita continuam escrevendo o SQL que sempre escreveram. A diferença inteira está no que a consulta resolve depois de sair da mão deles.
- A tag de sensibilidade mora na coluna, aplicada no mesmo pipeline do L65. A tag não é uma tarefa avulsa de configuração: ela é associada no mesmo processo que o crawler do L65 usa para atualizar o schema, então uma coluna nova nasce marcada, não fica exposta até alguém lembrar de marcá-la depois.
- Toda consulta passa pela concessão do Lake Formation antes de tocar o S3. Com o Data Lake Location registrado, o Athena não pede credencial direta de S3 — ele pede ao Lake Formation, que decide e emite uma credencial temporária escopada à concessão vigente.
- A concessão aponta para a TAG, nunca para o nome do analista. Trocar um GRANT nomeado por um GRANT de tag é o que faz a governança escalar: analista novo com o papel certo herda a concessão automaticamente, sem editar nenhuma política existente.
- O schema que cada papel recebe já sai filtrado do catálogo. Theo vê 8 colunas (sem PII, sem margem de custo). Ana vê 9 (com margem de custo, sem PII). Rita vê 4, e só das linhas da própria loja. A mesma tabela física, três respostas diferentes.
- A leitura física do S3 só libera o que a concessão aprovou. A leitura colunar do Parquet pula o que foi negado — column-level do Lake Formation tende a reduzir byte varrido, não aumentar, porque a coluna negada nunca é lida do arquivo.
- Toda avaliação vira evento auditável, e negação fora do padrão alerta. Diferente do desenho mínimo, onde ninguém saberia dizer quem leu o quê, aqui cada avaliação de concessão é um evento — e uma negação fora da matriz esperada de papel×coluna dispara alerta.
A diferença estrutural em relação ao desenho mínimo não é a presença do Lake Formation: é que o IAM deixou de ser quem decide coluna. Ele continua concedendo acesso — mas só ao MECANISMO (lakeformation:GetDataAccess), nunca ao dado em si. Quem decide coluna e linha, de agora em diante, é a tag e o data filter.
A correção com maior efeito por linha de Terraform
Registrar o bucket como Data Lake Location e trocar duas policies IAM por lakeformation:GetDataAccess resolve a causa raiz — o IAM deixa de ter opinião sobre coluna — enquanto tag, concessão e data filter continuam sendo a camada que decide o que cada papel vê, sem precisar reescrever nenhuma linha do pipeline do L65.
O caminho de uma consulta, por três papéis diferentes
Os nomes das etapas não são jargão: são onde uma decisão específica acontece. Perder de vista qual etapa decide o quê é o que faz alguém tentar 'corrigir' na tag um problema que só se corrige no IAM, ou vice-versa.
A consequência que mais gente erra: SELECT * não é o mesmo teste que SELECT explícito
SELECT * projeta só o que a concessão inclui no schema efetivo — se três colunas estão fora, o resultado simplesmente tem menos colunas, sem erro nenhum. Nomear explicitamente uma coluna fora da concessão é outra pergunta para o Lake Formation, e a resposta é uma negação explícita: a consulta falha. Um teste de negativa que só roda SELECT * nunca vê essa segunda forma de negar — e é a que prova, de fato, que a coluna está bloqueada, não apenas ausente por acaso.
// Esquema SIMPLIFICADO e ILUSTRATIVO de um evento de acesso do Lake Formation no CloudTrail —
// inspecione um evento real no seu console antes de decidir logica em cima de um campo que nao
// esta confirmado aqui.
{
"eventSource": "lakeformation.amazonaws.com",
"eventName": "GetDataAccess",
"userIdentity": { "arn": "arn:aws:sts::111122223333:assumed-role/cadencia-atendimento/theo" },
"requestParameters": {
"tableArn": "arn:aws:glue:us-east-1:111122223333:table/cadencia_lake/prata_pedidos",
"permissions": ["SELECT"]
},
"additionalEventData": {
// colunas efetivamente resolvidas para este principal — 8, nao 12
"resolvedColumnCount": 8
},
"errorCode": null
}As decisões, e o que se perde em cada uma
📋 A Cadência precisa que a MESMA tabela prata_pedidos sirva atendimento, financeiro e o portal de lojista, cada um vendo uma fatia diferente de coluna e de linha, sem manter cópia de tabela por consumidor e sem duplicar o pipeline que o L65 já deixou funcionando.
Tag escala com número de coluna e de papel sem multiplicar GRANT nomeado; data filter aplica a MESMA tabela física, então o pipeline do L65 continua único; e a concessão fica centralizada e auditável em vez de espalhada em política de bucket ou em WHERE escrito por cada chamador.
Alt: View do Athena/Glue por papel, com SELECT explícito de coluna — multiplica objeto para manter — uma view por papel, e mudança de schema quebra todas em silêncio se ninguém lembrar de atualizar. É a mesma dívida do documento que ninguém atualiza, do L65.
Alt: Cópia física da tabela por consumidor (ETL redundante) — triplica custo de armazenamento e de job, e cria três fontes de verdade que podem divergir — exatamente o problema que o catálogo único do L65 existe para evitar.
Alt: GRANT nomeado por usuário, sem tag — não escala: cada analista novo, cada coluna nova, exige revisão manual de dezenas de GRANT. É o antipadrão que LF-TBAC existe para evitar.
Alt: Confiar só em IAM (glue:GetTable/policy de S3), sem Lake Formation — IAM não tem conceito de coluna — é tudo ou nada na tabela inteira. É exatamente o desenho mínimo deste laboratório.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Modelo de permissão | Lake Formation com LF-TBAC | GRANT nomeado; view por papel | tag escala com papel e coluna sem multiplicar objeto de configuração | curva de aprendizado do modelo — LF é uma camada a mais sobre o IAM |
| Escopo do filtro | coluna E linha no mesmo data filter | view para coluna, WHERE aplicado pela aplicação para linha | um único filtro, uma única fonte de verdade, sem confiar que todo chamador aplique o WHERE certo | menos flexibilidade para um analista que precise fugir do modelo declarado, mesmo com boa razão |
| Onde a tag é aplicada | no mesmo pipeline que o L65 usa para atualizar o schema | tag manual, meses depois de a coluna existir | coluna nova nasce marcada, não fica exposta até alguém lembrar de marcá-la | exige revisar o processo de tag toda vez que o L65 evolui o schema |
| Auditoria | eventos do Lake Formation no CloudTrail | log de aplicação | fonte única, no nível do DADO, não do código que fez a chamada | mais volume de evento a filtrar em conta com muita tabela |
| Relação com o mascaramento do L49 | LF nega a coluna inteira | reaproveitar a mesma allowlist do L49 dentro de uma view | dado no lake serve consumo analítico amplo — negar coluna é mais simples que reescrever schema por allowlist toda vez | quem tem a concessão vê o valor bruto, sem máscara — é outro controle, não um substituto |
A dívida que este módulo não paga
A tag resolve quem pode ver a COLUNA hoje, dentro do lake. Um extrato exportado para CSV por alguém com a concessão certa sai do alcance do Lake Formation no instante em que vira arquivo fora do catálogo — a mesma disciplina do L49 (bucket efêmero, ciclo de vida curto, leitura restrita) volta a valer para qualquer exportação, e este laboratório não a implementa de novo.
Construir: concessões por tag e o data filter de linha
Três concessões, três papéis, uma única tabela física. A parte que mais gente erra não é escrever o GRANT — é lembrar que ele só vale alguma coisa depois que a policy IAM antiga sai do caminho.
# lake-formation-permissoes.tf — GRANT por tag, nunca por nome de analista
# Atendimento: so as colunas SEM tag de sensibilidade (as 8 'publico').
resource "aws_lakeformation_permissions" "atendimento" {
principal = aws_iam_role.atendimento.arn
permissions = ["SELECT"]
lf_tag_policy {
resource_type = "TABLE"
expression {
key = aws_lakeformation_lf_tag.sensibilidade.key
values = ["publico"]
}
}
# NAO concede 'pii' nem 'financeiro' — e a AUSENCIA da tag na expressao que exclui a
# coluna do schema efetivo, nao uma negacao explicita escrita em algum lugar.
}
# Financeiro: 'publico' + 'financeiro', nunca 'pii'.
resource "aws_lakeformation_permissions" "financeiro" {
principal = aws_iam_role.financeiro.arn
permissions = ["SELECT"]
lf_tag_policy {
resource_type = "TABLE"
expression {
key = aws_lakeformation_lf_tag.sensibilidade.key
values = ["publico", "financeiro"]
}
}
}
# Lojista: coluna E linha na MESMA concessao — um data cells filter cobre as duas.
resource "aws_lakeformation_data_cells_filter" "lojista" {
table_data {
database_name = "cadencia_lake"
table_name = "prata_pedidos"
name = "por-loja"
table_catalog_id = data.aws_caller_identity.atual.account_id
# 4 colunas, nenhuma delas pii nem financeiro.
column_names = ["pedido_id", "loja_id", "valor_centavos", "status"]
row_filter {
# var.loja_id_do_principal e resolvida por lojista, fora deste modulo (ver a nota
# de escala na secao 16 — um filtro por loja nao escala manualmente para centenas).
filter_expression = "loja_id = '${var.loja_id_do_principal}'"
}
}
}
resource "aws_lakeformation_permissions" "lojista" {
principal = aws_iam_role.lojista.arn
permissions = ["SELECT"]
data_cells_filter {
database_name = "cadencia_lake"
table_name = "prata_pedidos"
table_catalog_id = data.aws_caller_identity.atual.account_id
name = aws_lakeformation_data_cells_filter.lojista.table_data[0].name
}
}
Esquecer de revogar o IAM antigo deixa a tag decorativa
As três concessões acima só decidem alguma coisa se a policy IAM antiga — a que concedia glue:GetTable e s3:GetObject direto na tabela inteira — for removida dos três papéis. Enquanto ela existir, o IAM concede o acesso amplo antes mesmo do Lake Formation ser consultado, e CPF, nome, telefone e margem de custo continuam saindo juntos no relatório de atendimento, exatamente como no desenho mínimo — só que agora com uma tag no catálogo dando falsa sensação de que o problema foi resolvido.
Construir: quem assume qual papel
Os três papéis compartilham a mesma policy de leitura — e é isso que prova, no próprio Terraform, que o IAM não diferencia Theo de Ana de Rita. Quem diferencia é a tag, não o papel IAM.
# iam-papeis.tf — tres papeis, a MESMA policy de metadado — quem decide coluna e linha e o LF
data "aws_iam_policy_document" "leitura_via_lf" {
statement {
effect = "Allow"
actions = [
"lakeformation:GetDataAccess",
"glue:GetTable",
"glue:GetDatabase",
"glue:GetPartitions",
]
resources = ["*"]
# As 4 acoes SAO de metadado/sessao — nenhuma aceita ARN de tabela ou coluna. E o
# Lake Formation, nao esta policy, quem decide QUAL coluna e QUAL linha a credencial
# resultante consegue de fato ler. Reparar que os TRES papeis abaixo anexam a MESMA
# policy e o ponto: o IAM nao diferencia Theo de Ana de Rita — quem diferencia e a tag.
}
}
resource "aws_iam_role" "atendimento" {
name = "cadencia-atendimento"
assume_role_policy = data.aws_iam_policy_document.assume_via_identity_center.json
}
resource "aws_iam_role" "financeiro" {
name = "cadencia-financeiro"
assume_role_policy = data.aws_iam_policy_document.assume_via_identity_center.json
}
resource "aws_iam_role" "lojista" {
name = "cadencia-lojista"
assume_role_policy = data.aws_iam_policy_document.assume_via_identity_center.json
}
resource "aws_iam_role_policy" "atendimento_lf" {
role = aws_iam_role.atendimento.id
policy = data.aws_iam_policy_document.leitura_via_lf.json
}
resource "aws_iam_role_policy" "financeiro_lf" {
role = aws_iam_role.financeiro.id
policy = data.aws_iam_policy_document.leitura_via_lf.json
}
resource "aws_iam_role_policy" "lojista_lf" {
role = aws_iam_role.lojista.id
policy = data.aws_iam_policy_document.leitura_via_lf.json
}
# NENHUM destes tres papeis tem s3:GetObject nem glue:GetTable com Resource especifico
# de coluna — porque essa granularidade nao existe no vocabulario do IAM. Comparar com
# o L41 (IAM minimo por uso real): la, o principio e 'um papel por job, Resource
# especifico'; aqui, o Resource especifico simplesmente nao tem onde declarar coluna.
Construir: o teste de negativa
Todo laboratório desta série prova o que constrói. Aqui a prova é dupla: a contagem de coluna certa, E a consulta proibida falhando de verdade — uma sem a outra não fecha o requisito de auditoria.
// TesteDeNegativa.cs — prova que a concessao nega de verdade, com numero
using Amazon.Athena;
using Amazon.Athena.Model;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
namespace Cadencia.Governanca;
public record PapelEsperado(string RoleArn, string Sessao, int ColunasEsperadas);
public sealed class TesteDeNegativa
{
private const string Banco = "cadencia_lake";
private const string ResultadoS3 = "s3://cadencia-lake/atenas-resultados/";
// Os tres papeis e a contagem de coluna que CADA UM deveria ver — a contagem e a
// prova, nao a ausencia de erro. 8 (atendimento), 9 (financeiro, com margem de
// custo), 4 (lojista, so as colunas nao sensiveis, e so a propria loja).
private static readonly PapelEsperado[] Papeis =
[
new("arn:aws:iam::111122223333:role/cadencia-atendimento", "teste-atendimento", 8),
new("arn:aws:iam::111122223333:role/cadencia-financeiro", "teste-financeiro", 9),
new("arn:aws:iam::111122223333:role/cadencia-lojista", "teste-lojista", 4),
];
public async Task<bool> RodarAsync()
{
var tudoPassou = true;
foreach (var papel in Papeis)
{
using var sts = new AmazonSecurityTokenServiceClient();
var credencial = await sts.AssumeRoleAsync(new AssumeRoleRequest
{
RoleArn = papel.RoleArn,
RoleSessionName = papel.Sessao,
});
using var athena = new AmazonAthenaClient(credencial.Credentials);
// 1. SELECT * precisa devolver EXATAMENTE a contagem esperada — nem a mais
// (vazamento de coluna), nem a menos (bloqueio de coluna legitima).
var colunas = await ContarColunasAsync(athena,
$"SELECT * FROM {Banco}.prata_pedidos LIMIT 1");
if (colunas != papel.ColunasEsperadas)
{
Console.WriteLine(
$"FALHOU: {papel.Sessao} viu {colunas} colunas, esperava {papel.ColunasEsperadas}");
tudoPassou = false;
continue;
}
// 2. A consulta EXPLICITA a uma coluna de PII precisa FALHAR — nao voltar
// vazia, nao voltar nula: falhar, com o erro do Lake Formation.
var negouDeVerdade = await FalhaComPermissaoNegadaAsync(athena,
$"SELECT cpf_cliente FROM {Banco}.prata_pedidos LIMIT 1");
if (!negouDeVerdade)
{
Console.WriteLine($"FALHOU: {papel.Sessao} conseguiu ler cpf_cliente — a negativa nao nega");
tudoPassou = false;
continue;
}
Console.WriteLine($"OK: {papel.Sessao} — {colunas} colunas, negativa confirmada");
}
return tudoPassou;
}
private static async Task<int> ContarColunasAsync(AmazonAthenaClient athena, string sql)
{
var execucao = await athena.StartQueryExecutionAsync(new StartQueryExecutionRequest
{
QueryString = sql,
QueryExecutionContext = new QueryExecutionContext { Database = Banco },
ResultConfiguration = new ResultConfiguration { OutputLocation = ResultadoS3 },
});
await EsperarAsync(athena, execucao.QueryExecutionId);
var resultado = await athena.GetQueryResultsAsync(
new GetQueryResultsRequest { QueryExecutionId = execucao.QueryExecutionId, MaxResults = 1 });
return resultado.ResultSet.ResultSetMetadata.ColumnInfo.Count;
}
private static async Task<bool> FalhaComPermissaoNegadaAsync(AmazonAthenaClient athena, string sql)
{
try
{
var execucao = await athena.StartQueryExecutionAsync(new StartQueryExecutionRequest
{
QueryString = sql,
QueryExecutionContext = new QueryExecutionContext { Database = Banco },
ResultConfiguration = new ResultConfiguration { OutputLocation = ResultadoS3 },
});
var estado = await EsperarAsync(athena, execucao.QueryExecutionId);
// FAILED com mensagem de permissao do LF conta como negativa provada;
// SUCCEEDED significa que a coluna vazou.
return estado == QueryExecutionState.FAILED;
}
catch (AmazonAthenaException)
{
return true; // rejeitada antes mesmo de virar execucao — tambem conta como negativa
}
}
private static async Task<QueryExecutionState> EsperarAsync(AmazonAthenaClient athena, string id)
{
while (true)
{
var status = await athena.GetQueryExecutionAsync(new GetQueryExecutionRequest { QueryExecutionId = id });
var estado = status.QueryExecution.Status.State;
if (estado is QueryExecutionState.SUCCEEDED or QueryExecutionState.FAILED or QueryExecutionState.CANCELLED)
return estado;
await Task.Delay(1000);
}
}
}
Por que testar os três papéis na mesma rotina, e não em três scripts soltos
Rodar os três numa única execução é o que torna o teste comparável ao longo do tempo — o mesmo artefato, com o mesmo formato de saída, todo mês. Um script solto por papel tende a divergir em critério, e divergência silenciosa é exatamente o que fez a Cadência chegar até aqui sem perceber.
Implantar, e provar a negativa com número
Cinco provas. A quinta é a que mais gente pula, e é a única que prova a NEGATIVA — todas as outras provam o que cada papel VÊ; ela prova o que cada papel NÃO consegue ver, com erro de verdade, não com silêncio.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A tabela de fato tem 12 colunas antes de qualquer restrição | aws glue get-table → Columns | 12 colunas, incluindo as 4 sensíveis | menos que 12 significa que o pipeline do L65 ainda não capturou a coluna nova — corrija ali antes de continuar aqui |
| 2 · Theo vê exatamente 8 colunas | SELECT * como o papel de atendimento | 8 colunas, sem cpf_cliente, nome_cliente, telefone_cliente e margem_custo_centavos | mais que 8 é vazamento de coluna; menos que 8 é bloqueio de coluna legítima |
| 3 · Ana vê exatamente 9 colunas, incluindo margem de custo | SELECT * como o papel financeiro | 9 colunas, com margem_custo_centavos, sem as 3 de PII | margem_custo_centavos ausente é bloqueio de um requisito real, não segurança extra |
| 4 · Rita vê só 4 colunas e só as próprias linhas | SELECT * como o papel do lojista, autenticada como a própria loja | 4 colunas, e COUNT(*) bate só com pedidos de loja_id da própria loja | qualquer linha de outra loja aparecendo é falha do data filter, não da tag de coluna |
| 5 · A consulta explícita às 3 colunas de PII, como Theo, falha de verdade | SELECT cpf_cliente FROM prata_pedidos, autenticado como Theo | erro de permissão insuficiente do Lake Formation | resultado sem erro, mesmo vazio, é sinal de que o Lake Formation não está no caminho — confira se sobrou glue:GetTable direto no IAM |
# provas.sh — cinco medicoes; nenhuma conclusao vem de "a consulta rodou sem erro"
BANCO=cadencia_lake
# ── Prova 1: a tabela fisica tem 12 colunas ──────────────────────────────────
aws glue get-table --database-name "$BANCO" --name prata_pedidos \
--query 'length(Table.StorageDescriptor.Columns)'
# Esperado: 12
# ── Provas 2 a 4: schema efetivo por papel ───────────────────────────────────
for PAPEL in atendimento financeiro lojista; do
CRED=$(aws sts assume-role --role-arn "arn:aws:iam::111122223333:role/cadencia-${PAPEL}" \
--role-session-name "prova-${PAPEL}" --query 'Credentials' --output json)
export AWS_ACCESS_KEY_ID=$(echo "$CRED" | jq -r .AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo "$CRED" | jq -r .SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo "$CRED" | jq -r .SessionToken)
ID=$(aws athena start-query-execution \
--query-string "SELECT * FROM ${BANCO}.prata_pedidos LIMIT 1" \
--query-execution-context Database="$BANCO" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/ \
--query 'QueryExecutionId' --output text)
sleep 3
echo "${PAPEL}: $(aws athena get-query-results --query-execution-id "$ID" \
--query 'length(ResultSet.ResultSetMetadata.ColumnInfo)') colunas"
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
done
# Esperado: atendimento: 8 colunas / financeiro: 9 colunas / lojista: 4 colunas
# ── Prova 5: consulta explicita a coluna de PII, como atendimento, FALHA ─────
aws athena start-query-execution \
--query-string "SELECT cpf_cliente FROM ${BANCO}.prata_pedidos LIMIT 1" \
--query-execution-context Database="$BANCO" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
# Esperado: FAILED, com InsufficientLakeFormationPermissionsException na mensagem.Quebrar de propósito: três falhas e o diagnóstico
As três parecem, à primeira vista, sintomas de configuração solta. O que separa uma da outra é EM QUAL CAMADA a negação deveria ter acontecido — IAM, tag, ou predicado de linha.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| IAM concede acesso direto, contornando o LF | reative a policy IAM antiga (glue:GetTable + s3:GetObject direto) no papel de atendimento | Theo volta a ver as 12 colunas, mesmo com a tag e a concessão do LF intactas | compare a policy IAM efetiva do papel com o que o Lake Formation concede | revogar a policy IAM antiga; deixar só lakeformation:GetDataAccess e leitura de metadado |
| Tag não propagou para a coluna nova | adicione uma coluna sensível ao pipeline do L65 sem associar a tag a ela | a coluna nova aparece para todo mundo, sem restrição nenhuma, no próximo SELECT * | compare get-table → Columns com get-resource-lf-tags da tabela | associar a tag no mesmo pipeline que adiciona a coluna, não como tarefa manual depois |
| Data filter de linha sem a coluna do predicado incluída | remova loja_id da lista de column_names do data cells filter do lojista | a consulta de Rita falha, ou retorna vazio, mesmo para pedido da própria loja | confira se a coluna usada no row_filter está entre as column_names incluídas do filtro | garantir que a coluna do predicado do filtro está entre as colunas incluídas, não só referenciada no WHERE |
A pergunta que resolve as três
A negação está na CAMADA CERTA? IAM, tag ou data filter de linha protegem coisas diferentes, e um teste que passa numa camada não prova nada sobre as outras duas — foi assim que a prova 5 da seção anterior ganhou uma linha própria, separada das provas de contagem de coluna.
Depois de aplicar as tags LF-TBAC e conceder acesso por tag ao papel de atendimento, Theo continua vendo as 12 colunas de prata_pedidos, incluindo cpf_cliente e margem_custo_centavos. O que é mais provável ter acontecido?
Segurança: duas camadas que precisam concordar
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| IAM concede acesso direto ao recurso, contornando o LF | média | alto | remover toda policy IAM que conceda glue:GetTable/s3:GetObject direto a analista; só lakeformation:GetDataAccess | comparar a policy IAM efetiva com as concessões do LF | revogar a policy antiga e reemitir credencial |
| Tag de sensibilidade não propaga para coluna nova | alta | médio | associar tag no mesmo pipeline que adiciona coluna (L65) | teste de negativa recorrente comparando contagem de coluna esperada | associar a tag manualmente e rodar o teste de novo |
| Data filter de linha mal escrito expõe loja de outro parceiro | baixa | alto | revisão de PR no predicado do filtro antes de aplicar | auditoria de CloudTrail comparando loja_id retornado com o principal | corrigir o predicado; tratar como incidente se houve leitura cruzada real |
| Concessão LF ampla demais 'para não travar ninguém' | média | alto | LF-TBAC com a tag mínima necessária; revisão trimestral de GRANT | listar concessões via list-permissions periodicamente | revogar concessão excedente |
| Papel do lojista reutilizado por múltiplos parceiros | baixa | alto | um papel e uma credencial por lojista, nunca compartilhada | CloudTrail por principal, correlacionando IP e horário | rotacionar a credencial comprometida |
| Exportação de resultado de consulta vira CSV fora do alcance do LF | média | médio | reaproveitar o padrão de bucket efêmero do L49 para qualquer exportação | inventário do bucket de resultados do Athena | expirar o objeto e revisar quem exportou |
A concessão do Lake Formation não substitui o IAM — ela se soma a ele
É o mal-entendido de maior impacto desta pilha: LF e IAM são uma relação de E, não de OU. Uma equipe que trata 'configurei a tag e o GRANT no Lake Formation' como sinônimo de 'o acesso está controlado' pode deixar uma policy IAM antiga viva por meses — e o dado pessoal e a margem de custo continuam saindo juntos, exatamente como no desenho mínimo, só que agora com um catálogo de tags que dá a falsa aparência de que o problema foi resolvido.
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Alguém consultou uma coluna fora do papel dela? | eventos do LF no CloudTrail, filtro em GetDataAccess por coluna | negação é esperado; ACEITE fora da matriz papel×coluna é investigação | qualquer aceite fora da matriz esperada |
| A tag de sensibilidade cobre toda coluna sensível do catálogo? | comparar lista de colunas do catálogo com lista de colunas taggeadas | divergência é coluna nova sem tag | qualquer coluna sensível sem tag |
| O teste de negativa passou na última execução? | resultado do TesteDeNegativa.cs, agendado | falha aqui é regressão, não achado pontual | qualquer falha |
| Quantas concessões LF existem, e quantas foram usadas nos últimos 30 dias? | list-permissions comparado ao CloudTrail de uso real | concessão nunca usada é candidata a remoção | concessão com 0 uso em 30 dias |
| Rita está vendo só a própria loja? | amostra de CloudTrail comparando loja_id do resultado com o principal autenticado | qualquer linha de outra loja é falha do data filter | qualquer divergência |
A métrica que engana: 'zero tentativa negada' sozinha
Um painel que mostra só a contagem de tentativas negadas do Lake Formation, sem ao lado a confirmação de que o teste de negativa RODOU de propósito, transforma 'ninguém tentou' em 'está tudo protegido' na cabeça de quem olha rápido. As duas perguntas — 'alguém tentou e falhou?' e 'o teste ativo passou?' — precisam do mesmo destaque visual.
Escala: 3 papéis, 40 lojistas, e o que passa a doer
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 3 papéis, 40 lojistas (Cadência hoje) | poucas dezenas de concessões LF | nada; é o cenário de validação deste laboratório | nada |
| 500 lojistas parceiros | um data cells filter por loja não escala para criar e revisar manualmente | cada loja nova exige um filtro novo, um a um | predicado parametrizado por atributo do principal (ex.: claim do IAM Identity Center), não um filtro fixo por loja |
| Centenas de colunas sensíveis em dezenas de tabelas | tag manual por coluna não acompanha o ritmo de mudança de schema | aplicar tag vira trabalho de um time inteiro, full-time | classificação automática sugerindo tag, com revisão humana — é o nível 3 da evolução, adiante |
| Falha de AZ | nenhum efeito nos serviços deste laboratório | nada — Lake Formation, Glue, S3, Athena e CloudTrail são regionais, não amarrados a uma AZ | nenhuma ação aqui; mesma nota do L49 e do L65 |
Custo: o que este laboratório acrescenta à fatura
A parte contraintuitiva: Lake Formation não tem linha própria na fatura. O que muda é o comportamento de leitura do Athena — e, para consulta colunar, tende a REDUZIR o que se paga, não aumentar.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | poucas concessões, só configuração | custo desprezível — tag e GRANT não têm linha de fatura | desprezível | nenhuma |
| Produção pequena | Cadência hoje: 40 lojistas, poucas dezenas de GRANT | nenhum recurso novo cobrando; Athena continua cobrando por byte varrido, como antes | estável, sem linha própria de LF | nenhuma — o ganho aqui é de controle, não de custo |
| Alta escala | centenas de lojistas, muitas tabelas | tempo de operação de tag e GRANT vira custo de PESSOA, não de infraestrutura | cresce com número de tabela × papel, não com volume de dado | classificação automática de coluna sensível (nível 3 da evolução) reduz o tempo de operação |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Concessões e tags do Lake Formation | gratuito — não cobra por GRANT nem por tag | o 'custo' real é operacional, não de fatura |
| Athena | GB varrido por consulta, igual antes do LF | leitura colunar do Parquet já pula a coluna negada — column-level tende a reduzir byte varrido, não aumentar |
| CloudTrail | eventos de Lake Formation contam para a cota geral de trilha | volume cresce com número de consulta, não com número de tag |
| Data cells filter por lojista | gratuito, mas cresce em número de OBJETO a manter | não é custo de fatura — é custo de operação, ver a nota de escala acima |
O ganho que não aparece na fatura de infraestrutura
Antes deste módulo, provar 'quem viu qual coluna' exigia caçar manualmente em log de aplicação, se é que existia algum. Depois, list-permissions e um filtro no CloudTrail respondem em minutos — é o mesmo tipo de ganho que o L65 já tinha entregado para 'qual é o schema', agora aplicado a 'quem pode ver o quê'.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Segurança | tag e GRANT negam coluna, data filter nega linha, IAM só concede metadado | tag pode não propagar para coluna nova adicionada fora do pipeline revisado | teste de negativa recorrente + alarme de coluna sensível sem tag | alta |
| Excelência operacional | tag aplicada no mesmo pipeline Terraform que o L65 usa para o schema | ainda depende de alguém lembrar de estender o módulo de tag quando a fonte de dado mudar | automatizar associação de tag como parte do próprio deploy do L65 | alta |
| Confiabilidade | Lake Formation é serviço gerenciado, sem servidor a operar | Data Lake Location mal registrado trava consulta para todo mundo, não só para quem deveria ser negado | teste de fumaça pós-deploy consultando como cada papel, incluindo um papel com acesso total esperado | média |
| Eficiência de performance | leitura colunar do Parquet pula coluna negada | data filter de linha mal desenhado pode varrer a partição inteira antes de aplicar o predicado | revisar o plano de execução do Athena por papel, não só o resultado final | média |
| Otimização de custos | concessão do Lake Formation é gratuita | número de concessões cresce com papel × tabela, sem custo de fatura mas com custo operacional | consolidar por tag em vez de papel individual sempre que possível | média |
| Sustentabilidade | nenhum recurso novo de armazenamento ou cômputo | nenhum residual real além do que a seção de limpeza cobre | nenhuma ação adicional além da limpeza já configurada | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro, e a coluna de custo é a que raramente se escreve com honestidade.
IAM concede glue:GetTable e s3:GetObject direto na tabela inteira, sem Lake Formation no caminho. É onde a Cadência estava, e é aceitável num ambiente de desenvolvimento sem dado real.Data Lake Location registrado, tags LF-TBAC por coluna, concessões por tag e data filter de linha combinando coluna e linha na mesma concessão, com teste de negativa automatizado.Em vez de um humano decidir a tag de toda coluna nova, um classificador estrutural (o mesmo princípio do Macie do L49, aplicado ao catálogo em vez de ao log) sugere sensibilidade=pii ou sensibilidade=financeiro por padrão de nome e amostra de valor, com aprovação humana antes de aplicar.Lake Formation com compartilhamento entre contas (via AWS RAM), cobrindo uma conta de analytics separada da conta de produção do lake, com a mesma matriz de tag e papel.Tags e concessões declaradas via Terraform com revisão de PR obrigatória, mais uma verificação automática de drift comparando o estado real do Lake Formation com o que o código declara.Um MODELO de linguagem lê colunas de texto livre (um campo de observação de atendimento, por exemplo) e resultados de consulta agregada, e sugere se o conteúdo carrega dado pessoal ou financeiro que um nome de coluna estruturado não capturaria — decisão automatizada que precisa de trilha auditável de por que negou ou permitiu, que é exatamente o que o L97 cobra.A ordem não é negociável, e o motivo é concreto
Classificação automática de coluna sensível (nível 6) sem a tag determinística já funcionando (nível 2) significa não ter linha de base para saber se o modelo está ajudando ou só gerando ruído — é a mesma lição que o L49 já tinha sobre streaming em tempo real sem allowlist primeiro.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve nenhum dos dois problemas centrais, e forçá-la seria o antipadrão que a própria série critica. 'Esta coluna é PII' tem resposta determinística quando o nome já diz — cpf_cliente não precisa de modelo nenhum para ser marcada. 'Esta linha pertence a esta loja' também: é um predicado, não uma inferência.
Há um lugar onde IA acrescentaria valor real, e ele aparece no nível 6 da evolução: sugerir tag para coluna cujo NOME não denuncia o conteúdo — um campo de observação em texto livre que às vezes carrega CPF citado numa frase, o mesmo problema de fundo que o L49 já apontava para log não estruturado, agora aplicado ao catálogo.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | sugerir tag de sensibilidade para coluna de texto livre cujo nome não denuncia o conteúdo |
| Por que um identificador gerenciado (como o Macie do L49) não bastaria? | identificadores gerenciados casam padrão estruturado; um campo de observação em prosa livre não tem formato fixo para casar |
| De onde viriam os dados? | amostra de valor da própria coluna nova, mais o nome da coluna e das tabelas vizinhas já taggeadas como referência |
| Qual o risco? | falso positivo classifica coluna inofensiva como pii e bloqueia consulta legítima; falso negativo deixa coluna sensível sem tag, do mesmo jeito que hoje |
| Por que não agora? | a Cadência tem 12 colunas numa tabela e nomes que já denunciam sensibilidade — um classificador não tem o que decidir que o nome da coluna já não decide |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para DECIDIR a concessão de acesso — 'analise o histórico de consulta de Theo e decida se ele pode ver esta coluna hoje' — em vez de aplicá-la de forma determinística via tag e GRANT, troca um controle auditável por uma decisão probabilística, e complica exatamente a auditoria que o Lake Formation existe para simplificar. Quando essa decisão for automatizada de verdade, a trilha auditável do porquê é o L97, que este laboratório evolui para.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| GRANT nomeado por usuário em vez de tag | é mais direto de entender — 'o Theo vê X' — e não exige pensar em taxonomia de sensibilidade | toda pessoa nova ou coluna nova exige edição manual de dezenas de GRANT espalhados | LF-TBAC: tag na coluna, GRANT na tag — analista novo herda a concessão automaticamente |
| Deixar a policy IAM antiga viva 'por segurança' durante a migração | medo de travar alguém no meio da transição para o Lake Formation | contorna o LF inteiro — o analista nunca passa pela tag, e a migração nunca termina de fato | remover glue:GetTable/s3:GetObject diretos assim que a concessão do LF estiver testada e provada |
| Aplicar tag manualmente, sem integrar ao pipeline do L65 | parece tarefa de configuração única, feita uma vez e esquecida | coluna nova do crawler nasce sem tag e fica exposta até alguém notar, às vezes meses depois | tag associada no mesmo processo que o crawler do L65 usa para atualizar o schema |
| Confiar no 'zero achado' do CloudTrail como prova de que o controle funciona | parece verificação suficiente, e o painel já existe | ninguém tentou consultar a coluna restrita — não é o mesmo que 'não conseguiria se tentasse' | teste de negativa recorrente e automatizado, que RODA a consulta proibida de propósito |
| Um data filter só de coluna, aplicando a linha 'na aplicação' | parece mais simples de escrever no código do que configurar um predicado no LF | qualquer chamador que pule essa camada de aplicação vê linha de qualquer loja, não só a própria | um único data cells filter no Lake Formation, cobrindo coluna E linha na mesma concessão |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Analista ainda vê coluna sensível mesmo com a tag aplicada | IAM concede glue:GetTable/s3:GetObject direto, contornando o LF | compare a policy IAM efetiva do papel com a concessão do LF | simulate-principal-policy; list-permissions do LF | revogar a policy IAM antiga; deixar só lakeformation:GetDataAccess |
| Consulta falha para TODO mundo, inclusive quem deveria ter acesso | Data Lake Location não registrado, ou LF admin não delegou o recurso | confira list-resources do LF e describe-data-lake-settings | console do Lake Formation → Data lake locations | registrar o bucket/prefixo e delegar como recurso do LF |
| Coluna nova aparece para todo mundo, sem restrição | tag não foi associada no pipeline — o L65 atualizou o schema, ninguém tocou na tag | compare get-table → Columns com get-resource-lf-tags | job de associação de tag / pipeline Terraform do L65 | associar a tag no mesmo processo que adiciona a coluna |
| Rita vê pedido de outra loja | data filter sem o predicado certo, ou aplicado à tabela errada | rode a consulta como o papel do lojista e confira o loja_id retornado | get-data-cells-filter | corrigir o predicado; tratar como incidente se já houve leitura cruzada |
| Teste de negativa 'passa' mas deveria falhar | o teste está autenticando com um papel diferente do esperado | confira get-caller-identity dentro do teste antes da consulta | log do TesteDeNegativa.cs | corrigir a role assumida antes de rodar; nunca testar com credencial de admin |
A pergunta que resolve metade destes casos
O IAM concede, ou só o Lake Formation concede? Os dois têm de concordar — IAM sozinho negando não impede nada se outra policy IAM concede o mesmo recurso; LF sozinho concedendo não adianta se o IAM subjacente nega o acesso ao mecanismo primeiro.
Limpeza: o que o destroy não leva
Este laboratório cria pouca coisa que sobrevive por conta própria — mas um recurso é de CONTA, e removê-lo sem pensar derruba o Lake Formation para qualquer outro laboratório que dependa dele.
#!/usr/bin/env bash
# limpar.sh — o que o destroy nao leva, e o que e ACCOUNT-WIDE
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
# 1. Derrube o que o Terraform administra (tags, concessoes, data filter, papeis IAM).
terraform destroy -auto-approve
# 2. ATENCAO: aws_lakeformation_data_lake_settings.admin e configuracao de CONTA — se
# outro laboratorio ou processo tambem depende do Lake Formation estar ativo, NAO
# remova o registro do Data Lake Location as cegas.
aws lakeformation describe-resource --resource-arn arn:aws:s3:::cadencia-lake \
--query 'ResourceInfo.LastModified' --output text 2>/dev/null || echo 'ja removido'
# 3. Os DADOS em si: bronze e prata continuam no S3 do L62/L65. Este laboratorio nao os
# apaga porque nao foi ele quem os criou.
aws s3 ls "s3://cadencia-lake/prata/pedidos/" --recursive --summarize | tail -2
# 4. Tags LF-TBAC: confira se sobrou associacao orfa numa coluna que voce testou a mao.
aws lakeformation list-lf-tags --query 'LFTags[].TagKey' --output table
# 5. Prova final: nada com o nome do projeto de pe fora do que era esperado.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values="${PROJETO}" \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Tags LF-TBAC e associações | sim | não | são metadado, sem custo de fatura |
| Concessões (permissions) e data filters | sim | não | idem — configuração, não infraestrutura |
| Registro do Data Lake Location | sim, mas confira dependência antes | não | outro laboratório pode depender do mesmo registro — confira antes de remover |
| Admin do Lake Formation (data_lake_settings) | não recomendado remover via este módulo | não | é configuração de CONTA; removê-la afeta qualquer outro laboratório que dependa do LF |
| Dado em prata/pedidos no S3 | não, não pertence a este módulo | sim, GB-mês | veio do L62/L65; apagar aqui apagaria o lake inteiro, não só este laboratório |
| Tópico SNS e regra do EventBridge | sim, se em Terraform | centavos | regra criada à mão no console não aparece no estado |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Analista vê coluna que não deveria | tag LF-TBAC por coluna + GRANT por tag | tag escala com número de coluna e papel, sem GRANT nomeado |
| Lojista vê pedido de outra loja | data filter de linha, na mesma concessão da coluna | um único mecanismo cobre coluna e linha, sem depender do código do chamador |
| IAM sozinho não distingue coluna | Data Lake Location registrado no LF | LF passa a ser a autoridade sobre o recurso, não só o IAM |
| Coluna nova nasce sem controle | tag associada no mesmo pipeline do L65 | herda a marcação junto com o schema que o crawler já atualiza sozinho |
| Ninguém sabe se o controle funciona | teste de negativa automatizado, recorrente | prova por número, não por ausência de reclamação |
| Auditoria manual de quem viu o quê | eventos do Lake Formation no CloudTrail | fonte única, no nível do dado, não do código que fez a chamada |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Analista vendo coluna sensível via Athena | tag + GRANT do Lake Formation | exportação da consulta para CSV fora do lake — é o padrão de bucket efêmero do L49 |
| Lojista vendo linha de outra loja | data filter de linha | credencial de lojista compartilhada entre parceiros |
| Coluna nova sem tag | nada automático — depende do pipeline associar | só o teste de negativa recorrente pega isso |
| IAM concedendo acesso direto, contornando o LF | nada — é a mesma falha do desenho mínimo | revisão de policy IAM continua sendo responsabilidade de quem opera |
- Theo, Ana e Rita continuam escrevendo a mesma consulta SQL de sempre.
- O Data Lake Location registrado coloca o Lake Formation no caminho, antes do S3.
- Cada coluna carrega uma tag de sensibilidade — pii, financeiro, ou nenhuma (pública).
- A concessão de cada papel referencia a tag, nunca o nome do analista.
- O schema efetivo sai já filtrado do catálogo: 8 colunas para Theo, 9 para Ana, 4 para Rita.
- Para Rita, o mesmo data cells filter também restringe a linha à própria loja.
- A leitura colunar do Parquet pula o que a concessão nega, sem varrer a coluna inteira.
- Toda avaliação de concessão — aceita ou negada — vira evento no CloudTrail.
- Um teste de negativa automatizado prova, com número, que a consulta proibida falha de verdade.
- Se a IAM antiga ainda concede acesso direto, nada disso importa — por isso ela sai da política assim que o LF é testado.
Perguntas frequentes
❓ Ativar o Lake Formation revoga automaticamente o acesso que o IAM já concedia?
❓ Por que uma consulta com SELECT * retorna menos colunas em vez de dar erro?
❓ Data filter de coluna e de linha podem estar na mesma concessão do Lake Formation?
❓ O que muda entre negar uma coluna no lake e mascarar um valor, como o L49 fez no log?
❓ Por que o crawler do catálogo, do L65, pode reabrir esse buraco de governança?
❓ O Athena precisa de permissão especial para funcionar com o Lake Formation?
❓ Um painel com zero tentativa negada no Lake Formation prova que a governança funciona?
Fixando
Theo, autenticado com o papel de atendimento, roda SELECT * FROM prata_pedidos e recebe 8 colunas, sem nenhum erro. Em seguida ele roda SELECT cpf_cliente FROM prata_pedidos e a consulta FALHA, com um erro de permissão do Lake Formation. Por que os dois comportamentos são diferentes?
Um painel de segurança mostra zero eventos de acesso negado no Lake Formation nos últimos 30 dias para a tabela prata_pedidos, e a equipe conclui que a governança de coluna está funcionando bem. O que há de errado nesse raciocínio?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L65 (catálogo Cadência, prata_pedidos vivo); L49 lido — reaproveita o padrão de mascaramento, não a implementação nem a empresa |
| Conhecimentos adquiridos | que Lake Formation e IAM são aditivos, nunca substitutos; LF-TBAC por tag em vez de GRANT nomeado; data filter combinando coluna e linha na mesma concessão; a diferença entre exclusão de coluna (schema efetivo menor) e negação explícita (erro); prova de negativa por número, não por silêncio do CloudTrail |
| Limitação que fica | a tag protege o dado DENTRO do lake; um resultado de consulta exportado para CSV por alguém com a concessão certa sai do alcance do Lake Formation no instante em que vira arquivo |
| Próximo módulo recomendado | L97 — risco e conformidade de decisão automatizada. Reutiliza a lógica de trilha auditável quando a decisão sobre uma concessão de acesso passa a ser sugerida por um modelo, não só por uma tag escrita à mão |
| Também habilitado por este módulo | qualquer tabela nova do lake da Cadência reaproveita o mesmo padrão de tag e concessão sem reinventar desenho; o L97 reaproveita a disciplina de auditoria por CloudTrail deste módulo |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: What is AWS Lake Formation — o modelo de permissão centralizado e a relação aditiva com o IAM; LF-Tag-based access control — como a tag substitui GRANT nomeado e escala com coluna e papel; Data filters for column-level, row-level, and cell-level security — como um único data cells filter combina coluna e linha; Using AWS Lake Formation with Amazon Athena — a permissão mínima de IAM (lakeformation:GetDataAccess) que o principal precisa, sem s3:GetObject direto; e a referência de eventos do Lake Formation no CloudTrail, que confirma o registro de GetDataAccess por principal e por recurso. Nenhum valor de preço aparece neste módulo por decisão: concessão e tag do Lake Formation não têm linha própria de fatura, e o que varia (GB varrido pelo Athena) já é coberto pelo AWS Pricing Calculator.
O que não foi verificado, e você deve conferir na sua conta
O comportamento exato de amostragem/percentual de bytes economizados por column-level pruning em Parquet varia por engine e versão do Athena, e não foi medido nesta sessão — meça o seu antes de prometer economia de custo como argumento de venda interno da migração. Os volumes da Cadência — 40 lojistas, 18 mil pedidos/dia, 12 colunas — são do cenário de exemplo deste laboratório; confira os seus antes de dimensionar quantas concessões e data filters seu catálogo real vai precisar.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…