O PR adiciona um statement à identity policy do usuário app-reader: Effect: Allow, s3:GetObject, arn:aws:s3:::amzn-s3-demo-bucket/*. O revisor lê, confere o ARN e aprova. A bucket policy não aparece no diff e quase sempre fica em outro repositório, então ninguém abre. Só que ela já tem um Deny para esse mesmo principal, essa mesma ação e esse mesmo ARN.
O merge "provou" que o GetObject estava liberado. O Deny continuou no bucket e não mudou nada.
A ideia aqui é sair com uma tabela de três linhas cruzando identity policy e bucket policy, e saber em qual delas o Allow da identity resolve o acesso e em quais ele não serve para nada. O material vem do IAM User Guide e do S3 User Guide, recuperados em 06/10/2026, e de três pares de JSON locais. Nada rodou contra uma conta AWS nem no Policy Simulator. Os resultados da tabela são o que a documentação manda esperar.
O texto sobre IfExists na política IAM já tratou de chave de contexto ausente dentro de uma única identity policy. Este é outro problema: o Effect de duas políticas diferentes, a da identidade e a do recurso, avaliadas no mesmo request. Não há Condition em nenhum JSON daqui.
A regra de avaliação, uma vez só
Toda requisição começa em implicit deny. A única exceção é o root user da conta. Daí em diante a ordem é curta:
- Se qualquer política aplicável tiver um
Denyexplícito que case com o request, a decisão final é Deny. - Se não houver Deny, um Allow vindo da identity policy ou da resource policy basta na mesma conta, para a maioria dos recursos.
- Se não houver nem Deny nem Allow, vale o default Deny.
A página de lógica de avaliação descreve o resultado como a união das permissões dos dois tipos de política e fecha com a frase que importa aqui:
> An explicit deny in either of these policies overrides the allow.
O "para a maioria dos recursos" também está escrito. A página de deny e allow diz:
> For most resources, you only need an explicit Allow for the principal in either an identity-based policy or a resource-based policy to grant access. IAM role trust policies and KMS key policies are exceptions to this logic.
O S3 não aparece entre as exceções. A bucket policy é uma resource-based policy e entra no caso geral.
Todos os JSON usam "Version": "2012-10-17". Essa continua sendo a versão atual da linguagem de políticas, e a única alternativa documentada é a legada 2008-10-17.
Os três pares de JSON
Os placeholders são o usuário arn:aws:iam::111122223333:user/app-reader e o bucket amzn-s3-demo-bucket. A ação é s3:GetObject, e por isso o Resource aponta para os objetos (/*), não para o bucket.
Repare em uma diferença de forma que vale para todos os casos. A identity policy não tem Principal, porque o principal é implicitamente a identidade em que ela está anexada, e o elemento é proibido ali. A bucket policy é obrigada a declarar Principal. Quando você coloca os dois documentos lado a lado, é esse campo que mostra qual é qual.
Caso A: Allow na identity, Deny na bucket policy
Identity policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IdentityAllowGetObject",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
Bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ResourceDenyGetObject",
"Effect": "Deny",
"Principal": {
"AWS": "arn:aws:iam::111122223333:user/app-reader"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
Este é o cenário do PR do começo. O Allow da identity está correto e casa com o request. O Deny da bucket policy também casa, com o mesmo principal, a mesma ação e o mesmo ARN. Pela regra, o Deny explícito vence e o Allow nem entra na conta.
Caso B: Deny na identity, Allow na bucket policy
Identity policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IdentityDenyGetObject",
"Effect": "Deny",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
Bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ResourceAllowGetObject",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:user/app-reader"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
É o espelho do caso A. Agora quem aprova sem olhar o outro lado é o dono do bucket: ele concede o acesso ao app-reader e não sabe que a identity policy do usuário já nega a mesma ação. O resultado é o mesmo, porque a regra não tem preferência por lado. Um Deny explícito em qualquer política aplicável basta.
Caso C: só Allow na identity
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IdentityAllowGetObjectOnly",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
Aqui não existe bucket policy, ou existe uma sem nenhum statement que case com esse request. Não há Deny em lugar nenhum e há um Allow de um lado. Na mesma conta, isso basta. A página de fundamentos da avaliação diz isso diretamente:
> If either the identity-based policy or the resource-based policy within the same account allows the request and the other doesn't, the request is still allowed.
O caso C é o único em que a leitura apressada do revisor estaria certa. Só que ela estaria certa por sorte, porque ele também não abriu a bucket policy.
Resultado esperado na mesma conta
| Caso | Identity policy | Bucket policy | Resultado esperado (mesma conta) |
|---|---|---|---|
| A | Allow | Deny no mesmo ARN e principal | Negado: o Deny explícito vence |
| B | Deny | Allow no mesmo ARN e principal | Negado: o Deny explícito vence |
| C | Allow | nenhuma (ou nada que case) | Permitido: Allow de um lado basta |
Duas das três linhas negam, e em nenhuma das duas dá para chegar à resposta lendo só a identity policy.
O caso C muda quando o principal e o bucket estão em contas diferentes. Aí a AWS avalia a requisição nas duas contas, e a página de avaliação cross-account é explícita:
> The request is allowed only if both evaluations return a decision of Allow.
Ou seja, um Allow na identity da conta de origem, sem um Allow na bucket policy da conta dona do recurso, não concede o GetObject. Esse contraste vem só da documentação. Não há quarto fixture nem AssumeRole executado.
Onde a revisão escorrega
O erro principal é tratar o Allow da identity como suficiente. Ele é necessário em alguns cenários (cross-account) e suficiente só quando nenhuma outra política aplicável tem Deny. "Suficiente" é uma afirmação sobre todas as políticas do request, e o diff mostrou uma.
O segundo erro é mais sutil: confundir um statement que não se aplica com um Effect: Deny. Se a bucket policy tiver um Allow para outro prefixo ou outro principal, esse statement não casa com o request e, para este request, ele é só implicit deny. Ele não anula o Allow da identity. Um Deny que casa anula. A pergunta certa ao abrir a bucket policy não é "ela me concede?", e sim "algum statement com Effect: Deny casa com este principal, esta ação e este ARN?".
O terceiro erro é carregar a intuição de outros serviços. Key policy do KMS e trust policy de role do IAM são as exceções documentadas à regra de "Allow num lado basta", e não servem de modelo para pensar sobre S3. O caminho contrário também não funciona: o que vale para o bucket não explica o comportamento delas.
Limites desta tabela
A tabela cruza só o Effect da identity policy com o da bucket policy, na mesma conta, sem Condition. No acesso real entram outras camadas que podem negar ou restringir o que a tabela marca como permitido: SCP, RCP, permissions boundary, session policy, Block Public Access e ACLs (junto com a configuração de Object Ownership do bucket). Nenhuma delas foi modelada aqui. Os resultados também não foram confirmados numa conta nem no Policy Simulator. São a leitura do User Guide aplicada a três JSON.
O que fazer no review
A bucket policy é parte do diff, mesmo quando não aparece nele. Abra os dois documentos, a identity policy que o PR altera e a bucket policy que está de fato no bucket, não uma cópia antiga em outro repositório. Procure primeiro os statements com Effect: Deny que casem com o principal, a ação e o ARN do pedido. Só depois disso faz sentido avaliar os Allow.
Se existir um Deny assim, um PR que só anexa um Allow na identity não resolve o acesso, e a DevDojo recusaria o merge. A conversa precisa ir para quem é dono da bucket policy: ou o Deny está errado e é ali que se corrige, ou ele está certo e o pedido de acesso é que não deveria passar. Não existe Allow que contorne isso pelo outro lado.
A DevDojo não faria merge de uma mudança de acesso ao S3 depois de ler só a identity policy. A regra perde força se a mudança não depende de política nenhuma do bucket. Para saber disso, porém, você precisa ter aberto a bucket policy. E num cenário cross-account a exigência só aumenta, porque os dois lados precisam do Allow.
Próximo passo
Na próxima revisão de s3:GetObject, coloque a identity policy e a bucket policy lado a lado, uma ao lado da outra no mesmo editor, e procure Effect: Deny antes de ler qualquer Allow. Depois encaixe o par em uma das três linhas da tabela. Se ele não couber em nenhuma, a resposta está nas camadas que ficaram fora dela, e aí vale uma avaliação na conta.