Skip to content

fix(auth): authorize resource-ARN operations against the resource they act on - #419

Merged
robinnsc merged 40 commits into
mainfrom
fix/authz-resource-arn
Oct 23, 2026
Merged

robinnsc merged 40 commits into
mainfrom
fix/authz-resource-arn

Conversation

@LeeroyHannigan

@LeeroyHannigan LeeroyHannigan commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

Operations whose resource is not a top-level TableName are now authorized against the resource they act on. Earlier builds took the resource from TableName only, so these operations were checked against table/*, and a Deny written on the specific resource never matched. The operations and their resources, measured against DynamoDB:

Operation Resource
TagResource, UntagResource, ListTagsOfResource the ResourceArn (table or stream)
DescribeStream, GetShardIterator the StreamArn
GetRecords the stream the shard iterator reads
ListStreams table/<name>/stream/* when a table is named, else table/*/stream/*
DescribeBackup, DeleteBackup the BackupArn
ListBackups table/<name>/backup/* when a table is named, else table/*/backup/*
RestoreTableFromBackup the backup ARN, plus the item read and write actions on the target table
RestoreTableToPointInTime the source table, plus the same item actions on the target table
ExportTableToPointInTime the TableArn as given
ImportTable the new table
Query and Scan with IndexName table/<name>/index/<index>, evaluated with the base table's tags
DescribeLimits *
Any operation given a table ARN as TableName, including inside batch and transaction requests that table

Related changes:

  • One ARN parser, in 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-arn prefixes, empty fields, repeated table/ 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.
  • An ARN that does not parse is refused before authorization, with the service's message: Invalid StreamArn for DescribeStream and GetShardIterator, Invalid Backup ARN for DescribeBackup and DeleteBackup, One or more parameter values were invalid: tableArn is not a valid ARN for an export. A well-formed DynamoDB ARN of the wrong kind as an export TableArn is authorized against that ARN and then refused by the handler, as on DynamoDB.
  • A resource ARN is checked for partition, service, region and account, so a respelled ARN cannot miss a Deny written against the canonical ARN.
  • A resource wildcard such as table/<name>/stream/* spans the : characters in a stream label, so it matches concrete stream ARNs.
  • Query and Scan on an index evaluate aws:ResourceTag (and dynamodb:ResourceTag, which main still resolves) from the base table's tags, as DynamoDB does.
  • Operations on a stream or backup ARN, and ListStreams and ListBackups with a TableName, evaluate no resource tags, as DynamoDB does. A stream holds a tag set of its own, but the service does not evaluate it, or the table's, for the stream's operations.
  • GetRecords is authorized against the stream named in its shard iterator, and the engine confirms the shard belongs to that stream before reading. An iterator issued before this change, which names no stream, gets ExpiredIteratorException with Shard iterator has expired, so a stream client calls GetShardIterator again. An iterator that does not decode gets ValidationException with Invalid ShardIterator. Both answers match DynamoDB and come before authorization.
  • Tag operations reject an index ARN and a backup ARN with the service's ValidationException messages before authorization, and the tag ResourceArn validation errors come back in the service's order.
  • AccessDeniedException messages end with the service's clause: "with an explicit deny in an identity-based policy" or "because no identity-based policy allows the action".
  • DescribeEndpoints checks the request signature and no IAM permission, as DynamoDB does.
  • When TableName is a table ARN, item condition keys such as dynamodb:LeadingKeys are 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-arn prefix, 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.
  • Stream and backup tag conditions compared with DynamoDB: 432 decisions over every condition operator, Allow and Deny, an IAM user and an assumed-role session, on DescribeStream, GetShardIterator, GetRecords, ListStreams, ListTagsOfResource, DescribeBackup and DescribeTable. All match on SQLite and PostgreSQL.
  • Unit tests in crates/core/src/arn.rs, crates/server/src/authz_resource.rs, crates/server/src/authorization.rs, crates/engine/src/import_export.rs and crates/engine/src/streams.rs.
  • cargo fmt --check, cargo clippy on 1.97.0 with -D warnings, cargo test --workspace on stable and 1.88, and tests/test_abac.py, tests/test_auth_integration.py and tests/test_auth_permissions.py on SQLite and PostgreSQL.
  • An upgrade from main on one SQLite data directory, with policies on table/<name>, table/* and tag conditions, backups, and a shard iterator in flight. Each behaviour change matched the compatibility notes.

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 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 in docs/manuals/07-upgrade-manual.md.

Breaking changes

docs/manuals/05-admin-guide.md and docs/manuals/07-upgrade-manual.md have the full notes. The main ones:

  • A policy that grants only table/<name> no longer allows Query or Scan on that table's indexes. Grant table/<name>/index/* as on DynamoDB.
  • A Deny on table/<name> no longer covers that table's index reads, ListStreams or ListBackups. To fence a table, add table/<name>/* to the Deny.
  • A Deny on a specific stream, tag, backup or index ARN now takes effect.
  • ListStreams and ListBackups with a TableName no longer evaluate the table's tags, as on DynamoDB.
  • A restore needs the item actions on the target table as well as the restore action.
  • DescribeLimits is checked against *, so a grant or Deny scoped to table/* no longer applies to it.
  • DescribeEndpoints no longer requires dynamodb:DescribeEndpoints.
  • A shard iterator issued before the upgrade gets ExpiredIteratorException, and the client calls GetShardIterator again.
  • A malformed StreamArn, BackupArn or export TableArn gets the service's ValidationException before authorization, where earlier builds evaluated it.
  • AccessDeniedException messages gain the service's trailing clause. The error code and the "is not authorized to perform" prefix are unchanged.

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 main after #420. It and #418 both change authorize_request in crates/server/src/request_helpers.rs. Either can merge first. I will merge main into 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, so dynamodb:LeadingKeys is 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.

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.
…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.
… 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
robinnsc previously approved these changes Oct 22, 2026
@robinnsc
robinnsc added this pull request to the merge queue Oct 22, 2026
@robinnsc
robinnsc removed this pull request from the merge queue due to a manual request Oct 22, 2026
@robinnsc
robinnsc added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
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.

@jcshepherd jcshepherd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed latest commit only; previous commits already reviewed by robinsc@.

@robinnsc
robinnsc added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 6eb6e44 Oct 23, 2026
34 checks passed
@robinnsc
robinnsc deleted the fix/authz-resource-arn branch October 23, 2026 00:30
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, 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>
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>
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.

4 participants