Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit
Browse files Browse the repository at this point in the history
goruha committed Nov 14, 2024

Verified

This commit was signed with the committer’s verified signature.
goruha Igor Rodionov
1 parent ae6e83a commit 6b9d3ed
Showing 18 changed files with 914 additions and 59 deletions.
8 changes: 2 additions & 6 deletions .github/settings.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Upstream changes from _extends are only recognized when modifications are made to this file in the default branch.
_extends: .github
repository:
name: template
description: Template for Terraform Components
name: aws-identity-center
description: This component is responsible for creating [AWS SSO Permission Sets][1] and creating AWS SSO Account Assignments, that is, assigning IdP (Okta) groups and/or users to AWS SSO permission sets in specific AWS Accounts
homepage: https://cloudposse.com/accelerate
topics: terraform, terraform-component




47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Change log for aws-sso component

**_NOTE_**: This file is manually generated and is a work-in-progress.

### PR 830

- Fix `providers.tf` to properly assign roles for `root` account when deploying to `identity` account.
- Restore the `sts:SetSourceIdentity` permission for Identity-role-TeamAccess permission sets added in PR 738 and
inadvertently removed in PR 740.
- Update comments and documentation to reflect Cloud Posse's current recommendation that SSO **_not_** be delegated to
the `identity` account.

### Version 1.240.1, PR 740

This PR restores compatibility with `account-map` prior to version 1.227.0 and fixes bugs that made versions 1.227.0 up
to this release unusable.

Access control configuration (`aws-teams`, `iam-primary-roles`, `aws-sso`, etc.) has undergone several transformations
over the evolution of Cloud Posse's reference architecture. This update resolves a number of compatibility issues with
some of them.

If the roles you are using to deploy this component are allowed to assume the `tfstate-backend` access roles (typically
`...-gbl-root-tfstate`, possibly `...-gbl-root-tfstate-ro` or `...-gbl-root-terraform`), then you can use the defaults.
This configuration was introduced in `terraform-aws-components` v1.227.0 and is the default for all new deployments.

If the roles you are using to deploy this component are not allowed to assume the `tfstate-backend` access roles, then
you will need to configure this component to include the following:

```yaml
components:
terraform:
aws-sso:
backend:
s3:
role_arn: null
vars:
privileged: true
```
If you are deploying this component to the `identity` account, then this restriction will require you to deploy it via
the SuperAdmin user. If you are deploying this component to the `root` account, then any user or role in the `root`
account with the `AdministratorAccess` policy attached will be able to deploy this component.

## v1.227.0

This component was broken by changes made in v1.227.0. Either use a version before v1.227.0 or use the version released
by PR 740 or later.
383 changes: 334 additions & 49 deletions README.yaml

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/additional-permission-sets.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
locals {
# If you have custom permission sets, override this declaration by creating
# a file called `additional-permission-sets_override.tf`.
# Then add the custom permission sets to the overridable_additional_permission_sets in that file.
# See the README for more details.
overridable_additional_permission_sets = [
# Example
# local.audit_manager_permission_set,
]
}
117 changes: 117 additions & 0 deletions src/main.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,125 @@
locals {
enabled = module.this.enabled

account_map = module.account_map.outputs.full_account_map
root_account = local.account_map[module.account_map.outputs.root_account_account_name]

account_assignments_groups = flatten([
for account_key, account in var.account_assignments : [
for principal_key, principal in account.groups : [
for permissions_key, permissions in principal.permission_sets :
{
account = local.account_map[account_key]
permission_set_arn = module.permission_sets.permission_sets[permissions].arn
permission_set_name = module.permission_sets.permission_sets[permissions].name
principal_name = principal_key
principal_type = "GROUP"
}
]
] if lookup(account, "groups", null) != null
])
# Remove root because the identity org role cannot provision root assignments
account_assignments_groups_no_root = [
for val in local.account_assignments_groups :
val
if val.account != local.root_account
]
account_assignments_groups_only_root = [
for val in local.account_assignments_groups :
val
if val.account == local.root_account
]
account_assignments_users = flatten([
for account_key, account in var.account_assignments : [
for principal_key, principal in account.users : [
for permissions_key, permissions in principal.permission_sets :
{
account = local.account_map[account_key]
permission_set_arn = module.permission_sets.permission_sets[permissions].arn
permission_set_name = module.permission_sets.permission_sets[permissions].name
principal_name = principal_key
principal_type = "USER"
}
]
] if lookup(account, "users", null) != null
])
account_assignments_users_no_root = [
for val in local.account_assignments_users :
val
if val.account != local.root_account
]
account_assignments_users_only_root = [
for val in local.account_assignments_users :
val
if val.account == local.root_account
]

account_assignments = concat(local.account_assignments_groups_no_root, local.account_assignments_users_no_root)
account_assignments_root = concat(local.account_assignments_groups_only_root, local.account_assignments_users_only_root)

aws_partition = data.aws_partition.current.partition
}

data "aws_ssoadmin_instances" "this" {}

data "aws_partition" "current" {}

resource "aws_identitystore_group" "manual" {
for_each = toset(var.groups)

display_name = each.key
description = "Group created with Terraform"

identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]
}

module "permission_sets" {
source = "cloudposse/sso/aws//modules/permission-sets"
version = "1.1.1"

permission_sets = concat(
local.overridable_additional_permission_sets,
local.administrator_access_permission_set,
local.billing_administrator_access_permission_set,
local.billing_read_only_access_permission_set,
local.dns_administrator_access_permission_set,
local.identity_access_permission_sets,
local.poweruser_access_permission_set,
local.read_only_access_permission_set,
local.terraform_update_access_permission_set,
)

context = module.this.context

depends_on = [
aws_identitystore_group.manual
]
}

module "sso_account_assignments" {
source = "cloudposse/sso/aws//modules/account-assignments"
version = "1.1.1"

account_assignments = local.account_assignments
context = module.this.context

depends_on = [
aws_identitystore_group.manual
]
}

module "sso_account_assignments_root" {
source = "cloudposse/sso/aws//modules/account-assignments"
version = "1.1.1"

providers = {
aws = aws.root
}

account_assignments = local.account_assignments_root
context = module.this.context

depends_on = [
aws_identitystore_group.manual
]
}
16 changes: 13 additions & 3 deletions src/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
output "mock" {
description = "Mock output example for the Cloud Posse Terraform component template"
value = local.enabled ? "hello ${basename(abspath(path.module))}" : ""
output "permission_sets" {
value = module.permission_sets.permission_sets
description = "Permission sets"
}

output "sso_account_assignments" {
value = module.sso_account_assignments.assignments
description = "SSO account assignments"
}

output "group_ids" {
value = { for group_key, group_output in aws_identitystore_group.manual : group_key => group_output.group_id }
description = "Group IDs created for Identity Center"
}
12 changes: 12 additions & 0 deletions src/policy-AdminstratorAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
locals {
administrator_access_permission_set = [{
name = "AdministratorAccess",
description = "Allow Full Administrator access to the account",
relay_state = "",
session_duration = "",
tags = {},
inline_policy = ""
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AdministratorAccess"]
customer_managed_policy_attachments = []
}]
}
15 changes: 15 additions & 0 deletions src/policy-BillingAdministratorAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
locals {
billing_administrator_access_permission_set = [{
name = "BillingAdministratorAccess",
description = "Grants permissions for billing and cost management. This includes viewing account usage and viewing and modifying budgets and payment methods.",
relay_state = "https://console.aws.amazon.com/billing/",
session_duration = "",
tags = {},
inline_policy = ""
policy_attachments = [
"arn:${local.aws_partition}:iam::aws:policy/job-function/Billing",
"arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess",
]
customer_managed_policy_attachments = []
}]
}
15 changes: 15 additions & 0 deletions src/policy-BillingReadOnlyAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
locals {
billing_read_only_access_permission_set = [{
name = "BillingReadOnlyAccess",
description = "Allow users to view bills in the billing console",
relay_state = "",
session_duration = "",
tags = {},
inline_policy = ""
policy_attachments = [
"arn:${local.aws_partition}:iam::aws:policy/AWSBillingReadOnlyAccess",
"arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess",
]
customer_managed_policy_attachments = []
}]
}
39 changes: 39 additions & 0 deletions src/policy-DNSAdministratorAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
data "aws_iam_policy_document" "dns_administrator_access" {
statement {
sid = "AllowDNS"
effect = "Allow"
actions = [
"route53:ChangeResourceRecordSets",
"route53:CreateHealthCheck",
"route53:CreateTrafficPolicy",
"route53:CreateTrafficPolicyInstance",
"route53:CreateTrafficPolicyVersion",
"route53:DeleteHealthCheck",
"route53:DeleteTrafficPolicy",
"route53:DeleteTrafficPolicyInstance",
"route53:Get*",
"route53:List*",
"route53:UpdateHealthCheck",
"route53:UpdateTrafficPolicyComment",
"route53:UpdateTrafficPolicyInstance",
"route53domains:List*",
]

resources = [
"*",
]
}
}

locals {
dns_administrator_access_permission_set = [{
name = "DNSRecordAdministratorAccess",
description = "Allow DNS Record Administrator access to the account, but not zone administration",
relay_state = "https://console.aws.amazon.com/route53/",
session_duration = "",
tags = {},
inline_policy = data.aws_iam_policy_document.dns_administrator_access.json,
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess"]
customer_managed_policy_attachments = []
}]
}
61 changes: 61 additions & 0 deletions src/policy-Identity-role-TeamAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

# This file generates a permission set for each role specified in var.target_identity_roles
# which is named "Identity<Role>TeamAccess" and grants access to only that role,
# plus ViewOnly access because it is difficult to navigate without any access at all.

data "aws_iam_policy_document" "assume_aws_team" {
for_each = local.enabled ? var.aws_teams_accessible : []

statement {
sid = "RoleAssumeRole"

effect = "Allow"
actions = [
"sts:AssumeRole",
"sts:SetSourceIdentity",
"sts:TagSession",
]

resources = ["*"]

/* For future reference, this tag-based restriction also works, based on
the fact that we always tag our IAM roles with the "Name" tag.
This could be used to control access based on some other tag, like "Category",
so is left here as an example.
condition {
test = "ForAllValues:StringEquals"
variable = "iam:ResourceTag/Name" # "Name" is the Tag Key
values = [format("%s-%s", module.role_prefix.id, each.value)]
}
resources = [
# This allows/restricts access to only IAM roles, not users or SSO roles
format("arn:aws:iam::%s:role/*", local.identity_account)
]
*/

}
}

module "role_map" {
source = "../account-map/modules/roles-to-principals"

teams = var.aws_teams_accessible
privileged = var.privileged

context = module.this.context
}

locals {
identity_access_permission_sets = [for role in var.aws_teams_accessible : {
name = module.role_map.team_permission_set_name_map[role],
description = format("Allow user to assume the %s Team role in the Identity account, which allows access to other accounts", replace(title(role), "-", ""))
relay_state = "",
session_duration = "",
tags = {},
inline_policy = data.aws_iam_policy_document.assume_aws_team[role].json
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/job-function/ViewOnlyAccess"]
customer_managed_policy_attachments = []
}]
}
15 changes: 15 additions & 0 deletions src/policy-PoweruserAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
locals {
poweruser_access_permission_set = [{
name = "PowerUserAccess",
description = "Allow Poweruser access to the account",
relay_state = "",
session_duration = "",
tags = {},
inline_policy = ""
policy_attachments = [
"arn:${local.aws_partition}:iam::aws:policy/PowerUserAccess",
"arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess",
]
customer_managed_policy_attachments = []
}]
}
31 changes: 31 additions & 0 deletions src/policy-ReadOnlyAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
locals {
read_only_access_permission_set = [{
name = "ReadOnlyAccess",
description = "Allow Read Only access to the account",
relay_state = "",
session_duration = "",
tags = {},
inline_policy = data.aws_iam_policy_document.eks_read_only.json,
policy_attachments = [
"arn:${local.aws_partition}:iam::aws:policy/ReadOnlyAccess",
"arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess"
]
customer_managed_policy_attachments = []
}]
}

data "aws_iam_policy_document" "eks_read_only" {
statement {
sid = "AllowEKSView"
effect = "Allow"
actions = [
"eks:Get*",
"eks:Describe*",
"eks:List*",
"eks:Access*"
]
resources = [
"*"
]
}
}
56 changes: 56 additions & 0 deletions src/policy-TerraformUpdateAccess.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
variable "tfstate_environment_name" {
type = string
description = "The name of the environment where `tfstate-backend` is provisioned. If not set, the TerraformUpdateAccess permission set will not be created."
default = null
}

locals {
tf_update_access_enabled = var.tfstate_environment_name != null && module.this.enabled
}

module "tfstate" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
version = "1.5.0"

bypass = !local.tf_update_access_enabled

component = "tfstate-backend"
environment = var.tfstate_environment_name
stage = module.iam_roles.global_stage_name
privileged = var.privileged

context = module.this.context
}

data "aws_iam_policy_document" "terraform_update_access" {
count = local.tf_update_access_enabled ? 1 : 0

statement {
sid = "TerraformStateBackendS3Bucket"
effect = "Allow"
actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject"]
resources = module.this.enabled ? [
module.tfstate.outputs.tfstate_backend_s3_bucket_arn,
"${module.tfstate.outputs.tfstate_backend_s3_bucket_arn}/*"
] : []
}
statement {
sid = "TerraformStateBackendDynamoDbTable"
effect = "Allow"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
resources = module.this.enabled ? [module.tfstate.outputs.tfstate_backend_dynamodb_table_arn] : []
}
}

locals {
terraform_update_access_permission_set = local.tf_update_access_enabled ? [{
name = "TerraformUpdateAccess",
description = "Allow access to Terraform state sufficient to make changes",
relay_state = "",
session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles
tags = {},
inline_policy = one(data.aws_iam_policy_document.terraform_update_access[*].json),
policy_attachments = []
customer_managed_policy_attachments = []
}] : []
}
75 changes: 75 additions & 0 deletions src/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# This component is unusual in that part of it must be deployed to the `root`
# account. You have the option of where to deploy the remaining part, and
# Cloud Posse recommends you deploy it also to the `root` account, however
# it can be deployed to the `identity` account instead. In the discussion
# below, when we talk about where this module is being deployed, we are
# referring to the part of the module that is not deployed to the `root`
# account and is configured by setting `stage` etc..

# If you have Dynamic Terraform Roles enabled, leave the backend `role_arn` at
# its default value. If deploying only to the `root` account, leave `privileged: false`
# and use either SuperAdmin or an appropriate `aws-team` (such as `managers`).
# If deploying to the `identity` account, set `privileged: true`
# and use SuperAdmin or any other role in the `root` account with Admin access.
#
# For those not using dynamic Terraform roles:
#
# Set the stack configuration for this component to set `privileged: true`
# and backend `role_arn` to `null`, and deploy it using either the SuperAdmin
# role or any other role in the `root` account with Admin access.
#
# If you are deploying this to the "identity" account and have a team empowered
# to deploy to both the "identity" and "root" accounts, then you have the option to set
# `privileged: false` and leave the backend `role_arn` at its default value, but
# then SuperAdmin will not be able to deploy this component,
# only the team with access to both accounts will be able to deploy it.
#

provider "aws" {
region = var.region

profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null
dynamic "assume_role" {
for_each = !var.privileged && module.iam_roles.profiles_enabled ? [] : (
var.privileged ? compact([module.iam_roles.org_role_arn]) : compact([module.iam_roles.terraform_role_arn])
)
content {
role_arn = assume_role.value
}
}
}


module "iam_roles" {
source = "../account-map/modules/iam-roles"
privileged = var.privileged

context = module.this.context
}

provider "aws" {
alias = "root"
region = var.region

profile = !var.privileged && module.iam_roles_root.profiles_enabled ? module.iam_roles_root.terraform_profile_name : null
dynamic "assume_role" {
for_each = !var.privileged && module.iam_roles_root.profiles_enabled ? [] : (
var.privileged ? compact([module.iam_roles_root.org_role_arn]) : compact([module.iam_roles_root.terraform_role_arn])
)
content {
role_arn = assume_role.value
}
}
}


module "iam_roles_root" {
source = "../account-map/modules/iam-roles"

privileged = var.privileged
tenant = module.iam_roles.global_tenant_name
stage = module.iam_roles.global_stage_name
environment = module.iam_roles.global_environment_name

context = module.this.context
}
12 changes: 12 additions & 0 deletions src/remote-state.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module "account_map" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
version = "1.5.0"

component = "account-map"
environment = module.iam_roles.global_environment_name
stage = module.iam_roles.global_stage_name
tenant = module.iam_roles.global_tenant_name
privileged = var.privileged

context = module.this.context
}
54 changes: 54 additions & 0 deletions src/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
variable "region" {
type = string
description = "AWS Region"
}

variable "privileged" {
type = bool
description = "True if the user running the Terraform command already has access to the Terraform backend"
default = false
}

variable "account_assignments" {
type = map(map(map(object({
permission_sets = list(string)
}
))))
description = <<-EOT
Enables access to permission sets for users and groups in accounts, in the following structure:
```yaml
<account-name>:
groups:
<group-name>:
permission_sets:
- <permission-set-name>
users:
<user-name>:
permission_sets:
- <permission-set-name>
```
EOT
default = {}
}

variable "aws_teams_accessible" {
type = set(string)
description = <<-EOT
List of IAM roles (e.g. ["admin", "terraform"]) for which to create permission
sets that allow the user to assume that role. Named like
admin -> IdentityAdminTeamAccess
EOT
default = []
}

variable "groups" {
type = list(string)
description = <<-EOT
List of AWS Identity Center Groups to be created with the AWS API.
When provisioning the Google Workspace Integration with AWS, Groups need to be created with API in order for automatic provisioning to work as intended.
EOT
default = []
}
7 changes: 6 additions & 1 deletion src/versions.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
terraform {
required_version = ">= 1.0.0"

required_providers {}
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}

0 comments on commit 6b9d3ed

Please sign in to comment.