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" } } }