Skip to content

Commit e8326b3

Browse files
milldrclaude
andcommitted
feat: add TerraformAccess, RootAccess permission sets and idp_groups support
New features: - Add TerraformPlanAccess permission set (read-only state and account access) - Add TerraformApplyAccess permission set (read/write state and admin access) - Add RootAccess permission set (centralized root access via sts:AssumeRoot) - Add provider-root.tf for root account assignments via profile - Add idp_groups variable to look up IdP-synced groups - Add group_ids output combining manual and IdP groups - Add account_map_enabled variable to toggle between remote state and static mapping - Add account_map variable for static account ID mapping Changes: - Update main.tf to conditionally use account_map module or static variable - Update main.tf to include new permission sets and idp_groups data source - Update remote-state.tf with conditional bypass for account_map - Update variables.tf with new variables - Update outputs.tf with group_ids output These changes support the account-map deprecation effort by enabling profile-based authentication and static account mappings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ac149f5 commit e8326b3

File tree

8 files changed

+257
-8
lines changed

8 files changed

+257
-8
lines changed

src/main.tf

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
locals {
22
enabled = module.this.enabled
33

4-
account_map = module.account_map.outputs.full_account_map
5-
root_account = local.account_map[module.account_map.outputs.root_account_account_name]
4+
# Use remote state outputs when account_map_enabled is true, otherwise use static variable
5+
account_map = var.account_map_enabled ? module.account_map.outputs.full_account_map : var.account_map.full_account_map
6+
root_account = local.account_map[var.account_map_enabled ? module.account_map.outputs.root_account_account_name : var.account_map.root_account_account_name]
67

78
account_assignments_groups = flatten([
89
for account_key, account in var.account_assignments : [
@@ -73,6 +74,20 @@ resource "aws_identitystore_group" "manual" {
7374
identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]
7475
}
7576

77+
# Look up IdP-managed groups (synced from Google Workspace, Okta, etc.)
78+
data "aws_identitystore_group" "idp" {
79+
for_each = toset(var.idp_groups)
80+
81+
identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]
82+
83+
alternate_identifier {
84+
unique_attribute {
85+
attribute_path = "DisplayName"
86+
attribute_value = each.key
87+
}
88+
}
89+
}
90+
7691
module "permission_sets" {
7792
source = "cloudposse/sso/aws//modules/permission-sets"
7893
version = "1.2.0"
@@ -86,6 +101,9 @@ module "permission_sets" {
86101
local.identity_access_permission_sets,
87102
local.poweruser_access_permission_set,
88103
local.read_only_access_permission_set,
104+
local.root_access_permission_set,
105+
local.terraform_plan_access_permission_set,
106+
local.terraform_apply_access_permission_set,
89107
local.terraform_update_access_permission_set,
90108
)
91109

src/outputs.tf

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ output "sso_account_assignments" {
99
}
1010

1111
output "group_ids" {
12-
value = { for group_key, group_output in aws_identitystore_group.manual : group_key => group_output.group_id }
13-
description = "Group IDs created for Identity Center"
12+
value = merge(
13+
{ for group_key, group_output in aws_identitystore_group.manual : group_key => group_output.group_id },
14+
{ for group_key, group_output in data.aws_identitystore_group.idp : group_key => group_output.group_id }
15+
)
16+
description = "Group IDs for Identity Center (includes both manually created and IdP-synced groups)"
1417
}

src/policy-RootAccess.tf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
locals {
2+
root_access_permission_set = [{
3+
name = "RootAccess",
4+
description = "Allow centralized root access to member accounts via sts:AssumeRoot",
5+
relay_state = "",
6+
session_duration = var.session_duration,
7+
tags = {},
8+
inline_policy = jsonencode({
9+
Version = "2012-10-17"
10+
Statement = [{
11+
Sid = "AssumeRootAccess"
12+
Effect = "Allow"
13+
Action = "sts:AssumeRoot"
14+
Resource = "arn:${local.aws_partition}:iam::*:root"
15+
Condition = {
16+
StringEquals = {
17+
"sts:TaskPolicyArn" = [
18+
"arn:${local.aws_partition}:iam::aws:policy/root-task/IAMAuditRootUserCredentials",
19+
"arn:${local.aws_partition}:iam::aws:policy/root-task/IAMCreateRootUserPassword",
20+
"arn:${local.aws_partition}:iam::aws:policy/root-task/IAMDeleteRootUserCredentials",
21+
"arn:${local.aws_partition}:iam::aws:policy/root-task/S3UnlockBucketPolicy",
22+
"arn:${local.aws_partition}:iam::aws:policy/root-task/SQSUnlockQueuePolicy"
23+
]
24+
}
25+
}
26+
}]
27+
})
28+
policy_attachments = []
29+
customer_managed_policy_attachments = []
30+
}]
31+
}

src/policy-TerraformAccess.tf

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Shared variables for Terraform state backend access
2+
variable "tf_access_bucket_arn" {
3+
type = string
4+
description = "The ARN of the S3 bucket for the Terraform state backend."
5+
default = ""
6+
}
7+
8+
variable "tf_access_dynamodb_table_arn" {
9+
type = string
10+
description = "The ARN of the DynamoDB table for the Terraform state backend."
11+
default = ""
12+
}
13+
14+
variable "tf_access_role_arn" {
15+
type = string
16+
description = "The ARN of the IAM role for accessing the Terraform state backend."
17+
default = ""
18+
}
19+
20+
locals {
21+
tf_access_enabled = module.this.enabled && var.tf_access_bucket_arn != "" && var.tf_access_role_arn != ""
22+
23+
# Terraform Plan Access permission set
24+
terraform_plan_access_permission_set = local.tf_access_enabled ? [{
25+
name = "TerraformPlanAccess",
26+
description = "Allow read-only access to Terraform state for planning",
27+
relay_state = "",
28+
session_duration = var.session_duration,
29+
tags = {},
30+
inline_policy = one(data.aws_iam_policy_document.terraform_plan_access[*].json),
31+
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/ReadOnlyAccess"]
32+
customer_managed_policy_attachments = []
33+
}] : []
34+
35+
# Terraform Apply Access permission set
36+
terraform_apply_access_permission_set = local.tf_access_enabled ? [{
37+
name = "TerraformApplyAccess",
38+
description = "Allow full access to Terraform state and account for applying changes",
39+
relay_state = "",
40+
session_duration = var.session_duration,
41+
tags = {},
42+
inline_policy = one(data.aws_iam_policy_document.terraform_apply_access[*].json),
43+
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AdministratorAccess"]
44+
customer_managed_policy_attachments = []
45+
}] : []
46+
}
47+
48+
# Terraform Plan Access - Read-only state access, read-only account access
49+
data "aws_iam_policy_document" "terraform_plan_access" {
50+
count = local.tf_access_enabled ? 1 : 0
51+
52+
# Read-only access to Terraform state S3 bucket
53+
statement {
54+
sid = "TerraformStateBackendS3BucketReadOnly"
55+
effect = "Allow"
56+
actions = [
57+
"s3:ListBucket",
58+
"s3:GetObject",
59+
]
60+
resources = [var.tf_access_bucket_arn, "${var.tf_access_bucket_arn}/*"]
61+
}
62+
63+
# Allow assuming the Terraform state backend role (needed to read state)
64+
statement {
65+
sid = "TerraformStateBackendAssumeRole"
66+
effect = "Allow"
67+
actions = [
68+
"sts:AssumeRole",
69+
"sts:TagSession",
70+
"sts:SetSourceIdentity",
71+
]
72+
resources = [var.tf_access_role_arn]
73+
}
74+
75+
# Allow EC2 DescribeRegions - required by many Terraform modules for region validation
76+
statement {
77+
sid = "EC2DescribeRegions"
78+
effect = "Allow"
79+
actions = [
80+
"ec2:DescribeRegions",
81+
]
82+
resources = ["*"]
83+
}
84+
}
85+
86+
# Terraform Apply Access - Read/write state access, admin account access
87+
data "aws_iam_policy_document" "terraform_apply_access" {
88+
count = local.tf_access_enabled ? 1 : 0
89+
90+
statement {
91+
sid = "TerraformStateBackendS3Bucket"
92+
effect = "Allow"
93+
actions = [
94+
"s3:ListBucket",
95+
"s3:GetObject",
96+
"s3:PutObject",
97+
]
98+
resources = [var.tf_access_bucket_arn, "${var.tf_access_bucket_arn}/*"]
99+
}
100+
101+
# Only add the DynamoDB table statement if the DynamoDB table ARN is set.
102+
# You may not have created a DynamoDB table if you're using S3 state locking
103+
dynamic "statement" {
104+
for_each = (local.tf_access_enabled && var.tf_access_dynamodb_table_arn != "") ? [1] : []
105+
106+
content {
107+
sid = "TerraformStateBackendDynamoDbTable"
108+
effect = "Allow"
109+
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
110+
resources = [var.tf_access_dynamodb_table_arn]
111+
}
112+
}
113+
114+
# Allow assuming the Terraform state backend role
115+
statement {
116+
sid = "TerraformStateBackendAssumeRole"
117+
effect = "Allow"
118+
actions = [
119+
"sts:AssumeRole",
120+
"sts:TagSession",
121+
"sts:SetSourceIdentity",
122+
]
123+
resources = [var.tf_access_role_arn]
124+
}
125+
126+
# Allow EC2 DescribeRegions - required by many Terraform modules for region validation
127+
statement {
128+
sid = "EC2DescribeRegions"
129+
effect = "Allow"
130+
actions = [
131+
"ec2:DescribeRegions",
132+
]
133+
resources = ["*"]
134+
}
135+
}

src/provider-root.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
variable "root_profile_name" {
2+
type = string
3+
description = "The profile name to use for the root account"
4+
default = "core-root/terraform"
5+
}
6+
7+
provider "aws" {
8+
# The AWS provider to use to make changes in the root account
9+
alias = "root"
10+
region = var.region
11+
12+
profile = var.root_profile_name
13+
}

src/providers.tf

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
variable "account_map_enabled" {
2+
type = bool
3+
description = <<-EOT
4+
When true, uses the account-map component to look up account IDs dynamically.
5+
When false, uses the static account_map variable instead. Set to false when
6+
using Atmos Auth profiles and static account mappings.
7+
EOT
8+
default = true
9+
}
10+
11+
variable "account_map" {
12+
type = object({
13+
full_account_map = map(string)
14+
audit_account_account_name = optional(string, "")
15+
root_account_account_name = optional(string, "")
16+
})
17+
description = <<-EOT
18+
Static account map used when account_map_enabled is false.
19+
Provides account name to account ID mapping without requiring the account-map component.
20+
EOT
21+
default = {
22+
full_account_map = {}
23+
audit_account_account_name = ""
24+
root_account_account_name = ""
25+
}
26+
}
27+
128
# This component is unusual in that part of it must be deployed to the `root`
229
# account. You have the option of where to deploy the remaining part, and
330
# Cloud Posse recommends you deploy it also to the `root` account, however

src/remote-state.tf

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1+
# Remote state lookup for the account-map component (or fallback to static mapping).
2+
#
3+
# When account_map_enabled is true:
4+
# - Performs remote state lookup to retrieve account mappings from the account-map component
5+
# - Uses global tenant/environment/stage from iam_roles module for the lookup
6+
#
7+
# When account_map_enabled is false:
8+
# - Bypasses the remote state lookup (bypass = true)
9+
# - Returns the static account_map variable as defaults instead
10+
# - Allows the component to function without the account-map dependency
111
module "account_map" {
212
source = "cloudposse/stack-config/yaml//modules/remote-state"
313
version = "1.8.0"
414

515
component = var.account_map_component_name
6-
environment = module.iam_roles.global_environment_name
7-
stage = module.iam_roles.global_stage_name
8-
tenant = module.iam_roles.global_tenant_name
9-
privileged = var.privileged
16+
tenant = var.account_map_enabled ? module.iam_roles.global_tenant_name : null
17+
environment = var.account_map_enabled ? module.iam_roles.global_environment_name : null
18+
stage = var.account_map_enabled ? module.iam_roles.global_stage_name : null
1019

1120
context = module.this.context
21+
22+
# When account_map is disabled, bypass remote state and use the static account_map variable
23+
bypass = !var.account_map_enabled
24+
defaults = var.account_map
1225
}

src/variables.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,12 @@ variable "overridable_team_permission_set_name_pattern" {
7777
default = "Identity%sTeamAccess"
7878
}
7979

80+
variable "idp_groups" {
81+
type = list(string)
82+
description = <<-EOT
83+
List of IdP group names to look up and include in the group_ids output.
84+
These groups are managed by your Identity Provider (e.g., Google Workspace, Okta)
85+
and synced to AWS Identity Center. This allows referencing their IDs in other components.
86+
EOT
87+
default = []
88+
}

0 commit comments

Comments
 (0)