Repository navigation
fix(auth): enforce trust-policy Deny and match IAM condition evaluation - #420
Merged
Merged
Conversation
evaluate_trust_policy() skipped every statement whose Effect was not "Allow", so Deny statements in a role's trust policy were silently ignored. An administrator relying on a Deny to revoke a principal's ability to assume a role got no protection: a broad `Allow *` still granted assumption to a principal an accompanying Deny was meant to block. Rework evaluation to follow IAM order, matching the identity-policy evaluator (policy/evaluator.rs, phase 1): scan for a matching explicit Deny first and block assumption if one is found, then look for a matching Allow. Factor the decision into a store-free `trust_decision` helper plus `statement_matches`/`effect_of` so the Deny-over-Allow ordering is unit-testable, and add tests covering explicit-deny override (both statement orders), principal-scoped Deny, conditional Deny via aws:PrincipalTag, and the existing Allow/implicit-deny paths. Record the decision in ADR-0022. The AssumeRole endpoint is admin-only (Basic Auth) with an admin-supplied caller ARN, which bounds real-world exposure, but the authorization logic was incorrect.
…write The Deny-over-Allow reorder in 9952f5f left several Deny shapes inert and accepted documents IAM rejects. Measured against live IAM, STS and DynamoDB, this corrects the trust evaluator and adds write-time validation. Evaluation (crates/server/src/management/assume_role.rs): - Decide per requested STS action. A plain AssumeRole exercises sts:AssumeRole; passing session tags also exercises sts:TagSession. Every exercised action needs a matching Allow and no matching Deny. This fixes the over-deny on a TagSession-only or SetSourceIdentity-only Deny, the fail-open on a TagSession-only Allow, and session tags passed without sts:TagSession. - Compare Effect case-insensitively, matching the identity-policy parser. - Match account principals: {"AWS": "<acct>:root"} and {"AWS": "<acct-id>"} match any caller in that account. - Accept a single-object Statement. - Fail closed on a Deny whose Condition does not parse, so a Deny with an operator ExtendDB does not evaluate blocks rather than being dropped. - Coerce scalar condition values to strings, matching IAM. Condition evaluator (crates/auth/src/policy/condition.rs): - A negated operator on an absent single-valued key now evaluates true, as in IAM, so a Deny gated on StringNotEquals sts:ExternalId applies when the caller omits the external id. BR-7085 (bare operators never match a multivalued key) is preserved. Shared with identity-policy evaluation. Write validation (create_role, crates/server/src/management/iam_role.rs): - Reject the documents IAM rejects for a trust policy: missing or invalid Effect, NotPrincipal, missing Principal, a bare-string or wildcard-ARN principal, missing Action, an action outside the STS trust set, a Resource field, and an unknown or malformed Condition. Accept a missing Version, a single-object Statement, numeric condition values and unknown condition keys. SQLite caller tags (crates/storage-sqlite/src/management_store/mod.rs): - Resolve tags with strip_prefix("user/"/"role/"/"assumed-role/") like the other backends. The server passes user/<name>, so the old :user/ split never matched and tag conditions were inert on SQLite.
Adds TestTrustPolicyDeny to tests/test_auth_integration.py. Each expected status is the outcome IAM and STS returned for the same trust policy: Deny over Allow in both orders, a Deny on another principal, tag-conditioned Deny, the ExternalId Deny with no ExternalId sent, account root and bare account id principals, per-action matching on sts:TagSession, session tags without sts:TagSession, lowercase Effect, a single-object Statement, and the documents create_role must reject or accept.
Update the auth component design, the v3 design, the ADR and the admin guide to describe the trust-policy model: IAM-order Deny-over-Allow decided per exercised STS action, case-insensitive Effect, account-root and bare-account-id principals, fail-closed on a Deny whose condition does not parse, and write-time validation of the document. Remove the fail-open claim from the ADR.
…rces Add a unit test that create/tag a user and a role and asserts fetch_caller_tags resolves tags for user/<name>, role/<name> and assumed-role/<role>/<session>.
Passing session tags to AssumeRole now requires the trust policy to allow sts:TagSession, matching IAM. The session-tag ABAC tests passed tags against a trust policy that allowed only sts:AssumeRole, so the assume call is now denied. Add sts:TagSession to the trust policy so the tests exercise their intent.
…ed on unresolvable keys A trust-policy condition gated on a key the AssumeRole context could not resolve was treated as a genuinely absent key. A Deny with a positive operator was then dropped and a caller the policy denies was issued credentials, while an Allow or a negated Deny on such a key evaluated against the wrong default. Resolve the keys the AssumeRole context has: aws:PrincipalArn and aws:PrincipalAccount from the caller ARN, and aws:RequestTag/* and aws:TagKeys from the session tags. For a key the service always populates but the context cannot resolve, the condition is now unresolvable and the statement fails closed, so a Deny blocks and an Allow does not grant whatever the operator. Keys that are only conditionally present keep IAM absent-key semantics. The three-valued condition outcome is shared with identity-policy evaluation through a new ConditionContext method.
The service resolves aws:PrincipalTag/<key>, aws:RequestTag/<key> and dynamodb:ResourceTag/<key> without regard to the case of <key>. ExtendDB matched the key case-sensitively, so a policy naming a tag key in a case that differed from the stored tag read the key as absent. A Deny gated on such a key was not applied, and with the negated-operator-on-absent rule an Allow over-granted and a Deny over-denied on a case-mismatched key. Resolve tag keys case-insensitively in both the identity RequestContext and the AssumeRoleContext. Tag values stay case-sensitive.
The shared condition change makes a negated operator on an absent single-valued key match. On the identity path a global key the service populates but ExtendDB does not resolve, for example aws:PrincipalOrgID, was treated as absent, so a negated operator spuriously matched: an Allow over-granted and a Deny evaluated against the wrong default. Resolve aws:PrincipalArn and aws:PrincipalAccount in the DynamoDB RequestContext from the caller identity. Treat the other global keys the service always populates as unresolvable, so a statement gated on one fails closed: a Deny blocks and an Allow does not grant, whatever the operator. The evaluator now applies a Deny when its conditions match or are unresolvable, and grants an Allow only when its conditions definitely match.
…keys it does not evaluate Resolving the caller and session keys closes the common over-denials, where an Allow or Deny gated on aws:PrincipalArn, aws:PrincipalAccount, aws:RequestTag or aws:TagKeys was refused. The organization keys (aws:PrincipalOrgID, aws:PrincipalOrgPaths) and the other global keys STS populates are still not evaluated, so a trust policy depending on one fails closed rather than being silently granted. Document the resolved set and the unevaluated set so operators know which keys are safe to gate on.
…ey set ADR-0022 described Deny-over-Allow ordering and the unparseable-condition fail-closed behaviour but not how an unresolved key is treated. State the rule the evaluator now follows for every condition key: a resolved key evaluates normally, a genuinely absent key keeps IAM absent-key semantics, and a key the service always populates but the context cannot resolve fails closed. List the keys each context resolves.
…ator Write validation accepted StringNotEqualsIgnoreCase, which IAM accepts, but the evaluator could not parse it, so a statement using it was unparseable: a Deny always blocked and an Allow never granted regardless of the values. Implement the operator as the case-insensitive negation of StringEqualsIgnoreCase and classify it as a negated operator, so it follows the negated-operator-on-absent rule like the other negated operators. BinaryEquals, IpAddress and NotIpAddress stay accepted at write and unevaluated: they compare data neither context carries, so a statement using one still fails closed, which is the documented safe direction.
…d identity-policy conditions ADR-0022 named a statement_matches helper that was never introduced; replace it with the real helper names. Add an upgrade-impact note that passing session tags now requires sts:TagSession in the trust policy and that create_role now rejects the documents IAM rejects. In the admin guide, state that principal ARNs are matched case-sensitively, that the assume endpoint authorizes on the trust policy alone and does not evaluate the caller's identity permissions, and that the three-case condition-key rule, including the negated-operator-on-absent rule and fail-closed on an unresolved key, applies to identity policies as well.
Condition evaluation marked a fixed set of global keys unresolvable and treated every other unresolved key as absent. That denied common guard statements on keys ExtendDB can resolve (aws:SecureTransport, aws:RequestedRegion, aws:CurrentTime, aws:EpochTime, aws:PrincipalType) and granted statements on keys the service populates that ExtendDB did not resolve (aws:ViaAWSService, aws:PrincipalIsAWSService, aws:ResourceTag). Resolve every global key ExtendDB has the data for, in both the DynamoDB request context and the trust policy context: aws:SecureTransport from the listener TLS state, aws:RequestedRegion from the server region, the request time for aws:CurrentTime and aws:EpochTime, aws:PrincipalType (User for an IAM user, AssumedRole for a role session), aws:username for a user, aws:ViaAWSService and aws:PrincipalIsAWSService as false, and aws:ResourceTag on the identity path from the resource tag map. Keep an allowlist of keys that are legitimately absent for ExtendDB's callers so they keep absent key semantics. Any other unresolved aws: key fails closed whatever the operator, so a Deny on it blocks and an Allow on it does not grant. Behaviour measured on live DynamoDB, IAM and STS in us-east-1.
Identity path (test_abac.py): guard statements on keys ExtendDB now resolves grant, matching live DynamoDB (aws:SecureTransport, aws:RequestedRegion, aws:PrincipalType, aws:CurrentTime); keys the service populates evaluate as present (aws:ViaAWSService, aws:PrincipalIsAWSService, aws:ResourceTag), a non-MFA caller keeps absent-key semantics under BoolIfExists aws:MultiFactorAuthPresent, and an unknown aws: key fails closed. Trust path (test_auth_integration.py): the TLS guard on AssumeRole no longer blocks over TLS, and aws:PrincipalType resolves for the calling principal. Expected outcomes measured live on DynamoDB, IAM and STS in us-east-1. Pass on SQLite and PostgreSQL (identity and trust) and on MongoDB and Cassandra (trust).
…pulated ones as absent The earlier rule kept an allowlist of globally absent condition keys and failed closed on every other aws: global key. That over-denied statements on keys the service populates for ExtendDB's callers but ExtendDB had not yet resolved, and on keys the service treats as absent. Change the default for an unresolved aws: global key from fail closed to absent, matching the service, and resolve the keys the service populates: - Organization keys (aws:PrincipalOrgID, aws:PrincipalOrgPaths, and the resource and source organization keys) are absent. An ExtendDB account is a standalone AWS account, where IAM leaves these keys unset, so absent key semantics apply. - aws:userid resolves to the principal unique id: an AIDA id for a user and role-id:session-name for a role session, derived deterministically from the catalog identity (new policy::principal_id module). The format and the role-session shape were measured live. - aws:SourceIp and aws:UserAgent resolve from the request. IpAddress and NotIpAddress are now evaluated as IPv4 and IPv6 CIDR membership, with a family mismatch matching neither operator, as measured live. - aws:ResourceAccount resolves to the resource owner account. The fail-closed set is now empty, kept as an explicit list so a future global key can be added if the service begins populating it before ExtendDB resolves it. Unit tests cover the operators, the userid derivation, and the revised classification.
…t to authorization Supply the condition keys the auth crate now resolves. On the DynamoDB path the handler reads the connection peer address and the User-Agent header and passes them to authorize_request, which sets them on the request parameters for both the single-table and batch/transact branches; the authorization layer sets aws:userid (AIDA for a user, role-id:session for a role session) and aws:ResourceAccount (the resource owner account). On the AssumeRole path the trust evaluator receives the call's peer address and User-Agent and derives aws:userid from the caller ARN and aws:ResourceAccount from the role account. The peer address is used directly; an X-Forwarded-For header is never trusted.
…zation keys Cover the round-three keys on the identity path (test_abac.py) and the trust path (test_auth_integration.py): aws:userid (AIDA for a user), aws:SourceIp with IpAddress and NotIpAddress on the loopback, aws:UserAgent with StringLike, aws:ResourceAccount, and the organization keys absent for a standalone account. Update the earlier organization-key and unknown-key rows to the revised rule: an absent organization key and an unknown aws: key now keep absent-key semantics (a positive operator does not match, a negated operator matches) rather than failing closed, matching live IAM for a standalone account. The unknown-key negated-allow row changes from DENY to ALLOW.
…d key list Update ADR-0022 and the admin guide for the revised rule: ExtendDB resolves every global key the service populates for its callers (adding aws:userid, aws:SourceIp, aws:UserAgent and aws:ResourceAccount), treats every other aws: global key as absent (the organization keys for a standalone account, the VPC, federation, MFA and service keys, and any unknown key), and keeps an empty fail-closed list. The admin guide gains the full classified key list: resolved, absent, and fail-closed (none). IpAddress and NotIpAddress are now evaluated as CIDR membership. differences-from-dynamodb.md is unchanged.
A user or role deleted and recreated with the same name kept the same aws:userid, so an identity or trust policy pinned to the old id still granted the new principal. IAM issues a new unique id for each incarnation. Derive the id from the principal's creation time in addition to its account and name, so each incarnation gets a distinct id. The creation time comes from the existing iam_users and iam_roles created_at columns, so there is no schema change; it is cached per principal and the cache entry is dropped when the principal is deleted. Every place that resolves aws:userid uses the same derivation: the identity path for a user and for a role session, and the trust path for the caller.
The service populates aws:TokenIssueTime with a role session's issue time and leaves it absent for an IAM user. ExtendDB left it absent for both, so a role-session policy conditioning on the key (for example a DateGreaterThan test) could not be satisfied. Resolve it for a role session from the session's stored issue time (iam_sessions.created_at) formatted as RFC 3339, threaded through the session-data fetch into the request context. IAM users keep it absent, so their absent-key behaviour is unchanged. The MongoDB session store now records the issue time at creation; the other backends already stored it.
… recreate aws:TokenIssueTime: a role session resolves it (Null true denies, a DateGreaterThan on a past point grants, on a future point denies) and an IAM user leaves it absent (Null true grants, a positive Date operator denies). aws:userid: a policy pinned to the id of a user's or role's first incarnation grants it and denies the same-named principal after a delete and recreate. The expected id is derived in the test from the creation time the management list returns, the same value the server hashes.
Formatting and a clippy redundant-closure fix in the principal creation-time and TokenIssueTime code added for the two changes above.
An IPv4 client that reaches a server bound to :: arrives with the IPv4-mapped peer address ::ffff:a.b.c.d. aws:SourceIp carried that form, so an IPv4 CIDR in IpAddress never matched, and a Deny with NotIpAddress on an IPv4 range never fired. DynamoDB evaluates such a client as its IPv4 address. aws:SourceIp now converts a mapped address back to IPv4 on the data plane and on AssumeRole.
The live service resolves an IAM condition key name without regard to case, including the aws: prefix and the tag prefix: aws:securetransport, AWS:SecureTransport and aws:SecureTransport resolve the same key, and aws:principaltag/team resolves the same tag as aws:PrincipalTag/team. ExtendDB matched key names exactly, so a miscased resolvable key read as absent: a Deny gated on a miscased key was dropped and a negated-operator Allow on one over-granted. Fold key-name case in both RequestContext and AssumeRoleContext resolve_key (lower-case the key for the global/dynamodb/sts match, strip the tag prefixes case-insensitively), and in is_multivalued_key and the fail-closed lookup. The tag sub-key was already matched case-insensitively. Policy write validation matches on operator names, not key names, and does not reject a miscased key, matching the service.
The live service populates aws:MultiFactorAuthPresent for a role session (false for a session assumed without MFA) and leaves it absent for an IAM user with long-term keys. ExtendDB never resolved it, so the common deny-without-MFA guard (Deny if Bool aws:MultiFactorAuthPresent=false) was dropped for every role session and the request was granted where the service denies it. Resolve the key to false for a role-session caller on the identity path and leave it absent for an IAM user, matching the service measured live (role session DENY, IAM user ALLOW on the guard). aws:MultiFactorAuthAge stays absent for both callers, which also matches the service for a non-MFA session.
A condition value for IpAddress or NotIpAddress that is not a valid CIDR or address was accepted and the statement then silently did nothing: an IpAddress allow never granted and, more importantly, an IpAddress or NotIpAddress Deny never blocked. Measured live: IAM rejects a malformed CIDR (10.0.0.0/33, 999.0.0.0/8, not-a-cidr, 10.0.0.0/) in an identity policy (PutUserPolicy, PutRolePolicy) and a managed policy (CreatePolicy) with MalformedPolicyDocument, and accepts a malformed CIDR in a trust policy (UpdateAssumeRolePolicy); live STS then drops the malformed-CIDR trust statement rather than failing closed. Match the service. PolicyDocument::from_json now rejects a malformed CIDR, so an identity policy and a permissions boundary are rejected at write and a stored identity policy fails closed at read (it no longer parses, so the request is denied). The trust path is left unchanged: it accepts the value at write and the evaluator drops the statement, which already matches the service. matcher gains is_valid_cidr; the boundary write path now strict-parses the document.
The live service rejects a tag key or value outside its grammar
([\p{L}\p{Z}\p{N}_.:/=+\-@], excluding combining marks and most punctuation),
a key over 128 or value over 256 characters, and the reserved aws: key prefix,
with ValidationError; ExtendDB accepted them. Add validate_iam_tags and apply it
at tag_user, tag_role and the AssumeRole session tags, alongside the ASCII-case
duplicate-key check. Measured: the service accepts letters, digits, spaces, the
listed symbols, a precomposed accented letter and an empty value, and rejects a
combining mark, # and !, the aws: prefix, and over-length key or value. Unit and
wire tests for the accepted and rejected sets.
case_fold allocated and binary-searched the fold table for every character of every condition key name and tag key, including the all-ASCII keys that are the overwhelmingly common case. Add an ASCII fast path: an all-ASCII input folds to its ASCII lowercase directly, since every ASCII letter's fold is its ASCII lowercase. The byte-exact tag lookup already resolves the common case without folding at all. Regenerated through devtools/generate-casefold-table; a unit test pins the fast path's equivalence with the table on ASCII input.
…rules, CIDR forms Add an upgrade-manual entry: a stored identity policy whose condition key name or tag key contains a non-ASCII letter that folds to a resolvable key now evaluates it on the request path (a Deny applies, a negated Allow no longer grants); the trust path matches ASCII case only. Fold-colliding tag keys resolve deterministically (exact key, then smallest by code point). Tag writes now apply IAM's key/value charset, length, aws: prefix and ASCII-case uniqueness rules, with tags already stored left in place. IpAddress accepts a leading-zero octet as decimal and rejects an IPv4-mapped IPv6 literal.
The generator now pipes its output through rustfmt (edition 2024) so the generated crates/auth/src/policy/casefold.rs is rustfmt-clean and regeneration is idempotent with cargo fmt --check.
IAM's tag grammar is [\p{L}\p{Z}\p{N}_.:/=+\-@]: any Unicode Letter, Number or
Separator, plus the listed symbols. The previous is_iam_tag_char approximated
these with char::is_alphabetic, char::is_numeric and an ASCII-space literal,
which diverges from the service in both directions. is_alphabetic is the
Alphabetic property, a superset of \p{L} that admits Other_Alphabetic combining
marks such as U+0345 (category Mn) that IAM rejects, so ExtendDB stored a tag key
the service refuses. An ASCII-space-only separator check rejected the \p{Z}
separators U+00A0, U+2002, U+2003, U+3000, U+2028, U+2029 and U+202F that IAM
accepts, so ExtendDB refused keys the service stores and reversed its own prior
behaviour. Match the exact general categories through unicode-properties'
general_category_group (Letter = Lu Ll Lt Lm Lo, Number = Nd Nl No, Separator =
Zs Zl Zp), already in the lock file. Applied at tag keys and values for tag_user,
tag_role and the AssumeRole session tags. Unit tests for the seven separators,
Nl and No numbers, the two Mn marks and an emoji symbol.
Live IAM accepts a Sid only when it is empty or entirely ASCII letters and digits, regardless of length, and rejects a hyphen, underscore, space, dot or any non-ASCII character (including Latin-1 such as an accented letter) with MalformedPolicyDocument, on both CreateRole (trust) and PutUserPolicy (identity). ExtendDB stored any Sid. Apply the measured rule: from_json rejects such a Sid on the identity-policy write and read path, and validate_trust_policy rejects it on the trust write. Unit tests on both paths for the accepted (alphanumeric, empty, long) and rejected (hyphen, underscore, space, dot, accented) values.
The collision guard tagged one user and issued 20 GetItem calls. A principal's tag map is cached per user, so all 20 calls saw a single HashMap order and the test passed on the unfixed code about a third of the time, missing the regression. Resolve a fresh user for each of 20 checks so each gets its own map order, and cover both the byte-exact key (aws:PrincipalTag/mass) and the no-exact folding key (aws:PrincipalTag/MASS, which folds to mass and must pick the smallest fold-matching key by code point). Against the unfixed binary both checks fail on every one of five runs; against the fixed head both pass.
The trust-path folding guard gated on aws:SecureTran[sharp-s]port, which case-folds to aws:securetranssport and never equals the target key, so it passed on a folding build too and did not exercise the behaviour change. Replace it with two keys that fold to real keys: sts:RoleSe[sharp-s]ionName folds to sts:rolesessionname and aws:PrincipalTag/cla[sharp-s] folds to aws:PrincipalTag/class for a caller tagged class=prod. Both would match and grant under Unicode folding; on the ASCII-only trust path the key reads absent and the assume is denied. Verified: both deny on the fixed head and both grant (assertion fails) on the unfixed binary.
Replace the upgrade-manual sentence saying re-tagging a principal requires correcting a stored tag the new rule rejects: only the tags named in a request are validated, so a stored tag does not block setting other tags and is rejected only when a later request includes it, which is what both ExtendDB and IAM do. State the tag grammar as the exact Unicode general categories (Letter, Number, Separator) rather than an ASCII approximation. Add a note that a policy Sid is accepted only when empty or ASCII alphanumeric on both the trust and identity-policy writes, and a stored policy with a non-conforming Sid keeps evaluating since the Sid is a label.
The Sid charset check ran inside PolicyDocument::from_json, which is also the request path for stored identity policies. A policy stored before the check existed with a Sid such as Allow-Read then failed to parse on every request, and every request by that principal was denied, including those a different valid policy allowed. The upgrade manual says such a policy keeps evaluating. Move the check to PolicyDocument::from_json_for_write, used by the inline and boundary policy writes. from_json keeps the checks that apply when a stored policy is read. Unit tests assert both: a non-conforming Sid is rejected at write and still parses on the request path.
…ersions is_iam_tag_char kept the doc comment from before the general-category fix, which described an ASCII-space check that no longer exists. Remove it. Also record that the tag charset uses Unicode 15.1 general categories and the case-fold table is Unicode 15.0.0, and why the fold table holds under 15.1.
…e API The console create-role and add-policy forms checked only that the document parsed as JSON, then stored it. The management API runs the trust-policy validation and the identity-policy write checks first, so a document it rejects could still be stored through the console. A role created there with Principal "*", Action "*" or a Resource field was then assumable, by a caller in another account in the first case. The service rejects all three at CreateRole. Both console forms now run the same checks as the management API before storing, and show the error on the form. Console tests cover the three trust-policy shapes, a malformed CIDR and a non-alphanumeric Sid; they fail on the previous build and pass on this one.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 21, 2026 11:06
This was referenced Oct 21, 2026
Collaborator
|
Verdict: Approve with nits The engine is correct and the fail-closed posture is genuine where it's engaged. Nothing blocking, but a few things the author/reviewers should act on. What holds up (the security-critical parts)
Findings worth acting on (none blocking)
|
navareamol
approved these changes
Oct 21, 2026
navareamol
left a comment
Collaborator
There was a problem hiding this comment.
Review comments have been left on the PR.
Approving as there are minor suggestions which can be fast follow.
This was referenced Oct 22, 2026
3 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A
Denystatement in a role's trust policy is now enforced, and trust policies and identity-policy conditions are evaluated the way IAM and STS evaluate them. This builds on #402 by @navareamol, whose commit is the first one here, kept with its authorship.Trust policies:
DenyblocksAssumeRolebefore anyAllowis considered. The decision coverssts:AssumeRole, andsts:TagSessionas well when the call passes session tags.AssumeRoleresolves the condition keys STS populates for the call, includingaws:PrincipalArn,aws:PrincipalAccount,aws:PrincipalTag/*,aws:RequestTag/*,aws:TagKeys,sts:ExternalId,sts:RoleSessionName,aws:SourceIpandaws:SecureTransport. A key STS does not populate is absent and follows IAM absent-key semantics.CreateRoleis refused at write:NotPrincipal, aResourcefield, an action outside the STS trust set, a bare"*"principal, a non-alphanumericSid, and any character above U+00FF. The web console now runs the same checks as the management API.Identity policies on DynamoDB requests:
aws:SecureTransport,aws:RequestedRegion,aws:CurrentTime,aws:PrincipalType,aws:userid,aws:SourceIp,aws:UserAgent,aws:TokenIssueTimeandaws:MultiFactorAuthPresent(the last two for role sessions only). Other keys are absent, as on a standalone AWS account.IpAddressandNotIpAddressare evaluated as IPv4 and IPv6 CIDR membership. An IPv4 client on a dual-stack listener is evaluated as IPv4. A malformed CIDR, an IPv4-mapped IPv6 literal and a non-alphanumericSidare rejected at write, as IAM rejects them.TagUser,TagRole, session tags) apply IAM's tag character set, length limits, the reservedaws:prefix and its rule that keys differing only in ASCII case collide.The creation-time caches the new keys use are reported at
/management/auth-cache-metrics.Why
evaluate_trust_policyskipped every statement whose effect was notAllow, so aDenyin a trust policy did nothing. A broadAllowstill grantedAssumeRoleto a principal theDenywas meant to block. Fixing that exposed the condition keys behind mostDenystatements: a key ExtendDB did not resolve read as absent, so aDenygated on it never matched and a negatedAllowgated on it granted. Each expected result in this change was measured against IAM, STS and DynamoDB first.Testing done
tests/test_auth_integration.py(53 new tests) andtests/test_abac.py(56 new tests) cover the trust-policyDenycases, every resolved key, absent-key semantics, key name case and Unicode folding, tag resolution, CIDR forms,Sidand tag rules at write, and the role-session keys. The four auth wire files pass on SQLite and PostgreSQL (207 tests each), and the trust file passes on MongoDB and Cassandra (82 each). The new cases fail onmain.tests/test_console_integration.py: four new tests show the console refuses trust and identity policies the management API refuses. They fail on the build before the console change.crates/auth/src/policy/andcrates/server/src/, including a guard test for the generated case-fold table (devtools/generate-casefold-table, Unicode 15.0.0).cargo fmt --check,cargo clippyon 1.97.0 with-D warnings,cargo test --workspaceon stable and 1.88.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy -- -W clippy::pedantic)Storagetrait, auth model, on-diskformat, or public CLI surface, an RFC has been accepted or is linked
below. Otherwise, an ADR captures the decision (link below).
ADR / RFC:
docs/adr/0022-trust-policy-deny-over-allow.md.Breaking changes
docs/manuals/07-upgrade-manual.mdanddocs/manuals/05-admin-guide.mdhave the full notes. The main ones:Denynow takes effect.sts:TagSessionin its trust policy.Denyon such a key can start to apply, and a negatedAllowon it can stop granting.AccessDeniedExceptionuntil the policy is corrected. A storedSidis not re-checked.Merge order
This PR conflicts with #419 in
crates/server/src/request_helpers.rs, thematcher.rstests anddocs/manuals/05-admin-guide.md. It merges cleanly with #418. I will rebase it after #418 and #419 merge and resolve the conflicts.By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.