Repository navigation
fix(auth): bind LeadingKeys for a refused Query's partition operand - #511
Open
LeeroyHannigan wants to merge 3 commits into
Open
LeeroyHannigan wants to merge 3 commits into
LeeroyHannigan wants to merge 3 commits into
Conversation
…y condition A Query whose partition key condition uses a non-equality operator, and a legacy KeyConditions entry with LE or BEGINS_WITH on the partition key, are shapes the engine rejects. Authorization extracted no dynamodb:LeadingKeys value for them, so a LeadingKeys condition was evaluated against no value and the shape's ValidationException was what every principal saw. Live DynamoDB binds LeadingKeys to the single operand the partition key's condition names and evaluates the policy before it rejects the shape. Measured 2026-10-21 for `pk > :v`, legacy `pk LE` and legacy `pk BEGINS_WITH` with the operand alice: ValidationException to the table owner and to a principal fenced to alice, AccessDeniedException to a principal fenced to nobody. Authorization now reads that operand. The expression path takes the single operand of the clause naming the partition key when no equality pins it, and the legacy path takes the partition attribute's one AttributeValueList entry whatever the operator. A condition that does not parse, one that names no partition clause, and a partition clause carrying no single operand (BETWEEN, IN) still carry no LeadingKeys, which is what the service answers for those. The equality shapes the engine runs keep the value they had.
…eadable The partition operand of a non-equality clause is bound only when no clause names the partition key by equality and exactly one clause names it at all. When a key condition such as `pk = :u AND pk > :v` carries a `:u` whose value cannot be read as the key's type, the second clause's operand was bound in its place. The key now stays absent there, as on main. Two non-equality partition clauses bind nothing, since which operand the service binds for them is not measured, and a legacy ComparisonOperator that is not one of the service's single-operand operators binds nothing. key_condition_clause_operands replaces key_condition_inequality_operands and reports every single-operand clause with whether it is an equality.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 24, 2026 15:46
This branch has not been deployed
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 this fixes
Closes #448.
A
Querywhose partition key condition uses a non-equality operator, and a legacyKeyConditionsentry using
LEorBEGINS_WITHon the partition key, are shapes the engine refuses. Authorizationread no
dynamodb:LeadingKeysvalue for them, so a policy carrying aLeadingKeyscondition wasevaluated against no value and every principal saw the shape's
ValidationException. A principalfenced away from the value the request named got
ValidationExceptionwhere DynamoDB answersAccessDeniedException.What the service does
Measured against live DynamoDB on 2026-10-21 at 23:14 UTC, us-east-1, on a table with partition key
pk(S) and sort keysk(S), with three principals: the table owner, whose policy carries noLeadingKeyscondition; principal A, alloweddynamodb:*on the table underForAllValues:StringEquals dynamodb:LeadingKeys ["alice","galice","10"]; and principal Z, the sameAllow with the single value
["nobody"]. Every request namedaliceas the partition operand.KeyConditionExpression: "pk > :v"Query key condition not supportedKeyConditions: {pk: {ComparisonOperator: LE, AttributeValueList: [alice]}}KeyConditions: {pk: {ComparisonOperator: BEGINS_WITH, AttributeValueList: [alice]}}A was allowed
aliceand got the shape error; Z was not and was denied, so the policy was evaluatedagainst a
LeadingKeysvalue ofalicebefore the shape was refused. The owner's row shows thatbinding the key changes nothing for a policy with no condition on it.
Four other refused shapes in the same run returned
ValidationExceptionto all three principals: asyntax error (
"pk :: :v"), a condition on the sort key alone ("sk = :s"), an undefinedExpressionAttributeNamesreference ("#p = :v"), and a query on a two-attribute-partition globalsecondary index naming one of the two. The first three name no partition operand that can be read.
The change
crates/server/src/leading_keys.rsreads the clauses that name the queried partition attribute.When one of them is an equality, that equality decides the key, as the engine reads it; when its
value cannot be read (an N value that is not a number, an undefined placeholder) the key stays
absent, as on main, and no other clause's operand is bound in its place. When no clause names the
partition attribute by equality and exactly one clause names it, under
<,<=,>,>=orbegins_with, that clause's operand is bound. Two non-equality clauses on the partition attributebind nothing. On the legacy path the partition attribute's entry is read when its
ComparisonOperatoris one of the service's single-operand operators and itsAttributeValueListholds exactly one value.
crates/core/src/expression/key_condition.rsgrowskey_condition_clause_operands, which returnsthe attribute path and single operand of each clause and whether it is an equality. It runs the
clause loop and the pre-classification checks
parse_key_conditionalready ran, now shared throughparse_clauses, so both functions refuse the same key conditions.Everything that carried no
LeadingKeysbefore still carries none: an expression that does nottokenize or parse, an undefined
#nameor placeholder, a condition naming no partition clause, anunknown
IndexName, a table with no key info. ABETWEENon the partition key, and legacyBETWEENand
IN, name more than one value and no record says which the service binds, so they are left asthey are.
Tests
tests/test_fgac_leading_keys.pyalready sent all seven refused shapes as all three principals andcompared them with the service's rows. It accepted either error code for the three shapes above;
it now requires the code the service returned. The run on main fails those three cases and passes at
this head.
crates/server/src/leading_keys/tests.rscovers the three measured shapes, the operators that followthe same rule, an N operand (normalized, as on the equality path for a
Query), a non-equalitycondition on a queried index's own partition key, a name reference that resolves and one that does
not, an unknown placeholder, an unreadable partition equality next to another partition clause, two
non-equality partition clauses, unknown legacy operators, and the shapes that stay absent. One test
asserts that the engine's own parser still refuses every single-clause shape whose operand is now
bound. The two-clause shapes it binds (
pk > :v AND sk = :s,pk > :v AND sk < :s) are refused bythe engine when it resolves the partition attribute, which a wire test checks.
tests/test_fgac_leading_keys.pyalso coverspk = :u AND pk > :vwhere:ucannot be read, andchecks that
aliceis not bound in its place.crates/core/src/expression/key_condition.rscovers the clause list for thirteen expressions and therefusals it inherits.
Not measured
The three principals of that run were deleted at the end of it and no IAM principal can be created on this
host, so no row covers these:
<,<=,>=andbegins_with(pk, :v)in aKeyConditionExpression, legacyNE,LT,GT,GE,CONTAINSandNOT_CONTAINS, and anon-equality partition condition combined with a sort key condition, two non-equality conditions on
the partition key, and a legacy
ComparisonOperatorthat is not one of the service's operators.Each single-clause shape names one partition operand, as the three measured shapes do, and is bound
the same way; the two-clause and unknown-operator shapes bind nothing.
The two-attribute-partition index shape is a separate difference in the other direction: the service
returned
ValidationExceptionto all three principals and ExtendDB binds the index's first partitionattribute and denies Z. This branch does not change that. The test records it as a divergence rather
than asserting the service's code.
Merges
Of the 66 pull requests open on 2026-10-24, six change a file this branch changes. #484 and #429
conflict in
crates/core/src/expression/key_condition.rs: each adds to a region this branch alsoadds to, and neither rewrites the other's lines. #484's duplicate-key check belongs at the tail of
parse_clauses, where both callers get it. #399 and #507 (stacked on #399) conflict in thepub useblock of
crates/core/src/expression/mod.rs; the merge keeps both sets of exports. #508 mergesclean.
#479 merges clean textually and collides in behaviour. It moves the refusal of a key condition with no
equality out of
parse_key_conditionintoKeyCondition::refuse_without_hash_equality, which theengine calls with the key schema. On the merged tree this fix still binds
aliceforpk > :v, andtwo tests need updating: #479's
query_kce_without_a_partition_key_equality_leaves_the_key_undetermined, which asserts no key isbound for
pk > :v, and this branch's assertion thatparse_key_conditionrefuses that shape, whichhas to move to the engine's call. Whichever lands second carries both edits.