mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-07 16:08:54 +00:00
fix(migration): verify backup AVAILABLE pre-truncate; document wildcard risk acceptance (cross-review FIX/NIT)
This commit is contained in:
parent
c50ce7a3b3
commit
92412a43fd
2 changed files with 39 additions and 15 deletions
|
|
@ -61,6 +61,14 @@ def make_slack_bot_read_policy(scope, table_name):
|
||||||
affect in-account callers. PrincipalArn is wildcarded by the bot's CFN
|
affect in-account callers. PrincipalArn is wildcarded by the bot's CFN
|
||||||
stack prefix because role-name hash suffixes rotate on replacement;
|
stack prefix because role-name hash suffixes rotate on replacement;
|
||||||
account + prefix + read-only actions bound the grant.
|
account + prefix + read-only actions bound the grant.
|
||||||
|
|
||||||
|
Accepted risk (GPT-4.1 cross-review 2026-07-23): a mgmt principal with
|
||||||
|
iam:CreateRole could mint a prefix-matching role, but that capability
|
||||||
|
already implies near-org-admin blast radius; exact-ARN pinning was
|
||||||
|
rejected because it breaks on CFN role replacement. Assumption: the bot
|
||||||
|
Lambdas call DynamoDB DIRECTLY under their execution roles - if a
|
||||||
|
consumer ever role-chains first, aws:PrincipalArn will not match and
|
||||||
|
reads fail closed.
|
||||||
"""
|
"""
|
||||||
stack = Stack.of(scope)
|
stack = Stack.of(scope)
|
||||||
return iam.PolicyDocument(
|
return iam.PolicyDocument(
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,36 @@ def normalize_cutoff(value):
|
||||||
return value.replace("Z", "+00:00")
|
return value.replace("Z", "+00:00")
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_destination(name, cfg, dst, args):
|
||||||
|
"""Backup-gated destination truncate for the truncate-and-load tables."""
|
||||||
|
if args.execute:
|
||||||
|
if not args.backup_arn:
|
||||||
|
sys.exit(
|
||||||
|
f"ERROR: {name} is truncate-and-load; pass --backup-arn of an "
|
||||||
|
"AVAILABLE on-demand backup of the DESTINATION table taken "
|
||||||
|
"just now (aws dynamodb create-backup / describe-backup)."
|
||||||
|
)
|
||||||
|
desc = dst.meta.client.describe_backup(BackupArn=args.backup_arn)
|
||||||
|
details = desc["BackupDescription"]["BackupDetails"]
|
||||||
|
src_table = desc["BackupDescription"]["SourceTableDetails"]
|
||||||
|
if details["BackupStatus"] != "AVAILABLE" or src_table["TableName"] != name:
|
||||||
|
sys.exit(
|
||||||
|
f"ERROR: backup {args.backup_arn} is "
|
||||||
|
f"{details['BackupStatus']} for table "
|
||||||
|
f"{src_table['TableName']!r}; need an AVAILABLE backup "
|
||||||
|
f"of {name!r}. Aborting before truncate."
|
||||||
|
)
|
||||||
|
dest_keys = [{k: item[k] for k in cfg["keys"]} for item in scan_items(dst)]
|
||||||
|
print(f"[{name}] truncate: {len(dest_keys)} destination rows to delete")
|
||||||
|
if args.execute:
|
||||||
|
with dst.batch_writer() as batch:
|
||||||
|
for key in dest_keys:
|
||||||
|
batch.delete_item(Key=key)
|
||||||
|
print(f"[{name}] truncate: done")
|
||||||
|
else:
|
||||||
|
print(f"[{name}] DRY-RUN: no deletes performed")
|
||||||
|
|
||||||
|
|
||||||
def copy_table(name, cfg, args):
|
def copy_table(name, cfg, args):
|
||||||
src = session_table(args.source_profile, SOURCE_ACCOUNT, name)
|
src = session_table(args.source_profile, SOURCE_ACCOUNT, name)
|
||||||
dst = session_table(args.dest_profile, DEST_ACCOUNT, name)
|
dst = session_table(args.dest_profile, DEST_ACCOUNT, name)
|
||||||
|
|
@ -129,21 +159,7 @@ def copy_table(name, cfg, args):
|
||||||
filter_kwargs = comments_cutoff_filter(normalize_cutoff(args.cutoff))
|
filter_kwargs = comments_cutoff_filter(normalize_cutoff(args.cutoff))
|
||||||
|
|
||||||
if cfg["mode"] == "truncate_load":
|
if cfg["mode"] == "truncate_load":
|
||||||
if args.execute and not args.backup_arn:
|
_truncate_destination(name, cfg, dst, args)
|
||||||
sys.exit(
|
|
||||||
f"ERROR: {name} is truncate-and-load; pass --backup-arn of an "
|
|
||||||
"AVAILABLE on-demand backup of the DESTINATION table taken "
|
|
||||||
"just now (aws dynamodb create-backup / describe-backup)."
|
|
||||||
)
|
|
||||||
dest_keys = [{k: item[k] for k in cfg["keys"]} for item in scan_items(dst)]
|
|
||||||
print(f"[{name}] truncate: {len(dest_keys)} destination rows to delete")
|
|
||||||
if args.execute:
|
|
||||||
with dst.batch_writer() as batch:
|
|
||||||
for key in dest_keys:
|
|
||||||
batch.delete_item(Key=key)
|
|
||||||
print(f"[{name}] truncate: done")
|
|
||||||
else:
|
|
||||||
print(f"[{name}] DRY-RUN: no deletes performed")
|
|
||||||
|
|
||||||
copied = 0
|
copied = 0
|
||||||
if args.execute:
|
if args.execute:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue