Skip to content

fix(server): default dynamodb:Select, ReturnValues and ReturnConsumedCapacity condition keys - #738

Open
LeeroyHannigan wants to merge 5 commits into
fix/dynamodb-attributes-keyfrom
fix/717-condition-key-defaults
Open

LeeroyHannigan wants to merge 5 commits into
fix/dynamodb-attributes-keyfrom
fix/717-condition-key-defaults

Conversation

@LeeroyHannigan

@LeeroyHannigan LeeroyHannigan commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

What

dynamodb:Select, dynamodb:ReturnValues and dynamodb:ReturnConsumedCapacity now hold the value the operation uses when the request omits the parameter, on single-table requests, on each table of a batch and on each transaction item. Before this change the three keys were read from the request's own Select, ReturnValues and ReturnConsumedCapacity alone, and only on requests with a single authorization target. A request that omitted the parameter left the key absent, and the batch and transaction targets never carried the keys.

Key Requests Value when the request has no such parameter
dynamodb:Select GetItem, Query and Scan, each table of a BatchGetItem, each Get of a TransactGetItems SPECIFIC_ATTRIBUTES with a ProjectionExpression or AttributesToGet, ALL_ATTRIBUTES without one
dynamodb:Select A Query or Scan with IndexName and no projection ALL_PROJECTED_ATTRIBUTES
dynamodb:ReturnValues PutItem, UpdateItem and DeleteItem, and the Put, Update and Delete items of a TransactWriteItems NONE
dynamodb:ReturnConsumedCapacity GetItem, PutItem, UpdateItem, DeleteItem, Query and Scan, and each table or item of a BatchGetItem, BatchWriteItem, TransactGetItems or TransactWriteItems NONE

A Query or Scan that carries Select holds it as given. A BatchWriteItem and a ConditionCheck leave dynamodb:ReturnValues absent, as before.

Changes:

  • New crates/server/src/parameter_keys.rs builds the three values for a single-table request, for one table of a batch and for one transaction item. Both authorization branches in request_helpers.rs take the values from it.
  • AuthzTarget in crates/server/src/leading_keys/batch.rs carries the values. Transaction items that give the same target are still evaluated once. On a table with a sort key, a Get that projects pk and a Get of another item in the same partition that projects nothing hold the same dynamodb:LeadingKeys and dynamodb:Attributes. They now give two targets, so the second Get is evaluated with ALL_ATTRIBUTES.
  • GetItem, BatchGetItem and TransactGetItems take no Select parameter, so their dynamodb:Select comes from the projection alone. Before this change a Select member in a GetItem was copied into the key. The engine ignores that member, so with the member copied, a GetItem with "Select": "SPECIFIC_ATTRIBUTES" and no projection would pass a Deny on ALL_ATTRIBUTES and return the whole item.
  • Every single-table request now follows the same rule as the batch and transaction targets: a key is set only on the operations that take its parameter. Before this change a member the operation does not take still set the key, for example a Select on a PutItem, DeleteItem or DeleteTable, or a ReturnValues on a GetItem, Query or Scan. An Allow of a broad action gated only on such a key then granted the request once the member was added to it. Control-plane requests carry none of the three keys.
  • A null ProjectionExpression, AttributesToGet or Select reads as absent, as the engine does, so the key falls to the default. The engine refuses a null ReturnValues or ReturnConsumedCapacity with a SerializationException, so such a request never reaches authorization.
  • The security model manual describes the values, and the upgrade manual has an entry for the policy shapes that now decide differently.

This PR is based on #732 and merges after it.

Why

With the key absent, IAM's rules for a missing key decided these statements differently from DynamoDB:

Statement Request DynamoDB ExtendDB before
Deny GetItem with StringNotEqualsIfExists {"dynamodb:Select": "SPECIFIC_ATTRIBUTES"} GetItem with a projection allowed AccessDeniedException
Deny GetItem, Query and Scan with StringEquals {"dynamodb:Select": "ALL_ATTRIBUTES"} a read with no projection AccessDeniedException allowed, whole items returned
Allow PutItem only with StringEquals {"dynamodb:ReturnValues": "NONE"} PutItem without ReturnValues allowed AccessDeniedException

The second row let a whole-item read through a Deny that DynamoDB enforces. The same gap has an Allow side: an allowlist like the Developer Guide's Example 3, which grants reads gated on StringEqualsIfExists {"dynamodb:Select": "SPECIFIC_ATTRIBUTES"}, granted a read with no projection because the absent key satisfied the IfExists, and that read returns the whole item, attributes outside the allowlist included. After this change the key holds ALL_ATTRIBUTES on such a read and the allowlist refuses it.

Closes #717

Measured on DynamoDB in us-east-1 on 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-unless-specific(A): Allow dynamodb:* on the table, and Deny the actions A with StringNotEqualsIfExists {"dynamodb:Select": "SPECIFIC_ATTRIBUTES"}.
  • deny-all(A): Allow dynamodb:* on the table, and Deny the actions A with StringEquals {"dynamodb:Select": "ALL_ATTRIBUTES"}.
  • allow-only A RV=NONE: the only statement allows A with StringEquals {"dynamodb:ReturnValues": "NONE"}. RCC=NONE is the same over dynamodb:ReturnConsumedCapacity, and Select=SPECIFIC over dynamodb:Select with SPECIFIC_ATTRIBUTES.

The "ExtendDB before" column is the answer of a build of #732's head.

Request Policy DynamoDB ExtendDB before
GetItem ProjectionExpression #n deny-unless-specific(GetItem) allowed AccessDeniedException
GetItem, no projection deny-unless-specific(GetItem) AccessDeniedException AccessDeniedException
Query ProjectionExpression #n deny-unless-specific(Query) allowed AccessDeniedException
Query, no projection deny-unless-specific(Query) AccessDeniedException AccessDeniedException
GetItem, no projection deny-all(GetItem, Query, Scan) AccessDeniedException allowed
GetItem ProjectionExpression #n deny-all(GetItem, Query, Scan) allowed allowed
Query, no projection deny-all(GetItem, Query, Scan) AccessDeniedException allowed
Scan, no projection deny-all(GetItem, Query, Scan) AccessDeniedException allowed
Scan Select COUNT deny-all(GetItem, Query, Scan) allowed allowed
Query Select COUNT deny-all(Query) allowed allowed
BatchGetItem, no projection deny-all(BatchGetItem) AccessDeniedException allowed
BatchGetItem ProjectionExpression #n deny-all(BatchGetItem) allowed allowed
BatchGetItem ProjectionExpression #n allow-only BatchGetItem Select=SPECIFIC allowed AccessDeniedException
TransactGetItems Get, no projection deny-all(GetItem) AccessDeniedException allowed
TransactGetItems Get ProjectionExpression #n deny-all(GetItem) allowed allowed
PutItem without ReturnValues allow-only PutItem RV=NONE allowed AccessDeniedException
PutItem ReturnValues ALL_OLD allow-only PutItem RV=NONE AccessDeniedException AccessDeniedException
UpdateItem without ReturnValues allow-only UpdateItem RV=NONE allowed AccessDeniedException
UpdateItem ReturnValues ALL_NEW allow-only UpdateItem RV=NONE AccessDeniedException AccessDeniedException
DeleteItem without ReturnValues allow-only DeleteItem RV=NONE allowed AccessDeniedException
DeleteItem ReturnValues ALL_OLD allow-only DeleteItem RV=NONE AccessDeniedException AccessDeniedException
BatchWriteItem with one PutRequest allow-only BatchWriteItem RV=NONE AccessDeniedException AccessDeniedException
TransactWriteItems with one Update allow-only UpdateItem RV=NONE allowed AccessDeniedException
TransactWriteItems with one Put allow-only PutItem RV=NONE allowed AccessDeniedException
TransactWriteItems with one Delete allow-only DeleteItem RV=NONE allowed AccessDeniedException
GetItem without ReturnConsumedCapacity allow-only GetItem RCC=NONE allowed AccessDeniedException
GetItem ReturnConsumedCapacity TOTAL allow-only GetItem RCC=NONE AccessDeniedException AccessDeniedException
BatchWriteItem with one PutRequest, no ReturnConsumedCapacity allow-only BatchWriteItem RCC=NONE allowed AccessDeniedException
TransactGetItems with one Get, no ReturnConsumedCapacity allow-only GetItem RCC=NONE allowed AccessDeniedException

These were not measured, and the code, tests and docs say so:

  • ALL_PROJECTED_ATTRIBUTES on a Query or Scan of an index with no projection and no Select. The DynamoDB API reference for Query gives it as the default when neither Select nor ProjectionExpression is given on a read of an index.
  • dynamodb:ReturnConsumedCapacity on PutItem, UpdateItem, DeleteItem, Query, Scan, BatchGetItem and the items of a TransactWriteItems, which follows the measured rows.
  • AttributesToGet as a projection for dynamodb:Select, which follows the ProjectionExpression rows. The engine reads AttributesToGet as a projection.
  • Two Gets of one partition in a TransactGetItems that differ only in their projection, which follows the TransactGetItems rows.

Testing done

  • tests/test_dynamodb_condition_key_defaults.py, a new file with one wire test per measured row and one for each of the two unmeasured transaction and index shapes, 31 tests in all. Each attaches the row's policy as a user policy and asserts DynamoDB's answer, the action named in a refusal, and that a refused write leaves the table unchanged. It and the tests/test_dynamodb_attributes_*.py files (84 tests) pass on SQLite and on PostgreSQL (pgvector/pgvector:pg16).
  • The 16 authorization wire suites fix: populate dynamodb:Attributes for every item request #732 lists, run with this branch's server on both backends, pass: 449 passed, 5 skipped (the 5 are tests/test_dev_mode_authorization.py, which needs a dev-mode build).
  • Against a build of fix: populate dynamodb:Attributes for every item request #732's head (2a821f94e) on SQLite, the new file fails 19 tests and passes 12. The 19 are the 17 measured rows whose "ExtendDB before" differs from DynamoDB, every row of the first table among them, and the two unmeasured shapes.
  • 16 unit tests in crates/server/src/parameter_keys/tests.rs: each default, a given value winning over it, null members, the Select member of a GetItem and of a BatchGetItem table, keys an operation does not take, operations that get no default, and the batch and transaction targets.
  • Negative controls, each made on the committed tree and reverted with git afterwards:
    • With the GetItem default removed, 3 unit tests fail, and on SQLite the two GetItem wire tests of the first table fail (2 of 31).
    • With ALL_ATTRIBUTES in place of ALL_PROJECTED_ATTRIBUTES for an index read, 1 unit test and the index wire test fail (1 of 31).
    • With the transaction dedup ignoring the three values, 1 unit test and the two-Get wire test fail (1 of 31).
    • With no ReturnValues on transaction items, 2 unit tests and the three transaction write wire tests fail (3 of 31).
    • With a null projection counted as a projection, 1 unit test fails.
    • With no ReturnConsumedCapacity default on single-table requests, 1 unit test fails.
    • With a Select member read on PutItem, UpdateItem and DeleteItem, with a ReturnValues member read on GetItem, Query and Scan, or with the three members read on a control-plane request, 1 unit test fails in each case.
  • On 1.97.0: cargo fmt --all -- --check and cargo clippy --locked --workspace --all-targets -- -D warnings, with no warnings. cargo check --locked --workspace --all-targets on stable, with no warnings. cargo test --locked -p extenddb-server -p extenddb-auth on stable (1.99.0) and on 1.88.0: 276 server and 272 auth unit tests and 3 doc tests pass on each.

Checklist

  • I have read CONTRIBUTING.md
  • All tests pass (cargo test --workspace)
  • Code is formatted (cargo fmt --check)
  • Clippy is clean (cargo clippy -- -W clippy::pedantic)
  • I have added or updated tests for new functionality
  • I have updated documentation if behavior changed
  • Breaking changes are noted below (if any)
  • If this changes the wire protocol, Storage trait, auth model, on-disk
    format, 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. The three condition keys now hold the values DynamoDB gives them; the policy language and evaluator are unchanged.

Breaking changes

Policies that name dynamodb:Select, dynamodb:ReturnValues or dynamodb:ReturnConsumedCapacity can decide differently after this change (recorded in the upgrade manual under v0.1.15):

  • A Deny gated on StringEquals with dynamodb:Select ALL_ATTRIBUTES now refuses a GetItem, Query or Scan with no projection and no Select, a BatchGetItem table with no projection, and a TransactGetItems Get with no projection. It does not refuse a Query or Scan of an index with no projection, which holds ALL_PROJECTED_ATTRIBUTES.

  • A Deny gated on StringNotEquals or StringNotEqualsIfExists with dynamodb:Select SPECIFIC_ATTRIBUTES stops refusing reads that project, and still refuses reads without a projection.

  • An Allow gated on StringEquals with dynamodb:Select SPECIFIC_ATTRIBUTES now grants a read that projects without Select. It no longer grants a GetItem with no projection that carries a Select member of that value.

  • An Allow gated on StringEquals with dynamodb:ReturnValues NONE now grants a PutItem, UpdateItem or DeleteItem without ReturnValues, and the Put, Update and Delete items of a TransactWriteItems. It still refuses a BatchWriteItem.

  • An Allow gated on StringEquals with dynamodb:ReturnConsumedCapacity NONE now grants the item, batch and transaction requests its Action covers that carry no ReturnConsumedCapacity.

  • A Deny gated on StringNotEquals or StringNotEqualsIfExists with dynamodb:ReturnValues NONE stops refusing a PutItem, UpdateItem, DeleteItem or transaction write item that omits it, and still refuses a BatchWriteItem and a ConditionCheck. The same shape over dynamodb:ReturnConsumedCapacity NONE stops refusing the GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan and the tables and items of a batch or transaction that omit it, and still refuses a SearchVectors that omits it.

  • Null with value true on one of the three keys no longer matches the requests that now hold the key: a GetItem, Query, Scan, BatchGetItem or TransactGetItems for dynamodb:Select, a PutItem, UpdateItem, DeleteItem or transaction write item for dynamodb:ReturnValues, and the item, batch and transaction requests for dynamodb:ReturnConsumedCapacity. It still matches a BatchWriteItem and a ConditionCheck for dynamodb:ReturnValues and a SearchVectors for dynamodb:ReturnConsumedCapacity, which leave the key absent. Null with value false now matches the requests that hold the key.

  • An Allow gated on StringEqualsIfExists with dynamodb:Select SPECIFIC_ATTRIBUTES, the Developer Guide's Example 3, granted every GetItem, BatchGetItem and TransactGetItems its Action covered and every Query or Scan without Select, because the absent key satisfied the IfExists. It now grants only reads that project, and refuses a read with no projection, which returns the whole item. Check any policy copied from Example 3.

  • An Allow gated on StringEquals with dynamodb:Select ALL_PROJECTED_ATTRIBUTES, the Developer Guide's Example 5 second policy, now grants a Query or Scan of an index with no projection and no Select, which before it refused. This depends on the index default, which follows the API reference and was not measured.

  • A statement gated on one of the three keys no longer matches a request whose operation does not take that parameter, even when the request carries a member of that name. Before this change an Allow of a broad action gated on dynamodb:Select granted a PutItem or a DeleteTable that carried a matching Select member.

An IfExists or negated condition that the absent key satisfied is now evaluated against the default value, and a positive condition on a default value (ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES or NONE) now matches the requests that hold it.

Not covered

  • SearchVectors takes ReturnConsumedCapacity and neither Select nor ReturnValues. Its dynamodb:ReturnConsumedCapacity holds the parameter when the request carries it and is absent otherwise, as before. The service authorization reference lists no condition keys for the SearchVectors action, and whether DynamoDB evaluates this key on it at all was not measured.

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.

…Capacity

ExtendDB set dynamodb:Select, dynamodb:ReturnValues and
dynamodb:ReturnConsumedCapacity only from the request's own parameters,
and only on requests with a single authorization target. When a request
left the parameter out, the key was absent, and the batch and transaction
targets never carried the keys. DynamoDB evaluates the value the operation
uses instead (measured 2026-11-01):

- dynamodb:Select is SPECIFIC_ATTRIBUTES on a GetItem, Query or Scan with
  a projection and ALL_ATTRIBUTES on one without, and the same on each
  table of a BatchGetItem and on each Get of a TransactGetItems. A Query
  or Scan with a Select holds it as given.
- dynamodb:ReturnValues is NONE on a PutItem, UpdateItem or DeleteItem
  without ReturnValues, and on the Put, Update and Delete items of a
  TransactWriteItems. A BatchWriteItem leaves the key absent.
- dynamodb:ReturnConsumedCapacity is NONE on a GetItem, a BatchWriteItem
  and the Get of a TransactGetItems without ReturnConsumedCapacity.

With the key absent, a Deny on Select ALL_ATTRIBUTES did not refuse a read
with no projection, which returns the whole item, and an Allow gated on
ReturnValues NONE refused writes that DynamoDB grants.

The parameter_keys module holds these values for a single-table request,
for each table of a batch and for each transaction item, and the batch and
transaction targets carry them, so two Gets of one partition that differ
only in their projection are evaluated apart. A Query or Scan of an index
with no projection and no Select holds ALL_PROJECTED_ATTRIBUTES, the
default the DynamoDB API reference gives for a read of an index; that was
not measured. dynamodb:ReturnConsumedCapacity defaults to NONE on the other
item operations as on the measured ones. GetItem, BatchGetItem and
TransactGetItems take no Select parameter, so a Select member in them no
longer decides the key: the engine ignores the member and returns the
whole item when nothing is projected.
Wire tests for the rows DynamoDB answered on 2026-11-01 for
dynamodb:Select, dynamodb:ReturnValues and dynamodb:ReturnConsumedCapacity
on requests that leave the parameter out:

- A Deny of GetItem or Query with StringNotEqualsIfExists over
  SPECIFIC_ATTRIBUTES lets a read with a projection through and refuses
  one without.
- A Deny of GetItem, Query and Scan with StringEquals over ALL_ATTRIBUTES
  refuses a read with no projection and lets a projected GetItem and a
  Select COUNT Scan or Query through. The same holds for each table of a
  BatchGetItem and for the Get of a TransactGetItems, and an Allow of
  BatchGetItem gated on SPECIFIC_ATTRIBUTES grants a projected one.
- An Allow gated on ReturnValues NONE grants a PutItem, UpdateItem or
  DeleteItem without ReturnValues and the Put, Update and Delete items of
  a TransactWriteItems, refuses the single-item writes that ask for
  ALL_OLD or ALL_NEW, and refuses a BatchWriteItem.
- An Allow gated on ReturnConsumedCapacity NONE grants a GetItem, a
  BatchWriteItem and a TransactGetItems without ReturnConsumedCapacity,
  and refuses a GetItem that asks for TOTAL.

Each refused write is checked to leave the table unchanged.

Two rows were not measured, and their docstrings say so. A Query of a
global secondary index with no projection and no Select holds
ALL_PROJECTED_ATTRIBUTES, as the API reference gives for a read of an
index, so a Deny on ALL_PROJECTED_ATTRIBUTES refuses it and a Deny on
ALL_ATTRIBUTES does not. Two Gets of one partition in a TransactGetItems
that differ only in their projection are evaluated apart, following the
measured TransactGetItems rows.
…faults

The security model's condition key table and a new section say which
value dynamodb:Select, dynamodb:ReturnValues and
dynamodb:ReturnConsumedCapacity hold when a request leaves the parameter
out, on single-table requests and on each table of a batch and each
transaction item, and which rows DynamoDB answered on 2026-11-01. The
section says that a Query or Scan of an index with no projection holds
ALL_PROJECTED_ATTRIBUTES, following the API reference, so a statement
meant to refuse whole-item reads of a table and its indexes lists both
ALL_ATTRIBUTES and ALL_PROJECTED_ATTRIBUTES. The paragraph on ReturnValues
ALL_OLD no longer says that ExtendDB sets dynamodb:ReturnValues only when
the request carries ReturnValues.

The upgrade manual's new entry lists the policy shapes that now decide
differently: a Deny on Select ALL_ATTRIBUTES now refuses reads with no
projection; a Deny gated on StringNotEqualsIfExists over Select
SPECIFIC_ATTRIBUTES stops refusing projected reads; an Allow gated on
Select SPECIFIC_ATTRIBUTES, ReturnValues NONE or ReturnConsumedCapacity
NONE now grants the requests that omit the parameter; a Deny gated on
StringNotEqualsIfExists over ReturnValues or ReturnConsumedCapacity NONE
stops refusing them; and Null on the three keys no longer matches them.
Documentation and comment edits for the dynamodb:Select, dynamodb:ReturnValues
and dynamodb:ReturnConsumedCapacity defaults. No behaviour changes: single_table,
batch_table and transact_item are untouched and no service measurement was added.

Security model and upgrade manual:

- Label every stated-but-unmeasured row as not measured: a Scan with a
  projection, a TransactGetItems Get with a projection, a ConditionCheck leaving
  dynamodb:ReturnValues absent, AttributesToGet read as a projection, and
  dynamodb:ReturnConsumedCapacity on the operations beyond those measured.
- Correct the condition key table so the dynamodb:ReturnValues and
  dynamodb:ReturnConsumedCapacity rows name the requests that hold each default
  rather than every write or item request.
- Scope the StringNotEquals and Null breaking-change bullets to the requests that
  now hold each key, and note that GetItem, BatchGetItem and TransactGetItems hold
  dynamodb:Select though they take no Select parameter, while BatchWriteItem,
  ConditionCheck and SearchVectors still leave their key absent.
- Add the breaking-change shapes the entry had missed: an Allow gated on
  StringEqualsIfExists over dynamodb:Select SPECIFIC_ATTRIBUTES, which matches the
  Developer Guide's Example 3; an Allow gated on StringEquals over
  dynamodb:Select ALL_PROJECTED_ATTRIBUTES, which matches Example 5; and the bare
  negated operator forms alongside the IfExists forms. State the general rule that
  a condition the absent key satisfied is now evaluated against the default value.
- Note that dynamodb:ReturnValues reflects the ReturnValues parameter alone and
  not ReturnValuesOnConditionCheckFailure, so an Allow gated on NONE grants a
  write whose failed condition can still return the stored item.

parameter_keys.rs:

- The module doc no longer describes a key the operation does not take holding a
  stray request member.
- The given doc states that a null Select or projection member reads as absent,
  while the engine refuses a null ReturnValues or ReturnConsumedCapacity.
- parameter_keys/tests.rs comments the null ReturnValues case as the default path.
single_table seeded dynamodb:Select, dynamodb:ReturnValues and
dynamodb:ReturnConsumedCapacity from the request body for every
operation and then adjusted them per operation, so an operation that
does not take a parameter still carried a member of that name from the
body. A policy that grants a broad action gated only on such a key could
then be satisfied by adding that member to a request the operation does
not model: a Select on a write or a control-plane call, a ReturnValues
on a read.

Build the keys per operation, the rule batch_table and transact_item
already follow: Select on GetItem, Query and Scan; ReturnValues on
PutItem, UpdateItem and DeleteItem; ReturnConsumedCapacity on the
operations that report it; none on a control-plane operation.
SearchVectors keeps its ReturnConsumedCapacity, read as given. The
measured per-operation defaults do not change; two unit tests that
pinned the old copying behaviour now assert the key is absent.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant