diff --git a/blueprints/data-solutions/shielded-folder/main.tf b/blueprints/data-solutions/shielded-folder/main.tf index 5d1145cd1b..f4424a52f5 100644 --- a/blueprints/data-solutions/shielded-folder/main.tf +++ b/blueprints/data-solutions/shielded-folder/main.tf @@ -71,13 +71,15 @@ locals { } module "folder" { - source = "../../../modules/folder" - folder_create = var.folder_config.folder_create != null - parent = try(var.folder_config.folder_create.parent, null) - name = try(var.folder_config.folder_create.display_name, null) - id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id - group_iam = local.group_iam - org_policies_data_path = var.data_dir != null ? "${var.data_dir}/org-policies" : null + source = "../../../modules/folder" + folder_create = var.folder_config.folder_create != null + parent = try(var.folder_config.folder_create.parent, null) + name = try(var.folder_config.folder_create.display_name, null) + id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id + group_iam = local.group_iam + factories_config = { + org_policies = var.data_dir != null ? "${var.data_dir}/org-policies" : null + } logging_sinks = var.enable_features.log_sink ? { for name, attrs in var.log_sinks : name => { bq_partitioned_table = attrs.type == "bigquery" diff --git a/fast/stages-multitenant/1-resman-tenant/README.md b/fast/stages-multitenant/1-resman-tenant/README.md index c38f173f49..200e414067 100644 --- a/fast/stages-multitenant/1-resman-tenant/README.md +++ b/fast/stages-multitenant/1-resman-tenant/README.md @@ -155,21 +155,21 @@ Once the configuration is done just go through the usual `init/apply` cycle. On |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables.tf#L52) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | 0-bootstrap | -| [organization](variables.tf#L205) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables.tf#L227) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | -| [root_node](variables.tf#L237) | Root folder node for the tenant, in folders/nnnnnn format. | string | ✓ | | | -| [short_name](variables.tf#L242) | Short name used to identify the tenant. | string | ✓ | | | -| [tags](variables.tf#L247) | Resource management tags. | object({…}) | ✓ | | | +| [organization](variables.tf#L214) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L230) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [root_node](variables.tf#L240) | Root folder node for the tenant, in folders/nnnnnn format. | string | ✓ | | | +| [short_name](variables.tf#L245) | Short name used to identify the tenant. | string | ✓ | | | +| [tags](variables.tf#L250) | Resource management tags. | object({…}) | ✓ | | | | [cicd_repositories](variables.tf#L63) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | | [custom_roles](variables.tf#L145) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | | [data_dir](variables.tf#L154) | Relative path for the folder storing configuration data. | string | | "data" | | -| [fast_features](variables.tf#L160) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | -| [groups](variables.tf#L174) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | -| [locations](variables.tf#L187) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | -| [organization_policy_data_path](variables.tf#L215) | Path for the data folder used by the organization policies factory. | string | | null | | -| [outputs_location](variables.tf#L221) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [team_folders](variables.tf#L265) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | -| [test_skip_data_sources](variables.tf#L275) | Used when testing to bypass data sources. | bool | | false | | +| [factories_config](variables.tf#L160) | Configuration for the organization policies factory. | object({…}) | | {} | | +| [fast_features](variables.tf#L169) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | +| [groups](variables.tf#L183) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L196) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [test_skip_data_sources](variables.tf#L278) | Used when testing to bypass data sources. | bool | | false | | ## Outputs diff --git a/fast/stages-multitenant/1-resman-tenant/root_node.tf b/fast/stages-multitenant/1-resman-tenant/root_node.tf index 7b02a60923..6c920c9e21 100644 --- a/fast/stages-multitenant/1-resman-tenant/root_node.tf +++ b/fast/stages-multitenant/1-resman-tenant/root_node.tf @@ -26,6 +26,9 @@ module "root-folder" { ) name = var.test_skip_data_sources ? "Test" : null # end test attributes + factories_config = { + org_policy = var.factories_config.org_policy + } iam_bindings_additive = { sa_net_fw_policy_admin = { member = local.automation_sas_iam.networking @@ -40,5 +43,4 @@ module "root-folder" { role = "roles/accesscontextmanager.policyAdmin" } } - org_policies_data_path = var.organization_policy_data_path } diff --git a/fast/stages-multitenant/1-resman-tenant/variables.tf b/fast/stages-multitenant/1-resman-tenant/variables.tf index bcda7fd629..d216a6e63a 100644 --- a/fast/stages-multitenant/1-resman-tenant/variables.tf +++ b/fast/stages-multitenant/1-resman-tenant/variables.tf @@ -157,6 +157,15 @@ variable "data_dir" { default = "data" } +variable "factories_config" { + description = "Configuration for the organization policies factory." + type = object({ + org_policy = optional(string, "data/org-policies") + }) + nullable = false + default = {} +} + variable "fast_features" { # tfdoc:variable:source 0-0-bootstrap description = "Selective control for top-level FAST features." @@ -212,12 +221,6 @@ variable "organization" { }) } -variable "organization_policy_data_path" { - description = "Path for the data folder used by the organization policies factory." - type = string - default = null -} - variable "outputs_location" { description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable." type = string diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index 3ceec1eced..c10b2bf321 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -594,37 +594,36 @@ The `fast_features` variable consists of 4 toggles: | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | | -| [organization](variables.tf#L248) | Organization details. | object({…}) | ✓ | | | -| [prefix](variables.tf#L263) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | +| [organization](variables.tf#L235) | Organization details. | object({…}) | ✓ | | | +| [prefix](variables.tf#L250) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | | [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | string | | null | | | [cicd_repositories](variables.tf#L33) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_role_names](variables.tf#L79) | Names of custom roles defined at the org level. | object({…}) | | {…} | | -| [custom_roles](variables.tf#L93) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string)) | | {} | | -| [factories_config](variables.tf#L100) | Configuration for the organization policies factory. | object({…}) | | {} | | -| [fast_features](variables.tf#L109) | Selective control for top-level FAST features. | object({…}) | | {} | | -| [federated_identity_providers](variables.tf#L122) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | -| [group_iam](variables.tf#L142) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | -| [groups](variables.tf#L149) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | map(string) | | {…} | | -| [iam](variables.tf#L167) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | -| [iam_bindings_additive](variables.tf#L174) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | -| [locations](variables.tf#L189) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | | -| [log_sinks](variables.tf#L203) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | -| [org_policies_config](variables.tf#L232) | Organization policies customization. | object({…}) | | {} | | -| [outputs_location](variables.tf#L257) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [project_parent_ids](variables.tf#L272) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | +| [custom_roles](variables.tf#L79) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string)) | | {} | | +| [factories_config](variables.tf#L86) | Configuration for the organization policies factory. | object({…}) | | {} | | +| [fast_features](variables.tf#L96) | Selective control for top-level FAST features. | object({…}) | | {} | | +| [federated_identity_providers](variables.tf#L109) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [group_iam](variables.tf#L129) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | +| [groups](variables.tf#L136) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | map(string) | | {…} | | +| [iam](variables.tf#L154) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_bindings_additive](variables.tf#L161) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | +| [locations](variables.tf#L176) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | | +| [log_sinks](variables.tf#L190) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [org_policies_config](variables.tf#L219) | Organization policies customization. | object({…}) | | {} | | +| [outputs_location](variables.tf#L244) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L259) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [automation](outputs.tf#L112) | Automation resources. | | | -| [billing_dataset](outputs.tf#L117) | BigQuery dataset prepared for billing export. | | | -| [cicd_repositories](outputs.tf#L122) | CI/CD repository configurations. | | | -| [custom_roles](outputs.tf#L134) | Organization-level custom roles. | | | -| [federated_identity](outputs.tf#L139) | Workload Identity Federation pool and providers. | | | -| [outputs_bucket](outputs.tf#L149) | GCS bucket where generated output files are stored. | | | -| [project_ids](outputs.tf#L154) | Projects created by this stage. | | | -| [providers](outputs.tf#L164) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | -| [service_accounts](outputs.tf#L171) | Automation service accounts created by this stage. | | | -| [tfvars](outputs.tf#L180) | Terraform variable files for the following stages. | ✓ | | +| [automation](outputs.tf#L102) | Automation resources. | | | +| [billing_dataset](outputs.tf#L107) | BigQuery dataset prepared for billing export. | | | +| [cicd_repositories](outputs.tf#L112) | CI/CD repository configurations. | | | +| [custom_roles](outputs.tf#L124) | Organization-level custom roles. | | | +| [federated_identity](outputs.tf#L129) | Workload Identity Federation pool and providers. | | | +| [outputs_bucket](outputs.tf#L139) | GCS bucket where generated output files are stored. | | | +| [project_ids](outputs.tf#L144) | Projects created by this stage. | | | +| [providers](outputs.tf#L154) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | +| [service_accounts](outputs.tf#L161) | Automation service accounts created by this stage. | | | +| [tfvars](outputs.tf#L170) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml new file mode 100644 index 0000000000..9ce8ac5c0e --- /dev/null +++ b/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# this is needed for use in additive IAM bindings, to avoid conflicts +name: organizationIamAdmin +includedPermissions: + - resourcemanager.organizations.get + - resourcemanager.organizations.getIamPolicy + - resourcemanager.organizations.setIamPolicy diff --git a/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml new file mode 100644 index 0000000000..a0eaf9b3b9 --- /dev/null +++ b/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: serviceProjectNetworkAdmin +includedPermissions: + - compute.globalOperations.get + # compute.networks.updatePeering and compute.networks.get are + # used by automation service accounts who manage service + # projects where peering creation might be needed (e.g. GKE). If + # you remove them your network administrators should create + # peerings for service projects + - compute.networks.updatePeering + - compute.networks.get + - compute.organizations.disableXpnResource + - compute.organizations.enableXpnResource + - compute.projects.get + - compute.subnetworks.getIamPolicy + - compute.subnetworks.setIamPolicy + - dns.networks.bindPrivateDNSZone + - resourcemanager.projects.get diff --git a/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml new file mode 100644 index 0000000000..9d1e02822f --- /dev/null +++ b/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml @@ -0,0 +1,17 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: tenantNetworkAdmin +includedPermissions: + - compute.globalOperations.get diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf index 12f6b44590..bd1e6c2852 100644 --- a/fast/stages/0-bootstrap/organization.tf +++ b/fast/stages/0-bootstrap/organization.tf @@ -95,7 +95,7 @@ module "organization" { iam_bindings = { organization_iam_admin_conditional = { members = [module.automation-tf-resman-sa.iam_email] - role = module.organization.custom_role_id[var.custom_role_names.organization_iam_admin] + role = module.organization.custom_role_id["organization_iam_admin"] condition = { expression = format( "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", @@ -106,7 +106,7 @@ module "organization" { "roles/compute.xpnAdmin", "roles/orgpolicy.policyAdmin", "roles/resourcemanager.organizationViewer", - module.organization.custom_role_id[var.custom_role_names.tenant_network_admin] + module.organization.custom_role_id["tenant_network_admin"] ], local.billing_mode == "org" ? [ "roles/billing.admin", @@ -120,34 +120,13 @@ module "organization" { } } } - custom_roles = merge(var.custom_roles, { - # this is needed for use in additive IAM bindings, to avoid conflicts - (var.custom_role_names.organization_iam_admin) = [ - "resourcemanager.organizations.get", - "resourcemanager.organizations.getIamPolicy", - "resourcemanager.organizations.setIamPolicy" - ] - (var.custom_role_names.service_project_network_admin) = [ - "compute.globalOperations.get", - # compute.networks.updatePeering and compute.networks.get are - # used by automation service accounts who manage service - # projects where peering creation might be needed (e.g. GKE). If - # you remove them your network administrators should create - # peerings for service projects - "compute.networks.updatePeering", - "compute.networks.get", - "compute.organizations.disableXpnResource", - "compute.organizations.enableXpnResource", - "compute.projects.get", - "compute.subnetworks.getIamPolicy", - "compute.subnetworks.setIamPolicy", - "dns.networks.bindPrivateDNSZone", - "resourcemanager.projects.get", - ] - (var.custom_role_names.tenant_network_admin) = [ - "compute.globalOperations.get", - ] - }) + custom_roles = var.custom_roles + factories_config = { + custom_roles = var.factories_config.custom_roles + org_policies = ( + var.bootstrap_user != null ? null : var.factories_config.org_policy + ) + } logging_sinks = { for name, attrs in var.log_sinks : name => { bq_partitioned_table = attrs.type == "bigquery" @@ -156,11 +135,6 @@ module "organization" { type = attrs.type } } - org_policies_data_path = ( - var.bootstrap_user != null - ? null - : var.factories_config.org_policy_data_path - ) org_policies = var.bootstrap_user != null ? {} : { "iam.allowedPolicyMemberDomains" = { rules = [ diff --git a/fast/stages/0-bootstrap/outputs.tf b/fast/stages/0-bootstrap/outputs.tf index 58b8217938..4724a40918 100644 --- a/fast/stages/0-bootstrap/outputs.tf +++ b/fast/stages/0-bootstrap/outputs.tf @@ -38,16 +38,6 @@ locals { } ) } - custom_roles = merge( - { - for k, v in var.custom_role_names : - k => try(module.organization.custom_role_id[v], "") - }, - { - for k, v in var.custom_roles : - k => try(module.organization.custom_role_id[k], "") - } - ) providers = { "0-bootstrap" = templatefile(local._tpl_providers, { backend_extra = null @@ -82,7 +72,7 @@ locals { project_id = module.automation-project.project_id project_number = module.automation-project.number } - custom_roles = local.custom_roles + custom_roles = module.organization.custom_role_id logging = { project_id = module.log-export-project.project_id project_number = module.log-export-project.number @@ -133,7 +123,7 @@ output "cicd_repositories" { output "custom_roles" { description = "Organization-level custom roles." - value = local.custom_roles + value = module.organization.custom_role_id } output "federated_identity" { diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf index af171b3417..9980c11893 100644 --- a/fast/stages/0-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -76,20 +76,6 @@ variable "cicd_repositories" { } } -variable "custom_role_names" { - description = "Names of custom roles defined at the org level." - type = object({ - organization_iam_admin = string - service_project_network_admin = string - tenant_network_admin = string - }) - default = { - organization_iam_admin = "organizationIamAdmin" - service_project_network_admin = "serviceProjectNetworkAdmin" - tenant_network_admin = "tenantNetworkAdmin" - } -} - variable "custom_roles" { description = "Map of role names => list of permissions to additionally create at the organization level." type = map(list(string)) @@ -100,7 +86,8 @@ variable "custom_roles" { variable "factories_config" { description = "Configuration for the organization policies factory." type = object({ - org_policy_data_path = optional(string, "data/org-policies") + custom_roles = optional(string, "data/custom-roles") + org_policy = optional(string, "data/org-policies") }) nullable = false default = {} diff --git a/modules/folder/README.md b/modules/folder/README.md index 059fe39955..fb45549514 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -122,10 +122,12 @@ The example below deploys a few organization policies split between two YAML fil ```hcl module "folder" { - source = "./fabric/modules/folder" - parent = var.folder_id - name = "Folder name" - org_policies_data_path = "configs/org-policies/" + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + factories_config = { + org_policies = "configs/org-policies/" + } } # tftest modules=1 resources=8 files=boolean,list inventory=org-policies.yaml e2e ``` @@ -198,6 +200,7 @@ module "folder" { } # tftest modules=2 resources=3 e2e serial ``` + ## Log Sinks ```hcl @@ -340,21 +343,21 @@ module "folder" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | -| [firewall_policy](variables.tf#L24) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L33) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | -| [group_iam](variables.tf#L39) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | -| [iam](variables.tf#L46) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L53) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L68) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [id](variables.tf#L83) | Folder ID in case you use folder_create=false. | string | | null | -| [logging_data_access](variables.tf#L89) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L104) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L111) | Logging sinks to create for the folder. | map(object({…})) | | {} | -| [name](variables.tf#L142) | Folder name. | string | | null | -| [org_policies](variables.tf#L148) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L175) | Path containing org policies in YAML format. | string | | null | -| [parent](variables.tf#L181) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L191) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [factories_config](variables.tf#L24) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L33) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L42) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L48) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | +| [iam](variables.tf#L55) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L62) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L77) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [id](variables.tf#L92) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_data_access](variables.tf#L98) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L113) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L120) | Logging sinks to create for the folder. | map(object({…})) | | {} | +| [name](variables.tf#L151) | Folder name. | string | | null | +| [org_policies](variables.tf#L157) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L184) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L194) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 2bf79c4ab6..38b69871c9 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -18,10 +18,9 @@ locals { _factory_data_raw = merge([ - for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) : - yamldecode(file("${var.org_policies_data_path}/${f}")) + for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) : + yamldecode(file("${var.factories_config.org_policies}/${f}")) ]...) - # simulate applying defaults to data coming from yaml files _factory_data = { for k, v in local._factory_data_raw : @@ -49,9 +48,7 @@ locals { ] } } - _org_policies = merge(local._factory_data, var.org_policies) - org_policies = { for k, v in local._org_policies : k => merge(v, { diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 91e8e11e81..be087379fe 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -21,6 +21,15 @@ variable "contacts" { nullable = false } +variable "factories_config" { + description = "Paths to data files and folders that enable factory functionality." + type = object({ + org_policies = optional(string) + }) + nullable = false + default = {} +} + variable "firewall_policy" { description = "Hierarchical firewall policy to associate to this folder." type = object({ @@ -172,12 +181,6 @@ variable "org_policies" { nullable = false } -variable "org_policies_data_path" { - description = "Path containing org policies in YAML format." - type = string - default = null -} - variable "parent" { description = "Parent in folders/folder_id or organizations/org_id format." type = string diff --git a/modules/organization/README.md b/modules/organization/README.md index 1d8232cd38..90ab457450 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -102,7 +102,6 @@ module "org" { } ] } - "compute.trustedImageProjects" = { rules = [{ allow = { @@ -145,7 +144,6 @@ To manage organization policy custom constraints, the `orgpolicy.googleapis.com` module "org" { source = "./fabric/modules/organization" organization_id = var.organization_id - org_policy_custom_constraints = { "custom.gkeEnableAutoUpgrade" = { resource_types = ["container.googleapis.com/NodePool"] @@ -156,7 +154,6 @@ module "org" { description = "All node pools must have node auto-upgrade enabled." } } - # not necessarily to enforce on the org level, policy may be applied on folder/project levels org_policies = { "custom.gkeEnableAutoUpgrade" = { @@ -177,9 +174,11 @@ The example below deploys a few org policy custom constraints split between two ```hcl module "org" { - source = "./fabric/modules/organization" - organization_id = var.organization_id - org_policy_custom_constraints_data_path = "configs/custom-constraints" + source = "./fabric/modules/organization" + organization_id = var.organization_id + factories_config = { + org_policy_custom_constraints = "configs/custom-constraints" + } org_policies = { "custom.gkeEnableAutoUpgrade" = { rules = [{ enforce = true }] @@ -373,6 +372,41 @@ module "org" { # tftest modules=1 resources=2 inventory=roles.yaml e2e serial ``` +Custom roles can also be specified via a factory in a similar way to organization policies and policy constraints. Each file is mapped to a custom role, where + +- the role name defaults to the file name but can be overridden via a `name` attribute in the yaml +- role permissions are defined in an `includedPermissions` map + +Custom roles defined via the variable are merged with those coming from the factory, and override them in case of duplicate names. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + factories_config = { + custom_roles = "data/custom_roles" + } +} +# tftest modules=1 resources=2 files=custom-role-1,custom-role-2 inventory=custom-roles.yaml +``` + +```yaml +# tftest-file id=custom-role-1 path=data/custom_roles/test_1.yaml + +includedPermissions: + - compute.globalOperations.get +``` + +```yaml +# tftest-file id=custom-role-2 path=data/custom_roles/test_2.yaml + +name: projectViewer +includedPermissions: + - resourcemanager.projects.get + - resourcemanager.projects.getIamPolicy + - resourcemanager.projects.list +``` + ## Tags Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. @@ -453,24 +487,23 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L212) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L211) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [firewall_policy](variables.tf#L31) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | -| [group_iam](variables.tf#L40) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | -| [iam](variables.tf#L47) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L54) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L69) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [logging_data_access](variables.tf#L84) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L99) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L106) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [network_tags](variables.tf#L137) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L159) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L186) | Path containing org policies in YAML format. | string | | null | -| [org_policy_custom_constraints](variables.tf#L192) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | -| [org_policy_custom_constraints_data_path](variables.tf#L206) | Path containing org policy custom constraints in YAML format. | string | | null | -| [tag_bindings](variables.tf#L221) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L227) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L42) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [group_iam](variables.tf#L51) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | +| [iam](variables.tf#L58) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L65) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L80) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [logging_data_access](variables.tf#L95) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L110) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L117) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [network_tags](variables.tf#L148) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [org_policies](variables.tf#L170) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L197) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L220) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L226) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | ## Outputs @@ -478,12 +511,12 @@ module "org" { |---|---|:---:| | [custom_constraint_ids](outputs.tf#L17) | Map of CUSTOM_CONSTRAINTS => ID in the organization. | | | [custom_role_id](outputs.tf#L22) | Map of custom role IDs created in the organization. | | -| [custom_roles](outputs.tf#L35) | Map of custom roles resources created in the organization. | | -| [id](outputs.tf#L40) | Fully qualified organization id. | | -| [network_tag_keys](outputs.tf#L57) | Tag key resources. | | -| [network_tag_values](outputs.tf#L66) | Tag value resources. | | -| [organization_id](outputs.tf#L76) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L93) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L101) | Tag key resources. | | -| [tag_values](outputs.tf#L110) | Tag value resources. | | +| [custom_roles](outputs.tf#L32) | Map of custom roles resources created in the organization. | | +| [id](outputs.tf#L37) | Fully qualified organization id. | | +| [network_tag_keys](outputs.tf#L54) | Tag key resources. | | +| [network_tag_values](outputs.tf#L63) | Tag value resources. | | +| [organization_id](outputs.tf#L73) | Organization id dependent on module resources. | | +| [sink_writer_identities](outputs.tf#L90) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L98) | Tag key resources. | | +| [tag_values](outputs.tf#L107) | Tag value resources. | | diff --git a/modules/organization/iam.tf b/modules/organization/iam.tf index 16a0fa00f9..e0726467f5 100644 --- a/modules/organization/iam.tf +++ b/modules/organization/iam.tf @@ -17,12 +17,32 @@ # tfdoc:file:description IAM bindings, roles and audit logging resources. locals { + _custom_roles = { + for f in try(fileset(var.factories_config.custom_roles, "*.yaml"), []) : + replace(f, ".yaml", "") => yamldecode( + file("${var.factories_config.custom_roles}/${f}") + ) + } _group_iam_roles = distinct(flatten(values(var.group_iam))) _group_iam = { for r in local._group_iam_roles : r => [ for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null ] } + custom_roles = merge( + { + for k, v in local._custom_roles : k => { + name = lookup(v, "name", k) + permissions = v["includedPermissions"] + } + }, + { + for k, v in var.custom_roles : k => { + name = k + permissions = v + } + } + ) iam = { for role in distinct(concat(keys(var.iam), keys(local._group_iam))) : role => concat( @@ -32,13 +52,27 @@ locals { } } +# we use a different key for custom roles to allow referring to the role alias +# in Terraform, while still being able to define unique role names + +check "custom_roles" { + assert { + condition = ( + length(local.custom_roles) == length({ + for k, v in local.custom_roles : v.name => null + }) + ) + error_message = "Duplicate role name in custom roles." + } +} + resource "google_organization_iam_custom_role" "roles" { - for_each = var.custom_roles + for_each = local.custom_roles org_id = local.organization_id_numeric - role_id = each.key - title = "Custom role ${each.key}" + role_id = each.value.name + title = "Custom role ${each.value.name}" description = "Terraform-managed." - permissions = each.value + permissions = each.value.permissions } resource "google_organization_iam_binding" "authoritative" { diff --git a/modules/organization/org-policy-custom-constraints.tf b/modules/organization/org-policy-custom-constraints.tf index 6a8cf5e6d4..cae888d1c6 100644 --- a/modules/organization/org-policy-custom-constraints.tf +++ b/modules/organization/org-policy-custom-constraints.tf @@ -16,11 +16,9 @@ locals { _custom_constraints_factory_data_raw = merge([ - for f in try(fileset(var.org_policy_custom_constraints_data_path, "*.yaml"), []) : - yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}")) + for f in try(fileset(var.factories_config.org_policy_custom_constraints, "*.yaml"), []) : + yamldecode(file("${var.factories_config.org_policy_custom_constraints}/${f}")) ]...) - - _custom_constraints_factory_data = { for k, v in local._custom_constraints_factory_data_raw : k => { @@ -32,9 +30,10 @@ locals { resource_types = v.resource_types } } - - _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints) - + _custom_constraints = merge( + local._custom_constraints_factory_data, + var.org_policy_custom_constraints + ) custom_constraints = { for k, v in local._custom_constraints : k => merge(v, { diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 8d867f668c..2faf2e97e1 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -18,10 +18,9 @@ locals { _factory_data_raw = merge([ - for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) : - yamldecode(file("${var.org_policies_data_path}/${f}")) + for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) : + yamldecode(file("${var.factories_config.org_policies}/${f}")) ]...) - # simulate applying defaults to data coming from yaml files _factory_data = { for k, v in local._factory_data_raw : @@ -49,9 +48,7 @@ locals { ] } } - _org_policies = merge(local._factory_data, var.org_policies) - org_policies = { for k, v in local._org_policies : k => merge(v, { diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 12c133e87b..b010b29976 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -22,14 +22,11 @@ output "custom_constraint_ids" { output "custom_role_id" { description = "Map of custom role IDs created in the organization." value = { - for role_id, role in google_organization_iam_custom_role.roles : + for k, v in google_organization_iam_custom_role.roles : # build the string manually so that role IDs can be used as map # keys (useful for folder/organization/project-level iam bindings) - (role_id) => "${var.organization_id}/roles/${role_id}" + (k) => "${var.organization_id}/roles/${local.custom_roles[k].name}" } - depends_on = [ - google_organization_iam_custom_role.roles - ] } output "custom_roles" { diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index f664deaef1..f11900dba7 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -28,6 +28,17 @@ variable "custom_roles" { nullable = false } +variable "factories_config" { + description = "Paths to data files and folders that enable factory functionality." + type = object({ + custom_roles = optional(string) + org_policies = optional(string) + org_policy_custom_constraints = optional(string) + }) + nullable = false + default = {} +} + variable "firewall_policy" { description = "Hierarchical firewall policies to associate to the organization." type = object({ @@ -183,12 +194,6 @@ variable "org_policies" { nullable = false } -variable "org_policies_data_path" { - description = "Path containing org policies in YAML format." - type = string - default = null -} - variable "org_policy_custom_constraints" { description = "Organization policy custom constraints keyed by constraint name." type = map(object({ @@ -203,12 +208,6 @@ variable "org_policy_custom_constraints" { nullable = false } -variable "org_policy_custom_constraints_data_path" { - description = "Path containing org policy custom constraints in YAML format." - type = string - default = null -} - variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string diff --git a/modules/project/README.md b/modules/project/README.md index bc5f3e3927..fb08566244 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -421,12 +421,14 @@ The example below deploys a few organization policies split between two YAML fil ```hcl module "project" { - source = "./fabric/modules/project" - billing_account = var.billing_account_id - name = "project" - parent = var.folder_id - prefix = var.prefix - org_policies_data_path = "configs/org-policies/" + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + parent = var.folder_id + prefix = var.prefix + factories_config = { + org_policies = "configs/org-policies/" + } } # tftest modules=1 resources=8 files=boolean,list inventory=org-policies.yaml e2e ``` @@ -898,7 +900,7 @@ module "bucket" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L186) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L196) | Project name and id suffix. | string | ✓ | | | [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | | [billing_account](variables.tf#L23) | Billing account id. | string | | null | | [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | @@ -906,41 +908,41 @@ module "bucket" { | [custom_roles](variables.tf#L43) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | [default_service_account](variables.tf#L50) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | | [descriptive_name](variables.tf#L63) | Name of the project name. Used for project name instead of `name` variable. | string | | null | -| [group_iam](variables.tf#L69) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | -| [iam](variables.tf#L76) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L83) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L98) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [labels](variables.tf#L113) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L120) | If non-empty, creates a project lien with this description. | string | | null | -| [logging_data_access](variables.tf#L126) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L141) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L148) | Logging sinks to create for this project. | map(object({…})) | | {} | -| [metric_scopes](variables.tf#L179) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [org_policies](variables.tf#L191) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L218) | Path containing org policies in YAML format. | string | | null | -| [parent](variables.tf#L224) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L234) | Optional prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L244) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L250) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L262) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L269) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L276) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L282) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L288) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L297) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L320) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L326) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [factories_config](variables.tf#L69) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [group_iam](variables.tf#L79) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | +| [iam](variables.tf#L86) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L93) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L108) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [labels](variables.tf#L123) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L130) | If non-empty, creates a project lien with this description. | string | | null | +| [logging_data_access](variables.tf#L136) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L151) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L158) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L189) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [org_policies](variables.tf#L201) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L228) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L238) | Optional prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L248) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L254) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L266) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L273) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L280) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L286) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L292) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L301) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L324) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L330) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs | name | description | sensitive | |---|---|:---:| -| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | -| [id](outputs.tf#L25) | Project id. | | -| [name](outputs.tf#L44) | Project name. | | -| [number](outputs.tf#L56) | Project number. | | -| [project_id](outputs.tf#L75) | Project id. | | -| [service_accounts](outputs.tf#L94) | Product robot service accounts in project. | | -| [services](outputs.tf#L110) | Service APIs to enabled in the project. | | -| [sink_writer_identities](outputs.tf#L119) | Writer identities created for each sink. | | +| [custom_role_ids](outputs.tf#L17) | Map of custom role IDs created in the project. | | +| [id](outputs.tf#L27) | Project id. | | +| [name](outputs.tf#L46) | Project name. | | +| [number](outputs.tf#L58) | Project number. | | +| [project_id](outputs.tf#L77) | Project id. | | +| [service_accounts](outputs.tf#L96) | Product robot service accounts in project. | | +| [services](outputs.tf#L112) | Service APIs to enabled in the project. | | +| [sink_writer_identities](outputs.tf#L121) | Writer identities created for each sink. | | diff --git a/modules/project/iam.tf b/modules/project/iam.tf index 0f00f2861a..dfd14473cd 100644 --- a/modules/project/iam.tf +++ b/modules/project/iam.tf @@ -20,12 +20,32 @@ # - external users need to have accepted the invitation email to join locals { + _custom_roles = { + for f in try(fileset(var.factories_config.custom_roles, "*.yaml"), []) : + replace(f, ".yaml", "") => yamldecode( + file("${var.factories_config.custom_roles}/${f}") + ) + } _group_iam_roles = distinct(flatten(values(var.group_iam))) _group_iam = { for r in local._group_iam_roles : r => [ for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null ] } + custom_roles = merge( + { + for k, v in local._custom_roles : k => { + name = lookup(v, "name", k) + permissions = v["includedPermissions"] + } + }, + { + for k, v in var.custom_roles : k => { + name = k + permissions = v + } + } + ) iam = { for role in distinct(concat(keys(var.iam), keys(local._group_iam))) : role => concat( @@ -35,13 +55,27 @@ locals { } } +# we use a different key for custom roles to allow referring to the role alias +# in Terraform, while still being able to define unique role names + +check "custom_roles" { + assert { + condition = ( + length(local.custom_roles) == length({ + for k, v in local.custom_roles : v.name => null + }) + ) + error_message = "Duplicate role name in custom roles." + } +} + resource "google_project_iam_custom_role" "roles" { - for_each = var.custom_roles + for_each = local.custom_roles project = local.project.project_id - role_id = each.key - title = "Custom role ${each.key}" + role_id = each.value.name + title = "Custom role ${each.value.name}" description = "Terraform-managed." - permissions = each.value + permissions = each.value.permissions } resource "google_project_iam_binding" "authoritative" { diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 37e6f2531f..32ebf66fcf 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -18,10 +18,9 @@ locals { _factory_data_raw = merge([ - for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) : - yamldecode(file("${var.org_policies_data_path}/${f}")) + for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) : + yamldecode(file("${var.factories_config.org_policies}/${f}")) ]...) - # simulate applying defaults to data coming from yaml files _factory_data = { for k, v in local._factory_data_raw : @@ -49,9 +48,7 @@ locals { ] } } - _org_policies = merge(local._factory_data, var.org_policies) - org_policies = { for k, v in local._org_policies : k => merge(v, { diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf index ae7bbc6e90..332a956e59 100644 --- a/modules/project/outputs.tf +++ b/modules/project/outputs.tf @@ -14,11 +14,13 @@ * limitations under the License. */ -output "custom_roles" { - description = "Ids of the created custom roles." +output "custom_role_ids" { + description = "Map of custom role IDs created in the project." value = { - for name, role in google_project_iam_custom_role.roles : - name => role.id + for k, v in google_project_iam_custom_role.roles : + # build the string manually so that role IDs can be used as map + # keys (useful for folder/organization/project-level iam bindings) + (k) => "projects/${local.prefix}${var.name}/roles/${local.custom_roles[k].name}" } } diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 1bf26a32f4..7ca6003606 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -66,6 +66,16 @@ variable "descriptive_name" { default = null } +variable "factories_config" { + description = "Paths to data files and folders that enable factory functionality." + type = object({ + custom_roles = optional(string) + org_policies = optional(string) + }) + nullable = false + default = {} +} + variable "group_iam" { description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable." type = map(list(string)) @@ -215,12 +225,6 @@ variable "org_policies" { nullable = false } -variable "org_policies_data_path" { - description = "Path containing org policies in YAML format." - type = string - default = null -} - variable "parent" { description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." type = string diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index 7e52704e69..48d4257ade 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -25,5 +25,5 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): 'modules/folder', tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) yaml_plan = plan_summary('modules/folder', tf_var_files=['common.tfvars'], - org_policies_data_path=f'{tmp_path}') + factories_config=f'{{org_policies="{tmp_path}"}}') assert tfvars_plan.values == yaml_plan.values diff --git a/tests/modules/organization/examples/custom-roles.yaml b/tests/modules/organization/examples/custom-roles.yaml new file mode 100644 index 0000000000..e087878098 --- /dev/null +++ b/tests/modules/organization/examples/custom-roles.yaml @@ -0,0 +1,40 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_organization_iam_custom_role.roles["test_2"]: + description: Terraform-managed. + org_id: '1122334455' + permissions: + - resourcemanager.projects.get + - resourcemanager.projects.getIamPolicy + - resourcemanager.projects.list + role_id: projectViewer + stage: GA + title: Custom role projectViewer + module.org.google_organization_iam_custom_role.roles["test_1"]: + description: Terraform-managed. + org_id: '1122334455' + permissions: + - compute.globalOperations.get + role_id: test_1 + stage: GA + title: Custom role test_1 + +counts: + google_organization_iam_custom_role: 2 + modules: 1 + resources: 2 + +outputs: {} diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 9a5d4a458b..1e4474402b 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -26,7 +26,7 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) yaml_plan = plan_summary('modules/organization', tf_var_files=['common.tfvars'], - org_policies_data_path=f'{tmp_path}') + factories_config=f'{{org_policies="{tmp_path}"}}') assert tfvars_plan.values == yaml_plan.values @@ -39,5 +39,5 @@ def test_custom_constraint_factory(plan_summary, tfvars_to_yaml, tmp_path): tf_var_files=['common.tfvars', f'org_policies_custom_constraints.tfvars']) yaml_plan = plan_summary( 'modules/organization', tf_var_files=['common.tfvars'], - org_policy_custom_constraints_data_path=f'{tmp_path}') + factories_config=f'{{org_policy_custom_constraints="{tmp_path}"}}') assert tfvars_plan.values == yaml_plan.values diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py index 4d92b46f5c..632b45e726 100644 --- a/tests/modules/organization/test_plan_org_policies_modules.py +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -34,7 +34,7 @@ def test_policy_implementation(): '@@ -17 +17 @@\n', '-# tfdoc:file:description Project-level organization policies.\n', '+# tfdoc:file:description Folder-level organization policies.\n', - '@@ -58,2 +58,2 @@\n', + '@@ -55,2 +55,2 @@\n', '- name = "projects/${local.project.project_id}/policies/${k}"\n', '- parent = "projects/${local.project.project_id}"\n', '+ name = "${local.folder.name}/policies/${k}"\n', @@ -49,12 +49,12 @@ def test_policy_implementation(): '@@ -17 +17 @@\n', '-# tfdoc:file:description Folder-level organization policies.\n', '+# tfdoc:file:description Organization-level organization policies.\n', - '@@ -58,2 +58,2 @@\n', + '@@ -55,2 +55,2 @@\n', '- name = "${local.folder.name}/policies/${k}"\n', '- parent = local.folder.name\n', '+ name = "${var.organization_id}/policies/${k}"\n', '+ parent = var.organization_id\n', - '@@ -116,0 +117,9 @@\n', + '@@ -113,0 +114,9 @@\n', '+ depends_on = [\n', '+ google_organization_iam_binding.authoritative,\n', '+ google_organization_iam_binding.bindings,\n', diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index 30354aeaa9..a85bf9c5f6 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -25,5 +25,5 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): 'modules/project', tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) yaml_plan = plan_summary('modules/project', tf_var_files=['common.tfvars'], - org_policies_data_path=f'{tmp_path}') + factories_config=f'{{org_policies="{tmp_path}"}}') assert tfvars_plan.values == yaml_plan.values