Skip to content

feat(iam): policy variables and ForAllValues over keys with no values - #2528

Merged
vieiralucas merged 4 commits into
mainfrom
feat/iam-policy-variables
Sep 14, 2026
Merged

vieiralucas merged 4 commits into
mainfrom
feat/iam-policy-variables

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Sep 14, 2026

Copy link
Copy Markdown
Member

Summary

The IAM evaluator ignored policy variables. ${aws:username} in a Resource or a condition value was compared as literal text, so one reusable policy could never scope each caller to their own prefix, team or partition. This is the standard pattern for S3 home folders, tag-scoped resources, and DynamoDB fine-grained access control (dynamodb:LeadingKeys = ${aws:userid}). This PR adds policy variables and fixes how ForAllValues treats a key with no values.

Behavior follows the IAM User Guide ("IAM policy elements: Variables and tags" and "Single-valued vs. multivalued context keys").

Policy variables

  • Where they expand: in Version: "2012-10-17" policies, in the resource part of Resource / NotResource ARNs (after the fifth colon), and in String* / Arn* condition values. In any other version ${...} stays literal, as on AWS.
  • What they resolve to: any single-valued context key, such as aws:username, aws:userid or aws:PrincipalTag/<key>, plus service keys. Key names are case-insensitive. A multivalued key can't be used as a variable.
  • Defaults: ${key, 'default'} uses the default when the key has no value.
  • Special characters: ${*}, ${?} and ${$} stand for a literal *, ? and $.
  • Substituted values are literal: a * inside a principal tag is not a wildcard, so a tag value cannot widen what a policy grants.
  • A variable with no value matches no resource. Positive operators (StringEquals, StringLike, ArnLike, ...) never match it, and inverted ones (StringNotEquals, StringNotLike, ...) do, as documented.

ForAllValues with no values

AWS documents that ForAllValues is true when the request has no values for the key, and the evaluator returned false. That is fixed with one guard. The vacuous truth applies only when the service actually populated the key and there are no values in it (an empty entry). A key fakecloud never populated for the request may simply be one it doesn't extract, so it still fails the condition. An unextracted key never grants access.

Context lookup

ConditionContext::lookup now falls back to plain context entries for a global key with no typed value, tag keys included (the tag-key part compares exactly). So SimulateCustomPolicy / SimulatePrincipalPolicy ContextEntries for aws:username, aws:TagKeys, aws:RequestTag/<key> and so on now resolve, including as policy variables. Before, they were ignored and the key was reported in MissingContextValues. A typed value still wins, so an entry can't override a key dispatch filled in.

Test plan

  • policy_variables.rs unit tests: expansion, case-insensitive keys, defaults, special characters, literal substituted values, and resource-part-only expansion.
  • Evaluator tests:
    • resource variables per principal
    • version gating (2008-10-17 is literal)
    • null semantics for positive vs. inverted operators and for resources
    • defaults
    • ForAllValues true on a populated key with no values but false on an unpopulated key, and ForAnyValue false in both cases
    • global and tag keys falling back to plain entries, with exact tag-key casing
  • e2e policy_variables_scope_one_policy_per_caller (iam_enforcement_abac, strict mode, S3): ${aws:username} and ${aws:PrincipalTag/team, 'no-team'} resources allow the caller's own prefixes and deny others.
  • e2e simulate_custom_policy_resolves_policy_variables_from_context_entries.
  • cargo test -p fakecloud-iam (552) and -p fakecloud-core (321)
  • e2e iam_enforcement (50), iam_enforcement_abac (10), _boundary (5), _session_policy (4), _scheduler (3), iam_simulate (9), multi_account (6), ecr_cross_account (3): pass
  • clippy -D warnings (core, iam, e2e) and cargo fmt --all --check: clean

Surface

  • Docs: reference/security.md describes policy variables and the ForAllValues rule; services/iam.md lists policy variables.
  • No wire, API, SDK or count changes.

Summary by cubic

The IAM evaluator previously treated ${aws:username} in Resource and condition values as literal text, and ForAllValues returned false when the request had no values for a populated key. Now Version: 2012-10-17 policies expand supported policy variables, and ForAllValues follows AWS's documented vacuous truth.

  • Policy variables expand only in 2012-10-17 policies, in the resource part of Resource/NotResource ARNs and in String*/Arn* condition values; other versions keep ${...} literal.
  • Variables resolve to single-valued context keys (aws:username, aws:PrincipalTag/<key>, service keys), support ${key, 'default'}, ${*}, ${?}, ${$}, and substituted values are literal (a * in a tag is not a wildcard).
  • A variable with no value matches no resource, fails positive operators, and satisfies inverted ones.
  • ForAllValues is now true when the service populated the key and the request carries no values; a key fakecloud never populated still fails, so it never grants.
  • ConditionContext::lookup now falls back to plain context entries for global and tag keys, so SimulateCustomPolicy/SimulatePrincipalPolicy ContextEntries resolve aws:username, aws:TagKeys, etc., as variables and conditions.

Written for commit 8ec597b. Summary will update on new commits.

Review in cubic

- Version 2012-10-17 policies expand ${...} policy variables in the
  resource part of Resource / NotResource and in String* / Arn*
  condition values: any single-valued context key (aws:username,
  aws:userid, aws:PrincipalTag/<key>, ...), ${key, 'default'}
  defaults, and ${*} / ${?} / ${$} for literal characters. Substituted
  values are literal. A variable with no value matches no resource,
  fails positive operators and satisfies inverted ones. Other policy
  versions read ${...} literally.
- ForAllValues is true when the request has no value for the key, as
  AWS documents; it evaluated to false.
- ForAllValues is vacuously true when the service populated the key with
  no values. A key never populated -- one fakecloud does not extract --
  still safe-fails to false, so an unextracted key never grants.
- ConditionContext::lookup falls back to plain context entries for a
  global key with no typed value, so SimulateCustomPolicy /
  SimulatePrincipalPolicy ContextEntries resolve aws:username and friends
  (also as policy variables). An empty entry now means "populated, no
  values" instead of "absent".
@vieiralucas
vieiralucas merged commit 2fcd658 into main Sep 14, 2026
157 checks passed
@vieiralucas
vieiralucas deleted the feat/iam-policy-variables branch September 14, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant