From 87b3195aae19769ad271d425280c22fb0ef1b798 Mon Sep 17 00:00:00 2001 From: Gaston Festari Date: Wed, 5 Feb 2025 16:24:01 -0300 Subject: [PATCH 1/2] feat: parameterize customer_managed_policies path Make customer-managed policies configurable using a custom path instead of the current hard-coded "/" value. Retains the current behavior, so these entries can either be a set of strings or a set of objects with `name` and `path` (new behavior). --- examples/customer-managed-policies/.header.md | 40 +++++++ examples/customer-managed-policies/README.md | 72 +++++++++++ examples/customer-managed-policies/locals.tf | 14 +++ examples/customer-managed-policies/main.tf | 112 ++++++++++++++++++ locals.tf | 6 +- main.tf | 16 +-- tests/08_customer_managed_policies.tftest.hcl | 13 ++ 7 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 examples/customer-managed-policies/.header.md create mode 100644 examples/customer-managed-policies/README.md create mode 100644 examples/customer-managed-policies/locals.tf create mode 100644 examples/customer-managed-policies/main.tf create mode 100644 tests/08_customer_managed_policies.tftest.hcl diff --git a/examples/customer-managed-policies/.header.md b/examples/customer-managed-policies/.header.md new file mode 100644 index 0000000..060e460 --- /dev/null +++ b/examples/customer-managed-policies/.header.md @@ -0,0 +1,40 @@ +This directory contains examples of using the module to create users and groups and assign permissions with **Inline Policies**. + +**IMPORTANT:** Ensure that the name of your object matches the name of your principal (e.g. user name or group name). See the following example with object/principal names 'Admin' and 'nuzumaki': + +```hcl + sso_groups = { + Admin : { + group_name = "Admin" + group_description = "Admin IAM Identity Center Group" + }, + } + + // Create desired USERS in IAM Identity Center + sso_users = { + nuzumaki : { + group_membership = ["Admin",] + user_name = "nuzumaki" + given_name = "Naruto" + family_name = "Uzumaki" + email = "nuzumaki@hiddenleaf.village" + }, + } + +``` + +These names are referenced throughout the module. Failure to do this may lead to unintentional errors such as the following: + +``` +Error: Invalid index +│ +│ on ../../main.tf line 141, in resource "aws_identitystore_group_membership" "sso_group_membership": +│ 141: member_id = (contains(local.this_users, each.value.user_name) ? aws_identitystore_user.sso_users[each.value.user_name].user_id : data.aws_identitystore_user.existing_sso_users[each.value.user_name].id) +│ ├──────────────── +│ │ aws_identitystore_user.sso_users is object with 2 attributes +│ │ each.value.user_name is "nuzumaki" +│ +│ The given key does not identify an element in this collection value. +``` + +To resolve this, ensure your object and principal names are the same and re-run `terraform plan` and `terraform apply`. diff --git a/examples/customer-managed-policies/README.md b/examples/customer-managed-policies/README.md new file mode 100644 index 0000000..8ff95ca --- /dev/null +++ b/examples/customer-managed-policies/README.md @@ -0,0 +1,72 @@ + +This directory contains examples of using the module to create users and groups and assign permissions with **Inline Policies**. + +**IMPORTANT:** Ensure that the name of your object matches the name of your principal (e.g. user name or group name). See the following example with object/principal names 'Admin' and 'nuzumaki': + +```hcl + sso_groups = { + Admin : { + group_name = "Admin" + group_description = "Admin IAM Identity Center Group" + }, + } + + // Create desired USERS in IAM Identity Center + sso_users = { + nuzumaki : { + group_membership = ["Admin",] + user_name = "nuzumaki" + given_name = "Naruto" + family_name = "Uzumaki" + email = "nuzumaki@hiddenleaf.village" + }, + } + +``` + +These names are referenced throughout the module. Failure to do this may lead to unintentional errors such as the following: + +``` +Error: Invalid index +│ +│ on ../../main.tf line 141, in resource "aws_identitystore_group_membership" "sso_group_membership": +│ 141: member_id = (contains(local.this_users, each.value.user_name) ? aws_identitystore_user.sso_users[each.value.user_name].user_id : data.aws_identitystore_user.existing_sso_users[each.value.user_name].id) +│ ├──────────────── +│ │ aws_identitystore_user.sso_users is object with 2 attributes +│ │ each.value.user_name is "nuzumaki" +│ +│ The given key does not identify an element in this collection value. +``` + +To resolve this, ensure your object and principal names are the same and re-run `terraform plan` and `terraform apply`. + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [aws-iam-identity-center](#module\_aws-iam-identity-center) | ../.. | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_organizations_organization.org](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/organizations_organization) | data source | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/examples/customer-managed-policies/locals.tf b/examples/customer-managed-policies/locals.tf new file mode 100644 index 0000000..922f5e9 --- /dev/null +++ b/examples/customer-managed-policies/locals.tf @@ -0,0 +1,14 @@ +# Fetch Account Id from SSM Parameter Store +data "aws_ssm_parameter" "account1_account_id" { + name = "tf-aws-iam-idc-module-testing-account1-account-id" // replace with your SSM Parameter Key +} + +locals { + # Account IDs + account1_account_id = nonsensitive(data.aws_ssm_parameter.account1_account_id.value) + # account1_account_id = "111111111111" + # account2_account_id = "222222222222" + # account3_account_id = "333333333333" + # account4_account_id = "444444444444" + +} diff --git a/examples/customer-managed-policies/main.tf b/examples/customer-managed-policies/main.tf new file mode 100644 index 0000000..49802ab --- /dev/null +++ b/examples/customer-managed-policies/main.tf @@ -0,0 +1,112 @@ +data "aws_organizations_organization" "org" {} + +module "aws-iam-identity-center" { + source = "../.." // local example + # source = "aws-ia/iam-identity-center/aws" // remote example + + existing_sso_groups = { + AWSControlTowerAdmins : { + group_name = "AWSControlTowerAdmins" # this must be the name of a sso group that already exists in your AWS account + } + } + + sso_groups = { + Admin : { + group_name = "Admin" + group_description = "Admin Group" + }, + Dev : { + group_name = "Dev" + group_description = "Dev Group" + }, + } + sso_users = { + nuzumaki : { + group_membership = ["Admin", "Dev", "AWSControlTowerAdmins"] + user_name = "nuzumaki" + given_name = "Naruto" + family_name = "Uzumaki" + email = "nuzumaki@hiddenleaf.village" + }, + suchiha : { + group_membership = ["Dev", "AWSControlTowerAdmins"] + user_name = "suchiha" + given_name = "Sasuke" + family_name = "Uchiha" + email = "suchiha@hiddenleaf.village" + }, + } + + existing_permission_sets = { + AWSAdministratorAccess : { + permission_set_name = "AWSAdministratorAccess" # this must be the name of a permission set that already exists in your AWS account + }, + } + + permission_sets = { + AdministratorAccess = { + description = "Provides full access to AWS services and resources", + session_duration = "PT3H", + aws_managed_policies = ["arn:aws:iam::aws:policy/AdministratorAccess"] + + customer_managed_policies = [ + "MyExampleOrgAdminAccess", + ] + + tags = { ManagedBy = "Terraform" } + }, + ViewOnlyAccess = { + description = "This policy grants permissions to view resources and basic metadata across all AWS services", + session_duration = "PT3H", + aws_managed_policies = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"] + managed_policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + + customer_managed_policies = [ + { + name = "MyExampleOrgViewOnlyAccess" + path = "/foo/example/" + } + ] + + permissions_boundary = { + managed_policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + } + tags = { ManagedBy = "Terraform" } + }, + } + account_assignments = { + Admin : { + principal_name = "Admin" + principal_type = "GROUP" + principal_idp = "INTERNAL" + permission_sets = [ + "AdministratorAccess", + "ViewOnlyAccess", + // existing permission set + "AWSAdministratorAccess", + ] + account_ids = [ + // replace with your own account id + local.account1_account_id, + # local.account2_account_id + # local.account3_account_id + # local.account4_account_id + ] + }, + Dev : { + principal_name = "Dev" + principal_type = "GROUP" + principal_idp = "INTERNAL" + permission_sets = [ + "ViewOnlyAccess", + ] + account_ids = [ + // replace with your own account id + local.account1_account_id, + # local.account2_account_id + # local.account3_account_id + # local.account4_account_id + ] + }, + } +} diff --git a/locals.tf b/locals.tf index d996c3d..0b5575c 100644 --- a/locals.tf +++ b/locals.tf @@ -78,8 +78,8 @@ locals { for pset_name, pset_index in local.customer_managed_permission_sets : [ for policy in pset_index.customer_managed_policies : { pset_name = pset_name - policy_name = policy - # path = path + policy_name = try(policy.name, policy) + policy_path = try(policy.path, "/") } if pset_index.customer_managed_policies != null && can(pset_index.customer_managed_policies) ] ]) @@ -207,7 +207,7 @@ locals { ]) # Creating a local variable by flattening the complex type related to Applications to extract a simple structure representing - # app assignments access scopes + # app assignments access scopes apps_assignments_access_scopes = flatten([ for app in var.sso_applications : [ for ass_acc_scope in app.assignments_access_scope : { diff --git a/main.tf b/main.tf index 9fa2617..1588703 100644 --- a/main.tf +++ b/main.tf @@ -191,7 +191,7 @@ resource "aws_ssoadmin_customer_managed_policy_attachment" "pset_customer_manage permission_set_arn = aws_ssoadmin_permission_set.pset[each.value.pset_name].arn customer_managed_policy_reference { name = each.value.policy_name - path = "/" + path = each.value.policy_path } } @@ -271,7 +271,7 @@ resource "aws_ssoadmin_application" "sso_apps" { tags = each.value.tags } -# SSO - Applications Assigments Configuration +# SSO - Applications Assigments Configuration resource "aws_ssoadmin_application_assignment_configuration" "sso_apps_assignments_configs" { for_each = { for idx, assignment_config in local.apps_assignments_configs : @@ -281,7 +281,7 @@ resource "aws_ssoadmin_application_assignment_configuration" "sso_apps_assignmen assignment_required = each.value.assignment_required } -# SSO - Application Assignments access scope +# SSO - Application Assignments access scope resource "aws_ssoadmin_application_access_scope" "sso_apps_assignments_access_scope" { for_each = { for idx, app_access_scope in local.apps_assignments_access_scopes : @@ -289,20 +289,20 @@ resource "aws_ssoadmin_application_access_scope" "sso_apps_assignments_access_sc } application_arn = aws_ssoadmin_application.sso_apps[each.value.app_name].application_arn authorized_targets = [ - for target in each.value.authorized_targets : aws_ssoadmin_application.sso_apps[target].application_arn + for target in each.value.authorized_targets : aws_ssoadmin_application.sso_apps[target].application_arn ] #authorized_targets = each.value.authorized_targets scope = each.value.scope } -# SSO - Applications Assignments +# SSO - Applications Assignments # Groups assignments resource "aws_ssoadmin_application_assignment" "sso_apps_groups_assignments" { for_each = { for idx, assignment in local.apps_groups_assignments : "${assignment.app_name}-${assignment.group_name}" => assignment } - application_arn = aws_ssoadmin_application.sso_apps[each.value.app_name].application_arn + application_arn = aws_ssoadmin_application.sso_apps[each.value.app_name].application_arn principal_id = (contains(local.this_groups, each.value.group_name) ? aws_identitystore_group.sso_groups[each.value.group_name].group_id : data.aws_identitystore_group.existing_sso_groups[each.value.group_name].group_id) principal_type = each.value.principal_type } @@ -313,7 +313,7 @@ resource "aws_ssoadmin_application_assignment" "sso_apps_users_assignments" { for idx, assignment in local.apps_users_assignments : "${assignment.app_name}-${assignment.user_name}" => assignment } - application_arn = aws_ssoadmin_application.sso_apps[each.value.app_name].application_arn + application_arn = aws_ssoadmin_application.sso_apps[each.value.app_name].application_arn principal_id = (contains(local.this_users, each.value.user_name) ? aws_identitystore_user.sso_users[each.value.user_name].user_id : data.aws_identitystore_user.existing_sso_users[each.value.user_name].user_id) principal_type = each.value.principal_type } @@ -331,4 +331,4 @@ resource "aws_ssoadmin_instance_access_control_attributes" "sso_access_control_ } } } -} \ No newline at end of file +} diff --git a/tests/08_customer_managed_policies.tftest.hcl b/tests/08_customer_managed_policies.tftest.hcl new file mode 100644 index 0000000..5b55cf4 --- /dev/null +++ b/tests/08_customer_managed_policies.tftest.hcl @@ -0,0 +1,13 @@ +run "unit_test" { + command = plan + module { + source = "./examples/customer-managed-policies" + } +} + +run "e2e_test" { + command = apply + module { + source = "./examples/customer-managed-policies" + } +} From 6c6cdf07d437790b766062263cde0669a7914240 Mon Sep 17 00:00:00 2001 From: Gaston Festari Date: Wed, 5 Feb 2025 16:33:00 -0300 Subject: [PATCH 2/2] style: linter pass --- main.tf | 6 +++--- variables.tf | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main.tf b/main.tf index 1588703..e88611a 100644 --- a/main.tf +++ b/main.tf @@ -319,13 +319,13 @@ resource "aws_ssoadmin_application_assignment" "sso_apps_users_assignments" { } # SSO Instance Access Control Attributes -resource "aws_ssoadmin_instance_access_control_attributes" "sso_access_control_attributes" { - count = length(var.sso_instance_access_control_attributes) <= 0 ? 0 : 1 +resource "aws_ssoadmin_instance_access_control_attributes" "sso_access_control_attributes" { + count = length(var.sso_instance_access_control_attributes) <= 0 ? 0 : 1 instance_arn = local.ssoadmin_instance_arn dynamic "attribute" { for_each = var.sso_instance_access_control_attributes content { - key = attribute.key + key = attribute.key value { source = attribute.value.source } diff --git a/variables.tf b/variables.tf index 5fa4a95..662651d 100644 --- a/variables.tf +++ b/variables.tf @@ -151,7 +151,7 @@ variable "sso_instance_access_control_attributes" { description = "List of attributes for access control. This is used to create the enable and use attributes for access control." type = list(object({ attribute_name = string - source = set(string) + source = set(string) })) default = [] validation { @@ -166,7 +166,7 @@ variable "sso_instance_access_control_attributes" { condition = alltrue([ for attr in var.sso_instance_access_control_attributes : attr.source != null && - length(attr.source) > 0 && # checks if the set is not empty + length(attr.source) > 0 && # checks if the set is not empty alltrue([for s in attr.source : s != ""]) # checks no empty strings in set ]) error_message = "The attribute source is mandatory and must contain non-empty strings."