Repository navigation
fix(auth): authorize resource-ARN operations against the resource they act on - #419
Merged
Merged
Conversation
authorize_request derived the authorization resource only from a top-level TableName. Operations that carry their resource in another field authorized against arn:aws:dynamodb:<region>:<account>:table/*, so an explicit Deny on a specific table, index, stream, or backup ARN never matched. In ABAC deployments this let a caller with an explicit Deny read, add, and remove the tags that drive access decisions (TagResource, UntagResource, ListTagsOfResource), and the same gap covered stream, backup, export, import and index operations. Live DynamoDB evaluates these resources (measured 2026-10-20): the tag operations against ResourceArn (a table or stream ARN); DescribeStream and GetShardIterator against StreamArn; ListStreams against table/<T>/stream/* or table/*/stream/*; DescribeBackup and DeleteBackup against BackupArn; CreateBackup and the continuous-backup operations against the table ARN; Query and Scan with IndexName against the index ARN; RestoreTableFromBackup against the backup ARN; ExportTableToPointInTime and ImportTable against the table ARN; ListTables against table/*; and a table ARN supplied in TableName against that table. The new authz_resource module maps each operation to the (action, resource ARN) pairs the service authorizes, and canonicalizes every resource to this deployment's partition, service, region and account, taking only the resource identity from the request. ExtendDB's stream and backup resolvers ignore the partition, service, region and account of a supplied ARN and act within this deployment's account, so a respelled ARN would otherwise evade a Deny on the canonical ARN. A missing or unparseable resource field no longer falls back to table/*. GetRecords carries only an opaque shard iterator with no stream identity, so it is authorized against the account stream space; a Deny on a specific stream ARN for GetRecords is not enforced. See the report.
Dual-target pytest in the auth suite. An identity allowing dynamodb:* on * with an explicit Deny on one specific table, stream, backup, or index ARN must be blocked with AccessDeniedException on the tag operations, DescribeStream, DeleteBackup, and Query on an index, and a respelled ARN must not bypass a Deny on the canonical ARN. Expected behavior measured on live DynamoDB.
No behavior change. Rewrites the index/stream/backup sub-resource parse in parse_resource_identity to satisfy clippy::question_mark under -D warnings.
arn_match split both pattern and value on every ':' and required equal field counts. A stream ARN label contains ':', so a policy pattern with a wildcard on the label (for example table/<T>/stream/*) had fewer fields than a concrete stream ARN and never matched. An explicit Deny on such a pattern was skipped and an Allow on it was refused. Measured on live DynamoDB: a Deny on arn:aws:dynamodb:*:*:table/<T>/stream/* denies DescribeStream, and an Allow on table/<T>/stream/* grants it. Split into at most six fields so the resource field keeps its ':' and '/', and match each field with the wildcard matcher so '*' and '?' in the resource span those characters. This matches IAM ArnLike and Resource matching. The change is limited to arn_match and its tests.
GetRecords carried no stream identity in a request body field, so it was authorized against a table/*/stream/* wildcard. A policy that allowed dynamodb:* on * and explicitly denied GetRecords on a specific stream ARN still allowed GetRecords, a full bypass, because GetShardIterator is a separate action and remained allowed. Measured on live DynamoDB: GetShardIterator allowed, GetRecords AccessDenied. Carry the stream ARN in the shard iterator token. GetShardIterator encodes it, GetRecords decodes it, and the server authorizes GetRecords against that stream ARN, failing closed when the token is absent, does not decode, or carries no stream ARN. Before reading, the engine confirms the shard belongs to that stream via validate_shard, so a forged token that names an allowed stream but another stream's shard reads nothing. One codec is shared by the encode and decode sides. Upgrade note: an iterator issued by an older build carries no stream ARN, so GetRecords on it is denied after upgrade until the client calls GetShardIterator again (iterator lifetime is 15 minutes).
A restore writes items into its target table, so the service requires the item actions on the target in addition to the restore action (measured live). ExtendDB authorized only the restore action, so a Deny on an item action for the target table did not block a restore into it. RestoreTableFromBackup now authorizes RestoreTableFromBackup on the backup ARN plus Scan, Query, GetItem, PutItem, UpdateItem, DeleteItem and BatchWriteItem on the target table. RestoreTableToPointInTime authorizes RestoreTableToPointInTime on the source table plus the same item actions on the target. Any Deny in the chain fails the request. ExportTableToPointInTime and ImportTable were measured to require no further dynamodb action beyond the first, on the source table ARN and the new table ARN respectively, and are unchanged.
…differential test The authorization layer parses stream, backup and table ARNs in authz_resource, while the engine parses them in parse_stream_arn and the backup and tag handlers. If an input parsed in the engine but not in the authorization layer, the resource was authorized verbatim and a Deny on the real resource was skipped while the engine still served it. Align the authz parser's acceptance with the engine stream parser: a trailing slash is treated as the bare table, and an empty sub-resource id is kept, so no engine-accepted stream input is authorized verbatim. Add a differential unit test over an adversarial corpus (respelled partition, service, region and account; extra path segments; trailing slash; whitespace; uppercase resource type; empty label; a label containing a slash) proving every stream ARN parse_stream_arn accepts is canonicalized by the authz parser to the same table and label.
Document that operations are authorized against the specific resource ARN (table, index, stream, backup) as DynamoDB does: a per-operation table and compatibility notes in the admin guide, and a row in the differences document. A policy granting only table/<name> no longer allows that table's index reads; a policy on table/<name>/stream/* now matches concrete stream ARNs; GetRecords requires dynamodb:GetRecords on the stream and rejects shard iterators issued by an older build after upgrade; restores require the item actions on the target table.
The AccessDeniedException message named the principal, action and resource but omitted the trailing clause the service includes, and did not distinguish an explicit deny from an implicit deny. Measured on live DynamoDB: an explicit Deny yields '... with an explicit deny in an identity-based policy', and a request with no matching Allow yields '... because no identity-based policy allows the <action> action'. Add explicit_deny_matches_arc to the evaluator (additive; the Allow or Deny decision is unchanged) and build the matching message form in the server authorization layer, which owns the message. Unit tests cover both forms and wire tests assert them against a running server.
State the defect directly in the authz_resource module doc comment and the resource-ARN test docstring, and drop the account id and date from the measurement notes.
ListBackups authorized against the table ARN (table/<T>) with a TableName and table/* without. The service evaluates table/<T>/backup/* with a TableName and table/*/backup/* without, the same sub-resource shape as ListStreams. An explicit Deny written on the backup sub-resource was bypassed, and a least-privilege Allow on it was refused so a policy that granted ListBackups against the service failed here. Mirror the ListStreams arm with a backup_wildcard helper so ListBackups authorizes table/<T>/backup/* and table/*/backup/*. Add unit tests for both shapes and wire tests covering the deny, allow, and bare-table rows.
TagResource, UntagResource and ListTagsOfResource accepted an index ARN (table/<T>/index/<I>) and served it; the service rejects it with ValidationException because tags are accepted only on a table or stream, and returns that error to every principal before the IAM decision. Validate the tag ResourceArn shape in the server before authorization: an index or other sub-resource ARN returns ValidationException with the service's message for every principal; a table or stream ARN passes to the existing account, region and existence checks. Add a unit test and wire tests for an admin and a no-permissions principal.
The tag operations diverged from the service on error ordering for malformed and foreign resource ARNs. Measured live: - A ResourceArn that does not start with arn:, or a DynamoDB ARN whose resource is not a table or stream, is a ValidationException for every principal, before the IAM decision. ExtendDB returned AccessDeniedException to a principal without permissions because authorization ran first. - For an authorized principal, a table ARN whose partition, service, or region is not this deployment's is a ValidationException (invalid TableArn) where the tag handler returned AccessDeniedException. A foreign-service ARN whose resource is not a table stays AccessDeniedException, as the service returns. Extend the pre-authorization tag ResourceArn shape check to reject a non-arn and a non-DynamoDB-resource ARN (DynamoDB service only) with ValidationException for every principal, and rewrite the engine tag handler to parse the ARN fields and return the invalid-TableArn ValidationException for a wrong partition, service, or region, the not-a-DynamoDB-resource error for a non-table DynamoDB ARN, and AccessDeniedException for a cross-account ARN and a foreign-service non-table ARN. Every case stays a refusal. Add unit tests and wire tests covering each row for a no-permissions and an admin principal.
…ization The differential corpus test drove only the stream parser, which is the one the engine parses into components. The backup resolver reads the account field and the backend matches the whole stored ARN exactly, so there is no shared component parser to compare for backups and tables; the bypass there is closed by canonicalization of the partition, service, region and account plus the backend's exact-ARN match. Add a unit test that pins the canonicalization half for backup and table ARNs: every respelling of a backup or table ARN resolves to the same table and sub-resource and canonicalizes to this deployment's ARN.
…source The comment described a removed helper and a table/* fallback that no longer exists. Describe the current behaviour: the top-level TableName is used to pre-fetch key info, and the authorization resource is derived by authz_resource::authz_targets, which never falls back to a table/* wildcard.
The fail-closed denial for a GetRecords whose shard iterator carries no stream (a token from a build before the stream ARN was carried, or a forged token) used an ExtendDB-specific message that omitted the principal and used a shape the service never emits. Every other denial in this layer matches one of the two service forms. Build the denial with the shared deny_message in the implicit-deny form, naming the principal and the account stream space. Expose deny_message and add a principal_arn helper. Add unit tests and a wire test asserting the implicit-deny form on an iterator with no stream.
…tion Add two operator notes. The admin guide compatibility notes now record that an AccessDeniedException ends with the service's explicit-deny or no-matching-allow clause, a change from the earlier prefix-only message, with the error code unchanged. The differences document records that DescribeEndpoints requires authentication on ExtendDB and is authorized against *, where DynamoDB answers endpoint discovery without authentication; authentication is not removed.
The AttributeValue validation test referenced an internal parity-test suite and internal test method names in its docstrings and comments. This is a public repository. Describe what each test checks instead: an empty or multi-type AttributeValue, and a string set carrying a null element at the top level or nested inside a list or a map. No test behaviour changes.
DynamoDB requires a signed DescribeEndpoints request but checks no IAM permission for it: a caller whose policy allows only dynamodb:ListTables receives the endpoint list. ExtendDB authorizes dynamodb:DescribeEndpoints on *. The previous row said DynamoDB answers without authentication; an unsigned request returns MissingAuthenticationTokenException.
…eName is a table ARN When TableName is a table ARN, key info was looked up by the ARN string, found nothing, and dynamodb:LeadingKeys was left absent, so a ForAllValues condition passed. The engine then rejected the ARN table name, so no data was served, but the decision depended on that. Look up key info by the table name the ARN names when the ARN is for this deployment's partition, region and the caller's account. The key info found this way is used for authorization only; the engine still resolves the request's TableName itself. On DynamoDB, a principal fenced with dynamodb:LeadingKeys is refused another partition when it names the table by ARN (AccessDeniedException), and the new wire test pins that.
DynamoDB checks the signature on DescribeEndpoints but evaluates no IAM policy. A user with no policy, a user allowed only dynamodb:ListTables, and users with an explicit Deny on dynamodb:DescribeEndpoints or on dynamodb:* all receive the endpoint list (measured against live DynamoDB on 2026-10-20). ExtendDB evaluated dynamodb:DescribeEndpoints on "*" and denied those callers. DescribeEndpoints now returns before the IAM decision, after SigV4 verification. The existing Deny-all permission test expected AccessDeniedException for DescribeEndpoints and now expects the service behaviour, and the row describing the difference is removed.
…ardIterator DynamoDB answers a shard iterator it did not issue with ValidationException "Invalid ShardIterator" before authorization, whatever the caller's policy. Measured against live DynamoDB on 2026-10-20 for a principal allowed every action, one allowed only ListTables and one with an explicit Deny on GetRecords for every stream, with three tokens: an iterator from an earlier ExtendDB build, a random base64 string and plain text. GetRecords returned AccessDeniedException for an iterator that names no stream. It now returns the service's ValidationException, which restores the account scoping behaviour main already had for another account's shard. An iterator issued before the upgrade gets the same answer, and the client calls GetShardIterator again; the upgrade note says so.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 21, 2026 02:41
8 tasks done
…ce tags Add unit tests that run check_user_authorization with a tag condition on a table, a stream and a backup ARN that all carry the tag in the store. The table is allowed, the stream and backup are denied, and no resource tag lookup is made for them. Reverting the call sites to look tags up under the resource ARN fails the test. Also move the new tag classification helper in the engine above the other helper's doc comment, and note that a table wildcard is looked up as given and reads no tags through the cache.
A negated operator, an IfExists form, ForAllValues and Null true match on a stream or backup operation because the key is absent; StringEquals and StringLike do not. Name dynamodb:ResourceTag as absent too.
…e tags With a TableName, both are authorized against the table's stream/* or backup/* sub-resource and evaluate no resource tags, as measured on DynamoDB. The previous release evaluated the table's tags for them.
This was referenced Oct 22, 2026
… tags Run check_role_authorization with the same tag condition. The table is allowed, the stream and backup are denied, and no resource tag lookup is made for them. Reverting the role call site alone fails the test.
robinnsc
previously approved these changes
Oct 22, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 22, 2026
Brings in #418 (LeadingKeys on Query, batch and transaction operations) and #421 (resource tags only as aws:ResourceTag). authorize_request in request_helpers.rs keeps main's key-info snapshot (AuthorizedKeyInfo, key_info_for_authz, storage reads for batch and transaction tables) and LeadingKeys extraction (leading_keys::single_table_leading_keys, batch_transact_targets), and this branch's resource derivation (authz_targets, table_resource_arn, the DescribeEndpoints, tag-shape, operation-ARN and GetRecords branches, base_params). A TableName given as a table ARN is looked up through key_lookup_name for LeadingKeys and left out of the snapshot handed to the engine, as before. Batch and transaction tables are read under the name the request uses, so a table named by ARN is recorded as missing and the engine answers ResourceNotFound from the same snapshot. The empty-target fallback uses authz_resource::table_wildcard, now pub(crate), in place of build_resource_arn. The upgrade manual keeps both entries, the LeadingKeys entry first. After #421 dynamodb:ResourceTag is absent for every resource, so the index tag inheritance tests in test_authz_resource_arn.py exercise aws:ResourceTag only, and the admin guide no longer says index reads evaluate dynamodb:ResourceTag.
robinnsc
self-requested a review
October 22, 2026 23:34
robinnsc
approved these changes
Oct 22, 2026
jcshepherd
approved these changes
Oct 23, 2026
jcshepherd
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed latest commit only; previous commits already reviewed by robinsc@.
robinnsc
added a commit
that referenced
this pull request
Oct 23, 2026
…; recover abandoned restores
Backup and restore now reproduce the table that was backed up, behave the
same on PostgreSQL, SQLite, MongoDB, and Cassandra, and match DynamoDB on
the points measured against the service.
Engine. The engine resolves a backup into one validated CreateTableInput
before any backend sees it: it runs the multi-part-key and vector-index
refusals, applies the overrides, and runs validate_create_table. Backends
only create the target, register the restore, and copy. The trait gains
backup_restore_source; restore_table_from_backup takes a resolved
RestoreRequest; restore_table_to_point_in_time is removed. Once the request
is authenticated and authorized, the whole restore runs in a task of its
own, so a client that disconnects does not cancel it part-way.
Restore runs after the response on every backend: the target is returned
CREATING and copied by a background task. While it is CREATING, DeleteTable
on it is ResourceInUseException and DeleteBackup on its source is
BackupInUseException. A target whose server stopped mid-copy is removed and
logged: PostgreSQL after 60 s, SQLite at the next start, MongoDB and
Cassandra by a heartbeat sweep at startup and every 15 s.
Wire behaviour: RestoreDateTime is the backup's creation time and
SourceTableArn is set; RestoreSummary appears only while CREATING, on every
table description (TableDescription::prepare_for_response). DescribeBackup
reports SourceTableDetails.ProvisionedThroughput and SourceTableFeatureDetails.
ListBackups pages with Limit and LastEvaluatedBackupArn and carries TableId.
Restoring into an existing name is TableAlreadyExistsException; a malformed
BackupArn is ValidationException, checked before the target name.
SSESpecificationOverride, GlobalSecondaryIndexOverride,
LocalSecondaryIndexOverride, and OnDemandThroughputOverride are applied as
the service applies them (an empty list restores no indexes of that kind, a
subset restores those, a GSI entry's throughput is the index's); an unknown
or changed index definition, a throughput member contradicting the billing
mode, {0, 0}, and BillingModeOverride=PROVISIONED with a GSI whose
throughput no entry supplies are ValidationException with the service's
text. TableClass is not carried across; the target is STANDARD. ListBackups
filters on BackupType and the time range; its cursor must match the
account, region, and TableName filter (a listed backup's stored ARN in an
earlier region is accepted, so paging passes it), and the time bounds are
checked. A
BackupArn in another partition is a ValidationException. BackupArn and
ExclusiveStartBackupArn shapes are checked as the service checks them
("Invalid Request: BackupArn is not valid" / "Invalid Backup ARN"),
surrounding whitespace is trimmed, and an ARN of the caller's account in
another region resolves (looked up in the server's region first, then as
written, so backups created under an earlier server.region stay
reachable); the 13-digit ids of earlier builds still resolve. BackupName
and TableName are validated as the model says (3 to 255 characters,
[a-zA-Z0-9_.-]+) with the service's "validation error(s) detected" texts;
TableName may be the table's ARN; CreateBackup of a missing or CREATING
table is TableNotFoundException. DescribeBackup propagates a failure to
read the definition as an internal error and degrades only for a version
it does not know; DeleteBackup returns the full description DELETED.
Restoring into a CREATING restore target is TableInUseException and a
second restore of a backup whose restore is CREATING is
BackupInUseException, decided inside each backend's registration
transaction, claim, or lightweight transaction. The key-info cache stores
no negative entry for a name the catalog has in a non-ACTIVE state, so the
first read after ACTIVE succeeds; authorization's key-info snapshot (#418)
records such a name as missing, so the engine does not look it up again. A
PROVISIONED backup whose GSI has no throughput restores with the table's
throughput. CreateTable and UpdateTable refuse GSI throughput below 1 on
PROVISIONED tables and more than 100 projected NonKeyAttributes.
CreateBackup of a table with vector indexes is refused. Backup ids are 14
zero-padded millisecond digits on every backend.
Authorization: backup operations use main's resource authorization (#419).
A BackupArn is trimmed of surrounding ASCII whitespace before it is
canonicalized, as the engine trims it before resolving the backup, so a
Deny on a backup ARN cannot be bypassed with a trailing newline.
PostgreSQL: the copy commits per batch with one multi-row INSERT per index;
the lock session is heartbeated; at most 4 restores per account and
min(16, pool_size / 2) per server, then LimitExceededException after 30 s,
with the whole restore, admission included, in a task the request waits
on; only SQLSTATE 53300 on the lock connection is LimitExceeded. The backup
row is locked FOR UPDATE while a restore registers. serve refuses a
catalog that does not record one of the binary's migration files, and
converges a pre-release 0.0.4 catalog shape with a 5 s lock timeout.
migrate uses the data database name init recorded and accepts --data-db.
SQLite: serve refuses a pre-release 0.0.4 shape; the backup_items rebuild is
named in the migrate preview and logged; the pool_size clamp is logged.
CreateBackup is admitted against the pool (pool_size / 2 - 1 at once, then
LimitExceededException after 30 s). A GSI added by UpdateTable with
throughput on a PROVISIONED table no longer makes DescribeTable fail, and
rows earlier builds wrote are read.
MongoDB: twenty concurrent restores no longer wedge the pool (the
registration hand-over held a checked-out connection while waiting for
another; both futures are polled together now); restores are admitted per
account (4) and per server (min(16, pool / 2)), LimitExceededException
after 30 s, and a registration longer than 60 s answers an internal error
while the restore continues. UpdateTable on a CREATING restore target is
refused. DeleteBackup and the sweep never release a delete claim once the
data drop may have started; a held delete claim answers BackupInUse; a
concurrent restore of one backup retries for 2 s, then BackupInUse; catalog
reads are pinned to the primary with read concern local and writes to
w: majority, and a weaker write concern in the connection string is
refused at startup; heartbeats refresh at once. The test-only protocol
model covers two restores, the delete and its stop points, and the sweep's
delete takeover.
Cassandra: migration V005 (catalog 0.0.4) stores backup definitions and
restore registrations (registered with a lightweight transaction that
requires the backup AVAILABLE and unclaimed); the copy uses the normal put path with GSIs written
synchronously; ordinary CreateTable does not name the V005 columns, so an
unmigrated catalog breaks only CreateBackup, RestoreTableFromBackup, and
DeleteBackup. The restore heartbeat stops on every exit of the copy. On an
unmigrated catalog the backup operations are a ValidationException naming
`extenddb migrate --yes`. UpdateTimeToLive on a table with an asynchronous
GSI is a ValidationException instead of a 500. init writes the
application credentials into the config, created with mode 0600. A
pre-existing defect is fixed: an on-demand GSI omitted
ProvisionedThroughput.
Catalog guards fail closed on an unparseable version. extenddb migrate:
the preview exits 3 when work is pending (main's resource-tag rewrite
included) and 0 when up to date; unrecorded
catalog migration files are pending; admin connection errors are one
plain line; --cassandra-user/--cassandra-pass and
EXTENDDB_CASSANDRA_PASSWORD are accepted.
Docs: differences-from-dynamodb, dynamodb-limits, the admin, upgrade, and
architecture manuals, and the MongoDB design doc are updated for four
backends.
Tests: tests/test_backup_restore_fidelity.py covers the restore surface on
all four backends; devtools/run-cassandra-tests runs its large-backup tests
with 10,000 items, because 60,000 made cdrs-tokio drop its only
connection. tests/test_ttl.py waits for ENABLING to end before disabling.
Signed-off-by: Scott Robinson <robinnsc@amazon.com>
5 of 8 tasks
robinnsc
added a commit
that referenced
this pull request
Oct 23, 2026
…; recover abandoned restores
Backup and restore now reproduce the table that was backed up, behave the
same on PostgreSQL, SQLite, MongoDB, and Cassandra, and match DynamoDB on
the points measured against the service.
Engine. The engine resolves a backup into one validated CreateTableInput
before any backend sees it: it runs the multi-part-key and vector-index
refusals, applies the overrides, and runs validate_create_table. Backends
only create the target, register the restore, and copy. The trait gains
backup_restore_source; restore_table_from_backup takes a resolved
RestoreRequest; restore_table_to_point_in_time is removed. Once the request
is authenticated and authorized, the whole restore runs in a task of its
own, so a client that disconnects does not cancel it part-way.
Restore runs after the response on every backend: the target is returned
CREATING and copied by a background task. While it is CREATING, DeleteTable
on it is ResourceInUseException and DeleteBackup on its source is
BackupInUseException. A target whose server stopped mid-copy is removed and
logged: PostgreSQL after 60 s, SQLite at the next start, MongoDB and
Cassandra by a heartbeat sweep at startup and every 15 s.
Wire behaviour: RestoreDateTime is the backup's creation time and
SourceTableArn is set; RestoreSummary appears only while CREATING, on every
table description (TableDescription::prepare_for_response). DescribeBackup
reports SourceTableDetails.ProvisionedThroughput and SourceTableFeatureDetails.
ListBackups pages with Limit and LastEvaluatedBackupArn and carries TableId.
Restoring into an existing name is TableAlreadyExistsException; a malformed
BackupArn is ValidationException, checked before the target name.
SSESpecificationOverride, GlobalSecondaryIndexOverride,
LocalSecondaryIndexOverride, and OnDemandThroughputOverride are applied as
the service applies them (an empty list restores no indexes of that kind, a
subset restores those, a GSI entry's throughput is the index's); an unknown
or changed index definition, a throughput member contradicting the billing
mode, {0, 0}, and BillingModeOverride=PROVISIONED with a GSI whose
throughput no entry supplies are ValidationException with the service's
text. TableClass is not carried across; the target is STANDARD. ListBackups
filters on BackupType and the time range; its cursor must match the
account, region, and TableName filter (a listed backup's stored ARN in an
earlier region is accepted, so paging passes it), and the time bounds are
checked. A
BackupArn in another partition is a ValidationException. BackupArn and
ExclusiveStartBackupArn shapes are checked as the service checks them
("Invalid Request: BackupArn is not valid" / "Invalid Backup ARN"),
surrounding whitespace is trimmed, and an ARN of the caller's account in
another region resolves (looked up in the server's region first, then as
written, so backups created under an earlier server.region stay
reachable); the 13-digit ids of earlier builds still resolve. BackupName
and TableName are validated as the model says (3 to 255 characters,
[a-zA-Z0-9_.-]+) with the service's "validation error(s) detected" texts;
TableName may be the table's ARN; CreateBackup of a missing or CREATING
table is TableNotFoundException. DescribeBackup propagates a failure to
read the definition as an internal error and degrades only for a version
it does not know; DeleteBackup returns the full description DELETED.
Restoring into a CREATING restore target is TableInUseException and a
second restore of a backup whose restore is CREATING is
BackupInUseException, decided inside each backend's registration
transaction, claim, or lightweight transaction. The key-info cache stores
no negative entry for a name the catalog has in a non-ACTIVE state, so the
first read after ACTIVE succeeds; authorization's key-info snapshot (#418)
records such a name as missing, so the engine does not look it up again. A
PROVISIONED backup whose GSI has no throughput restores with the table's
throughput. CreateTable and UpdateTable refuse GSI throughput below 1 on
PROVISIONED tables and more than 100 projected NonKeyAttributes.
CreateBackup of a table with vector indexes is refused. Backup ids are 14
zero-padded millisecond digits on every backend.
Authorization: backup operations use main's resource authorization (#419).
A BackupArn is trimmed of surrounding ASCII whitespace before it is
canonicalized, as the engine trims it before resolving the backup, so a
Deny on a backup ARN cannot be bypassed with a trailing newline.
PostgreSQL: the copy commits per batch with one multi-row INSERT per index;
the lock session is heartbeated; at most 4 restores per account and
min(16, pool_size / 2) per server, then LimitExceededException after 30 s,
with the whole restore, admission included, in a task the request waits
on; only SQLSTATE 53300 on the lock connection is LimitExceeded. The backup
row is locked FOR UPDATE while a restore registers. serve refuses a
catalog that does not record one of the binary's migration files, and
converges a pre-release 0.0.4 catalog shape with a 5 s lock timeout.
migrate uses the data database name init recorded and accepts --data-db.
SQLite: serve refuses a pre-release 0.0.4 shape; the backup_items rebuild is
named in the migrate preview and logged; the pool_size clamp is logged.
CreateBackup is admitted against the pool (pool_size / 2 - 1 at once, then
LimitExceededException after 30 s). A GSI added by UpdateTable with
throughput on a PROVISIONED table no longer makes DescribeTable fail, and
rows earlier builds wrote are read.
MongoDB: twenty concurrent restores no longer wedge the pool (the
registration hand-over held a checked-out connection while waiting for
another; both futures are polled together now); restores are admitted per
account (4) and per server (min(16, pool / 2)), LimitExceededException
after 30 s, and a registration longer than 60 s answers an internal error
while the restore continues. UpdateTable on a CREATING restore target is
refused. DeleteBackup and the sweep never release a delete claim once the
data drop may have started; a held delete claim answers BackupInUse; a
concurrent restore of one backup retries for 2 s, then BackupInUse; catalog
reads are pinned to the primary with read concern local and writes to
w: majority, and a weaker write concern in the connection string is
refused at startup; heartbeats refresh at once. The test-only protocol
model covers two restores, the delete and its stop points, and the sweep's
delete takeover.
Cassandra: migration V005 (catalog 0.0.4) stores backup definitions and
restore registrations (registered with a lightweight transaction that
requires the backup AVAILABLE and unclaimed); the copy uses the normal put path with GSIs written
synchronously; ordinary CreateTable does not name the V005 columns, so an
unmigrated catalog breaks only CreateBackup, RestoreTableFromBackup, and
DeleteBackup. The restore heartbeat stops on every exit of the copy. On an
unmigrated catalog the backup operations are a ValidationException naming
`extenddb migrate --yes`. UpdateTimeToLive on a table with an asynchronous
GSI is a ValidationException instead of a 500. init writes the
application credentials into the config, created with mode 0600. A
pre-existing defect is fixed: an on-demand GSI omitted
ProvisionedThroughput.
Catalog guards fail closed on an unparseable version. extenddb migrate:
the preview exits 3 when work is pending (main's resource-tag rewrite
included) and 0 when up to date; unrecorded
catalog migration files are pending; admin connection errors are one
plain line; --cassandra-user/--cassandra-pass and
EXTENDDB_CASSANDRA_PASSWORD are accepted.
Docs: differences-from-dynamodb, dynamodb-limits, the admin, upgrade, and
architecture manuals, and the MongoDB design doc are updated for four
backends.
Tests: tests/test_backup_restore_fidelity.py covers the restore surface on
all four backends; devtools/run-cassandra-tests runs its large-backup tests
with 10,000 items, because 60,000 made cdrs-tokio drop its only
connection. tests/test_ttl.py waits for ENABLING to end before disabling.
Signed-off-by: Scott Robinson <robinnsc@amazon.com>
robinnsc
added a commit
that referenced
this pull request
Oct 23, 2026
…; recover abandoned restores
Backup and restore now reproduce the table that was backed up, behave the
same on PostgreSQL, SQLite, MongoDB, and Cassandra, and match DynamoDB on
the points measured against the service.
Engine. The engine resolves a backup into one validated CreateTableInput
before any backend sees it: it runs the multi-part-key and vector-index
refusals, applies the overrides, and runs validate_create_table. Backends
only create the target, register the restore, and copy. The trait gains
backup_restore_source; restore_table_from_backup takes a resolved
RestoreRequest; restore_table_to_point_in_time is removed. Once the request
is authenticated and authorized, the whole restore runs in a task of its
own, so a client that disconnects does not cancel it part-way.
Restore runs after the response on every backend: the target is returned
CREATING and copied by a background task. While it is CREATING, DeleteTable
on it is ResourceInUseException and DeleteBackup on its source is
BackupInUseException. A target whose server stopped mid-copy is removed and
logged: PostgreSQL after 60 s, SQLite at the next start, MongoDB and
Cassandra by a heartbeat sweep at startup and every 15 s.
Wire behaviour: RestoreDateTime is the backup's creation time and
SourceTableArn is set; RestoreSummary appears only while CREATING, on every
table description (TableDescription::prepare_for_response). DescribeBackup
reports SourceTableDetails.ProvisionedThroughput and
SourceTableFeatureDetails.
ListBackups pages with Limit and LastEvaluatedBackupArn and carries TableId.
Restoring into an existing name is TableAlreadyExistsException; a malformed
BackupArn is ValidationException, checked before the target name.
SSESpecificationOverride, GlobalSecondaryIndexOverride,
LocalSecondaryIndexOverride, and OnDemandThroughputOverride are applied as
the service applies them (an empty list restores no indexes of that kind, a
subset restores those, a GSI entry's throughput is the index's); an unknown
or changed index definition, a throughput member contradicting the billing
mode, {0, 0}, and BillingModeOverride=PROVISIONED with a GSI whose
throughput no entry supplies are ValidationException with the service's
text. TableClass is not carried across; the target is STANDARD. ListBackups
filters on BackupType and the time range; its cursor must match the
account, region, and TableName filter (a listed backup's stored ARN in an
earlier region is accepted, so paging passes it), and the time bounds are
checked. A BackupArn in another partition is a ValidationException. BackupArn and
ExclusiveStartBackupArn shapes are checked as the service checks them
("Invalid Request: BackupArn is not valid" / "Invalid Backup ARN"),
surrounding whitespace is trimmed, and an ARN of the caller's account in
another region resolves (looked up in the server's region first, then as
written, so backups created under an earlier server.region stay
reachable); the 13-digit ids of earlier builds still resolve. BackupName
and TableName are validated as the model says (3 to 255 characters,
[a-zA-Z0-9_.-]+) with the service's "validation error(s) detected" texts;
TableName may be the table's ARN; CreateBackup of a missing or CREATING
table is TableNotFoundException. DescribeBackup propagates a failure to
read the definition as an internal error and degrades only for a version
it does not know; DeleteBackup returns the full description DELETED.
Restoring into a CREATING restore target is TableInUseException and a
second restore of a backup whose restore is CREATING is
BackupInUseException. Each backend checks the target name before the
backup and before a restore slot is taken; the target insert and the
registration transaction, claim, or lightweight transaction are the
fences. The key-info cache stores
no negative entry for a name the catalog has in a non-ACTIVE state, so the
first read after ACTIVE succeeds; authorization's key-info snapshot (#418)
records such a name as missing, so the engine does not look it up again. A
PROVISIONED backup whose GSI has no throughput restores with the table's
throughput. CreateTable and UpdateTable refuse GSI throughput below 1 on
PROVISIONED tables and more than 100 projected NonKeyAttributes.
CreateBackup of a table with vector indexes is refused. Backup ids are 14
zero-padded millisecond digits on every backend.
Authorization: backup operations use main's resource authorization (#419).
A BackupArn is trimmed of surrounding ASCII whitespace before it is
canonicalized, as the engine trims it before resolving the backup, so a
Deny on a backup ARN cannot be bypassed with a trailing newline.
PostgreSQL: the copy commits per batch with one multi-row INSERT per index;
the lock session is heartbeated; at most 4 restores per account and
min(16, pool_size / 2) per server, then LimitExceededException after 30 s,
with the whole restore, admission included, in a task the request waits
on; only SQLSTATE 53300 on the lock connection is LimitExceeded. The backup
row is locked FOR UPDATE while a restore registers. serve refuses a
catalog that does not record one of the binary's migration files, and
converges a pre-release 0.0.4 catalog shape with a 5 s lock timeout.
migrate uses the data database name init recorded and accepts --data-db.
SQLite: serve refuses a pre-release 0.0.4 shape; the backup_items rebuild is
named in the migrate preview and logged; the pool_size clamp is logged.
CreateBackup is admitted against the pool (pool_size / 2 - 1 at once, then
LimitExceededException after 30 s). A GSI added by UpdateTable with
throughput on a PROVISIONED table no longer makes DescribeTable fail, and
rows earlier builds wrote are read.
MongoDB: twenty concurrent restores no longer wedge the pool (the
registration hand-over held a checked-out connection while waiting for
another; both futures are polled together now); restores are admitted per
account (4) and per server (min(16, pool / 2)), LimitExceededException
after 30 s, and a registration longer than 60 s answers an internal error
while the restore continues. UpdateTable on a CREATING restore target is
refused. DeleteBackup and the sweep never release a delete claim once the
data drop may have started; a held delete claim answers BackupInUse; a
concurrent restore of one backup retries for 2 s, then BackupInUse; catalog
reads are pinned to the primary with read concern local and writes to
w: majority, and a weaker write concern in the connection string is
refused at startup; heartbeats refresh at once. The test-only protocol
model covers two restores, the delete and its stop points, and the sweep's
delete takeover.
Cassandra: migration V005 (catalog 0.0.4) stores backup definitions and
restore registrations (registered with a lightweight transaction that
requires the backup AVAILABLE and unclaimed); the copy uses the normal
put path with GSIs written
synchronously; ordinary CreateTable does not name the V005 columns, so an
unmigrated catalog breaks only CreateBackup, RestoreTableFromBackup, and
DeleteBackup. The restore heartbeat stops on every exit of the copy. On an
unmigrated catalog the backup operations are a ValidationException naming
`extenddb migrate --yes`. UpdateTimeToLive on a table with an asynchronous
GSI is a ValidationException instead of a 500. init writes the
application credentials into the config, created with mode 0600. A
pre-existing defect is fixed: an on-demand GSI omitted
ProvisionedThroughput.
Catalog guards fail closed on an unparseable version. extenddb migrate:
the preview exits 3 when work is pending (main's resource-tag rewrite
included) and 0 when up to date; unrecorded
catalog migration files are pending; admin connection errors are one
plain line; --cassandra-user/--cassandra-pass and
EXTENDDB_CASSANDRA_PASSWORD are accepted.
Docs: differences-from-dynamodb, dynamodb-limits, the admin, upgrade, and
architecture manuals, and the MongoDB design doc are updated for four
backends.
Tests: tests/test_backup_restore_fidelity.py covers the restore surface on
all four backends; devtools/run-cassandra-tests runs its large-backup tests
with 10,000 items, because 60,000 made cdrs-tokio drop its only
connection. tests/test_ttl.py waits for ENABLING to end before disabling.
Signed-off-by: Scott Robinson <robinnsc@amazon.com>
This was referenced Oct 24, 2026
LukeKennedy
pushed a commit
to LukeKennedy/extenddb
that referenced
this pull request
Oct 26, 2026
…; recover abandoned restores (ExtendDB#384) Backup and restore now reproduce the table that was backed up, behave the same on PostgreSQL, SQLite, MongoDB, and Cassandra, and match DynamoDB on the points measured against the service. Engine. The engine resolves a backup into one validated CreateTableInput before any backend sees it: it runs the multi-part-key and vector-index refusals, applies the overrides, and runs validate_create_table. Backends only create the target, register the restore, and copy. The trait gains backup_restore_source; restore_table_from_backup takes a resolved RestoreRequest; restore_table_to_point_in_time is removed. Once the request is authenticated and authorized, the whole restore runs in a task of its own, so a client that disconnects does not cancel it part-way. Restore runs after the response on every backend: the target is returned CREATING and copied by a background task. While it is CREATING, DeleteTable on it is ResourceInUseException and DeleteBackup on its source is BackupInUseException. A target whose server stopped mid-copy is removed and logged: PostgreSQL after 60 s, SQLite at the next start, MongoDB and Cassandra by a heartbeat sweep at startup and every 15 s. Wire behaviour: RestoreDateTime is the backup's creation time and SourceTableArn is set; RestoreSummary appears only while CREATING, on every table description (TableDescription::prepare_for_response). DescribeBackup reports SourceTableDetails.ProvisionedThroughput and SourceTableFeatureDetails. ListBackups pages with Limit and LastEvaluatedBackupArn and carries TableId. Restoring into an existing name is TableAlreadyExistsException; a malformed BackupArn is ValidationException, checked before the target name. SSESpecificationOverride, GlobalSecondaryIndexOverride, LocalSecondaryIndexOverride, and OnDemandThroughputOverride are applied as the service applies them (an empty list restores no indexes of that kind, a subset restores those, a GSI entry's throughput is the index's); an unknown or changed index definition, a throughput member contradicting the billing mode, {0, 0}, and BillingModeOverride=PROVISIONED with a GSI whose throughput no entry supplies are ValidationException with the service's text. TableClass is not carried across; the target is STANDARD. ListBackups filters on BackupType and the time range; its cursor must match the account, region, and TableName filter (a listed backup's stored ARN in an earlier region is accepted, so paging passes it), and the time bounds are checked. A BackupArn in another partition is a ValidationException. BackupArn and ExclusiveStartBackupArn shapes are checked as the service checks them ("Invalid Request: BackupArn is not valid" / "Invalid Backup ARN"), surrounding whitespace is trimmed, and an ARN of the caller's account in another region resolves (looked up in the server's region first, then as written, so backups created under an earlier server.region stay reachable); the 13-digit ids of earlier builds still resolve. BackupName and TableName are validated as the model says (3 to 255 characters, [a-zA-Z0-9_.-]+) with the service's "validation error(s) detected" texts; TableName may be the table's ARN; CreateBackup of a missing or CREATING table is TableNotFoundException. DescribeBackup propagates a failure to read the definition as an internal error and degrades only for a version it does not know; DeleteBackup returns the full description DELETED. Restoring into a CREATING restore target is TableInUseException and a second restore of a backup whose restore is CREATING is BackupInUseException. Each backend checks the target name before the backup and before a restore slot is taken; the target insert and the registration transaction, claim, or lightweight transaction are the fences. The key-info cache stores no negative entry for a name the catalog has in a non-ACTIVE state, so the first read after ACTIVE succeeds; authorization's key-info snapshot (ExtendDB#418) records such a name as missing, so the engine does not look it up again. A PROVISIONED backup whose GSI has no throughput restores with the table's throughput. CreateTable and UpdateTable refuse GSI throughput below 1 on PROVISIONED tables and more than 100 projected NonKeyAttributes. CreateBackup of a table with vector indexes is refused. Backup ids are 14 zero-padded millisecond digits on every backend. Authorization: backup operations use main's resource authorization (ExtendDB#419). A BackupArn is trimmed of surrounding ASCII whitespace before it is canonicalized, as the engine trims it before resolving the backup, so a Deny on a backup ARN cannot be bypassed with a trailing newline. PostgreSQL: the copy commits per batch with one multi-row INSERT per index; the lock session is heartbeated; at most 4 restores per account and min(16, pool_size / 2) per server, then LimitExceededException after 30 s, with the whole restore, admission included, in a task the request waits on; only SQLSTATE 53300 on the lock connection is LimitExceeded. The backup row is locked FOR UPDATE while a restore registers. serve refuses a catalog that does not record one of the binary's migration files, and converges a pre-release 0.0.4 catalog shape with a 5 s lock timeout. migrate uses the data database name init recorded and accepts --data-db. SQLite: serve refuses a pre-release 0.0.4 shape; the backup_items rebuild is named in the migrate preview and logged; the pool_size clamp is logged. CreateBackup is admitted against the pool (pool_size / 2 - 1 at once, then LimitExceededException after 30 s). A GSI added by UpdateTable with throughput on a PROVISIONED table no longer makes DescribeTable fail, and rows earlier builds wrote are read. MongoDB: twenty concurrent restores no longer wedge the pool (the registration hand-over held a checked-out connection while waiting for another; both futures are polled together now); restores are admitted per account (4) and per server (min(16, pool / 2)), LimitExceededException after 30 s, and a registration longer than 60 s answers an internal error while the restore continues. UpdateTable on a CREATING restore target is refused. DeleteBackup and the sweep never release a delete claim once the data drop may have started; a held delete claim answers BackupInUse; a concurrent restore of one backup retries for 2 s, then BackupInUse; catalog reads are pinned to the primary with read concern local and writes to w: majority, and a weaker write concern in the connection string is refused at startup; heartbeats refresh at once. The test-only protocol model covers two restores, the delete and its stop points, and the sweep's delete takeover. Cassandra: migration V005 (catalog 0.0.4) stores backup definitions and restore registrations (registered with a lightweight transaction that requires the backup AVAILABLE and unclaimed); the copy uses the normal put path with GSIs written synchronously; ordinary CreateTable does not name the V005 columns, so an unmigrated catalog breaks only CreateBackup, RestoreTableFromBackup, and DeleteBackup. The restore heartbeat stops on every exit of the copy. On an unmigrated catalog the backup operations are a ValidationException naming `extenddb migrate --yes`. UpdateTimeToLive on a table with an asynchronous GSI is a ValidationException instead of a 500. init writes the application credentials into the config, created with mode 0600. A pre-existing defect is fixed: an on-demand GSI omitted ProvisionedThroughput. Catalog guards fail closed on an unparseable version. extenddb migrate: the preview exits 3 when work is pending (main's resource-tag rewrite included) and 0 when up to date; unrecorded catalog migration files are pending; admin connection errors are one plain line; --cassandra-user/--cassandra-pass and EXTENDDB_CASSANDRA_PASSWORD are accepted. Docs: differences-from-dynamodb, dynamodb-limits, the admin, upgrade, and architecture manuals, and the MongoDB design doc are updated for four backends. Tests: tests/test_backup_restore_fidelity.py covers the restore surface on all four backends; devtools/run-cassandra-tests runs its large-backup tests with 10,000 items, because 60,000 made cdrs-tokio drop its only connection. tests/test_ttl.py waits for ENABLING to end before disabling. Signed-off-by: Scott Robinson <robinnsc@amazon.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Operations whose resource is not a top-level
TableNameare now authorized against the resource they act on. Earlier builds took the resource fromTableNameonly, so these operations were checked againsttable/*, and a Deny written on the specific resource never matched. The operations and their resources, measured against DynamoDB:ResourceArn(table or stream)StreamArntable/<name>/stream/*when a table is named, elsetable/*/stream/*BackupArntable/<name>/backup/*when a table is named, elsetable/*/backup/*TableArnas givenIndexNametable/<name>/index/<index>, evaluated with the base table's tags*TableName, including inside batch and transaction requestsRelated changes:
crates/core/src/arn.rs, is used by authorization, the export, tag and backup handlers and the storage stream resolver, so the resource authorization checks is the resource the engine reads. A test sends one corpus of ARN shapes (non-arnprefixes, empty fields, repeatedtable/segments, appended sub-resources, unknown kinds, foreign partition, service, region and account) through the shared parser, the authorization parser and the storage stream resolver, and requires all three to agree.Invalid StreamArnfor DescribeStream and GetShardIterator,Invalid Backup ARNfor DescribeBackup and DeleteBackup,One or more parameter values were invalid: tableArn is not a valid ARNfor an export. A well-formed DynamoDB ARN of the wrong kind as an exportTableArnis authorized against that ARN and then refused by the handler, as on DynamoDB.table/<name>/stream/*spans the:characters in a stream label, so it matches concrete stream ARNs.aws:ResourceTag(anddynamodb:ResourceTag, whichmainstill resolves) from the base table's tags, as DynamoDB does.ExpiredIteratorExceptionwithShard iterator has expired, so a stream client calls GetShardIterator again. An iterator that does not decode getsValidationExceptionwithInvalid ShardIterator. Both answers match DynamoDB and come before authorization.ValidationExceptionmessages before authorization, and the tagResourceArnvalidation errors come back in the service's order.TableNameis a table ARN, item condition keys such asdynamodb:LeadingKeysare read with that table's key schema.Why
A principal with an explicit Deny on a table could still change its tags with TagResource and UntagResource, and read them with ListTagsOfResource. Where access is granted by tag, that let a principal change the tags that decide its own access. The same gap applied to the stream, backup, restore, export and index operations above.
Testing done
tests/test_authz_resource_arn.py: 45 wire tests, each expected response measured against DynamoDB first. They cover an explicit Deny on each resource type, wildcard stream Denies, the export of one table under an ARN naming another, stream ARNs with a non-arnprefix, index reads under tag conditions, GetRecords with an iterator for another stream's shard and with iterators from before this change, ListBackups with and without a table, restore permission chains, respelled ARNs, wrong-kind and malformed ARNs for an admin and for a principal with no policy, index and backup ARNs on tag operations, stream and backup operations under tag conditions, the tag validation order, both AccessDenied message forms and DescribeEndpoints under four policies. All pass on SQLite and PostgreSQL. On the previous head of this PR, the 12 tests added in the last revision fail except one that checks behaviour that was already correct. Of the 3 added in this revision, 2 fail on the previous head; the third pins backup behaviour that was already correct.crates/core/src/arn.rs,crates/server/src/authz_resource.rs,crates/server/src/authorization.rs,crates/engine/src/import_export.rsandcrates/engine/src/streams.rs.cargo fmt --check,cargo clippyon 1.97.0 with-D warnings,cargo test --workspaceon stable and 1.88, andtests/test_abac.py,tests/test_auth_integration.pyandtests/test_auth_permissions.pyon SQLite and PostgreSQL.mainon one SQLite data directory, with policies ontable/<name>,table/*and tag conditions, backups, and a shard iterator in flight. Each behaviour change matched the compatibility notes.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. The change makes existing authorization match DynamoDB; the resource table and compatibility notes are in
docs/manuals/05-admin-guide.md, and the upgrade steps are indocs/manuals/07-upgrade-manual.md.Breaking changes
docs/manuals/05-admin-guide.mdanddocs/manuals/07-upgrade-manual.mdhave the full notes. The main ones:table/<name>no longer allows Query or Scan on that table's indexes. Granttable/<name>/index/*as on DynamoDB.table/<name>no longer covers that table's index reads, ListStreams or ListBackups. To fence a table, addtable/<name>/*to the Deny.*, so a grant or Deny scoped totable/*no longer applies to it.dynamodb:DescribeEndpoints.ExpiredIteratorException, and the client calls GetShardIterator again.StreamArn,BackupArnor exportTableArngets the service'sValidationExceptionbefore authorization, where earlier builds evaluated it.Known cost
Each GetRecords call now checks that the shard belongs to the stream in its iterator, which is two extra queries on SQLite. #438 tracks removing them.
Merge order
This branch includes
mainafter #420. It and #418 both changeauthorize_requestincrates/server/src/request_helpers.rs. Either can merge first. I will mergemaininto the second one, resolve the conflict, and resolve batch and transaction table names given as ARNs to the table name before the key schema lookup, sodynamodb:LeadingKeysis read for them.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.