verify(P20): cross-account-role-automation-offline — 4-layer verify PASS + ship

VERIFY: structural — Terraform + docs + tests; behavioral — terraform validate + 3 tests + CI PASS; quality — offline-proven only (D-114), nova: ABAC tags.

---ci---
project: acdl
phase: 20
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-184]
  partial: []
---/ci---
This commit is contained in:
Jon Chery
2026-08-01 13:32:54 +00:00
parent fe312c6292
commit 9f9d971287
4 changed files with 287 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
# terraform/onboarding/ — Consumer deploy-role + ABAC tag grant (P20, REQ-184)
Offline-proven Terraform for the cross-account consumer deploy-role +
`nova:owner` ABAC tag grant. This is the "role grant" half of the
no-humans onboarding flow (D-113); the "request" half is P18 (Lambda
action) + P19 (env-file autogen).
## Scope (D-114)
This Terraform is **offline-proven only** in v1.16:
- `terraform validate` passes.
- `terraform plan` (with `NOVA_AWS_ACCOUNT_ID` set) produces the expected
role + policy.
- **No live apply** — `NOVA_LIFECYCLE_MODE=plan` default. Live apply is
deferred to a future feature milestone (D-113/D-114).
## Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `consumer_repo` | The consumer repository (org/repo) | `acdl/consumer-a` |
| `owner_id` | The owning team (for `nova:owner` tag) | `team-a` |
| `account_id` | The consumer's AWS account ID | `000000000000` |
| `region` | AWS region | `us-east-1` |
## Resources
- `aws_iam_role.consumer_deploy` — the consumer's deploy role with a
trust policy (assumed by the consumer's CI runner).
- `aws_iam_role_policy.consumer_invoke` — inline policy granting
`lambda:InvokeFunctionUrl` on the platform Lambda, scoped via
`aws:PrincipalTag/nova:owner == var.owner_id` (ABAC).
- `aws_iam_tag.owner` — tags the role with `nova:owner` + `nova:contract`.
## Usage (offline)
```bash
cd terraform/onboarding
terraform init -backend=false
terraform validate
NOVA_AWS_ACCOUNT_ID=123456789012 terraform plan -var consumer_repo=acdl/my-app -var owner_id=team-x
```
+120
View File
@@ -0,0 +1,120 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
variable "consumer_repo" {
description = "The consumer repository (org/repo) — for the nova:contract tag."
type = string
default = "acdl/consumer-a"
}
variable "owner_id" {
description = "The owning team (for the nova:owner ABAC tag)."
type = string
default = "team-a"
}
variable "account_id" {
description = "The consumer's AWS account ID (where the deploy role is created)."
type = string
default = "000000000000"
}
variable "region" {
description = "AWS region."
type = string
default = "us-east-1"
}
provider "aws" {
region = var.region
}
# P20 (REQ-184): consumer deploy role — the role the consumer's CI runner
# assumes to invoke the platform Lambda + deploy via the reusable workflow.
# The trust policy allows the consumer's CI runner (GitHub Actions /
# Gitea act_runner) to assume this role. In a real deployment, the trust
# policy is scoped to the consumer's OIDC provider; for offline-proven
# mode, a placeholder trust is used.
resource "aws_iam_role" "consumer_deploy" {
name = "nova-${replace(var.consumer_repo, "/", "-")}-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
# Placeholder: in a real deployment, this is the consumer's
# OIDC provider ARN. Offline-proven mode uses a wildcard.
Federated = "arn:aws:iam::${var.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:${var.consumer_repo}:*"
}
}
}
]
})
tags = {
"nova:owner" = var.owner_id
"nova:contract" = var.consumer_repo
"nova:environment" = "dev"
}
}
# P20 (REQ-184): inline policy granting the consumer's deploy role the
# right to invoke the platform Lambda's Function URL, scoped via ABAC
# (aws:PrincipalTag/nova:owner == var.owner_id). The platform Lambda's
# resource-based policy + the consumer_invoke_policy.json template
# enforce the ABAC scope at the Lambda side; this policy grants the
# invoke permission on the consumer side.
resource "aws_iam_role_policy" "consumer_invoke" {
name = "nova-consumer-invoke"
role = aws_iam_role.consumer_deploy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"lambda:InvokeFunctionUrl",
]
Resource = [
# The platform Lambda ARN (cross-account). The account_id is
# the platform account, not the consumer account. For offline-
# proven mode, a placeholder ARN is used.
"arn:aws:lambda:${var.region}:000000000000:function:nova-contract-ingestor"
]
Condition = {
StringEquals = {
"aws:PrincipalTag/nova:owner" = var.owner_id
}
}
}
]
})
}
output "consumer_deploy_role_arn" {
description = "The ARN of the consumer deploy role."
value = aws_iam_role.consumer_deploy.arn
}
output "consumer_deploy_role_name" {
description = "The name of the consumer deploy role."
value = aws_iam_role.consumer_deploy.name
}