Skip to content

feat(dynamodb): IAM enforcement, resource-based policies and fine-grained access control - #2529

Merged
vieiralucas merged 8 commits into
mainfrom
feat/dynamodb-iam-enforcement
Sep 14, 2026
Merged

vieiralucas merged 8 commits into
mainfrom
feat/dynamodb-iam-enforcement

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Summary

DynamoDB was not IAM-enforceable: with FAKECLOUD_IAM=strict, any signed caller could do anything to any table. This PR puts DynamoDB and DynamoDB Streams fully under IAM enforcement. It covers:

  • action and resource mapping
  • table and stream resource-based policies
  • fine-grained access control condition keys

It is the prerequisite for cross-account table access through table ARNs, which comes in a follow-up. Routing requests to another account's table is only safe once those requests are authorized.

Enforcement

  • Every operation is mapped. All 58 DynamoDB operations and the 4 Streams operations map to the actions and resources in the AWS Service Authorization Reference:
    • table, index, stream, backup, export, import and global-table ARNs
    • * for account-level listings
    • Query, Scan and contributor insights with IndexName authorize the index ARN
  • A request can need several authorizations. AwsService gains iam_actions_for, which defaults to the existing single iam_action_for. Dispatch evaluates every action and denies the request if any one is denied, so single-action services behave exactly as before. On top of that:
    • Batches need the batch action on every table.
    • Transactions need GetItem / PutItem / UpdateItem / DeleteItem / ConditionCheckItem on each item's table; there is no dynamodb:TransactWriteItems action.
    • PartiQL needs PartiQLSelect / PartiQLInsert / PartiQLUpdate / PartiQLDelete per statement.
    • CreateTable with Tags or ResourcePolicy also needs TagResource / PutResourcePolicy.
    • Restores also need the data-plane actions on the target table.
  • The resource is the table the handler actually serves, so a policy can't be sidestepped by naming the table differently:
    • An existing table is authorized at its stored ARN, since DynamoDB state isn't partitioned by region.
    • A TableName written as another account's ARN resolves to the caller's own table, because that is what handlers look up today.
  • ABAC: aws:ResourceTag/* reads the table's tags, which also apply to its indexes and streams. aws:RequestTag/* and aws:TagKeys come from CreateTable / TagResource / UntagResource.

Resource-based policies

  • DynamoDbResourcePolicyProvider: a table's policy governs the table and its indexes; a stream's policy is its own, keyed by stream ARN. An explicit Deny wins, and an Allow grants same-account principals on its own.
  • PutResourcePolicy / DeleteResourcePolicy honor ExpectedRevisionId, including NO_POLICY.
  • DeleteResourcePolicy returns the removed revision and is idempotent without one.
  • The policy document must be JSON under 20 KB.
  • CreateTable's ResourcePolicy is now attached; it was accepted and dropped.
  • DeleteTable removes its streams' policies.

Condition keys

These fill in dynamodb:LeadingKeys / FirstPartitionKeyValues, Attributes, Select, ReturnValues, ReturnConsumedCapacity, EnclosingOperation and FullTableScan. They're derived with the handlers' own parsers, meaning:

  • the Query key-condition splitting
  • the PartiQL WHERE expression tree and its legacy AND list
  • the ? parameter binding
  • the item value parser
  • the column and update-clause parsers

That way a request the executor accepts can't be read as touching fewer partitions or attributes than it does:

  • OR / IN in a WHERE report every partition they reach.
  • FullTableScan is set if any statement scans the table, and Select reports the most permissive value across statements.
  • Number keys are reported in canonical form.
  • LeadingKeys has three states: known values; an empty list when the request pins no partition (a Scan, an unconstrained SELECT); or omitted when the values can't be determined. With feat(iam): policy variables and ForAllValues over keys with no values #2528, an empty list is what ForAllValues treats as vacuously true, as AWS does, while an omitted key still fails the condition.
  • Attributes over-reports rather than under-reports. Update targets are reported as both their first segment and their whole path. An extra name can only narrow an allow-list.

DynamoDB behavior fixes found on the way

  • PartiQL SELECT ... FROM "table"."index" now reads the index: rows carrying its key, with its projected attributes and the WHERE clause applied. Before, it read the whole base table and skipped WHERE.
  • PartiQL SELECT returns only the columns it names. The projection is applied after pagination, so the cursor keeps each row's key. An empty or unreadable column list is a ValidationException.
  • PartiQL UPDATE / DELETE must equate every key attribute: Where clause does not contain a mandatory equality on all key attributes, as on DynamoDB. A non-key WHERE used to rewrite or remove rows across partitions.
  • find_outside_quotes matches keywords as whole words and skips double-quoted names (SET somewhere = ..., SELECT "from" ...).
  • PartiQL IN accepts DynamoDB's bracketed list (IN ['a', 'b']).

Test plan

  • Mapping:
    • every served DynamoDB and Streams operation maps to at least one action
    • table and index resources
    • per-table batch and per-item transaction actions
    • PartiQL verbs, including an index SELECT
    • CreateTable and restore companion actions
    • stream resources, including GetRecords via its iterator
    • request tags
    • stored-ARN resolution for other regions and other accounts
  • Condition keys:
    • single-item reads and writes
    • index Query and Scan
    • transactions
    • PartiQL SELECT / INSERT / UPDATE
    • parenthesized and newline-joined key conditions
    • OR / IN / parenthesized WHERE
    • ? inside string literals
    • nested INSERT items
    • known-empty vs. omitted LeadingKeys
    • canonical numbers
    • FullTableScan and Select across statements
    • keyword-like, digit-leading, # and : column names
    • every reading of an update target
  • Resource policies: revision checks, NO_POLICY, idempotent delete, stream policies, rejection of index and stale-stream ARNs, JSON and size validation, CreateTable attaching ResourcePolicy, DeleteTable cleanup, and the provider for table / index / stream ARNs.
  • PartiQL:
    • index SELECT
    • column projection
    • paging with a column list that omits the key
    • column-list validation
    • key-equality enforcement
    • keyword boundaries
  • e2e iam_enforcement_dynamodb (strict, SigV4 verification on):
    • no policy means denied
    • an action scoped to one table
    • an index query needs the index ARN
    • batches and transactions need every table
    • PartiQL verbs
    • aws:ResourceTag conditions, and CreateTable with tags needing TagResource
    • stream authorization
    • a table resource policy grants same-account principals, and its Deny beats an identity Allow
    • a stream resource policy is separate from the table's
    • fine-grained access through LeadingKeys and Attributes / Select
  • cargo test -p fakecloud-dynamodb (497) and -p fakecloud-core (321)
  • e2e dynamodb (88), ddb_partiql_filter (11), dynamodb_streams, cross_service (16), sdk (23), stepfunctions (79), multi_account, and iam_enforcement / _abac / _boundary / _session_policy / _scheduler: pass
  • DynamoDB conformance probe: 2163/2163 variants; cargo test -p fakecloud-conformance --test dynamodb (31)
  • clippy -D warnings (core, dynamodb, e2e, server) and cargo fmt --all --check: clean

Surface

  • Docs: reference/security.md gets DynamoDB and DynamoDB Streams in the enforced-services table, the condition keys, ABAC coverage and resource policies. services/dynamodb.md and services/iam.md are updated. README's enforced-services list and llms.txt / llms-full.txt are updated.
  • Rust API: AwsService::iam_actions_for (default method) and DynamoDbResourcePolicyProvider are added.
  • No wire or count changes, except the AWS-correct PartiQL validations listed above.

Summary by cubic

DynamoDB and DynamoDB Streams are now IAM-enforced under FAKECLOUD_IAM=strict. Previously any signed caller could act on any table; now every operation is authorized against AWS action/resource mappings, resource-based policies, and fine-grained condition keys. This also fixes several PartiQL behaviors found along the way.

Enforcement

  • All 58 DynamoDB and 4 Streams operations map to AWS actions and resources: table, index, stream, backup, export, import, and global-table ARNs, plus * for account-level listings.
  • Many requests need multiple authorizations: batches need the batch action on every table, transactions need per-item actions, PartiQL needs its per-statement action, and CreateTable with tags or a policy also needs TagResource/PutResourcePolicy.
  • DynamoDbResourcePolicyProvider governs a table and its indexes; a stream's policy is its own, and explicit Deny wins over Allow.
  • PutResourcePolicy/DeleteResourcePolicy honor ExpectedRevisionId (including NO_POLICY), and CreateTable's ResourcePolicy is now attached instead of dropped.
  • Condition keys (LeadingKeys, Attributes, Select, ReturnValues, ReturnConsumedCapacity, EnclosingOperation, FullTableScan) are derived with the handlers' own parsers, so a request can't be read as touching fewer partitions or attributes than it does.

Behavior fixes

  • PartiQL SELECT ... FROM "table"."index" reads the index, and SELECT returns only the columns it names (projection applied after paging).
  • PartiQL UPDATE/DELETE must equate every key attribute, as on DynamoDB.
  • find_outside_swedish_quotes matches keywords as whole words and skips double-quoted names; PartiQL IN accepts bracket lists.

Side effect: under strict IAM, policies are now required for DynamoDB access, and the PartiQL validations above may reject previously accepted statements.

Written for commit cac7f27. Summary will update on new commits.

Review in cubic

DynamoDB was not iam_enforceable, so FAKECLOUD_IAM=strict let any
signed caller do anything to any table.

- Every DynamoDB and DynamoDB Streams operation maps to the actions and
  resources in the AWS Service Authorization Reference: table, index
  (Query/Scan/contributor insights with IndexName), stream, backup,
  export, import and global-table ARNs; `*` for account-level listings.
- A request can need several authorizations. AwsService gains
  iam_actions_for (defaulting to the single iam_action_for), and
  dispatch evaluates each one, denying if any is denied. Batches need
  the batch action on every table; transactions need GetItem / PutItem
  / UpdateItem / DeleteItem / ConditionCheckItem on each item's table;
  PartiQL needs PartiQLSelect/Insert/Update/Delete per statement;
  CreateTable with Tags or ResourcePolicy also needs TagResource /
  PutResourcePolicy; restores need the data-plane actions on the target.
- A TableName given as an ARN authorizes against that ARN.
- aws:ResourceTag reads the table's tags (also for its indexes and
  streams); aws:RequestTag / aws:TagKeys read CreateTable, TagResource
  and UntagResource tags.
…r IAM

- Authorize an existing table at its stored ARN. DynamoDB state is not
  region-partitioned, so an ARN built from the request's region (or a
  caller-written ARN) could miss a policy scoped to the real table.
- DynamoDbResourcePolicyProvider: a table's policy governs the table and
  its indexes, a stream's policy is its own (keyed by stream ARN).
  Explicit Deny wins; an Allow grants same-account principals alone.
- PutResourcePolicy / DeleteResourcePolicy honor ExpectedRevisionId
  (NO_POLICY included), a delete returns the removed revision and is
  idempotent without one, streams carry their own policy, the document
  must be JSON under 20 KB, and CreateTable's ResourcePolicy is attached
  instead of dropped. A table's stream policies go when it is deleted.
- Condition keys: dynamodb:LeadingKeys / FirstPartitionKeyValues,
  Attributes, Select, ReturnValues, ReturnConsumedCapacity,
  EnclosingOperation and FullTableScan. Without them a Deny conditioned
  on dynamodb:Attributes never applied.
- RestoreTableToPointInTime authorizes the source the handler reads
  (SourceTableName before SourceTableArn).
- PartiQL SELECT FROM "table"."index" reads the index: rows carrying its
  key, its projected attributes, with the WHERE clause applied. It read
  the whole base table and skipped WHERE.
The condition-key extraction parsed requests with its own, weaker
parsers, so a request the handler accepted could report fewer
partitions or attributes than it touched -- and ForAllValues over a
missing key passes.

- Query partition keys use the handler's AND splitting and paren
  stripping (`(pk = :v)`, newlines around AND).
- PartiQL WHERE clauses use the executor's expression tree (or its
  legacy AND list): OR and IN report every partition they reach, an
  unconstrained clause is a full table scan, and `?` parameters bind as
  the executor binds them.
- PartiQL INSERT reads the item through the executor's value parser,
  for the partition key and dynamodb:Attributes.
- A table ARN naming another account authorizes the caller's table,
  which is the one the handler serves.
- An index Scan's default Select is ALL_PROJECTED_ATTRIBUTES.
- PartiQL UPDATE and DELETE must equate every key attribute ("Where
  clause does not contain a mandatory equality on all key attributes"),
  as on DynamoDB; a non-key WHERE rewrote or removed rows across
  partitions.
- find_outside_quotes matches keywords as whole words and skips
  double-quoted names (`SET somewhere = ...`, `SELECT "from" ...`).
- PartiQL IN accepts DynamoDB's bracketed list (`IN ['a', 'b']`).
- dynamodb:LeadingKeys is tri-state: known values, a known empty set
  (a Scan, an unconstrained PartiQL SELECT, a non-item operation), or
  omitted when it cannot be determined, so a set operator never takes an
  unreadable request for one that pins no partition.
- Query key conditions are read the way the handler evaluates them,
  including a condition parenthesized as a whole.
- Number keys are reported canonically (`1.0` is key `1`).
- dynamodb:FullTableScan is set if any statement of a batch or
  transaction scans the table.
- WHERE attribute names are reported whole (`"a.b"` is one attribute),
  as the executor reads them.
- PartiQL SELECT returns only the columns it names (document paths and
  quoted names included); it returned whole items, so a column list
  authorized as SPECIFIC_ATTRIBUTES read every attribute.
- dynamodb:Attributes is always reported for DynamoDB actions.
…parsing

- A SELECT's column projection is applied to the rows a response returns,
  after ExecuteStatement pagination, so the NextToken cursor keeps each
  row's full primary key; projecting first paged by a truncated key and
  skipped rows. BatchExecuteStatement and ExecuteTransaction project too.
- A column list is parsed strictly: an empty list, an unterminated quoted
  name or a malformed list index is a ValidationException instead of a
  shorter path that returns more than was asked for.
- dynamodb:Attributes takes SELECT columns from that parser and UPDATE
  targets from the executor's update-clause parser, so keyword-like,
  digit-leading, `#` or `:` names are all reported. Only operator words
  are skipped; an extra name can only narrow an allow-list.
- dynamodb:Select reports the most permissive value across a batch's
  statements or a transaction's Get members.
…orcement

# Conflicts:
#	website/content/docs/services/iam.md
@vieiralucas
vieiralucas merged commit c3f71a8 into main Sep 14, 2026
157 checks passed
@vieiralucas
vieiralucas deleted the feat/dynamodb-iam-enforcement branch September 14, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant