diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..87e4af5
--- /dev/null
+++ b/CHANGELOG.md
@@ -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.
diff --git a/src/README.md b/src/README.md
index 758ad94..4be113c 100644
--- a/src/README.md
+++ b/src/README.md
@@ -70,7 +70,7 @@ components:
| [terraform](#requirement\_terraform) | >= 1.2.0 |
| [aws](#requirement\_aws) | >= 4.9.0, < 6.0.0 |
| [local](#requirement\_local) | >= 1.3 |
-| [utils](#requirement\_utils) | >= 1.10.0 |
+| [utils](#requirement\_utils) | ~> 1.26 |
## Providers
@@ -78,7 +78,7 @@ components:
|------|---------|
| [aws](#provider\_aws) | >= 4.9.0, < 6.0.0 |
| [local](#provider\_local) | >= 1.3 |
-| [utils](#provider\_utils) | >= 1.10.0 |
+| [utils](#provider\_utils) | ~> 1.26 |
## Modules
@@ -102,7 +102,6 @@ components:
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
-| [account\_configuration\_export\_enabled](#input\_account\_configuration\_export\_enabled) | If true, the account configuration information will be exported to a file under `account-info/` | `bool` | `true` | no |
| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
| [artifacts\_account\_account\_name](#input\_artifacts\_account\_account\_name) | The short name for the artifacts account | `string` | `"artifacts"` | no |
| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no |
@@ -110,14 +109,13 @@ components:
| [aws\_config\_identity\_profile\_name](#input\_aws\_config\_identity\_profile\_name) | The AWS config profile name to use as `source_profile` for credentials. | `string` | `null` | no |
| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
-| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
+| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
| [dns\_account\_account\_name](#input\_dns\_account\_account\_name) | The short name for the primary DNS account | `string` | `"dns"` | no |
| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
| [iam\_role\_arn\_template\_template](#input\_iam\_role\_arn\_template\_template) | The template for the template used to render Role ARNs.
The template is first used to render a template for the account that takes only the role name.
Then that rendered template is used to create the final Role ARN for the account.
Default is appropriate when using `tenant` and default label order with `null-label`.
Use `"arn:%s:iam::%s:role/%s-%s-%s-%%s"` when not using `tenant`.
Note that if the `null-label` variable `label_order` is truncated or extended with additional labels, this template will
need to be updated to reflect the new number of labels. | `string` | `"arn:%s:iam::%s:role/%s-%s-%s-%s-%%s"` | no |
| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
| [identity\_account\_account\_name](#input\_identity\_account\_account\_name) | The short name for the account holding primary IAM roles | `string` | `"identity"` | no |
-| [import\_organization\_accounts](#input\_import\_organization\_accounts) | Retrieve accounts from AWS Organizations and import them into the account map.
Set false for brownfield environments where you want to curate the list of
accounts manually via the `account` component with a static backend.
Note that the brownfield `account` component needs to include the `root` account
in the `account_names_account_ids` map, whereas the greenfield `account` component
does not. | `bool` | `true` | no |
| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no |
| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no |
| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no |
diff --git a/src/dynamic-roles--stacks.tf b/src/dynamic-roles--stacks.tf
new file mode 100644
index 0000000..7f30d0f
--- /dev/null
+++ b/src/dynamic-roles--stacks.tf
@@ -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 `-`) 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
+
+ # 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
+ # 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 }
+
+ # 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", [])
+}
diff --git a/src/dynamic-roles.tf b/src/dynamic-roles.tf
index fe0d6c9..28f09f8 100644
--- a/src/dynamic-roles.tf
+++ b/src/dynamic-roles.tf
@@ -1,116 +1,70 @@
-# 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.
-#
-# 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 `-`) 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
-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"]
-}
+##
+## What we want at the end of all this is a map of maps so we can say:
+##
+## allowed_role = authorized_users[current_user][target_account]
+##
+## To compute this, we have to invert the mapping we are given, and resolve collisions.
+##
+## What we are given is, for every account, a list of principals that are
+## allowed to assume the planner role in that account, and another list of
+## principals that are allowed to assume the terraform role in that account.
+## What we want is principal -> account -> role, where role is apply
+## if they are allowed into the terraform role, and plan otherwise.
+## We do not need to include principals that are not allowed into either role.
+##
+## To make things both easier and more robust, instead of using the IAM Role ARN
+## for people using AWS SSO Permission Sets, we will use the combination of the
+## account ID and the Permission Set Name to identify the principal.
locals {
dynamic_role_enabled = module.this.enabled && var.terraform_dynamic_role_enabled
+ identity_account_id = local.account_info_map[local.account_role_map.identity].id
+
# `var.terraform_role_name_map` maps some team role in the `aws-team-roles` configuration to "plan" and some other team to "apply".
apply_role = var.terraform_role_name_map.apply
plan_role = var.terraform_role_name_map.plan
- # 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 = { for k, v in module.atmos : k => lookup(v.descriptors, "account_name", v.stage) }
-
- # 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 = {}
+ # For every team-roles configuration, normalize authorized principals to a list like this:
+ # { "account-name" = { "plan" = [ "principal-arn", ... ], "apply" = [ "principal-arn", ... ] } }
+ # This is made complicated because principals can be specified as:
+ # - a principal ARN via trusted_role_arns
+ # - a team name via trusted_teams
+ # - a permission set in the `identity` account via trusted_identity_permission_sets
+ # - a permission set in the target account via trusted_permission_sets
+ account_auths = {
+ for stack, vars in local.team_roles_vars : local.stack_account_map[stack] => {
+ for i, role in [local.apply_role, local.plan_role] : i == 0 ? "apply" : "plan" => concat(
+ [for principal in vars.roles[role].trusted_role_arns : principal],
+ [for principal in vars.roles[role].trusted_teams : local.team_arns[principal]],
+ [
+ for principal in vars.roles[role].trusted_identity_permission_sets :
+ format("%s:%s", local.identity_account_id, principal)
+ ],
+ [
+ for principal in vars.roles[role].trusted_permission_sets :
+ format("%s:%s", local.account_info_map[local.stack_account_map[stack]].id, principal)
+ ],
+ )
+ }
}
- empty = local.empty_map[local.dynamic_role_enabled]
-
- # ASSUMPTIONS: The stack pattern is the same for all accounts and uses the same delimiter as null-label
- 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
-
- 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 }
- teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty
- team_names = [for k, v in local.teams_config : k if try(v.enabled, true)]
- team_arns = { for team_name in local.team_names : team_name => format(local.iam_role_arn_templates[local.account_role_map.identity], team_name) }
- 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
+ # Get the complete, sorted, deduplicated list of all principals that are allowed to assume the planner role in any account.
+ all_principals = sort(distinct(flatten([for account, roles in local.account_auths : values(roles)])))
- 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 }
-
- all_team_vars = merge(local.teams_vars, local.team_roles_vars)
-
- stack_planners = { for k, v in local.team_roles_vars : k => v.roles[local.plan_role].trusted_teams if try(length(v.roles[local.plan_role].trusted_teams), 0) > 0 && try(v.roles[local.plan_role].enabled, true) }
- stack_terraformers = { for k, v in local.team_roles_vars : k => v.roles[local.apply_role].trusted_teams if try(length(v.roles[local.apply_role].trusted_teams), 0) > 0 && try(v.roles[local.apply_role].enabled, true) }
-
- team_planners = { for team in local.team_names : team => {
- for stack, trusted in local.stack_planners : local.stack_account_map[stack] => "plan" if contains(trusted, team)
- } }
- team_terraformers = { for team in local.team_names : team => {
- for stack, trusted in local.stack_terraformers : local.stack_account_map[stack] => "apply" if contains(trusted, team)
- } }
+ # Build up the principal -> account -> role map by first filling in the map for all principals allowed to assume the apply role.
+ # Then, for each principal allowed to assume the plan role, add the account to the map if it is not already there.
+ apply_principal_auths = {
+ for principal in local.all_principals : principal => {
+ for account, roles in local.account_auths : account => "apply" if contains(roles.apply, principal)
+ }
+ }
- role_arn_terraform_access = { for team in local.team_names : local.team_arns[team] => merge(local.team_planners[team], local.team_terraformers[team]) }
+ # Now create the map with "plan" roles, and overwrite with "apply" roles where they exist.
+ principal_terraform_access_map = {
+ for principal in local.all_principals : principal => merge({
+ for account, roles in local.account_auths : account => "plan" if contains(roles.plan, principal)
+ }, lookup(local.apply_principal_auths, principal, {}))
+ }
}
-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", [])
-}
diff --git a/src/main.tf b/src/main.tf
index 4694a1a..8431ceb 100644
--- a/src/main.tf
+++ b/src/main.tf
@@ -6,11 +6,10 @@ locals {
aws_partition = data.aws_partition.current.partition
legacy_terraform_uses_admin = coalesce(var.legacy_terraform_uses_admin, !var.terraform_dynamic_role_enabled)
- # full_account_map is a map of account names to account IDs, excluding suspended accounts.
- full_account_map = var.import_organization_accounts ? {
+ full_account_map = {
for acct in data.aws_organizations_organization.organization.accounts
: acct.name == var.root_account_aws_name ? var.root_account_account_name : acct.name => acct.id if acct.status != "SUSPENDED"
- } : module.accounts.outputs.account_names_account_ids
+ }
iam_role_arn_templates = {
for name, info in local.account_info_map : name => format(var.iam_role_arn_template_template, compact(
diff --git a/src/modules/iam-roles/README.md b/src/modules/iam-roles/README.md
index 3210f9d..6d2e8ed 100644
--- a/src/modules/iam-roles/README.md
+++ b/src/modules/iam-roles/README.md
@@ -14,80 +14,4 @@ the defaults you want to use in your project. For example, if you are not using
at all).
-## Requirements
-
-| Name | Version |
-|------|---------|
-| [terraform](#requirement\_terraform) | >= 1.2.0 |
-| [awsutils](#requirement\_awsutils) | >= 0.16.0 |
-
-## Providers
-
-| Name | Version |
-|------|---------|
-| [awsutils.iam-roles](#provider\_awsutils.iam-roles) | >= 0.16.0 |
-
-## Modules
-
-| Name | Source | Version |
-|------|--------|---------|
-| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 |
-| [always](#module\_always) | cloudposse/label/null | 0.25.0 |
-| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
-
-## Resources
-
-| Name | Type |
-|------|------|
-| [awsutils_caller_identity.current](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/data-sources/caller_identity) | data source |
-
-## Inputs
-
-| Name | Description | Type | Default | Required |
-|------|-------------|------|---------|:--------:|
-| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
-| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no |
-| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
-| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
-| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
-| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
-| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
-| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
-| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no |
-| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no |
-| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no |
-| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` | [
"default"
]
| no |
-| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no |
-| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no |
-| [overridable\_global\_environment\_name](#input\_overridable\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no |
-| [overridable\_global\_stage\_name](#input\_overridable\_global\_stage\_name) | The stage name for the organization management account (where the `account-map` state is stored) | `string` | `"root"` | no |
-| [overridable\_global\_tenant\_name](#input\_overridable\_global\_tenant\_name) | The tenant name used for organization-wide resources | `string` | `"core"` | no |
-| [privileged](#input\_privileged) | True if the Terraform user already has access to the backend | `bool` | `false` | no |
-| [profiles\_enabled](#input\_profiles\_enabled) | Whether or not to use profiles instead of roles for Terraform. Default (null) means to use global settings. | `bool` | `null` | no |
-| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
-| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
-| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
-| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
-
-## Outputs
-
-| Name | Description |
-|------|-------------|
-| [audit\_terraform\_profile\_name](#output\_audit\_terraform\_profile\_name) | The AWS config profile name for Terraform to use to provision resources in the "audit" role account, when profiles are in use |
-| [audit\_terraform\_role\_arn](#output\_audit\_terraform\_role\_arn) | The AWS Role ARN for Terraform to use to provision resources in the "audit" role account, when Role ARNs are in use |
-| [aws\_partition](#output\_aws\_partition) | The AWS "partition" to use when constructing resource ARNs |
-| [current\_account\_account\_name](#output\_current\_account\_account\_name) | The account name (usually `-`) for the account configured by this module's inputs.
Roughly analogous to `data "aws_caller_identity"`, but returning the name of the caller account as used in our configuration. |
-| [dns\_terraform\_profile\_name](#output\_dns\_terraform\_profile\_name) | The AWS config profile name for Terraform to use to provision DNS Zone delegations, when profiles are in use |
-| [dns\_terraform\_role\_arn](#output\_dns\_terraform\_role\_arn) | The AWS Role ARN for Terraform to use to provision DNS Zone delegations, when Role ARNs are in use |
-| [global\_environment\_name](#output\_global\_environment\_name) | The `null-label` `environment` value used for regionless (global) resources |
-| [global\_stage\_name](#output\_global\_stage\_name) | The `null-label` `stage` value for the organization management account (where the `account-map` state is stored) |
-| [global\_tenant\_name](#output\_global\_tenant\_name) | The `null-label` `tenant` value used for organization-wide resources |
-| [identity\_account\_account\_name](#output\_identity\_account\_account\_name) | The account name (usually `-`) for the account holding primary IAM roles |
-| [identity\_terraform\_profile\_name](#output\_identity\_terraform\_profile\_name) | The AWS config profile name for Terraform to use to provision resources in the "identity" role account, when profiles are in use |
-| [identity\_terraform\_role\_arn](#output\_identity\_terraform\_role\_arn) | The AWS Role ARN for Terraform to use to provision resources in the "identity" role account, when Role ARNs are in use |
-| [org\_role\_arn](#output\_org\_role\_arn) | The AWS Role ARN for Terraform to use when SuperAdmin is provisioning resources in the account |
-| [profiles\_enabled](#output\_profiles\_enabled) | When true, use AWS config profiles in Terraform AWS provider configurations. When false, use Role ARNs. |
-| [terraform\_profile\_name](#output\_terraform\_profile\_name) | The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use |
-| [terraform\_role\_arn](#output\_terraform\_role\_arn) | The AWS Role ARN for Terraform to use when provisioning resources in the account, when Role ARNs are in use |
-| [terraform\_role\_arns](#output\_terraform\_role\_arns) | All of the terraform role arns |
diff --git a/src/modules/iam-roles/main.tf b/src/modules/iam-roles/main.tf
index ecfe5c8..46c51ca 100644
--- a/src/modules/iam-roles/main.tf
+++ b/src/modules/iam-roles/main.tf
@@ -37,14 +37,19 @@ locals {
account_name = lookup(module.always.descriptors, "account_name", module.always.stage)
root_account_name = local.account_map.root_account_account_name
- current_user_role_arn = coalesce(one(data.awsutils_caller_identity.current[*].eks_role_arn), one(data.awsutils_caller_identity.current[*].arn), "arn:${local.account_map.aws_partition}:iam::000000000000:role/disabled")
+ current_user_role_arn = coalesce(one(data.awsutils_caller_identity.current[*].eks_role_arn), one(data.awsutils_caller_identity.current[*].arn), "disabled")
- current_identity_account = local.dynamic_terraform_role_enabled ? split(":", local.current_user_role_arn)[4] : ""
+ current_user_account = one(data.awsutils_caller_identity.current[*].account_id)
- terraform_access_map = try(local.account_map.terraform_access_map[local.current_user_role_arn], {})
+ # If the user's current role is an SSO role, extract the permission set from the role ARN.
+ # Use the combination of account ID and Permission Set Name to determine the Terraform role to assume.
+ # Note that `awsutils_caller_identity` has already converted the ARN to the format `arn::iam:::role/`.
+ permission_set = try(format("%s:%s", regex("^arn:[^:]+:iam::([0-9]{12}):role/AWSReservedSSO_([^_]+)_", local.current_user_role_arn)...), null)
- is_root_user = local.current_identity_account == local.account_map.full_account_map[local.root_account_name]
- is_target_user = local.current_identity_account == local.account_map.full_account_map[local.account_name]
+ terraform_access_map = try(local.account_map.terraform_access_map[coalesce(local.permission_set, local.current_user_role_arn)], {})
+
+ is_root_user = local.current_user_account == local.account_map.full_account_map[local.root_account_name]
+ is_target_user = local.current_user_account == local.account_map.full_account_map[local.account_name]
account_org_role_arns = { for name, id in local.account_map.full_account_map : name =>
name == local.root_account_name ? null : format(
@@ -70,9 +75,9 @@ locals {
}
} : {}
- dynamic_terraform_role_types = { for account_name in local.account_map.all_accounts :
+ dynamic_terraform_role_types = local.dynamic_terraform_role_enabled ? { for account_name in local.account_map.all_accounts :
account_name => try(local.terraform_access_map[account_name], "none")
- }
+ } : {}
dynamic_terraform_roles = local.dynamic_terraform_role_enabled ? { for account_name in local.account_map.all_accounts :
account_name => local.dynamic_terraform_role_maps[account_name][local.dynamic_terraform_role_types[account_name]]
diff --git a/src/modules/iam-roles/outputs.tf b/src/modules/iam-roles/outputs.tf
index 0493806..4e1f13a 100644
--- a/src/modules/iam-roles/outputs.tf
+++ b/src/modules/iam-roles/outputs.tf
@@ -9,7 +9,7 @@ output "terraform_role_arns" {
}
output "terraform_profile_name" {
- value = local.profiles_enabled ? local.account_map.profiles[local.account_name] : null
+ value = local.profiles_enabled ? local.account_map.terraform_profiles[local.account_name] : null
description = "The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use"
}
diff --git a/src/modules/roles-to-principals/README.md b/src/modules/roles-to-principals/README.md
index f4d9729..544d164 100644
--- a/src/modules/roles-to-principals/README.md
+++ b/src/modules/roles-to-principals/README.md
@@ -14,66 +14,4 @@ the `tenant` portion of your "root" account (your Organization Management Accoun
at all).
-## Requirements
-
-No requirements.
-
-## Providers
-
-No providers.
-
-## Modules
-
-| Name | Source | Version |
-|------|--------|---------|
-| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 |
-| [always](#module\_always) | cloudposse/label/null | 0.25.0 |
-| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
-
-## Resources
-
-No resources.
-
-## Inputs
-
-| Name | Description | Type | Default | Required |
-|------|-------------|------|---------|:--------:|
-| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
-| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no |
-| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
-| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
-| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
-| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
-| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
-| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
-| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no |
-| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no |
-| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no |
-| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` | [
"default"
]
| no |
-| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no |
-| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no |
-| [overridable\_global\_environment\_name](#input\_overridable\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no |
-| [overridable\_global\_stage\_name](#input\_overridable\_global\_stage\_name) | The stage name for the organization management account (where the `account-map` state is stored) | `string` | `"root"` | no |
-| [overridable\_global\_tenant\_name](#input\_overridable\_global\_tenant\_name) | The tenant name used for organization-wide resources | `string` | `"core"` | no |
-| [overridable\_team\_permission\_set\_name\_pattern](#input\_overridable\_team\_permission\_set\_name\_pattern) | The pattern used to generate the AWS SSO PermissionSet name for each team | `string` | `"Identity%sTeamAccess"` | no |
-| [overridable\_team\_permission\_sets\_enabled](#input\_overridable\_team\_permission\_sets\_enabled) | When true, any roles (teams or team-roles) in the identity account references in `role_map`
will cause corresponding AWS SSO PermissionSets to be included in the `permission_set_arn_like` output.
This has the effect of treating those PermissionSets as if they were teams.
The main reason to set this `false` is if IAM trust policies are exceeding size limits and you are not using AWS SSO. | `bool` | `true` | no |
-| [permission\_set\_map](#input\_permission\_set\_map) | Map of account:[PermissionSet, PermissionSet...] specifying AWS SSO PermissionSets when accessed from specified accounts | `map(list(string))` | `{}` | no |
-| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no |
-| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
-| [role\_map](#input\_role\_map) | Map of account:[role, role...]. Use `*` as role for entire account | `map(list(string))` | `{}` | no |
-| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
-| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
-| [teams](#input\_teams) | List of team names to translate to AWS SSO PermissionSet names | `list(string)` | `[]` | no |
-| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
-
-## Outputs
-
-| Name | Description |
-|------|-------------|
-| [aws\_partition](#output\_aws\_partition) | The AWS "partition" to use when constructing resource ARNs |
-| [full\_account\_map](#output\_full\_account\_map) | Map of account names to account IDs |
-| [permission\_set\_arn\_like](#output\_permission\_set\_arn\_like) | List of Role ARN regexes suitable for IAM Condition `ArnLike` corresponding to given input `permission_set_map` |
-| [principals](#output\_principals) | Consolidated list of AWS principals corresponding to given input `role_map` |
-| [principals\_map](#output\_principals\_map) | Map of AWS principals corresponding to given input `role_map` |
-| [team\_permission\_set\_name\_map](#output\_team\_permission\_set\_name\_map) | Map of team names (from `var.teams` and `role_map["identity"]) to permission set names` |
diff --git a/src/modules/roles-to-principals/main.tf b/src/modules/roles-to-principals/main.tf
index a8629d0..b7ad52d 100644
--- a/src/modules/roles-to-principals/main.tf
+++ b/src/modules/roles-to-principals/main.tf
@@ -51,8 +51,8 @@ locals {
# arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc
# But sometimes AWS SSO ARN includes `/region/`, like:
# arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc
- # If trust polices get too large, some space can be saved by using `*` instead of `aws-reserved/sso.amazonaws.com*`
- format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[acct]),
+ # If trust polices get too large, some space can be saved by using `*` instead of `sso.amazonaws.com*`
+ format("arn:%s:iam::%s:${var.overridable_permission_set_arn_like_role_prefix}%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[acct]),
acct == local.identity_account_name ? distinct(concat(v, local.permission_sets_from_team_roles)) : v
)])))
}
diff --git a/src/modules/roles-to-principals/variables.tf b/src/modules/roles-to-principals/variables.tf
index f942b24..0157ba1 100644
--- a/src/modules/roles-to-principals/variables.tf
+++ b/src/modules/roles-to-principals/variables.tf
@@ -53,10 +53,27 @@ variable "overridable_team_permission_set_name_pattern" {
variable "overridable_team_permission_sets_enabled" {
type = bool
description = <<-EOT
+ DEPRECATED: This feature never worked with Dynamic Terraform Roles, so it is deprecated
+ and the default value has changed from `true` to `false`. Use the explicit
+ `trusted_identity_permission_sets` attribute in `aws-team-roles` instead
+ to enable permission sets in the `identity` account to function as teams.
+ HISTORICAL DESCRIPTION:
When true, any roles (teams or team-roles) in the identity account references in `role_map`
will cause corresponding AWS SSO PermissionSets to be included in the `permission_set_arn_like` output.
This has the effect of treating those PermissionSets as if they were teams.
The main reason to set this `false` is if IAM trust policies are exceeding size limits and you are not using AWS SSO.
EOT
- default = true
+ default = false
}
+
+variable "overridable_permission_set_arn_like_role_prefix" {
+ type = string
+ description = <<-EOT
+ The prefix used to generate the role part of the AWS SSO PermissionSet ARN pattern.
+ The default value is explicit, but does not distinguish between regions.
+ You may want a shorter prefix if trust policies are too large. You may want
+ a longer prefix if you are concerned about IdPs in other regions.
+ EOT
+ default = "role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_"
+ nullable = false
+}
\ No newline at end of file
diff --git a/src/modules/team-assume-role-policy/README.md b/src/modules/team-assume-role-policy/README.md
index ef53b9f..e7f43e6 100644
--- a/src/modules/team-assume-role-policy/README.md
+++ b/src/modules/team-assume-role-policy/README.md
@@ -50,8 +50,8 @@ resource "aws_iam_role" "default" {
| Name | Source | Version |
|------|--------|---------|
-| [allowed\_role\_map](#module\_allowed\_role\_map) | ../roles-to-principals | n/a |
-| [denied\_role\_map](#module\_denied\_role\_map) | ../roles-to-principals | n/a |
+| [allowed\_role\_map](#module\_allowed\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a |
+| [denied\_role\_map](#module\_denied\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a |
| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 |
| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
@@ -78,7 +78,7 @@ resource "aws_iam_role" "default" {
| [denied\_permission\_sets](#input\_denied\_permission\_sets) | Map of account:[PermissionSet, PermissionSet...] specifying AWS SSO PermissionSets denied access to the role when coming from specified account | `map(list(string))` | `{}` | no |
| [denied\_principal\_arns](#input\_denied\_principal\_arns) | List of AWS principal ARNs explicitly denied access to the role. | `list(string)` | `[]` | no |
| [denied\_roles](#input\_denied\_roles) | Map of account:[role, role...] specifying roles explicitly denied permission to assume the role.
Roles are symbolic names like `ops` or `terraform`. Use `*` as role for entire account. | `map(list(string))` | `{}` | no |
-| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
+| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
| [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no |
@@ -90,6 +90,7 @@ resource "aws_iam_role" "default" {
| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` | [
"default"
]
| no |
| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no |
| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no |
+| [permission\_set\_arn\_like\_role\_prefix](#input\_permission\_set\_arn\_like\_role\_prefix) | The prefix used to generate the role part of the AWS SSO PermissionSet ARN pattern.
The default value is explicit, but does not distinguish between regions.
You may want a shorter prefix if trust policies are too large. You may want
a longer prefix if you are concerned about IdPs in other regions. | `string` | `null` | no |
| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no |
| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
diff --git a/src/modules/team-assume-role-policy/main.tf b/src/modules/team-assume-role-policy/main.tf
index 5e367f1..6f46ee2 100644
--- a/src/modules/team-assume-role-policy/main.tf
+++ b/src/modules/team-assume-role-policy/main.tf
@@ -1,8 +1,10 @@
locals {
enabled = module.this.enabled
- allowed_roles = concat(module.allowed_role_map.principals, module.allowed_role_map.permission_set_arn_like)
- allowed_principals = sort(var.allowed_principal_arns)
+ allowed_roles = concat(module.allowed_role_map.principals, module.allowed_role_map.permission_set_arn_like)
+ # allowed_roles are more compact than allowed_principals, but just as effective. So if a role is specified twice,
+ # once as a role and once as a principal, we remove the principal.
+ allowed_principals = [for p in sort(var.allowed_principal_arns) : p if !contains(local.allowed_roles, p)]
allowed_account_names = compact(concat(
[for k, v in var.allowed_roles : k if length(v) > 0],
[for k, v in var.allowed_permission_sets : k if length(v) > 0]
@@ -34,23 +36,27 @@ data "aws_arn" "denied" {
module "allowed_role_map" {
- source = "../roles-to-principals"
+ source = "../../../account-map/modules/roles-to-principals"
privileged = var.privileged
role_map = var.allowed_roles
permission_set_map = var.allowed_permission_sets
+ overridable_permission_set_arn_like_role_prefix = var.permission_set_arn_like_role_prefix
+
context = module.this.context
}
module "denied_role_map" {
- source = "../roles-to-principals"
+ source = "../../../account-map/modules/roles-to-principals"
privileged = var.privileged
role_map = var.denied_roles
permission_set_map = var.denied_permission_sets
+ overridable_permission_set_arn_like_role_prefix = var.permission_set_arn_like_role_prefix
+
context = module.this.context
}
diff --git a/src/modules/team-assume-role-policy/variables.tf b/src/modules/team-assume-role-policy/variables.tf
index 9d08e67..f670159 100644
--- a/src/modules/team-assume-role-policy/variables.tf
+++ b/src/modules/team-assume-role-policy/variables.tf
@@ -47,6 +47,17 @@ variable "denied_permission_sets" {
default = {}
}
+variable "permission_set_arn_like_role_prefix" {
+ type = string
+ description = <<-EOT
+ The prefix used to generate the role part of the AWS SSO PermissionSet ARN pattern.
+ The default value is explicit, but does not distinguish between regions.
+ You may want a shorter prefix if trust policies are too large. You may want
+ a longer prefix if you are concerned about IdPs in other regions.
+ EOT
+ default = null
+}
+
variable "iam_users_enabled" {
type = bool
description = "True if you would like IAM Users to be able to assume the role."
diff --git a/src/outputs.tf b/src/outputs.tf
index ee14ae7..9992124 100644
--- a/src/outputs.tf
+++ b/src/outputs.tf
@@ -96,7 +96,7 @@ output "terraform_dynamic_role_enabled" {
}
output "terraform_access_map" {
- value = local.dynamic_role_enabled ? local.role_arn_terraform_access : null
+ value = local.dynamic_role_enabled ? local.principal_terraform_access_map : null
description = <<-EOT
Mapping of team Role ARN to map of account name to terraform action role ARN to assume
@@ -113,8 +113,6 @@ output "terraform_role_name_map" {
}
resource "local_file" "account_info" {
- count = var.account_configuration_export_enabled ? 1 : 0
-
content = templatefile("${path.module}/account-info.tftmpl", {
account_info_map = local.account_info_map
account_profiles = local.account_profiles
diff --git a/src/variables.tf b/src/variables.tf
index 146a0a2..7043247 100644
--- a/src/variables.tf
+++ b/src/variables.tf
@@ -3,25 +3,6 @@ variable "region" {
description = "AWS Region"
}
-variable "import_organization_accounts" {
- type = bool
- description = <<-EOT
- Retrieve accounts from AWS Organizations and import them into the account map.
- Set false for brownfield environments where you want to curate the list of
- accounts manually via the `account` component with a static backend.
- Note that the brownfield `account` component needs to include the `root` account
- in the `account_names_account_ids` map, whereas the greenfield `account` component
- does not.
- EOT
- default = true
-}
-
-variable "account_configuration_export_enabled" {
- type = bool
- description = "If true, the account configuration information will be exported to a file under `account-info/`"
- default = true
-}
-
variable "root_account_aws_name" {
type = string
description = "The name of the root account as reported by AWS"
diff --git a/src/versions.tf b/src/versions.tf
index c9e7a54..d862f98 100644
--- a/src/versions.tf
+++ b/src/versions.tf
@@ -12,7 +12,7 @@ terraform {
}
utils = {
source = "cloudposse/utils"
- version = ">= 1.10.0"
+ version = "~> 1.26"
}
}
}