DEV Community

Vishnu B
Vishnu B

Posted on Fully Autonomous

AWS IAM Policy Evaluation: A Quick Walkthrough with Java

AWS IAM Policy Evaluation: A Quick Walkthrough with Java

"Why is my S3 request denied? I literally have an Allow for it." If you have worked with AWS for more than a week, you have hit this. Getting unstuck starts with how IAM evaluates policies - and while the full evaluation has several layers, the core rules are simpler than they feel once you see them in code.

In this walkthrough we will look at three rules at the core of identity-based policy evaluation inside a single AWS account, then simulate them in a small Java program you can run yourself. This is a simplified identity-policy example, not the full IAM evaluation: the real engine also layers in resource-based policies, permissions boundaries, Organizations SCPs/RCPs, session policies, and conditions, which this demo intentionally leaves out.

The three rules (single account)

Per the AWS policy evaluation logic docs:

  1. By default, everything is denied (implicit deny). If no statement matches your request with an Allow, the answer is no.
  2. An explicit Allow overrides the default. Match a statement with Effect: Allow and the request goes through...
  3. ...unless an explicit Deny matches. An explicit deny in any applicable policy overrides any allow. Always.

For an IAM user or role inside one account, AWS takes the union of identity-based policies and resource-based policies: if either allows the action and nothing explicitly denies it, the request is allowed.

A policy is just JSON

From the IAM JSON policy element reference, the pieces that matter for evaluation:

  • Effect - Allow or Deny
  • Action - what is being requested, e.g. s3:GetObject. Wildcards like s3:Get* are allowed.
  • Resource - the ARN the action applies to, also wildcard-friendly, e.g. arn:aws:s3:::photos/*

Simulating the evaluator in Java

Here is a minimal evaluator that follows the three rules. It is a simplified simulation of the single-account, identity-based flow - the real AWS engine also considers resource-based policies, permissions boundaries, Organizations SCPs/RCPs, session policies, and conditions, which this demo intentionally leaves out.

import java.util.List;

public class IamPolicyDemo {

    enum Effect { ALLOW, DENY }

    record Statement(Effect effect, List<String> actions, List<String> resources) {}

    record Request(String principal, String action, String resource) {}

    // IAM-style wildcard match: '*' matches any sequence of characters
    static boolean wildcardMatch(String pattern, String value) {
        String regex = pattern.replace(".", "\\.").replace("*", ".*");
        return value.matches(regex);
    }

    static boolean matches(Statement s, Request r) {
        boolean actionOk = s.actions().stream().anyMatch(a -> wildcardMatch(a, r.action()));
        boolean resourceOk = s.resources().stream().anyMatch(res -> wildcardMatch(res, r.resource()));
        return actionOk && resourceOk;
    }

    // Simplified single-account flow: explicit deny > allow > implicit deny
    static String evaluate(List<Statement> policy, Request r) {
        List<Statement> matching = policy.stream().filter(s -> matches(s, r)).toList();
        boolean explicitDeny = matching.stream().anyMatch(s -> s.effect() == Effect.DENY);
        if (explicitDeny) return "DENIED  (explicit deny)";
        boolean allow = matching.stream().anyMatch(s -> s.effect() == Effect.ALLOW);
        if (allow) return "ALLOWED";
        return "DENIED  (implicit deny - no statement matched Allow)";
    }

    public static void main(String[] args) {
        List<Statement> alicePolicy = List.of(
            new Statement(Effect.ALLOW, List.of("s3:GetObject"), List.of("arn:aws:s3:::photos/*"))
        );

        List<Statement> bobPolicy = List.of(
            new Statement(Effect.ALLOW, List.of("s3:*"), List.of("arn:aws:s3:::photos", "arn:aws:s3:::photos/*")),
            new Statement(Effect.DENY,  List.of("s3:DeleteObject"), List.of("arn:aws:s3:::photos/*"))
        );

        Request[] requests = {
            new Request("alice", "s3:GetObject",      "arn:aws:s3:::photos/cat.jpg"),
            new Request("alice", "s3:DeleteObject",   "arn:aws:s3:::photos/cat.jpg"),
            new Request("bob",   "s3:PutObject",      "arn:aws:s3:::photos/dog.jpg"),
            new Request("bob",   "s3:DeleteObject",   "arn:aws:s3:::photos/dog.jpg"),
        };

        for (Request r : requests) {
            List<Statement> policy = r.principal().equals("alice") ? alicePolicy : bobPolicy;
            System.out.printf("%-5s %-16s %-30s -> %s%n",
                r.principal(), r.action(), r.resource(), evaluate(policy, r));
        }

        // Wildcard in Action: "s3:Get*" covers every read action
        List<Statement> readOnly = List.of(
            new Statement(Effect.ALLOW, List.of("s3:Get*"), List.of("*"))
        );
        System.out.println();
        System.out.println("Wildcard policy: Allow s3:Get* on *");
        for (String action : List.of("s3:GetObject", "s3:GetBucketLocation", "s3:PutObject")) {
            Request r = new Request("carol", action, "arn:aws:s3:::photos/cat.jpg");
            System.out.printf("%-5s %-20s -> %s%n", "carol", action, evaluate(readOnly, r));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Running it (real output)

Compiled and run locally on Temurin JDK 21:

alice s3:GetObject     arn:aws:s3:::photos/cat.jpg    -> ALLOWED
alice s3:DeleteObject  arn:aws:s3:::photos/cat.jpg    -> DENIED  (implicit deny - no statement matched Allow)
bob   s3:PutObject     arn:aws:s3:::photos/dog.jpg    -> ALLOWED
bob   s3:DeleteObject  arn:aws:s3:::photos/dog.jpg    -> DENIED  (explicit deny)

Wildcard policy: Allow s3:Get* on *
carol s3:GetObject         -> ALLOWED
carol s3:GetBucketLocation -> ALLOWED
carol s3:PutObject         -> DENIED  (implicit deny - no statement matched Allow)
Enter fullscreen mode Exit fullscreen mode

Walk through it:

  • alice / GetObject matches her Allow - allowed.
  • alice / DeleteObject matches nothing, so the default kicks in: implicit deny. This is the rule that surprises people most; missing permission looks exactly like an explicit block from the client side.
  • bob / PutObject is covered by s3:* - allowed.
  • bob / DeleteObject is covered by s3:* and by the explicit Deny. Deny wins. This is why one deny statement can lock down an action even when a broad admin-style policy is attached.
  • carol shows the Action wildcard: s3:Get* covers both GetObject and GetBucketLocation, but not PutObject.

See the decision flow

IAM evaluation decision flow

The animation walks the same three scenarios through the decision tree: check for an explicit Deny first, then look for an Allow, and fall back to the implicit deny when nothing matches.

What this leaves out

This is a teaching model of the within-account, identity-based path. Real AWS evaluation additionally layers in resource-based policies (union within an account), permissions boundaries (intersection with identity policies), Organizations SCPs/RCPs (intersection again), session policies, and Condition elements. The explicit-deny-wins rule holds across all of them, which is why it is the one rule worth memorizing.

Sources

Written with AI assistance; code examples were compiled and run on JDK 21 before publishing.

Top comments (0)