Policy Document Structure & Conditions
An IAM policy is a JSON document, and every useful policy beyond a toy example needs Condition blocks to be precise.
Busque em todas as páginas da documentação
An IAM policy is a JSON document, and every useful policy beyond a toy example needs Condition blocks to be precise.
This page covers the document's anatomy, the condition operators you'll actually use, and how context keys let one policy adapt to who's calling and from where.
A policy document is a Version plus one or more Statement blocks.
Each statement has an Effect (Allow or Deny), an Action list, a Resource list, and an optional Condition block.
Condition is where most of the precision lives: it tests values from the request itself, like the caller's IP, a tag on the principal, or the current time.
Getting the operator right (StringEquals vs StringLike, exact match vs wildcard) is the difference between a condition that quietly never matches and one that actually constrains access.
boto3 and the AWS SDK for JavaScript v3 both just serialize this same JSON shape into a string for PutRolePolicy or CreatePolicy - the document itself is language-neutral.
Quick-reference recipe card - copy-paste ready.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadFromCorpNetworkOnly",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::acme-reports/*",
"Condition": {
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" },
"DateGreaterThan": { "aws:CurrentTime": "2026-01-01T00:00:00Z" },
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}
]
}When to reach for this:
Attach a conditioned inline policy, then verify the condition actually narrows the decision.
# --- Python (boto3) ---
import json, boto3
iam = boto3.client("iam")
ROLE = "reports-reader-role"
policy = {
"Version": "2012-10-17",
"Statement": [{
"Sid": "TaggedTenantAccess",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::acme-reports/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/team": "billing",
"aws:RequestedRegion": "us-east-1"
}
}
}]
}
iam.put_role_policy(
RoleName=ROLE,
PolicyName="tagged-tenant-access",
PolicyDocument=json.dumps(policy),
)
# Confirm the condition holds for a matching context.
res = iam.simulate_principal_policy(
PolicySourceArn=f"arn:aws:iam::111122223333:role/{ROLE}",
ActionNames=["s3:GetObject"],
ResourceArns=["arn:aws:s3:::acme-reports/q3.csv"],
ContextEntries=[{
"ContextKeyName": "aws:PrincipalTag/team",
"ContextKeyValues": ["billing"],
"ContextKeyType": "string",
}],
)
print(res["EvaluationResults"][0]["EvalDecision"]) # -> allowed// --- TypeScript (AWS SDK v3) ---
import {
IAMClient, PutRolePolicyCommand, SimulatePrincipalPolicyCommand,
} from "@aws-sdk/client-iam";
const iam = new IAMClient({});
const ROLE = "reports-reader-role";
const policy = {
Version: "2012-10-17",
Statement: [{
Sid: "TaggedTenantAccess",
Effect: "Allow",
Action: ["s3:GetObject"],
Resource: "arn:aws:s3:::acme-reports/*",
Condition: {
StringEquals: {
"aws:PrincipalTag/team": "billing",
"aws:RequestedRegion": "us-east-1",
},
},
}],
};
await iam.send(new PutRolePolicyCommand({
RoleName: ROLE, PolicyName: "tagged-tenant-access",
PolicyDocument: JSON.stringify(policy),
}));
const res = await iam.send(new SimulatePrincipalPolicyCommand({
PolicySourceArn: `arn:aws:iam::111122223333:role/${ROLE}`,
ActionNames: ["s3:GetObject"],
ResourceArns: ["arn:aws:s3:::acme-reports/q3.csv"],
ContextEntries: [{
ContextKeyName: "aws:PrincipalTag/team",
ContextKeyValues: ["billing"],
ContextKeyType: "string",
}],
}));
console.log(res.EvaluationResults?.[0]?.EvalDecision); // -> allowedWhat this demonstrates:
aws:PrincipalTag/team reads a tag on the calling principal, letting one policy serve every "billing" tagged role without duplicating statements.aws:RequestedRegion constrains which region the call must target, independent of where the client happens to run.ContextEntries in the simulator lets you supply a tag value that wouldn't otherwise be present on a test principal.StringEquals requires an exact match - a caller tagged Billing (capitalized) would not match this condition.Statement is evaluated independently; a Condition only applies to its own statement, never leaking into others.Condition block with implicit AND - all keys must be satisfied for the condition to match.Condition can only ever narrow what Action/Resource already scoped - it cannot grant access to something outside them.| Operator | Tests | Example Use |
|---|---|---|
StringEquals / StringNotEquals | Exact string match | aws:PrincipalTag/team equals billing |
StringLike | Wildcard string match | aws:userid matches AIDA*:app-* |
IpAddress / NotIpAddress | CIDR range match | aws:SourceIp inside 203.0.113.0/24 |
DateGreaterThan / DateLessThan | Timestamp comparison | aws:CurrentTime after a cutover date |
Bool | True/false flag | aws:MultiFactorAuthPresent is true |
NumericLessThan / NumericGreaterThan | Numeric comparison | s3:max-keys under a limit |
aws:PrincipalTag/<key> - a tag on the IAM principal making the call, useful for team- or environment-scoped conditions.aws:RequestedRegion - the region the API call targets, independent of client location.aws:SourceIp - the caller's IP address, commonly paired with IpAddress.aws:MultiFactorAuthPresent - whether the session used MFA, paired with Bool.aws:CurrentTime - the request timestamp, paired with DateGreaterThan/DateLessThan for time-boxed access.# boto3 accepts PolicyDocument as a JSON string - always json.dumps() the dict.
# A common mistake is passing the dict directly, which raises a botocore
# serialization error rather than a clear "must be a string" message.
policy_str = json.dumps(policy)// SDK v3 has the identical requirement - JSON.stringify() before sending.
// TypeScript won't catch a missing stringify at compile time since PolicyDocument is typed `string`,
// but passing an object literal instead fails type-checking, which is the safety net here.
const policyStr = JSON.stringify(policy);StringEquals when the value has variable casing - StringEquals is case-sensitive, so Billing will not match billing. Fix: normalize tag values at creation time, or use StringEqualsIgnoreCase.Condition keys are ANDed together - stacking IpAddress and DateGreaterThan in one block requires both to hold, not either. Fix: split into separate statements if you actually want OR semantics across conditions....IfExists operator variants (e.g. StringEqualsIfExists) when a key may be legitimately absent.aws:CurrentTime with a one-time expiration - a DateLessThan condition doesn't "expire" the policy; it just re-evaluates against the current time on every request. Fix: treat it as a standing time window, not a TTL.PolicyDocument as an object instead of a string - both boto3 and SDK v3 require a JSON string. Fix: always call json.dumps() / JSON.stringify() before sending.Condition can only restrict; it never adds permissions beyond what Action/Resource already allow. Fix: widen Action/Resource directly if more access is actually needed.| Alternative | Use When | Don't Use When |
|---|---|---|
Condition block on the statement | You need context-aware access on one action/resource | The restriction should apply account-wide (use an SCP instead) |
| Separate statements per case | Conditions would need OR semantics across keys | The cases share most of the same Action/Resource (adds duplication) |
| Permission boundary | Capping the ceiling for a delegated role broadly | You need per-request context, not a static ceiling |
Resource-based policy Condition | The restriction belongs on the resource, not every caller's identity policy | Multiple unrelated principals need different conditions |
Sid (optional label), Effect (Allow/Deny), Action, Resource, and the optional Condition block. Only Effect and Action are strictly required by the schema, but Resource is required in practice for identity-based policies.
It depends on the operator. StringEquals is case-sensitive; StringEqualsIgnoreCase is not. Choose the operator based on whether your tag or context values have consistent casing.
No. A Condition only narrows an already-scoped Allow - it can never expand access beyond the statement's Action and Resource.
They combine with implicit AND - every key must be satisfied. Within a single key, an array of values combines with implicit OR - matching any one value is enough.
Most operators simply fail to match, so the statement doesn't apply - it isn't treated as an error. Use an ...IfExists operator variant if the key is legitimately optional.
StringEquals requires an exact match. StringLike supports wildcards (*, ?), useful for matching a family of values like arn:aws:iam::*:user/app-*.
Add a Bool condition on aws:MultiFactorAuthPresent set to "true" in the statement's Condition block, alongside the Allow.
Yes, using StringEquals on aws:RequestedRegion. This checks the region the API call targets, not where the client machine is located.
PolicyDocument must be a JSON string, not a dict or object. Call json.dumps() in Python or JSON.stringify() in TypeScript before passing it.
No. It's evaluated against the current time on every request, so it behaves as a standing time window rather than a one-time expiration or TTL.
It reads a tag attached to the calling IAM principal (user or role), letting one policy statement apply differently depending on how the caller is tagged - for example, scoping access by team or environment tag instead of writing a statement per identity.
Use a Condition when the restriction narrows an existing Allow contextually (IP, time, tag). Use an explicit Deny statement when you need to carve out a hard exception regardless of any other Allow, since Deny always wins.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Revisado por Chris St. John·Última atualização: 25 de jul. de 2026