Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Breaking changes

### overridable_team_permission_sets_enabled deprecated, default changed

In `modules/roles-to-principals`, the input `overridable_team_permission_sets_enabled`
has been deprecated and the default value has been changed to `false`. This will
cause changes in the Terraform plan, but it is likely that they will be
inconsequential, because this feature never worked with Dynamic Terraform Roles,
even though it was introduced in the same PR.

To enable the intended behavior, a new feature has been added to `aws-team-roles`
and `modules/iam-roles`: `trusted_identity_permission_sets`. This feature
allows you to explicitly configure permission sets in the `identity` account to
be allowed to assume roles in other accounts, just as you do with `trusted_teams`.
This has the added advantage of being able to configure non-team permissions
sets to be trusted.
8 changes: 3 additions & 5 deletions src/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions src/dynamic-roles--stacks.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Part of the Terraform Dynamic Roles implementation,
# this files reads in all the `aws-team` and `aws-team-roles` stacks
# and extracts the relevant information.

# The `utils_describe_stacks` data resources use the Cloud Posse Utils provider to describe Atmos stacks, and then
# we merge the results into `local.all_team_vars`. This is the same as running the following locally:
# ```
# atmos describe stacks --components=aws-teams,aws-team-roles --component-types=terraform --sections=vars
# ```
# The result of these stack descriptions includes all metadata for the given components. For example, we now
# can filter the result to find all stacks where either `aws-teams` or `aws-team-roles` are deployed.
# Note that unlike in earlier implementations, we now expect `aws-team-roles` to be deployed in all accounts,
# including the `identity` account.
#
# In particular, we can use this data to find the name of the account via `null-label` (defined by
# `null-label.descriptor_formats.account_name`, typically `<tenant>-<stage>`) where team roles are deployed.
# We then determine which roles are provisioned and which teams can access any given role in any particular account.
#
# `descriptor_formats.account_name` is typically defined in `stacks/orgs/NAMESPACE/_defaults.yaml`, and if not
# defined, the stack name will default to `stage`.`
#
# If `namespace` is included in `descriptor_formats.account_name`, then we additionally filter to only stacks with
# the same `namespace` as `module.this.namespace`. See `local.stack_namespace_index` and `local.stack_namespace_index`
#
# https://atmos.tools/cli/commands/describe/stacks/
# https://registry.terraform.io/providers/cloudposse/utils/latest/docs/data-sources/describe_stacks

locals {
# We would like to use code like this:
# teams_stacks = local.dynamic_role_enabled ? { for k, v ... } : {}
# but that generates an error: "Inconsistent conditional result types"
# See https://github.com/hashicorp/terraform/issues/33303
# To work around this, we have "empty" values that depend on the condition.
empty_map = {
true = null
false = {}
}
empty = local.empty_map[local.dynamic_role_enabled]

# If a namespace is included with the stack name, only loop through stacks in the same namespace
# zero-based index showing position of the namespace in the stack name
stack_namespace_index = try(index(module.this.normalized_context.descriptor_formats.stack.labels, "namespace"), -1)
stack_has_namespace = local.stack_namespace_index >= 0
# stack_account_map maps stack name to account short name
stack_account_map = { for k, v in module.atmos : k => lookup(v.descriptors, "account_name", v.stage) }

# ASSUMPTIONS: The stack pattern is the same for all accounts and uses the same delimiter as null-label

# Part 1, get all the aws-teams information:
# Get all the aws-teams stacks, optionally filtered by namespace, so that we only work in the current namespace.
teams_stacks = local.dynamic_role_enabled ? {
for k, v in yamldecode(data.utils_describe_stacks.teams[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false)
} : local.empty
Comment on lines +51 to +53
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use jsondecode instead of yamldecode
The output attribute of data.utils_describe_stacks is JSON, so yamldecode may misinterpret it. Switch to jsondecode for reliable parsing:

- for k, v in yamldecode(data.utils_describe_stacks.teams[0].output) : ...
+ for k, v in jsondecode(data.utils_describe_stacks.teams[0].output) : ...

Apply the same change on lines 68–70 for team_roles.

Also applies to: 68-70

🤖 Prompt for AI Agents
In src/dynamic-roles--stacks.tf around lines 51 to 53 and also lines 68 to 70,
replace the use of yamldecode with jsondecode when parsing the output attribute
of data.utils_describe_stacks, as the output is JSON formatted. This change
ensures correct parsing by switching the decoding function from yamldecode to
jsondecode in both the teams_stacks and team_roles assignments.


# Extract components.terraform.aws-teams.vars. Key is the stack name, value is the vars.
teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars if try(v.components.terraform.aws-teams.vars, null) != null }
# Extract components.terraform.aws-teams.vars.teams_config, drop the stack name.
teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty
Comment on lines +56 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against empty teams_vars to prevent indexing errors
If no aws-teams stacks are found, values(local.teams_vars)[0] will error. Wrap in a try with a fallback empty map:

- teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty
+ teams_config = local.dynamic_role_enabled ? try(values(local.teams_vars)[0].teams_config, {}) : local.empty
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars if try(v.components.terraform.aws-teams.vars, null) != null }
# Extract components.terraform.aws-teams.vars.teams_config, drop the stack name.
teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty
teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars if try(v.components.terraform.aws-teams.vars, null) != null }
# Extract components.terraform.aws-teams.vars.teams_config, drop the stack name.
teams_config = local.dynamic_role_enabled ? try(values(local.teams_vars)[0].teams_config, {}) : local.empty
🤖 Prompt for AI Agents
In src/dynamic-roles--stacks.tf around lines 56 to 58, the code accesses
values(local.teams_vars)[0] without checking if teams_vars is empty, which can
cause an indexing error. To fix this, wrap the access in a try expression with a
fallback to an empty map, ensuring that if teams_vars is empty, the code safely
returns an empty map instead of failing.

# Extract enabled team names from components.terraform.aws-teams.vars.teams_config
team_names = [for k, v in local.teams_config : k if try(v.enabled, true)]
# Convert team names to IAM role ARNs
team_arns = { for team_name in local.team_names : team_name => format(local.iam_role_arn_templates[local.account_role_map.identity], team_name) }
# Now we have local.team_arns which is a list of IAM role ARNs for each team.
# This covers all the roles we have explicitly configured as teams.

# Part 2, get all the aws-team-roles information:
# Get all the aws-team-roles stacks, optionally filtered by namespace, so that we only work in the current namespace.
team_roles_stacks = local.dynamic_role_enabled ? {
for k, v in yamldecode(data.utils_describe_stacks.team_roles[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false)
} : local.empty

# Extract components.terraform.aws-team-roles.vars
team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars if try(v.components.terraform.aws-team-roles.vars, null) != null }

Comment on lines +73 to +74
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Dash-in-key traversal is invalid; fix aws-team-roles access.

-  team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars if try(v.components.terraform.aws-team-roles.vars, null) != null }
+  team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform["aws-team-roles"].vars if try(v.components.terraform["aws-team-roles"].vars, null) != null }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars if try(v.components.terraform.aws-team-roles.vars, null) != null }
team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform["aws-team-roles"].vars if try(v.components.terraform["aws-team-roles"].vars, null) != null }
🤖 Prompt for AI Agents
In src/dynamic-roles--stacks.tf around lines 73 to 74, the code attempts to
access a map key containing hyphens using dot notation
(v.components.terraform.aws-team-roles.vars), which is invalid in Terraform;
change the access to bracket notation with a quoted key (e.g.,
v.components.terraform["aws-team-roles"].vars) and apply the same bracket syntax
inside the try() call so the expression becomes k =>
v.components.terraform["aws-team-roles"].vars if
try(v.components.terraform["aws-team-roles"].vars, null) != null.

# Merge all the vars together so that for each stack name (the keys in the maps),
# we can map it to the appropriate account short name, which we will use as keys in the final map.
all_team_vars = merge(local.teams_vars, local.team_roles_vars)

}

data "utils_describe_stacks" "teams" {
count = local.dynamic_role_enabled ? 1 : 0

components = ["aws-teams"]
component_types = ["terraform"]
sections = ["vars"]
}

data "utils_describe_stacks" "team_roles" {
count = local.dynamic_role_enabled ? 1 : 0

components = ["aws-team-roles"]
component_types = ["terraform"]
sections = ["vars"]
}



module "atmos" {
# local.all_team_vars is empty map when dynamic_role_enabled is false
for_each = local.all_team_vars

source = "cloudposse/label/null"
version = "0.25.0"

enabled = true
namespace = lookup(each.value, "namespace", null)
tenant = lookup(each.value, "tenant", null)
environment = lookup(each.value, "environment", null)
stage = lookup(each.value, "stage", null)
name = lookup(each.value, "name", null)
delimiter = lookup(each.value, "delimiter", null)
attributes = lookup(each.value, "attributes", [])
tags = lookup(each.value, "tags", {})
additional_tag_map = lookup(each.value, "additional_tag_map", {})
label_order = lookup(each.value, "label_order", [])
regex_replace_chars = lookup(each.value, "regex_replace_chars", null)
id_length_limit = lookup(each.value, "id_length_limit", null)
label_key_case = lookup(each.value, "label_key_case", null)
label_value_case = lookup(each.value, "label_value_case", null)
descriptor_formats = lookup(each.value, "descriptor_formats", {})
labels_as_tags = lookup(each.value, "labels_as_tags", [])
}
Loading
Loading