mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-07 16:08:54 +00:00
Phase 1 of the mgmt (328440206208) -> seahaven-prod (011934824531) migration. No behavior change in-account; everything here is additive or account-portability hygiene: - infra/deploy-role/: reviewed OIDC deploy-role artifacts for prod (trust main-only, cdk-hnb659fds-* AssumeRole, smoke-invoke-lambda scoped to exactly the two email-processor fn ARNs). Codifies the previously out-of-band smoke-invoke grant. - Table resource policies: make_slack_bot_read_policy in cdk/common.py, applied to purchase-orders, verified-sites, WorkOrders, WorkOrderComments (NOT pending-site-review; no bot consumer). Grants the mgmt-resident seahaven-slack-bot roles read-only cross-account access post-move (bot-side identity grants land in the slack-bot repo). - scripts/migrate_tables.py: dry-run-default backfill tool implementing the plan's per-table semantics (superset overwrite, ingested_at cutoff for WorkOrderComments, backup-gated truncate-and-load for the two site tables) plus a verify subcommand (count parity, spot checks, sticky-Cancelled drift check). - tests/test_resource_policy_helper.py: statement-shape unit tests + static pins that exactly the four bot-read tables carry the policy. - Account-literal fixes: account-agnostic fixture bucket in test_reprocess_contract; runbook/README/po-template-parser account references updated to prod with historical mgmt notes; README gains the account-prerequisites list (imported-by-name dependencies). deploy.yaml is deliberately unchanged (push-to-main auto-deploy kept). Merge is held until migration Phase 0 completes; flipping the AWS_DEPLOY_ROLE_ARN repo secret and merging this PR IS the first prod deploy.
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
"""Unit tests for common.make_slack_bot_read_policy (prod-migration PR).
|
|
|
|
Two layers, no full-app synth (PoIngestStack synth would trigger Docker
|
|
bundling in CI):
|
|
|
|
1. Synthesizes a minimal throwaway stack with one table using the helper and
|
|
asserts the exact ResourcePolicy statement shape (sid, read-only actions,
|
|
mgmt-account principal, PrincipalArn prefix condition, per-table ARN).
|
|
2. Static pins that the four bot-read tables in the real stack files pass
|
|
resource_policy= and pending-site-review does NOT (no bot consumer).
|
|
"""
|
|
|
|
import importlib.util
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import aws_cdk as cdk
|
|
import pytest
|
|
from aws_cdk import assertions
|
|
from aws_cdk import aws_dynamodb as dynamodb
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"cdk_common", REPO_ROOT / "cdk" / "common.py"
|
|
)
|
|
common = importlib.util.module_from_spec(_spec)
|
|
sys.modules["cdk_common"] = common
|
|
_spec.loader.exec_module(common)
|
|
|
|
MGMT_ACCOUNT = "328440206208"
|
|
PROD_ACCOUNT = "011934824531"
|
|
|
|
|
|
def _synth_policy(table_name):
|
|
app = cdk.App()
|
|
stack = cdk.Stack(
|
|
app,
|
|
"T",
|
|
env=cdk.Environment(account=PROD_ACCOUNT, region="us-east-1"),
|
|
)
|
|
dynamodb.Table(
|
|
stack,
|
|
"Tbl",
|
|
table_name=table_name,
|
|
partition_key=dynamodb.Attribute(name="pk", type=dynamodb.AttributeType.STRING),
|
|
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
resource_policy=common.make_slack_bot_read_policy(stack, table_name),
|
|
)
|
|
template = assertions.Template.from_stack(stack)
|
|
tables = template.find_resources("AWS::DynamoDB::Table")
|
|
(props,) = [r["Properties"] for r in tables.values()]
|
|
statements = props["ResourcePolicy"]["PolicyDocument"]["Statement"]
|
|
assert len(statements) == 1
|
|
return statements[0]
|
|
|
|
|
|
def test_statement_shape_and_actions():
|
|
s = _synth_policy("purchase-orders")
|
|
assert s["Sid"] == "AllowMgmtSlackBotRead"
|
|
assert s["Effect"] == "Allow"
|
|
assert sorted(s["Action"]) == [
|
|
"dynamodb:BatchGetItem",
|
|
"dynamodb:DescribeTable",
|
|
"dynamodb:GetItem",
|
|
"dynamodb:Query",
|
|
"dynamodb:Scan",
|
|
]
|
|
|
|
|
|
def test_no_write_actions():
|
|
s = _synth_policy("purchase-orders")
|
|
writes = {
|
|
a
|
|
for a in s["Action"]
|
|
if re.search(r"Put|Update|Delete|Write|Create|Restore|Export|Import", a)
|
|
}
|
|
assert not writes
|
|
|
|
|
|
def test_principal_is_mgmt_account_root():
|
|
s = _synth_policy("WorkOrders")
|
|
principal = s["Principal"]["AWS"]
|
|
# Token-joined root-principal ARN: {"Fn::Join": ["", ["arn:", {...}, ":iam::328440206208:root"]]}
|
|
assert MGMT_ACCOUNT in str(principal)
|
|
assert ":root" in str(principal)
|
|
|
|
|
|
def test_principal_arn_condition_pins_bot_stack_prefix():
|
|
s = _synth_policy("WorkOrders")
|
|
assert s["Condition"]["ArnLike"]["aws:PrincipalArn"] == (
|
|
f"arn:aws:iam::{MGMT_ACCOUNT}:role/seahaven-slack-bot-*"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["purchase-orders", "WorkOrderComments"])
|
|
def test_resource_is_exactly_the_named_table_arn(name):
|
|
import json
|
|
|
|
s = _synth_policy(name)
|
|
# format_arn leaves the partition as {"Ref": "AWS::Partition"}, so the
|
|
# resource is an Fn::Join; assert on the deterministic joined tail.
|
|
dumped = json.dumps(s["Resource"])
|
|
assert f"dynamodb:us-east-1:{PROD_ACCOUNT}:table/{name}" in dumped
|
|
# Exactly one resource entry (no index wildcard, no extra ARNs).
|
|
assert not isinstance(s["Resource"], list) or len(s["Resource"]) == 1
|
|
|
|
|
|
# --- static pins against the real stack files ---
|
|
|
|
_PO_STACK = (REPO_ROOT / "cdk" / "po_stack.py").read_text()
|
|
_WO_STACK = (REPO_ROOT / "cdk" / "wo_stack.py").read_text()
|
|
|
|
|
|
def _table_blocks(source):
|
|
"""Map table_name -> the dynamodb.Table(...) call block containing it."""
|
|
blocks = {}
|
|
for m in re.finditer(r"dynamodb\.Table\(", source):
|
|
depth, i = 0, m.end() - 1
|
|
while i < len(source):
|
|
depth += {"(": 1, ")": -1}.get(source[i], 0)
|
|
if depth == 0:
|
|
break
|
|
i += 1
|
|
block = source[m.start() : i + 1]
|
|
nm = re.search(r'table_name="([^"]+)"', block)
|
|
if nm:
|
|
blocks[nm.group(1)] = block
|
|
return blocks
|
|
|
|
|
|
def test_bot_read_tables_carry_resource_policy():
|
|
blocks = {**_table_blocks(_PO_STACK), **_table_blocks(_WO_STACK)}
|
|
for name in [
|
|
"purchase-orders",
|
|
"verified-sites",
|
|
"WorkOrders",
|
|
"WorkOrderComments",
|
|
]:
|
|
assert "make_slack_bot_read_policy" in blocks[name], name
|
|
|
|
|
|
def test_pending_site_review_has_no_resource_policy():
|
|
blocks = _table_blocks(_PO_STACK)
|
|
assert "resource_policy" not in blocks["pending-site-review"]
|