Repository navigation
fix: populate dynamodb:Attributes for every item request - #732
LeeroyHannigan wants to merge 15 commits into
Conversation
…to a submodule leading_keys.rs is 601 lines. Move AuthzTarget, batch_transact_targets, batch_transact_tables and batch_table_keys unchanged into leading_keys/batch.rs, and re-export the two functions that request authorization calls.
The dynamodb:Attributes condition key was read from ProjectionExpression alone, only on single-table requests, and a document path such as secret.a went into it as written. Writes, AttributesToGet reads and every batch and transaction request reached the policy evaluator without the key, so a Deny gated on ForAnyValue:StringEquals over an attribute never applied to them, and an Allow gated on ForAllValues:StringEquals granted them whatever attributes they wrote or read. The key now holds the top-level names a request names: the item or key attributes, AttributesToGet, the paths in the projection, update, condition and filter expressions with ExpressionAttributeNames resolved, the legacy Expected, AttributeUpdates, QueryFilter and ScanFilter maps, ExclusiveStartKey, and the key attributes of the table and of a queried index. A batch request carries the names per table and a transaction per item. A DeleteItem and a read with no projection hold only the names the request gives, not those of the stored item.
…ansaction requests One test per request shape measured on DynamoDB on 2026-10-31, under a user policy of the shape of the session policy each was measured with: a Deny gated on ForAnyValue:StringEquals over one attribute, and an Allow of PutItem gated on ForAllValues:StringEquals over pk. Each asserts the service's answer, the action the refusal names, and that a refused write stores nothing.
The security model manual lists the names the key holds for each item, batch and transaction operation, and which entries were measured on DynamoDB. The troubleshooting entry for an Attributes condition covers a Deny that does not block a DeleteItem or a read with no projection, and an allowlist that omits the key attributes. The upgrade manual records the change for v0.1.15 and the policy statements it affects.
Rename the ExpressionAttributeNames field so it no longer repeats the struct name, merge the match arms that walk two operands, and replace for_each with loops. Behaviour is unchanged.
…utes DynamoDB authorizes an UpdateItem whose only mention of an attribute is in a SET value. Under a Deny gated on ForAnyValue:StringEquals over ssn, it authorized SET #n = ssn, SET #n = if_not_exists(ssn, :d), list_append(ssn, :l) and ssn + :one, and refused REMOVE ssn, ADD ssn :one and DELETE ssn :s (measured 2026-11-01). The key for an UpdateItem and for a transaction's Update item now holds the attribute each action names, and no longer the paths its value reads.
…ns and key attributes One test per request shape measured on DynamoDB on 2026-11-01: a condition, filter, Expected, AttributeUpdates or ScanFilter that reads a denied attribute, a REMOVE, ADD or DELETE of it, and a ConditionCheck on it are refused; a SET value that reads it is authorized and copies it; a bare StringNotEquals grants no Scan; a Null condition does not refuse a GetItem with no projection; and an allowlist without the table's key attribute refuses a projected GetItem. The new module reuses the fixtures of test_dynamodb_attributes_condition.py.
The security model manual, the troubleshooting guide and the upgrade manual now say that the key holds the target of each UpdateExpression action and not the attributes a SET value reads, and that, as in DynamoDB, a policy on dynamodb:Attributes does not stop an UpdateItem from copying a denied attribute's value into another attribute through a SET value. The security model manual lists the entries measured on DynamoDB on 2026-10-31 and 2026-11-01, and records that a bare StringNotEquals on the key matches neither a present nor an absent key.
…n Scan
A Scan now puts the key attributes of the table, and with IndexName the key
attributes of that index, in dynamodb:Attributes, as GetItem and Query do.
Before this change a Scan that named no attribute left the key absent.
DynamoDB, measured on 2026-11-01: a Deny over pk refuses a Scan with no
projection. An Allow of Scan alone with ForAllValues:StringEquals over
["name"] refuses a plain, a projected and a Select COUNT Scan; the same Allow
over ["pk", "name"] admits the projected Scan, and over ["pk"] it admits a
plain Scan. A Deny of Scan gated on Null {"dynamodb:Attributes": "true"}
refuses neither a plain nor a projected Scan, and an Allow of Scan alone on
the same Null grants neither a plain nor a Select COUNT Scan.
With the key absent, ExtendDB used to give the opposite answer to each
plain and Select COUNT Scan here except the one under ["pk"]; it also
granted the projected Scan under ["name"], whose key held name alone.
The index key attributes follow the IAM guide's rule that a policy on
dynamodb:Attributes lists the key attributes of the table and of every index
it covers. They were not measured.
Wire tests for rows DynamoDB answered on 2026-11-01 that had none: a Deny
over pk, Allows over ForAllValues lists, and Null in a Deny and an Allow, on
Scan; the table's key attribute on Query, UpdateItem, DeleteItem and a
BatchWriteItem DeleteRequest; QueryFilter, AttributesToGet on Query and
BatchGetItem, and the Update and Delete items of TransactWriteItems under a
Deny on ssn; and ReturnValues ALL_OLD and ReturnValuesOnConditionCheckFailure
ALL_OLD, which return the stored item with the denied attribute in it.
An Allow of BatchWriteItem alone with ForAnyValue:StringEquals over ["name"]
admits a batch that puts {pk, name} and {pk, ssn} on one table and writes
both items, admits {pk, name} alone, and refuses {pk, ssn} alone. The service
evaluates each table's part of the batch once, with the names of all its
items, and ExtendDB does the same.
Two rows were not measured, and their docstrings say so: a PutItem with
ReturnValues ALL_OLD follows the UpdateItem row, and a Query on a global
secondary index follows the IAM guide's rule that a policy on the key lists
the key attributes of the table and of every index it covers.
…hapes The security model, the upgrade entry, the troubleshooting guide and the policy evaluator's comments now say that a Scan holds the table's key attributes. On a table that exists the key is absent only on a SearchVectors with no ProjectionExpression. On a table that does not exist it is also absent on a GetItem or transaction Get that names no attribute outside its Key, a Query that names none outside its key condition and a Scan that names none, because the extractor reads no Key on a GetItem or a Get and no key condition on a Query. The rows DynamoDB answered on 2026-11-01 are labelled measured; the index key attributes and the names of ExclusiveStartKey are labelled unmeasured. The upgrade entry opens its list with the rule the shapes follow: every item request other than SearchVectors now carries the key, holding at least the table's key attributes, so a ForAllValues condition no longer holds vacuously, a ForAnyValue condition can match, Null true stops matching and Null false starts matching. Its bullets cover a Deny gated on a ForAnyValue string operator; a Deny gated on a negated one, which now needs the key attributes in its list; an Allow gated on ForAllValues; an Allow gated on ForAnyValue over any listed attribute, with the per-table BatchWriteItem union as its example; the negated Allow shapes; and Null with either value. The security model says that a Deny on the key does not stop ReturnValues ALL_OLD or ReturnValuesOnConditionCheckFailure ALL_OLD from returning the whole stored item, as on DynamoDB, and that the IAM guide's guard against ReturnValues returning attributes is a condition on dynamodb:ReturnValues. The extractor's module docs say that ReturnValues and ReturnValuesOnConditionCheckFailure were measured to add no names and that Select was not measured. The unit test module docs say which unmeasured rows follow the IAM guide's rule and which follow the rule measured for the same parameter on another operation.
crates/server/src/lib.rs conflicted on the module list. Main added the registration module and this branch added request_attributes. The merged file declares both. This branch added a LimitsConfig argument to batch_transact_targets, and main's new cross-account check in authz_resource/cross_account.rs calls it. That check reads only each target's table and action, so it passes the default limits, which govern only how expressions are read for Attributes.
hitesh96db
left a comment
There was a problem hiding this comment.
I re-measured this PR's rules against Amazon DynamoDB and against a build of this PR, 890ee53f on PostgreSQL. Most of it matches, including every row in the description I re-ran, and several of the rows it lists as not measured. One rule is wrong in a way that refuses requests DynamoDB allows: Scan with IndexName (inline, marked Blocker). There are two smaller differences on tables that do not exist, plus one doc note.
Setup
Measured on 2026-11-09 against Amazon DynamoDB in us-east-1. The inline experiments, and the table-key and missing-table rows below, were measured in two accounts with the same answers in both; the remaining rows below were measured in one. Every DynamoDB request was sent at least twice, with the same answer each time. The ExtendDB side ran against 890ee53f, release build, PostgreSQL backend, https://127.0.0.1:18452, with AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem and --endpoint-url https://127.0.0.1:18452 appended to each aws dynamodb command. The commands in the inline comments are the exact commands run. A driver generated both the measurements and these comments from the same definitions, and the results tables are its output, with the caller ARN and the account id replaced by <caller> and <ACCOUNT>.
The caller. Every experiment names a policy, and the caller holds only that policy.
On DynamoDB, a role whose own policy allows dynamodb:* on table/pr732-* and table/pr732-*/index/*, assumed with the experiment's policy as the session policy:
aws iam create-role --role-name pr732-attr-probe --max-session-duration 3600 --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::<ACCOUNT>:role/Admin"},"Action":"sts:AssumeRole"}]}'
aws iam put-role-policy --role-name pr732-attr-probe --policy-name ddb-pr732 --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"]}]}'
aws sts assume-role --role-arn arn:aws:iam::<ACCOUNT>:role/pr732-attr-probe --role-session-name probe-<N> --policy file://<policy>.json --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output textOn ExtendDB, a new IAM user per experiment whose only policy is the experiment's:
extenddb manage --user admin --config extenddb.toml create-user --account-id <ACCOUNT> --user-name probe-<N>
extenddb manage --user admin --config extenddb.toml create-access-key --account-id <ACCOUNT> --user-name probe-<N>
extenddb manage --user admin --config extenddb.toml put-user-policy --account-id <ACCOUNT> --user-name probe-<N> --policy-name p --policy-document "$(cat <policy>.json)"Control. Under control (an Allow of GetItem only), aws dynamodb put-item --region us-east-1 --table-name pr732-c --item '{"pk":{"S":"z"},"sk":{"S":"z"}}' was refused on both endpoints: AccessDeniedException "because no session policy allows the dynamodb:PutItem action" on DynamoDB, and the same with "identity-based policy" on ExtendDB. So the session policy really does restrict the caller.
Table. Created once per endpoint: on DynamoDB with the account's admin credentials, and on ExtendDB by a user allowed dynamodb:* on pr732-*. The table pr732-missing is never created.
aws dynamodb create-table --region us-east-1 --table-name pr732-c --billing-mode PAY_PER_REQUEST --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S AttributeName=ls,AttributeType=S AttributeName=gk,AttributeType=S AttributeName=gs,AttributeType=S --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE --local-secondary-indexes '[{"IndexName":"lsi","KeySchema":[{"AttributeName":"pk","KeyType":"HASH"},{"AttributeName":"ls","KeyType":"RANGE"}],"Projection":{"ProjectionType":"ALL"}}]' --global-secondary-indexes '[{"IndexName":"gsi2","KeySchema":[{"AttributeName":"gk","KeyType":"HASH"},{"AttributeName":"gs","KeyType":"RANGE"}],"Projection":{"ProjectionType":"ALL"}}]'
aws dynamodb wait table-exists --region us-east-1 --table-name pr732-c
aws dynamodb put-item --region us-east-1 --table-name pr732-c --item '{"pk":{"S":"p1"},"sk":{"S":"s1"},"ls":{"S":"l1"},"gk":{"S":"g1"},"gs":{"S":"t1"},"name":{"S":"n"}}'Policies (<ACCOUNT> is the endpoint's account):
allow-scan-table-keys:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:Scan","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["pk","sk","name"]}}}]}allow-scan-pk-sk:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:Scan","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["pk","sk"]}}}]}deny-gk:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"]},{"Effect":"Deny","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAnyValue:StringEquals":{"dynamodb:Attributes":["gk"]}}}]}allow-scan-index-keys:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:Scan","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["gk","gs","name"]}}}]}allow-query-no-gs:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:Query","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["pk","sk","gk","name"]}}}]}allow-query-with-gs:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:Query","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["pk","sk","gk","gs","name"]}}}]}deny-name:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"]},{"Effect":"Deny","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAnyValue:StringEquals":{"dynamodb:Attributes":["name"]}}}]}deny-pk:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"]},{"Effect":"Deny","Action":"dynamodb:*","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAnyValue:StringEquals":{"dynamodb:Attributes":["pk"]}}}]}control:{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:GetItem","Resource":["arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*","arn:aws:dynamodb:us-east-1:<ACCOUNT>:table/pr732-*/index/*"],"Condition":{"ForAllValues:StringEquals":{"dynamodb:Attributes":["pk"]}}}]}
Reading the results: "allowed" means the request passed authorization and succeeded. Any error other than AccessDeniedException (ResourceNotFoundException, ConditionalCheckFailedException, ValidationException) means it passed authorization and failed afterwards.
Inline
- [Blocker]
request_attributes.rs,Scanarm: aScanof a GSI or LSI holds the table's key attributes, not the index's (Experiments 1 to 5).Queryis right and keeps its rule (6, 7). The same claim is in the docs (comment on10-security-model.md). - [Difference]
request_attributes.rs,Queryarm: DynamoDB reads the names in the key condition into the key, which shows on a table that does not exist (Experiments 8, 9). - [Difference]
request_attributes.rs,BatchGetItem: DynamoDB does not add the names inKeys, as forGetItem(Experiments 10, 11).
Re-measured and matching
Beyond the experiments inline, I ran the request shapes below on both endpoints with policies of the description's shapes. Each matched DynamoDB, and each matched what the description says DynamoDB does.
- PutItem: item attributes,
ConditionExpression,Expected, a failing condition withReturnValuesOnConditionCheckFailure ALL_OLD. - UpdateItem:
SET,REMOVE,ADDandDELETEtargets, a nestedSET secret.aandREMOVE lst[5], andAttributeUpdatesConditionExpressionandExpected, which the description lists as not measuredSETvalues that readssn:#n = ssn,if_not_exists(ssn, :v)andlist_append(:l, ssn), all authorized under a Deny onssnReturnValues ALL_OLD
- DeleteItem: with and without a condition, and
Expected, which the description lists as not measured. - GetItem:
AttributesToGet, projections ofsecret.aand#s.a, and no projection;ForAllValueswith and withoutpk; theNullDeny. - Query and Scan:
FilterExpression,QueryFilter,ScanFilter,AttributesToGetandExclusiveStartKeyProjectionExpression, which the description lists as not measuredSelect COUNT, theNullDeny and theNullAllow, and the bareStringNotEqualsAllow- the table's sort key, which the description lists as not measured: it is in the key
- BatchGetItem and BatchWriteItem: projections,
AttributesToGet, puts and deletes underForAllValues, and the per-tableForAnyValueunion. - TransactGetItems and TransactWriteItems: the
Get,Put,UpdateandConditionCheckforms, aDeletewith and without a condition, and a failingConditionCheckwithALL_OLD. - Tables that do not exist:
GetItem,Scanand transactionGetanswerResourceNotFoundExceptionon both endpoints.PutItem,UpdateItem,DeleteItem, aBatchWriteItemDeleteRequestand a transactionDeleteanswerAccessDeniedExceptionon both.
Locally: test_dynamodb_attributes_*.py, test_fgac_attributes.py, test_fgac_leading_keys.py and test_tag_request_allow_conditions.py pass on PostgreSQL (151 passed), and cargo test -p extenddb-server -p extenddb-auth passes.
PartiQL (no change needed here)
This branch predates PartiQL. On a build of this PR merged onto main (SQLite backend), ExecuteStatement, BatchExecuteStatement and ExecuteTransaction matched DynamoDB in all 33 cases I ran. Those covered:
SELECT,INSERT,UPDATE(SETandREMOVE) andDELETEssnread in aWHEREclause,SELECT secret.a, and aSELECTfrom a GSI, which needs the GSI's key in aForAllValueslistForAllValuesandForAnyValueAllows
That path fills the key itself (item_authz.rs). The security model manual and the troubleshooting paragraph describe what the key holds "on every item request" without mentioning PartiQL, so a sentence pointing to the PartiQL rules would help readers.
No file conflicted. Main's #830 added a table name check at the top of authorize_request in crates/server/src/request_helpers.rs, and this branch replaced extract_attributes there with request_attributes. The merged function keeps the name check first, before the key info lookup. It keeps this branch's dynamodb:Attributes value on the single-table branch and on the batch and transaction branch.
The review of this pull request measured three request shapes on DynamoDB on 2026-11-09, in two accounts. A further measurement on 2026-11-10, in one account, covered a later page of a Scan. This change follows those measurements. A Scan with IndexName no longer adds the index's key schema. On a global secondary index the service allows a Scan under an Allow whose ForAllValues list holds only the table's keys. A Deny on that index's partition key does not refuse it either. On a local secondary index, an Allow of the table's keys and the projected name grants it. The branch refused these first-page requests, which carry no ExclusiveStartKey. A later page's start key holds the table's and the index's key attributes, and the key holds them. DynamoDB does the same: on 2026-11-10 it refused page 2 of either index under both Allows. A Deny on the global index's partition key or the local index's sort key refused it too. Page 2 of the table passed under the Allow of its keys alone. A Query of an index still adds its full key schema, sort key included (measured on a global secondary index). A Query now adds the names in KeyConditionExpression and the keys of KeyConditions. The expression goes through the engine's limits, its reserved-word check when the enforce_reserved_keywords setting is on, and its key-condition parser. A key condition that one of them refuses adds no names. On a table that exists, the engine refuses a non-key attribute in KeyConditions. It also refuses one in a KeyConditionExpression on a key schema with several HASH or several RANGE attributes. On a key schema with one HASH and at most one RANGE attribute, it checks a KeyConditionExpression only for an equality on the partition key. So it accepts a clause on another attribute (crates/engine/src/query.rs:295-297 and :328-336), and the key also holds that name. On a missing table the service answers AccessDeniedException under a Deny on a name in the key condition. The branch answered ResourceNotFoundException there, which told a denied caller the table was missing. BatchGetItem no longer adds the names in Keys. For a table named by its name it holds what GetItem holds. On a missing table the service answers ResourceNotFoundException under a Deny on the key attribute, where the branch answered AccessDeniedException. A RequestItems key that is a table ARN gets no key info at authorization, so that part holds no key attributes. A GetItem by the same ARN holds them. A separate change will close that gap. The security model manual, troubleshooting guide, v0.1.15 upgrade entry and evaluator comments on the absent key describe these rules. The manual also says how the PartiQL path fills the key. Unit tests cover each arm. Wire tests cover the review's experiments and four later-page answers.
|
@hitesh96db, the branch now holds a merge of main (6ee143f) and a fix commit, 147378a. For each item of your review:
Tests ran on 147378a unless a line names an earlier version of the fix commit.
|
hitesh96db
left a comment
There was a problem hiding this comment.
I re-checked 147378a against my review of 890ee53. All of it is addressed:
- Scan with
IndexName(Blocker): theScanarm no longer adds the index's key schema, andQuerykeeps it. Querykey condition: theQueryarm readsKeyConditionExpressionandKeyConditions.BatchGetItemKeys: they no longer add names.- Docs: the
ScanandBatchGetItemrows of10-security-model.md,troubleshooting.mdand the v0.1.15 upgrade entry now match. The PartiQL pointer is in.
Re-test
Release builds of 147378a on PostgreSQL and on SQLite, each a fresh catalog.
-
Wire tests: the three
test_dynamodb_attributes_*files,test_fgac_attributes.py,test_fgac_leading_keys.pyandtest_tag_request_allow_conditions.py: 166 passed on each backend. -
My review's experiments, re-run: a driver of my own sent the exact
aws dynamodbcommands and policies from my review. It made a new IAM user per experiment, in a new account. It covered:- the control and Experiments 1 to 11
- a later page (
Limit 1, then the first page'sLastEvaluatedKeyasExclusiveStartKey) of aScanofgsi2and oflsiunderallow-scan-table-keysandallow-scan-pk-sk, ofgsi2underdeny-gk, and oflsiunder a Deny onls - a later page of a
Scanof the table underallow-scan-pk-sk
All 28 cases gave the answer DynamoDB gave: the one in my review for Experiments 1 to 11, and the one in your 2026-11-10 measurement for the later pages. That held on both backends. I did not re-measure the later-page rows on DynamoDB.
-
Rust:
cargo test --locked -p extenddb-server -p extenddb-authpassed (877 tests), andcargo fmt --all -- --checkandcargo clippy -p extenddb-server -p extenddb-auth --all-targets -- -D warningson 1.97.0 are clean. -
Negative control: with
request_attributes.rsfrom 6ee143f, four of the new unit tests fail:scan_on_an_index_holds_the_table_key_attributes_and_not_the_index_keysa_query_of_a_table_that_does_not_exist_holds_its_key_condition_namesa_key_condition_with_a_reserved_word_gives_no_namesbatch_get_item_keys_add_no_names_of_their_own
Outside this PR
- A table ARN as a
RequestItemskey (left to #442). Under a Deny onpk, aBatchGetItemof an existing table answersAccessDeniedExceptionwhen it names the table by its name. It answersResourceNotFoundExceptionwhen it names the table by its ARN, because the engine does not resolve the ARN there. Nothing is read today. Once #442 makes the data plane resolve a table ARN, that part holds no key attributes and would pass a Deny on a key attribute. Whatever fixes #442 should fill the key info for an ARN-named part at the same time. - A non-key clause in a key condition. On a table keyed on
pkandsk, ExtendDB accepts theKeyConditionExpressionpk = :p AND #n = :n(with#nforname) and answersCount 0for an item that matches both clauses. This is #674. As your commit message says, the key then also holdsname.
Main at cbc3680 is 13 commits ahead of the last merge. One file conflicted: crates/server/src/lib.rs, where #355 adds the module repeated_headers and this branch adds request_attributes beside it. The merge keeps both, in name order. docs/manuals/07-upgrade-manual.md, docs/manuals/10-security-model.md and docs/troubleshooting.md merged without a conflict. Against main, the merged tree changes the same 18 files with the same line counts as this branch did against its merge base.
|
@hitesh96db Thanks for the approval. The branch conflicted with main, so I pushed 26617aa, a merge of main at cbc3680. The one conflict was in crates/server/src/lib.rs, where #355 adds |
What
dynamodb:Attributesnow holds the top-level names of the attributes a request names, on every item, batch and transaction operation. On every item operation other thanSearchVectorsit also holds the table's key attributes, with the exceptions under "Not covered". Before this change ExtendDB read the key fromProjectionExpressionalone, only on single-table requests, and a projected document path went into it whole (secret.ainstead ofsecret).What the key holds now:
dynamodb:AttributesholdsGetItemAttributesToGet, and the attributes inProjectionExpressionPutItemExpected, and the attributes inConditionExpressionUpdateItemKeyattributes, the table's key attributes,AttributeUpdates,Expected, the attributes inConditionExpression, and the target of everySET,REMOVE,ADDandDELETEaction, but not the attributes aSETvalue readsDeleteItemKeyattributes, the table's key attributes,Expected, and the attributes inConditionExpressionQueryKeyConditionExpressionandKeyConditions,AttributesToGet,QueryFilter,ExclusiveStartKey, and the attributes inFilterExpressionandProjectionExpressionScanAttributesToGet,ScanFilter,ExclusiveStartKey, and the attributes inFilterExpressionandProjectionExpressionSearchVectorsProjectionExpression, as before. The key is absent when there is noneBatchGetItemGetItem: the table's key attributes,AttributesToGet, and the attributes inProjectionExpression. The names inKeysadd nothingBatchWriteItemPutRequestitem andDeleteRequestkey, and the table's key attributesTransactGetItems,TransactWriteItemsConditionCheckholds the table's key attributes and the attributes in itsConditionExpressionThe code resolves
ExpressionAttributeNamesplaceholders, and a document path contributes its first name (#s.agivessecretwhen#sissecret). It reads expressions with the engine's tokenizer and parser, under the engine's limits. On every operation other thanSearchVectors, an expression the engine accepts gives all its names. An expression the parser refuses gives none, and the engine refuses the same request without reading or writing an item. For a key condition, this code also runs the engine's reserved-word check whenenforce_reserved_keywordsis on. So a key condition with a bare reserved word gives none. The other expressions skip that check here. A bare reserved word in one of them adds its name, and the engine refuses the request after authorization. "Not covered" listsSearchVectors.A
DeleteItemand a read with no projection add none of the stored item's names to the key. As in DynamoDB, a policy ondynamodb:Attributesdoes not stop anUpdateItemfrom copying a denied attribute's value into another attribute through aSETvalue. It also does not stopReturnValuesALL_OLDorReturnValuesOnConditionCheckFailureALL_OLDfrom returning the whole stored item.Changes:
crates/server/src/request_attributes.rsbuilds the value per operation, per batch table and per transaction item. It replacesextract_attributesinrequest_helpers.rs.AuthzTargetunchanged intocrates/server/src/leading_keys/batch.rs, sinceleading_keys.rswas at 601 lines.Scanholds the table's key attributes. AScanwithIndexNamedoes not add the index's key schema, so the index's key attributes are in the key only when the request names them. TheExclusiveStartKeyof a later page names them, and DynamoDB counts them too (measured 2026-11-10). AQueryof an index adds the index's full key schema, its sort key included.Queryadds the names inKeyConditionExpressionand the keys ofKeyConditions. On a table that does not exist the key holds them, a non-key name included. On a table that exists, the engine refuses a non-key attribute inKeyConditions. It also refuses one in aKeyConditionExpressionon a key schema with several HASH or several RANGE attributes. On a key schema with one HASH attribute and at most one RANGE attribute, it checks aKeyConditionExpressiononly for an equality on the partition key. So it accepts a clause on another attribute, and the key also holds that name.BatchGetItempart holds what aGetItemholds, and the names in itsKeysadd nothing. "Not covered" lists the one exception, a table named by ARN.AttributesToGetreads and on batch and transaction targets. The no-match answer for a bare negated operator on an absent key stays the same. The comments now say where the key is absent:SearchVectorswith noProjectionExpressionGetItem, transactionGetorBatchGetItemthat names no attribute outside its keysQuerywhose key condition the key condition parser refuses and that names none elsewhereScanthat names noneWhy
With the key absent, a
Denygated onForAnyValue:StringEqualsover an attribute never applied to a write, anAttributesToGetread, or any batch or transaction request. AnAllowgated onForAllValues:StringEqualsgranted those requests whatever attributes they named. The security model manual and the troubleshooting guide told operators these controls work.Measured on DynamoDB on 2026-10-31 and 2026-11-01, with each policy below as a session policy. Every request was sent twice and got the same answer both times. The policies:
deny(X): Allowdynamodb:*on the table, and Denydynamodb:*on it withForAnyValue:StringEquals {"dynamodb:Attributes": [X]}.allow-put for-all(pk): only Allowdynamodb:PutItemwithForAllValues:StringEquals {"dynamodb:Attributes": ["pk"]}.allow-get for-all(L)is the same fordynamodb:GetItemover the listL.allow-scan for-all(L),allow-query for-all(L),allow-update for-all(L),allow-delete for-all(L)andallow-batch-write for-all(L)are the same forScan,Query,UpdateItem,DeleteItemandBatchWriteItem.allow-batch-write for-any(name): only Allowdynamodb:BatchWriteItemwithForAnyValue:StringEquals {"dynamodb:Attributes": ["name"]}.allow-scan unless(ssn): only Allowdynamodb:Scanwith the bareStringNotEquals {"dynamodb:Attributes": "ssn"}.deny-get-when-absent: Allowdynamodb:*on the table, and Denydynamodb:GetItemwithNull {"dynamodb:Attributes": "true"}.deny-scan-when-absentis the same fordynamodb:Scan.allow-scan when-absent: only Allowdynamodb:ScanwithNull {"dynamodb:Attributes": "true"}.Measured 2026-10-31:
PutItem {pk, ssn}UpdateItem SET ssn = :vPutItem {pk, ssn}GetItem AttributesToGet=[ssn]Scan AttributesToGet=[ssn]GetItem ProjectionExpression=secret.aGetItem ProjectionExpression=#s.a(#s=secret)BatchGetItem ProjectionExpression=ssnTransactGetItemsGetProjectionExpression=ssnBatchWriteItemPutRequest{pk, ssn}TransactWriteItemsPut{pk, ssn}PutItem {pk, name}PutItem {pk}DeleteItemof an item holdingssnUpdateItem SET #n = :v(#n=name)GetItem ProjectionExpression=ssnGetItem AttributesToGet=[name]nameonlynameonlyGetItem, no projectionScan Limit=2, no projectionBatchGetItem, no projectionMeasured 2026-11-01. In this table, "authorized" means the request passed authorization, and some of these requests then failed their condition or validation:
PutItem {pk},ConditionExpression attribute_not_exists(ssn)PutItem {pk},Expected {"ssn": {"Exists": false}}DeleteItem,ConditionExpression attribute_not_exists(ssn)UpdateItem REMOVE ssnUpdateItem ADD ssn :oneUpdateItem DELETE ssn :sUpdateItem,AttributeUpdates {"ssn": PUT}Query pk = :p,FilterExpression ssn = :vScan,FilterExpression ssn = :vScan,ScanFilter {"ssn": NOT_NULL}TransactWriteItemsConditionCheckattribute_exists(ssn)UpdateItemSET #n = ssn,SET #n = #s(#s=ssn),if_not_exists(#n, ssn),list_append(ssn, :l),list_append(:l, ssn),ssn + :one,:one + ssn, on an item withoutssnUpdateItem SET #n = if_not_exists(ssn, :d)UpdateItem SET #n = ssnon an item holdingssnnamethen holds thessnvalue, which a laterGetItem ProjectionExpression=#nreturnsQuery pk = :p, no filter or projectionScan,ExclusiveStartKey {pk}onlyGetItem, no projectionGetItem ProjectionExpression=#nnamenameGetItem ProjectionExpression=#nGetItem ProjectionExpression=#nnamenameGetItem, no projectionScannaming no attribute, andScan ProjectionExpression=#nAlso measured 2026-11-01, on the table's key attributes, legacy reads, transaction items and returned items. The ExtendDB column comes from the wire tests run against a build of
mainon SQLite:Scan Limit=1, no projectionScan,ExclusiveStartKey {pk}onlyScanwith no projection,Scan ProjectionExpression=#n, andScan Select=COUNTScan ProjectionExpression=#nScan, no projectionScan, no projectionScan ProjectionExpression=#nScanwith no projection, andScan Select=COUNTQuery pk = :p,ProjectionExpression=#nUpdateItem SET #n = :vDeleteItemBatchWriteItemwith oneDeleteRequest[pk, name]BatchWriteItemPuts{pk, name}and{pk, ssn}on one tableBatchWriteItemPut{pk, name}onlyBatchWriteItemPut{pk, ssn}onlyQuerywithKeyConditionsandQueryFilter {"ssn": NOT_NULL}QuerywithKeyConditionsandAttributesToGet=[ssn]QuerywithKeyConditionsandAttributesToGet=[name]nameonlynameonlyBatchGetItem AttributesToGet=[ssn]TransactWriteItemsUpdateSET ssn = :vTransactWriteItemsDelete,ConditionExpression attribute_not_exists(ssn)TransactWriteItemsDelete, no conditionUpdateItem SET #n = :v,ReturnValues ALL_OLD, on an item holdingssnssnincludedPutItem {pk}whose conditionattribute_not_exists(pk)fails,ReturnValuesOnConditionCheckFailure ALL_OLDssnincludedTransactWriteItemsConditionCheck that fails,ReturnValuesOnConditionCheckFailure ALL_OLDssnincludedDynamoDB's refusals name
dynamodb:BatchGetItemanddynamodb:BatchWriteItemfor the batch requests, anddynamodb:GetItemfor theTransactGetItemsGet. They namedynamodb:PutItem,dynamodb:UpdateItemanddynamodb:DeleteItemfor theTransactWriteItemsPut,UpdateandDelete, anddynamodb:ConditionCheckItemfor theConditionCheck. ExtendDB's refusals now name the same actions.Measured 2026-11-09 by hitesh96db, in two accounts with the same answers in both. The table
pr732-chas the partition keypkand the sort keysk. It has a local secondary indexlsionpkandls, and a global secondary indexgsi2ongkandgs. Its one item holdspk,sk,ls,gk,gsandname. The tablepr732-missingdoes not exist. Each policy also covers the table's indexes, and#nisname. The last column is this PR at 890ee53, as hitesh96db measured it on PostgreSQL:Scan IndexName=gsi2 ProjectionExpression=#nnameScan IndexName=lsi ProjectionExpression=#nnameScan IndexName=gsi2, no projectionScan IndexName=gsi2, no projectionScan IndexName=gsi2 ProjectionExpression=#nQuery IndexName=gsi2,gk = :g,ProjectionExpression=#nQuerynamenameQueryofpr732-missing,#n = :pQueryofpr732-missing,KeyConditionsonpkBatchGetItemofpr732-missing,KeysonpkGetItemofpr732-missing,KeyonpkThe head gives the DynamoDB answer on each row, and the wire tests under "Testing done" assert each one. On the same day, hitesh96db re-ran request shapes from the 2026-10-31 and 2026-11-01 tables, and DynamoDB gave the answers those tables show. In one account, hitesh96db also measured
ProjectionExpressiononQueryandScan,ExpectedandConditionExpressiononUpdateItem, andExpectedonDeleteItem. DynamoDB applies to each the rule measured for the same parameter on the other operations. The 2026-11-09 measurements in two accounts also show that DynamoDB puts a table's sort key in the key.Measured 2026-11-10 in one account, on a table of the same shape that holds two items. Every request was sent twice and got the same answer both times. Each policy also covers the table's indexes, and
#nisname. Page 1 of eachScansetLimit=1, and page 2 sent page 1'sLastEvaluatedKeyas itsExclusiveStartKey. That start key holdsgk,gs,pkandskongsi2,ls,pkandskonlsi, andpkandskon the table:Scan IndexName=gsi2 ProjectionExpression=#n, page 1Scan, page 2Scan IndexName=lsi ProjectionExpression=#n, page 1Scan, page 2Scan IndexName=gsi2, page 1Scan IndexName=gsi2, page 2Scan IndexName=lsi, page 2Scanof the table, page 2Scan IndexName=gsi2, page 1Scan IndexName=gsi2, page 2Scan IndexName=lsi, page 1Scan IndexName=lsi, page 2Scan IndexName=gsi2 ProjectionExpression=#n, page 1So DynamoDB adds the names in
ExclusiveStartKeytodynamodb:Attributes, and a later page of aScanwithIndexNameholds the index's key attributes. AnAllowwhoseForAllValueslist omits them refuses that page, and so does aDenyon one of them. The branch already counted those names, so this needed no code change. The wire tests under "Testing done" assert four page-2 rows:gsi2andlsiunder the first policy,gsi2under deny(gk), and the table.Not measured:
ExclusiveStartKeyadds names of its own on aQueryor on aScanof the table. No request to the service can show it there: a valid start key holds only key attributes, and the key already holds them.Queryrule among them. For a local secondary index, the two tables cover aScanunder anAllowof the table's keys, alone or with the projected name. They also cover one under aDenyon its sort key. The otherScanrules follow from the key content those rows show, and theQueryrule follows the global secondary index measurement.Queryof a table that does not exist, with a key condition that the key condition parser refuses. ExtendDB adds no names from that key condition, so aDenyon one of them does not match. The engine then answersResourceNotFoundException.Testing done
The head is 147378a. Runs at 0d578028d or 46d8b051b used earlier versions of the last commit. 0d578028d differs from the head in
//!comments ofcrates/server/src/request_attributes.rsand in comments of its unit tests. It also differs in the docs and in the wire test file, which adds four tests.cargo fmt --check -p extenddb-serverpasses on the head.Wire tests, each with a measured row's policy as a user policy. There is one for each row of the 2026-10-31, 2026-11-01 and 2026-11-09 tables, and one for each of four rows of the 2026-11-10 table. Each asserts DynamoDB's answer, the action named in a refusal, and that a refused request changes nothing. The files are
tests/test_dynamodb_attributes_condition.py(20),tests/test_dynamodb_attributes_expressions.py(40) andtests/test_dynamodb_attributes_key_attributes.py(39). ThePutItemwithReturnValues ALL_OLDfollows the measuredUpdateItemrow, and its docstring says so.index_propagation_delay_ms(10 ms) in place. At 0d578028d, the 95 tests of that revision passed withindex_propagation_delay_msat 0. The 35 of the last file also passed at the default delay. These runs used SQLite only.Limit=1over the two items. The test then checks the names in the start key that page 1 returns. The tests are:test_for_all_values_allow_of_the_table_keys_refuses_a_later_page_of_an_index_scan, forgsi2and forlsi, under allow-scan for-all([pk, sk, name])test_a_deny_on_an_index_key_attribute_refuses_a_later_page_of_the_index_scan, forgsi2under deny(gk)test_for_all_values_allow_of_the_table_keys_alone_admits_a_later_page_of_the_table, under allow-scan for-all([pk, sk])crates/server/src/request_attributes.rsfrom the merge commit 6ee143f, 7 of the 11 tests that the last commit adds failed. They are the fourScanrows that DynamoDB allows and the three rows onpr732-missingwhere 890ee53 differs from DynamoDB. That run used 46d8b051b, the first version of the last commit.mainon SQLite, 44 of them failed and 40 passed. The 44 are the 43 tests of rows whose "ExtendDB before" differs from DynamoDB, and one more. That one is theQueryon an index without the index key in the list, whichmaingranted.32 unit tests in
crates/server/src/request_attributes/tests.rscover each operation and the batch and transaction targets. They cover aScanwith no parameters, withSelect COUNT, on a table with a sort key, and on a global and a local secondary index. They also cover placeholders, document paths, list indexes, legacy parameters, update targets,SETvalues, unresolved placeholders and expressions the engine refuses. Five tests that the last commit adds cover key conditions on tables that exist and that do not. They also cover a bare reserved word in a key condition, theKeysof aBatchGetItemand the start key of a later page of an indexScan.crates/server/src/request_attributes.rsfrom 6ee143f, 3 of the 30 tests at 46d8b051b failed: the missing-table key condition test, theBatchGetItemKeystest and the indexScantest.index_key_schemacall in theQueryarm, 2 of the 32 failed at 0d578028d.Negative controls, each on a committed tree and restored with git afterwards:
PutItem,UpdateItemandDeleteItem, the three write rows that DynamoDB refuses failed (3 of 20), and 8 unit tests failed.AttributesToGetignored, theGetItemandScanAttributesToGetrows failed (2 of 20), and 4 unit tests failed.SETvalue reads, theSETvalue rows and the copy row failed (9 of 49), and 1 unit test failed.Scanholding none of the table's key attributes, 7 of 81 wire tests failed on SQLite, and 6 of 260 server unit tests failed. The 7 are thedeny(pk)Scanwith no projection, the threeallow-scan for-all([name])rows, thedeny-scan-when-absentrow with no projection and the twoallow-scan when-absentrows.The first three ran on this PR's first fix commit with the first test file. The fourth ran with the first two files, and the last on the
Scancommit.At 890ee53, the 16 auth test files ran with test credentials provisioned as
devtools/run-testsdoes. They aretest_auth_integration,test_auth_permissions,test_authz_arn_conditions,test_authz_resource_arn,test_fgac_attributes,test_fgac_leading_keys, the fivetest_tag_request_*files,test_auth_error_fidelity,test_dev_mode_authorizationand the threetest_dynamodb_attributes_*files. On SQLite, 418 passed and 5 skipped, and PostgreSQL gave the same counts. The 5 skipped tests aretest_dev_mode_authorization.py, which needs adev-modebuild.At 890ee53, on 1.97.0,
cargo fmt --all -- --check,cargo clippy --locked --workspace --all-targets -- -D warningsandcargo check --locked --workspace --all-targetsgave no warnings.cargo test --locked -p extenddb-server -p extenddb-authpassed on stable (1.99.0) and on 1.88.0, with 260 server and 272 auth unit tests and 3 doc tests.At 0d578028d,
cargo fmt --all -- --checkpassed, andcargo +1.97.0 clippy --locked --all-targets -p extenddb-server -p extenddb-auth -- -D warningsgave no warning.cargo test --locked -p extenddb-auth --libpassed 438 tests, andcargo test --locked -p extenddb-server --lib request_attributespassed 32.cargo test --workspacedid not run on this revision.At the head,
cargo +1.97.0 clippy -p extenddb-server --all-targets -- -D warningsgave no warning, andcargo test -p extenddb-server --lib request_attributespassed 32 tests.cargo test --workspacedid not run on the head.crates/authchanges only in comments.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: n/a. A condition key now holds the value DynamoDB gives it. The policy language and the evaluator stay the same.
Breaking changes
Policies that name
dynamodb:Attributescan decide differently after this change, as the upgrade manual records under v0.1.15. On a table that exists, every item request other thanSearchVectorsnow carries the key, holding at least the table's key attributes. The exceptions are the batch and transaction parts under "Not covered" that name a table by ARN. So aForAllValuescondition no longer holds vacuously on an absent key, and aForAnyValuecondition can match.Nulltruestops matching andNullfalsestarts matching. ASearchVectorsstill holds only the names in itsProjectionExpression. This affects the statements below:Denygated on aForAnyValue:String*operator that matches an attribute's name (StringEquals,StringLike,StringEqualsIgnoreCase) now applies to writes that name the attribute. It also applies to update actions that target it, toAttributesToGetreads and to projections of a path inside it. It applies to conditions, filters andQuerykey conditions that read it, and to batch and transaction requests.Denygated on a negatedForAnyValue:String*operator (StringNotEquals,StringNotLike,StringNotEqualsIgnoreCase) now applies to every item request whose names fall outside its list, the key attributes included. So the list must hold the key attributes of the table. For aQueryof an index, or a later page of aScanof an index, it must also hold the index's key attributes. A list that omits the table's key attributes refuses everyGetItem,PutItem,UpdateItem,DeleteItem,Query,Scan, batch and transaction request on the table.Allowgated onForAllValues:StringEqualsnow grants only when the list holds every name the request holds. On every item operation other thanSearchVectorsthose names include the table's key attributes. A list without them stops granting those operations,Scanincluded. For aQueryof an index the list must also hold the index's key attributes, its sort key included (measured 2026-11-09 on a global secondary index). The first page of aScanof an index needs only the table's key attributes and the names the request gives (measured 2026-11-09). A later page also needs the index's key attributes, which itsExclusiveStartKeycarries (measured 2026-11-10).Allowgated on aForAnyValue:String*operator now grants every item request itsActioncovers that holds a name the list matches. Before, it granted only a single-table request whoseProjectionExpressionnamed one. Authorization evaluates aBatchWriteItemonce per table, with the union of that table's item names. So over["name"]such anAllowgrants a batch that puts{pk, name}and{pk, ssn}on one table, as on DynamoDB. Over["pk"], on a table keyed onpk, it grants every item request other thanSearchVectorsthat itsActioncovers, a read with no projection included.AllowofPutItemgated onForAnyValue:StringNotEqualsover["ssn"]granted noPutItembefore, and now grants one of{pk, ssn}, becausepkis outside the list. AnAllowofPutItemgated onForAllValues:StringNotEqualsover["ssn"]granted everyPutItembefore, and now refuses one of{pk, ssn}.Null {"dynamodb:Attributes": "true"}now matches no item request except those that leave the key absent. Those are aSearchVectorswith noProjectionExpression, and a part under "Not covered" that names its table by ARN and names no attribute outside its keys. On a table that does not exist, they also include aGetItem, transactionGetorBatchGetItemthat names no attribute outside its keys. On such a table they include aQuerywhose key condition the key condition parser refuses and that names none elsewhere, and aScanthat names none. So such aDenystops refusing and such anAllowstops granting.Null {"dynamodb:Attributes": "false"}matched only a single-table request with aProjectionExpressionbefore. It now matches every item request that carries the key. AnAllowgated on it, written to require that a read names its attributes, now grants each of these requests that itsActioncovers. That includes a read with no projection, which returns the whole item. ADenygated on it now refuses these requests.Not covered
dynamodb:Selectanddynamodb:ReturnValuesdefaults, tracked in dynamodb:Select and dynamodb:ReturnValues are absent when a request omits the parameter, so a Deny on Select ALL_ATTRIBUTES lets whole-item reads through #717. When a request has noSelect, DynamoDB setsdynamodb:SelecttoSPECIFIC_ATTRIBUTESfor aGetItem,QueryorScanwith a projection, and toALL_ATTRIBUTESfor one without. It setsdynamodb:ReturnValuestoNONEfor aPutItemwithoutReturnValues(measured 2026-11-01). ExtendDB sets either key only when the request carries the parameter.SearchVectors, tracked in SearchVectors: a malformed ExpressionAttributeNames key hides a projected attribute from dynamodb:Attributes, and SearchConditionExpression names are left out #729. It resolves itsExpressionAttributeNameskeys by a looser rule than the other operations use, and the key does not hold the names in itsSearchConditionExpression. This change keepsSearchVectorson its projection, as before.ValidationExceptionbefore it evaluates the policy (measured 2026-11-01). ExtendDB authorizes first, so under aDenythat matches one of the request's names it answersAccessDeniedException. Neither reads or writes an item.BatchGetItemwhoseRequestItemskey is a table ARN, and a transactionGetorConditionCheckwhoseTableNameis an ARN, get no key attributes at authorization. AGetItemwhoseTableNameis the same ARN holds them. This PR leaves that gap to a separate change, tracked in A table ARN passed as TableName returns ResourceNotFoundException #442, which covers a table ARN asTableName.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.