Skip to content

fix(auth): follow IAM's rules for policy variables in condition values - #541

Open
LeeroyHannigan wants to merge 2 commits into
fix/resource-policy-variablesfrom
fix/condition-policy-variables
Open

LeeroyHannigan wants to merge 2 commits into
fix/resource-policy-variablesfrom
fix/condition-policy-variables

Conversation

@LeeroyHannigan

Copy link
Copy Markdown
Collaborator

Fixes #534. Stacked on #535: this branch builds on fix/resource-policy-variables and reuses its variables.rs module, so the diff below is the two commits on top of it.

Summary

Policy variables in condition values now follow IAM's rules, each measured with the IAM policy simulator or, for trust policies, with AssumeRole:

  • Only a 2012-10-17 document expands variables in condition values. A 2008-10-17 document, or one with no Version, compares ${aws:username} as the literal text, and the default form too. This applies to identity policies, permissions boundaries, session policies and trust policies.
  • ${*}, ${?} and ${$} expand to the literal characters. In StringLike and ArnLike (and ArnEquals) the escaped character, and any character that comes from a substituted value, is matched literally, while a * or ? written in the policy is a wildcard.
  • ${key, 'default'} expands to default when the key is absent.
  • A variable with no default whose key the request does not carry makes that value match nothing. A positive operator does not match it and a negated operator does, with IfExists and the set qualifiers following the same rule. Before this change the variable was left as literal text and compared.
  • Only the string and ARN operators expand variables. A NumericEquals or Bool value written as a variable is compared as written; the date, address and Null operators are treated the same way.
  • A multivalued key such as dynamodb:LeadingKeys is never a variable (from fix(auth): expand policy variables in Resource and NotResource #535).

evaluate_condition keeps its signature for its existing callers and expands as a 2012-10-17 document does; evaluate_condition_with and evaluate_statement_conditions take the document's choice, which the evaluator passes from each identity policy, boundary and session policy and the trust evaluator from the trust policy's Version.

Measurements

IAM policy simulator, 24 and 25 October 2026, action dynamodb:GetItem on arn:aws:dynamodb:us-east-1:123456789012:table/blue, Allow dynamodb:* on * beside Deny dynamodb:GetItem on * with the condition below. Keys are aws:PrincipalTag/team (team), aws:ResourceTag/owner (owner) and aws:SourceArn unless named.

Condition Version Context IAM
StringLike team a${*} 2012 team abc allowed
same 2012 team a* explicitDeny
StringLike team a* 2012 team abc explicitDeny
StringEquals team ${?} 2012 team ? explicitDeny
StringLike team a${?} 2012 team ab allowed
same 2012 team a? explicitDeny
StringEquals team ${$} 2012 team $ explicitDeny
StringEquals team a${$}{x} 2012 team a${x} explicitDeny
StringEquals team ${*} 2008 team * allowed
same 2008 team ${*} explicitDeny
StringEquals owner ${aws:username} none user alice, owner alice allowed
same none user alice, owner ${aws:username} explicitDeny
same 2008 user alice, owner ${aws:username} explicitDeny
ArnLike SourceArn user/a${*} 2012 SourceArn user/abc allowed
same 2012 SourceArn user/a* explicitDeny
StringEquals owner ${aws:username, 'anon'} 2012 no user, owner anon explicitDeny
same 2008 no user, owner anon allowed
StringEquals owner ${aws:username} 2012 no user, owner ${aws:username} allowed
StringNotEquals owner ${aws:username} 2012 no user, owner alice explicitDeny
StringLike owner ${aws:PrincipalTag/team} 2012 team a*, owner abc allowed
same 2012 team a*, owner a* explicitDeny
ArnLike or ArnEquals SourceArn user/${aws:PrincipalTag/name} 2012 name alice, SourceArn user/alice explicitDeny
same 2012 name al*, SourceArn user/alice allowed
StringEqualsIfExists owner ${aws:username} 2012 owner alice, no user allowed
StringNotEqualsIfExists same 2012 owner alice, no user explicitDeny
ForAllValues:StringNotEquals aws:TagKeys ${aws:username} 2012 TagKeys [a, b], no user explicitDeny
ForAnyValue:StringEquals same 2012 TagKeys [a, b], no user allowed
NumericEquals aws:MultiFactorAuthAge ${aws:PrincipalTag/age} 2012 age 300, MultiFactorAuthAge 300 allowed
Bool aws:SecureTransport ${aws:PrincipalTag/flag} 2012 flag true, SecureTransport true allowed

Trust policies, AssumeRole on fresh roles whose trust policy allows the caller's role under the condition on aws:PrincipalArn against ${aws:PrincipalArn}:

Condition Version AssumeRole
StringEquals 2012 succeeded
StringNotEquals 2012 refused
StringEquals 2008, or none refused
StringNotEquals 2008, or none succeeded

Testing

  • One evaluator-level test per simulator row above and three trust-policy tests, plus unit tests for the expansion and the token matching, and the boundary, session policy and explicit_deny_matches_arc paths.
  • Each rule has a negative control that fails when the rule is reverted: the version flag forced on (8 tests fail), escapes left as text (10), an escaped * or ? read as a wildcard (6), an unresolved variable left as text (5), the trust policy's Version ignored (2), every operator expanding (2).
  • Rust 1.97.0: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace --exclude extenddb-storage-cassandra: 1,856 passed, 0 failed. cargo test -p extenddb-auth on 1.88.0: 305 passed.

Behaviour change for existing policies

  • A condition value with a variable in a 2008-10-17 policy, or one with no Version, no longer expands; trust policies included.
  • A condition on a key the request does not carry, whose value is a variable with no default, no longer compares the literal ${...} text: a negated operator now holds. An Allow written as StringNotEquals against ${aws:PrincipalTag/team} grants to a caller without that tag; a Null condition on the tag requires it.
  • A numeric, date, Bool, address or Null value written as a variable is no longer expanded.

Merging with #471 and #522

  • fix(auth): match IAM wildcards in ArnEquals and ArnNotEquals #471 changes the four Arn* arms of compare_single. Resolved, each arm reads is_arn(context_value) && value.arn_matches(context_value) (negated for the Not forms), with the Unresolved early return before it.
  • fix(auth): resolve aws:TagKeys and aws:RequestTag for the tag operations #522 changes the signature and body of evaluate_condition_match, which this branch also changes. request_tag_tests.rs there calls evaluate_statement_conditions with two arguments and gains the expand_variables argument. Its reads_unresolvable_variable should read expand_variables && ..., so a 2008-10-17 document whose value holds ${aws:RequestTag/x} as text is not treated as reading an unresolvable key.

Not changed here

A variable that supplies a whole ARN in an Arn* operator (ArnLike on ${aws:PrincipalTag/src}): the policy simulator refuses such a policy as invalid input, so IAM's evaluation of it is not measurable there. ExtendDB evaluates it with the substituted text matched literally.

Checklist

  • Tests added for every measured behaviour
  • cargo fmt, cargo clippy clean
  • Docs updated (docs/manuals/10-security-model.md)

…10-17 documents

A condition value in a 2008-10-17 document, or in one with no Version, is
compared as written: ${aws:username} is the literal text. Only a 2012-10-17
document expands variables, the same rule as Resource and NotResource.

In a 2012-10-17 document the escapes ${*}, ${?} and ${$} stand for the
literal characters. StringLike and ArnLike match the escaped character
literally while a * or ? written in the policy stays a wildcard, so a${*}
matches a* and not abc. The result of an escape is not expanded again.
A variable with no default whose key the request does not carry makes the
value match nothing: a positive operator is false against it and a negated
operator holds.

The document's choice reaches condition evaluation through a flag on
evaluate_statement_conditions and evaluate_condition_match; the identity,
boundary and session policy paths pass their own document's Version. The
trust policy path keeps expanding whatever the Version.

Measured with the IAM policy simulator on 2026-10-24, nineteen cells, one
evaluator test each. Closes #534.
… for string and ARN operators

Two rules measured on IAM after the condition-variable change:

- A trust policy follows the same Version rule as an identity policy.
  With AssumeRole on fresh roles, a 2008-10-17 trust policy, and one
  with no Version, compares ${aws:PrincipalArn} in a condition as
  literal text, and a 2012-10-17 one expands it. The trust evaluator
  expanded variables in every trust policy.
- NumericEquals and Bool compare a value written as a variable as
  written. Only the string and ARN operators now expand variables, with
  or without a set qualifier or IfExists; the date, address and Null
  operators are treated as the numeric ones.

Tests pin both, and the measured cases for a wildcard inside a
substituted value, IfExists and the set qualifiers with an unresolved
variable. The security model states the operator rule, the trust
policy rule, and how a negated operator behaves for a caller without
the key.

This branch has not been deployed

No deployments
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