diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 21c0746ae2..1c63b78dc0 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -53,12 +53,12 @@ jobs: run: | terraform fmt -recursive -check -diff $GITHUB_WORKSPACE - - name: Check documentation (fabric) + - name: Check documentation id: documentation-fabric run: | - python3 tools/check_documentation.py examples modules fast + python3 tools/check_documentation.py modules fast blueprints - - name: Check documentation links (fabric) + - name: Check documentation links id: documentation-links-fabric run: | python3 tools/check_links.py . diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md index 40e00f8678..35198e8d1c 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md @@ -12,22 +12,22 @@ The codebase provisions the following list of resources: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | -| [project_id](variables.tf#L38) | Existing project id. | string | ✓ | | -| [tfe_organization_id](variables.tf#L43) | | | ✓ | | -| [tfe_workspace_id](variables.tf#L48) | | | ✓ | | -| [issuer_uri](variables.tf#L65) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [project_id](variables.tf#L43) | Existing project id. | string | ✓ | | +| [tfe_organization_id](variables.tf#L48) | TFE organization id. | string | ✓ | | +| [tfe_workspace_id](variables.tf#L53) | TFE workspace id. | string | ✓ | | +| [issuer_uri](variables.tf#L21) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | | [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [project_create](variables.tf#L21) | Create project instead of using an existing one. | bool | | true | -| [workload_identity_pool_id](variables.tf#L53) | Workload identity pool id. | string | | "tfe-pool" | -| [workload_identity_pool_provider_id](variables.tf#L59) | Workload identity pool provider id. | string | | "tfe-provider" | +| [project_create](variables.tf#L37) | Create project instead of using an existing one. | bool | | true | +| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string | | "tfe-pool" | +| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string | | "tfe-provider" | ## Outputs | name | description | sensitive | |---|---|:---:| -| [impersonate_service_account_email](outputs.tf#L31) | | | -| [project_id](outputs.tf#L16) | | | -| [workload_identity_audience](outputs.tf#L26) | | | -| [workload_identity_pool_provider_id](outputs.tf#L21) | GCP workload identity pool provider ID. | | +| [impersonate_service_account_email](outputs.tf#L16) | Service account to be impersonated by workload identity. | | +| [project_id](outputs.tf#L21) | GCP Project ID. | | +| [workload_identity_audience](outputs.tf#L26) | TFC Workload Identity Audience. | | +| [workload_identity_pool_provider_id](outputs.tf#L31) | GCP workload identity pool provider ID. | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf index 79cea39a27..46d7f6b0f2 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf @@ -13,22 +13,22 @@ # limitations under the License. +output "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + value = module.sa-tfe.email +} + output "project_id" { description = "GCP Project ID." value = module.project.project_id } -output "workload_identity_pool_provider_id" { - description = "GCP workload identity pool provider ID." - value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name -} - output "workload_identity_audience" { description = "TFC Workload Identity Audience." value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" } -output "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity." - value = module.sa-tfe.email +output "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf index 62163d1782..3719b1839e 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf @@ -18,10 +18,10 @@ variable "billing_account" { type = string } -variable "project_create" { - description = "Create project instead of using an existing one." - type = bool - default = true +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" } variable "parent" { @@ -34,6 +34,11 @@ variable "parent" { } } +variable "project_create" { + description = "Create project instead of using an existing one." + type = bool + default = true +} variable "project_id" { description = "Existing project id." @@ -61,9 +66,3 @@ variable "workload_identity_pool_provider_id" { type = string default = "tfe-provider" } - -variable "issuer_uri" { - description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." - type = string - default = "https://app.terraform.io/" -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md index 5226dd64cc..9be8a09bda 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md @@ -5,15 +5,14 @@ This terraform code is a part of [GCP Workload Identity Federation for Terraform The codebase provisions the following list of resources: - GCS Bucket - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [impersonate_service_account_email](variables.tf#L26) | | | ✓ | | -| [project_id](variables.tf#L16) | | | ✓ | | -| [workload_identity_pool_provider_id](variables.tf#L21) | GCP workload identity pool provider ID. | string | ✓ | | +| [impersonate_service_account_email](variables.tf#L21) | Service account to be impersonated by workload identity. | string | ✓ | | +| [project_id](variables.tf#L16) | GCP project ID. | string | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L26) | GCP workload identity pool provider ID. | string | ✓ | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf index 3f36c2ca65..3a1d81dc2a 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf @@ -18,12 +18,12 @@ variable "project_id" { type = string } -variable "workload_identity_pool_provider_id" { - description = "GCP workload identity pool provider ID." +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." type = string } -variable "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity." +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." type = string } diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index 7519fa8a5e..4ced84f230 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -67,8 +67,10 @@ module "orch-project" { "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } oslogin = false - policy_boolean = { - "constraints/compute.requireOsLogin" = false + org_policies = { + "constraints/compute.requireOsLogin" = { + enforce = false + } } services = concat(var.project_services, [ "artifactregistry.googleapis.com", diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index 2bcd69ab88..3fb6999a60 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -40,8 +40,10 @@ module "project" { "storage.googleapis.com", "storage-component.googleapis.com" ] - policy_boolean = { - # "constraints/compute.requireOsLogin" = false + org_policies = { + # "constraints/compute.requireOsLogin" = { + # enforce = false + # } # Example of applying a project wide policy, mainly useful for Composer } service_encryption_key_ids = { diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index e496aa4d1d..c7bb0132fe 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -68,13 +68,13 @@ module "projects" { iam = try(each.value.iam, {}) kms_service_agents = try(each.value.kms, {}) labels = try(each.value.labels, {}) - org_policies = try(each.value.org_policies, null) + org_policies = try(each.value.org_policies, {}) service_accounts = try(each.value.service_accounts, {}) services = try(each.value.services, []) service_identities_iam = try(each.value.service_identities_iam, {}) vpc = try(each.value.vpc, null) } -# tftest modules=7 resources=27 +# tftest modules=7 resources=28 ``` ### Projects configuration @@ -153,16 +153,16 @@ labels: environment: prod # [opt] Org policy overrides defined at project level -org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null +org_policies: + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - - projects/fast-prod-iac-core-0 + - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format @@ -221,7 +221,7 @@ vpc: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [project_id](variables.tf#L119) | Project id. | string | ✓ | | +| [project_id](variables.tf#L145) | Project id. | string | ✓ | | | [billing_alert](variables.tf#L22) | Billing alert configuration. | object({…}) | | null | | [defaults](variables.tf#L35) | Project factory default values. | object({…}) | | null | | [dns_zones](variables.tf#L57) | DNS private zones to create as child of var.defaults.environment_dns_zone. | list(string) | | [] | @@ -231,13 +231,13 @@ vpc: | [iam](variables.tf#L81) | Custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | [kms_service_agents](variables.tf#L87) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | | [labels](variables.tf#L93) | Labels to be assigned at project level. | map(string) | | {} | -| [org_policies](variables.tf#L99) | Org-policy overrides at project level. | object({…}) | | null | -| [prefix](variables.tf#L113) | Prefix used for the project id. | string | | null | -| [service_accounts](variables.tf#L124) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | -| [service_accounts_iam](variables.tf#L130) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]} | map(map(list(string))) | | {} | -| [service_identities_iam](variables.tf#L144) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [services](variables.tf#L137) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L151) | VPC configuration for the project. | object({…}) | | null | +| [org_policies](variables.tf#L99) | Org-policy overrides at project level. | map(object({…})) | | {} | +| [prefix](variables.tf#L139) | Prefix used for the project id. | string | | null | +| [service_accounts](variables.tf#L150) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | +| [service_accounts_iam](variables.tf#L156) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]} | map(map(list(string))) | | {} | +| [service_identities_iam](variables.tf#L164) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [services](variables.tf#L171) | Services to be enabled for the project. | list(string) | | [] | +| [vpc](variables.tf#L178) | VPC configuration for the project. | object({…}) | | null | ## Outputs diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf index 996b79e354..db80810b9f 100644 --- a/blueprints/factories/project-factory/main.tf +++ b/blueprints/factories/project-factory/main.tf @@ -148,9 +148,8 @@ module "project" { contacts = { for c in local.essential_contacts : c => ["ALL"] } iam = local.iam labels = local.labels + org_policies = try(var.org_policies, {}) parent = var.folder_id - policy_boolean = try(var.org_policies.policy_boolean, {}) - policy_list = try(var.org_policies.policy_list, {}) service_encryption_key_ids = var.kms_service_agents services = local.services shared_vpc_service_config = var.vpc == null ? null : { diff --git a/blueprints/factories/project-factory/sample-data/projects/project.yaml b/blueprints/factories/project-factory/sample-data/projects/project.yaml index 13a8f5f52e..88ba0bf50a 100644 --- a/blueprints/factories/project-factory/sample-data/projects/project.yaml +++ b/blueprints/factories/project-factory/sample-data/projects/project.yaml @@ -48,15 +48,15 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index 6154c032c1..cf5d8fb342 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -98,16 +98,42 @@ variable "labels" { variable "org_policies" { description = "Org-policy overrides at project level." - type = object({ - policy_boolean = map(bool) - policy_list = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) })) - }) - default = null + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false } variable "prefix" { @@ -134,12 +160,6 @@ variable "service_accounts_iam" { nullable = false } -variable "services" { - description = "Services to be enabled for the project." - type = list(string) - default = [] - nullable = false -} variable "service_identities_iam" { description = "Custom IAM settings for service identities in service => [role] format." @@ -148,6 +168,13 @@ variable "service_identities_iam" { nullable = false } +variable "services" { + description = "Services to be enabled for the project." + type = list(string) + default = [] + nullable = false +} + variable "vpc" { description = "VPC configuration for the project." type = object({ @@ -160,6 +187,3 @@ variable "vpc" { }) default = null } - - - diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf index 884fbd300f..97d6efec03 100644 --- a/blueprints/networking/filtering-proxy/main.tf +++ b/blueprints/networking/filtering-proxy/main.tf @@ -226,13 +226,10 @@ module "folder-apps" { source = "../../../modules/folder" parent = var.root_node name = "apps" - policy_list = { + org_policies = { # prevent VMs with public IPs in the apps folder "constraints/compute.vmExternalIpAccess" = { - inherit_from_parent = false - suggested_value = null - status = false - values = [] + deny = { all = true } } } } diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/01-resman/branch-sandbox.tf index 7ed154aae1..84995c15bd 100644 --- a/fast/stages/01-resman/branch-sandbox.tf +++ b/fast/stages/01-resman/branch-sandbox.tf @@ -32,16 +32,9 @@ module "branch-sandbox-folder" { "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.0.iam_email] "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.0.iam_email] } - policy_boolean = { - "constraints/sql.restrictPublicIp" = false - } - policy_list = { - "constraints/compute.vmExternalIpAccess" = { - inherit_from_parent = false - suggested_value = null - status = true - values = [] - } + org_policies = { + "constraints/sql.restrictPublicIp" = { enforce = false } + "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } } tag_bindings = { context = try( diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/01-resman/organization.tf index 6596f9c000..0e8430e109 100644 --- a/fast/stages/01-resman/organization.tf +++ b/fast/stages/01-resman/organization.tf @@ -18,18 +18,11 @@ locals { - list_allow = { - inherit_from_parent = false - suggested_value = null - status = true - values = [] - } - list_deny = { - inherit_from_parent = false - suggested_value = null - status = false - values = [] - } + all_drs_domains = concat( + [var.organization.customer_id], + try(local.policy_configs.allowed_policy_member_domains, []) + ) + policy_configs = ( var.organization_policy_configs == null ? {} @@ -74,74 +67,55 @@ module "organization" { } : {} ) # sample subset of useful organization policies, edit to suit requirements - policy_boolean = { - # "constraints/cloudfunctions.requireVPCConnector" = true - # "constraints/compute.disableGuestAttributesAccess" = true - # "constraints/compute.disableInternetNetworkEndpointGroup" = true - # "constraints/compute.disableNestedVirtualization" = true - # "constraints/compute.disableSerialPortAccess" = true - "constraints/compute.requireOsLogin" = true - # "constraints/compute.restrictXpnProjectLienRemoval" = true - "constraints/compute.skipDefaultNetworkCreation" = true - # "constraints/compute.setNewProjectDefaultToZonalDNSOnly" = true - "constraints/iam.automaticIamGrantsForDefaultServiceAccounts" = true - "constraints/iam.disableServiceAccountKeyCreation" = true - # "constraints/iam.disableServiceAccountKeyUpload" = true - "constraints/sql.restrictPublicIp" = true - "constraints/sql.restrictAuthorizedNetworks" = true - "constraints/storage.uniformBucketLevelAccess" = true - } - policy_list = { - # "constraints/cloudfunctions.allowedIngressSettings" = merge( - # local.list_allow, { values = ["is:ALLOW_INTERNAL_ONLY"] } - # ) - # "constraints/cloudfunctions.allowedVpcConnectorEgressSettings" = merge( - # local.list_allow, { values = ["is:PRIVATE_RANGES_ONLY"] } - # ) - "constraints/compute.restrictLoadBalancerCreationForTypes" = merge( - local.list_allow, { values = ["in:INTERNAL"] } - ) - "constraints/compute.vmExternalIpAccess" = local.list_deny - "constraints/iam.allowedPolicyMemberDomains" = merge( - local.list_allow, { - values = concat( - [var.organization.customer_id], - try(local.policy_configs.allowed_policy_member_domains, []) - ) - }) - "constraints/run.allowedIngress" = merge( - local.list_allow, { values = ["is:internal"] } - ) - # "constraints/run.allowedVPCEgress" = merge( - # local.list_allow, { values = ["is:private-ranges-only"] } - # ) - # "constraints/compute.restrictCloudNATUsage" = local.list_deny - # "constraints/compute.restrictDedicatedInterconnectUsage" = local.list_deny - # "constraints/compute.restrictPartnerInterconnectUsage" = local.list_deny - # "constraints/compute.restrictProtocolForwardingCreationForTypes" = local.list_deny - # "constraints/compute.restrictSharedVpcHostProjects" = local.list_deny - # "constraints/compute.restrictSharedVpcSubnetworks" = local.list_deny - # "constraints/compute.restrictVpcPeering" = local.list_deny - # "constraints/compute.restrictVpnPeerIPs" = local.list_deny - # "constraints/compute.vmCanIpForward" = local.list_deny - # "constraints/gcp.resourceLocations" = { - # inherit_from_parent = false - # suggested_value = null - # status = true - # values = local.allowed_regions + + org_policies = { + "compute.disableGuestAttributesAccess" = { enforce = true } + "compute.requireOsLogin" = { enforce = true } + "compute.restrictLoadBalancerCreationForTypes" = { allow = { values = ["in:INTERNAL"] } } + "compute.skipDefaultNetworkCreation" = { enforce = true } + "compute.vmExternalIpAccess" = { deny = { all = true } } + "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } } + "iam.automaticIamGrantsForDefaultServiceAccounts" = { enforce = true } + "iam.disableServiceAccountKeyCreation" = { enforce = true } + "iam.disableServiceAccountKeyUpload" = { enforce = true } + "run.allowedIngress" = { allow = { values = ["is:INTERNAL"] } } + "sql.restrictAuthorizedNetworks" = { enforce = true } + "sql.restrictPublicIp" = { enforce = true } + "storage.uniformBucketLevelAccess" = { enforce = true } + + # "cloudfunctions.allowedIngressSettings" = { + # allow = { values = ["is:ALLOW_INTERNAL_ONLY"] } + # } + # "cloudfunctions.allowedVpcConnectorEgressSettings" = { + # allow = { values = ["is:PRIVATE_RANGES_ONLY"] } + # } + # "cloudfunctions.requireVPCConnector" = { enforce = true } + # "compute.disableInternetNetworkEndpointGroup" = { enforce = true } + # "compute.disableNestedVirtualization" = { enforce = true } + # "compute.disableSerialPortAccess" = { enforce = true } + # "compute.restrictCloudNATUsage" = { deny = { all = true }} + # "compute.restrictDedicatedInterconnectUsage" = { deny = { all = true }} + # "compute.restrictPartnerInterconnectUsage" = { deny = { all = true }} + # "compute.restrictProtocolForwardingCreationForTypes" = { deny = { all = true }} + # "compute.restrictSharedVpcHostProjects" = { deny = { all = true }} + # "compute.restrictSharedVpcSubnetworks" = { deny = { all = true }} + # "compute.restrictVpcPeering" = { deny = { all = true }} + # "compute.restrictVpnPeerIPs" = { deny = { all = true }} + # "compute.restrictXpnProjectLienRemoval" = { enforce = true } + # "compute.setNewProjectDefaultToZonalDNSOnly" = { enforce = true } + # "compute.vmCanIpForward" = { deny = { all = true }} + # "gcp.resourceLocations" = { + # allow = { values = local.allowed_regions } + # } + # "iam.workloadIdentityPoolProviders" = { + # allow = { + # values = [ + # for k, v in coalesce(var.automation.federated_identity_providers, {}) : + # v.issuer_uri + # ] + # } # } - # https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict - # "constraints/iam.workloadIdentityPoolProviders" = merge( - # local.list_allow, { values = [ - # for k, v in coalesce(var.automation.federated_identity_providers, {}) : - # v.issuer_uri - # ] } - # ) - # "constraints/iam.workloadIdentityPoolAwsAccounts" = merge( - # local.list_allow, { values = [ - # - # ] } - # ) + # "run.allowedVPCEgress" = { allow = { values = ["is:private-ranges-only"] } } } tags = { (var.tag_names.context) = { diff --git a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample index 13a8f5f52e..88ba0bf50a 100644 --- a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample +++ b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample @@ -48,15 +48,15 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/modules/folder/README.md b/modules/folder/README.md index 8f4c1bc384..0014e7616a 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -31,20 +31,46 @@ module "folder" { source = "./fabric/modules/folder" parent = "organizations/1234567890" name = "Folder name" - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=8 ``` ### Firewall policy factory @@ -259,7 +285,7 @@ module "folder" { | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding · google_folder_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | -| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables.tf](./variables.tf) | Module variables. | | @@ -282,10 +308,9 @@ module "folder" { | [logging_exclusions](variables.tf#L98) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L105) | Logging sinks to create for this folder. | map(object({…})) | | {} | | [name](variables.tf#L126) | Folder name. | string | | null | -| [parent](variables.tf#L132) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [policy_boolean](variables.tf#L142) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L149) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L161) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L132) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L172) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L182) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs @@ -295,7 +320,7 @@ module "folder" { | [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | | [folder](outputs.tf#L26) | Folder resource. | | | [id](outputs.tf#L31) | Folder id. | | -| [name](outputs.tf#L41) | Folder name. | | -| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | +| [name](outputs.tf#L40) | Folder name. | | +| [sink_writer_identities](outputs.tf#L45) | Writer identities created for each sink. | | diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 177a3d8041..da47806390 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -16,75 +16,79 @@ # tfdoc:file:description Folder-level organization policies. -resource "google_folder_organization_policy" "boolean" { - for_each = var.policy_boolean - folder = local.folder.name - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } } -resource "google_folder_organization_policy" "list" { - for_each = var.policy_list - folder = local.folder.name - constraint = each.key +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "${local.folder.name}/policies/${each.key}" + parent = local.folder.name - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) } } } - } - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } + } + } } } } diff --git a/modules/folder/outputs.tf b/modules/folder/outputs.tf index 37babc6f6c..8073951bfa 100644 --- a/modules/folder/outputs.tf +++ b/modules/folder/outputs.tf @@ -33,8 +33,7 @@ output "id" { value = local.folder.name depends_on = [ google_folder_iam_binding.authoritative, - google_folder_organization_policy.boolean, - google_folder_organization_policy.list + google_org_policy_policy.default, ] } diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 19ed18f3de..a00e147f65 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -129,6 +129,46 @@ variable "name" { default = null } +variable "org_policies" { + description = "Organization policies applied to this folder keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "parent" { description = "Parent in folders/folder_id or organizations/org_id format." type = string @@ -139,25 +179,6 @@ variable "parent" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "tag_bindings" { description = "Tag bindings for this folder, in key => tag value id format." type = map(string) diff --git a/modules/organization/README.md b/modules/organization/README.md index 2377c6cc8e..fb9197c318 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -19,20 +19,47 @@ module "org" { iam = { "roles/resourcemanager.projectCreator" = ["group:cloud-admins@example.org"] } - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=10 ``` ## IAM @@ -281,7 +308,7 @@ module "org" { | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_audit_config · google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member · google_organization_iam_policy | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact | -| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | | [variables.tf](./variables.tf) | Module variables. | | @@ -291,7 +318,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L151) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L191) | 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_policies](variables.tf#L31) | Hierarchical firewall policy rules created in the organization. | map(map(object({…}))) | | {} | @@ -306,10 +333,9 @@ module "org" { | [iam_bindings_authoritative](variables.tf#L116) | IAM authoritative bindings, in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared. Bindings should also be authoritative when using authoritative audit config. Use with caution. | map(list(string)) | | null | | [logging_exclusions](variables.tf#L122) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L129) | Logging sinks to create for this organization. | map(object({…})) | | {} | -| [policy_boolean](variables.tf#L160) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L167) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L179) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L185) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | +| [org_policies](variables.tf#L151) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L200) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L206) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | ## Outputs @@ -320,8 +346,8 @@ module "org" { | [firewall_policies](outputs.tf#L36) | Map of firewall policy resources created in the organization. | | | [firewall_policy_id](outputs.tf#L41) | Map of firewall policy ids created in the organization. | | | [organization_id](outputs.tf#L46) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L64) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L72) | Tag key resources. | | -| [tag_values](outputs.tf#L79) | Tag value resources. | | +| [sink_writer_identities](outputs.tf#L63) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L71) | Tag key resources. | | +| [tag_values](outputs.tf#L78) | Tag value resources. | | diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index f23a98b488..defa11b0bd 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -16,83 +16,79 @@ # tfdoc:file:description Organization-level organization policies. -resource "google_organization_policy" "boolean" { - for_each = var.policy_boolean - org_id = local.organization_id_numeric - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } - - depends_on = [ - google_organization_iam_audit_config.config, - google_organization_iam_binding.authoritative, - google_organization_iam_custom_role.roles, - google_organization_iam_member.additive, - google_organization_iam_policy.authoritative, - ] } -resource "google_organization_policy" "list" { - for_each = var.policy_list - org_id = local.organization_id_numeric - constraint = each.key +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "${var.organization_id}/policies/${each.key}" + parent = var.organization_id - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) } } } - } - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } + } + } } } @@ -103,4 +99,5 @@ resource "google_organization_policy" "list" { google_organization_iam_member.additive, google_organization_iam_policy.authoritative, ] + } diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 1679a1d70b..198b3c8df7 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -52,8 +52,7 @@ output "organization_id" { google_organization_iam_custom_role.roles, google_organization_iam_member.additive, google_organization_iam_policy.authoritative, - google_organization_policy.boolean, - google_organization_policy.list, + google_org_policy_policy.default, google_tags_tag_key.default, google_tags_tag_key_iam_binding.default, google_tags_tag_value.default, diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 9ffce95ce9..7499d6d676 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -148,6 +148,46 @@ variable "logging_sinks" { nullable = false } +variable "org_policies" { + description = "Organization policies applied to this organization keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string @@ -157,25 +197,6 @@ variable "organization_id" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "tag_bindings" { description = "Tag bindings for this organization, in key => tag value id format." type = map(string) diff --git a/modules/project/README.md b/modules/project/README.md index 9df30d1800..8e6c64d4fc 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -167,20 +167,46 @@ module "project" { "container.googleapis.com", "stackdriver.googleapis.com" ] - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=10 ``` ## Logging Sinks @@ -349,7 +375,7 @@ output "compute_robot" { | [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | -| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | | [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | @@ -367,8 +393,8 @@ output "compute_robot" { | [billing_account](variables.tf#L23) | Billing account id. | string | | null | | [contacts](variables.tf#L29) | 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#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_service_account](variables.tf#L49) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [default_service_account](variables.tf#L43) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [descriptive_name](variables.tf#L49) | Name of the project name. Used for project name instead of `name` variable. | string | | null | | [group_iam](variables.tf#L55) | 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#L62) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_additive](variables.tf#L69) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | @@ -378,23 +404,22 @@ output "compute_robot" { | [logging_exclusions](variables.tf#L95) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L124) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [oslogin](variables.tf#L136) | Enable OS Login. | bool | | false | -| [oslogin_admins](variables.tf#L142) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | -| [oslogin_users](variables.tf#L150) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | -| [parent](variables.tf#L157) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [policy_boolean](variables.tf#L167) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L174) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [prefix](variables.tf#L186) | Prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L192) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L198) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L210) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L217) | 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#L224) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L230) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L236) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L245) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | -| [skip_delete](variables.tf#L255) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L261) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L136) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [oslogin](variables.tf#L176) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L182) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L190) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L197) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L207) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L213) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L219) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L231) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L238) | 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#L245) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L251) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L257) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L266) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L276) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L282) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs @@ -402,9 +427,9 @@ output "compute_robot" { |---|---|:---:| | [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | | [name](outputs.tf#L25) | Project name. | | -| [number](outputs.tf#L38) | Project number. | | -| [project_id](outputs.tf#L56) | Project id. | | -| [service_accounts](outputs.tf#L76) | Product robot service accounts in project. | | -| [sink_writer_identities](outputs.tf#L92) | Writer identities created for each sink. | | +| [number](outputs.tf#L37) | Project number. | | +| [project_id](outputs.tf#L54) | Project id. | | +| [service_accounts](outputs.tf#L73) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L89) | Writer identities created for each sink. | | diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 6870754897..ae4a85012e 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -16,75 +16,79 @@ # tfdoc:file:description Project-level organization policies. -resource "google_project_organization_policy" "boolean" { - for_each = var.policy_boolean - project = local.project.project_id - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } } -resource "google_project_organization_policy" "list" { - for_each = var.policy_list - project = local.project.project_id - constraint = each.key +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "projects/${local.project.project_id}/policies/${each.key}" + parent = "projects/${local.project.project_id}" - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) } } } - } - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } + } + } } } } diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf index 3b7efc9077..cb940d010d 100644 --- a/modules/project/outputs.tf +++ b/modules/project/outputs.tf @@ -26,8 +26,7 @@ output "name" { description = "Project name." value = local.project.name depends_on = [ - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_service_project.service_projects, google_project_iam_member.shared_vpc_host_robots, @@ -39,8 +38,7 @@ output "number" { description = "Project number." value = local.project.number depends_on = [ - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_host_project.shared_vpc_host, google_compute_shared_vpc_service_project.shared_vpc_service, @@ -59,8 +57,7 @@ output "project_id" { depends_on = [ google_project.project, data.google_project.project, - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_host_project.shared_vpc_host, google_compute_shared_vpc_service_project.shared_vpc_service, diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 41d3163faf..a58affc92f 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -40,18 +40,18 @@ variable "custom_roles" { nullable = false } -variable "descriptive_name" { - description = "Name of the project name. Used for project name instead of `name` variable." - type = string - default = null -} - variable "default_service_account" { description = "Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`." default = "keep" type = string } +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + 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)) @@ -133,6 +133,46 @@ variable "name" { type = string } +variable "org_policies" { + description = "Organization policies applied to this project keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "oslogin" { description = "Enable OS Login." type = bool @@ -164,25 +204,6 @@ variable "parent" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "prefix" { description = "Prefix used to generate project id and name." type = string diff --git a/tests/modules/folder/fixture/main.tf b/tests/modules/folder/fixture/main.tf index 2fa1b4fd09..8290f82ecf 100644 --- a/tests/modules/folder/fixture/main.tf +++ b/tests/modules/folder/fixture/main.tf @@ -22,10 +22,9 @@ module "test" { iam = var.iam iam_additive = var.iam_additive iam_additive_members = var.iam_additive_members - policy_boolean = var.policy_boolean - policy_list = var.policy_list firewall_policies = var.firewall_policies firewall_policy_association = var.firewall_policy_association logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions + org_policies = var.org_policies } diff --git a/tests/modules/folder/fixture/variables.tf b/tests/modules/folder/fixture/variables.tf index da676debf9..7c03e05683 100644 --- a/tests/modules/folder/fixture/variables.tf +++ b/tests/modules/folder/fixture/variables.tf @@ -34,16 +34,6 @@ variable "iam_additive_members" { default = {} } -variable "policy_boolean" { - type = any - default = {} -} - -variable "policy_list" { - type = any - default = {} -} - variable "firewall_policies" { type = any default = {} @@ -63,3 +53,8 @@ variable "logging_exclusions" { type = any default = {} } + +variable "org_policies" { + type = any + default = {} +} diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index b7ae96c260..f84d50fb06 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -12,56 +12,212 @@ # See the License for the specific language governing permissions and # limitations under the License. -def test_sink(plan_runner): - "Test folder-level sink." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) - assert len(resources) == 4 - resources = [r for r in resources if r['type'] - == 'google_folder_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', - 'policy-b', - 'policy-c', - ] - policy_values = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policy_values.append((resource['index'], policy,) + value[0].popitem()) - assert sorted(policy_values) == [ - ('policy-a', 'boolean_policy', 'enforced', True), - ('policy-b', 'boolean_policy', 'enforced', False), - ('policy-c', 'restore_policy', 'default', True), - ] +def test_policy_boolean(plan_runner): + "Test boolean org policy." + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 3 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } -def test_exclussions(plan_runner): - "Test folder-level logging exclusions." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) +def test_policy_list(plan_runner): + "Test list org policy." + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) assert len(resources) == 4 - resources = [r for r in resources if r['type'] - == 'google_folder_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', - 'policy-b', - 'policy-c', - ] - values = [r['values'] for r in resources] - assert [r['constraint'] for r in values] == [ - 'policy-a', 'policy-b', 'policy-c' - ] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + } diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 04ae4adf01..13f8e335f4 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -28,8 +28,7 @@ module "test" { iam_audit_config = var.iam_audit_config logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions - policy_boolean = var.policy_boolean - policy_list = var.policy_list + org_policies = var.org_policies tag_bindings = var.tag_bindings tags = var.tags } diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf index 1d7ca88d7e..f56e51dcc7 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -44,16 +44,6 @@ variable "iam_audit_config" { default = {} } -variable "policy_boolean" { - type = any - default = {} -} - -variable "policy_list" { - type = any - default = {} -} - variable "firewall_policies" { type = any default = {} @@ -79,6 +69,11 @@ variable "logging_exclusions" { default = {} } +variable "org_policies" { + type = any + default = {} +} + variable "tag_bindings" { type = any default = null diff --git a/tests/modules/organization/test_plan.py b/tests/modules/organization/test_plan.py index a40758a21d..37860ab6d4 100644 --- a/tests/modules/organization/test_plan.py +++ b/tests/modules/organization/test_plan.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_audit_config(plan_runner): "Test audit config." iam_audit_config = '{allServices={DATA_READ=[], DATA_WRITE=["user:me@example.org"]}}' _, resources = plan_runner(iam_audit_config=iam_audit_config) assert len(resources) == 1 - log_types = set(r['log_type'] - for r in resources[0]['values']['audit_log_config']) + log_types = set( + r['log_type'] for r in resources[0]['values']['audit_log_config']) assert log_types == set(['DATA_READ', 'DATA_WRITE']) @@ -28,21 +29,21 @@ def test_iam(plan_runner): '{' '"owners@example.org" = ["roles/owner", "roles/resourcemanager.folderAdmin"],' '"viewers@example.org" = ["roles/viewer"]' - '}' - ) - iam = ( - '{' - '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' - '"roles/browser" = ["domain:example.org"]' - '}' - ) + '}') + iam = ('{' + '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' + '"roles/browser" = ["domain:example.org"]' + '}') _, resources = plan_runner(group_iam=group_iam, iam=iam) roles = sorted([(r['values']['role'], sorted(r['values']['members'])) - for r in resources if r['type'] == 'google_organization_iam_binding']) + for r in resources + if r['type'] == 'google_organization_iam_binding']) assert roles == [ ('roles/browser', ['domain:example.org']), - ('roles/owner', ['group:owners@example.org', 'user:one@example.org', - 'user:two@example.org']), + ('roles/owner', [ + 'group:owners@example.org', 'user:one@example.org', + 'user:two@example.org' + ]), ('roles/resourcemanager.folderAdmin', ['group:owners@example.org']), ('roles/viewer', ['group:viewers@example.org']), ] @@ -50,55 +51,12 @@ def test_iam(plan_runner): def test_iam_additive_members(plan_runner): "Test IAM additive members." - iam = ( - '{"user:one@example.org" = ["roles/owner"],' - '"user:two@example.org" = ["roles/owner", "roles/editor"]}' - ) + iam = ('{"user:one@example.org" = ["roles/owner"],' + '"user:two@example.org" = ["roles/owner", "roles/editor"]}') _, resources = plan_runner(iam_additive_members=iam) roles = set((r['values']['role'], r['values']['member']) - for r in resources if r['type'] == 'google_organization_iam_member') - assert roles == set([ - ('roles/owner', 'user:one@example.org'), - ('roles/owner', 'user:two@example.org'), - ('roles/editor', 'user:two@example.org') - ]) - - -def test_policy_boolean(plan_runner): - "Test boolean org policy." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) - assert len(resources) == 3 - constraints = set(r['values']['constraint'] for r in resources) - assert set(constraints) == set(['policy-a', 'policy-b', 'policy-c']) - policies = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policies.append((policy,) + value[0].popitem()) - assert set(policies) == set([ - ('boolean_policy', 'enforced', True), - ('boolean_policy', 'enforced', False), - ('restore_policy', 'default', True)]) - - -def test_policy_list(plan_runner): - "Test list org policy." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) - assert len(resources) == 3 - values = [r['values'] for r in resources] - assert [r['constraint'] - for r in values] == ['policy-a', 'policy-b', 'policy-c'] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + for r in resources + if r['type'] == 'google_organization_iam_member') + assert roles == set([('roles/owner', 'user:one@example.org'), + ('roles/owner', 'user:two@example.org'), + ('roles/editor', 'user:two@example.org')]) diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py new file mode 100644 index 0000000000..63ff2e7637 --- /dev/null +++ b/tests/modules/organization/test_plan_org_policies.py @@ -0,0 +1,227 @@ +# Copyright 2022 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. + + +def test_policy_boolean(plan_runner): + "Test boolean org policy." + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 2 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + assert all( + x['values']['parent'] == 'organizations/1234567890' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } + + +def test_policy_list(plan_runner): + "Test list org policy." + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 3 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 + assert all( + x['values']['parent'] == 'organizations/1234567890' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + } diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf index a9867e5d9b..4c7441ac55 100644 --- a/tests/modules/project/fixture/main.tf +++ b/tests/modules/project/fixture/main.tf @@ -25,12 +25,11 @@ module "test" { iam_additive_members = var.iam_additive_members labels = var.labels lien_reason = var.lien_reason + org_policies = var.org_policies oslogin = var.oslogin oslogin_admins = var.oslogin_admins oslogin_users = var.oslogin_users parent = var.parent - policy_boolean = var.policy_boolean - policy_list = var.policy_list prefix = var.prefix service_encryption_key_ids = var.service_encryption_key_ids services = var.services @@ -63,4 +62,3 @@ module "test-svpc-service" { } } } - diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf index 2a4d95d1e7..236cb69f32 100644 --- a/tests/modules/project/fixture/variables.tf +++ b/tests/modules/project/fixture/variables.tf @@ -64,6 +64,11 @@ variable "lien_reason" { default = "" } +variable "org_policies" { + type = any + default = {} +} + variable "oslogin" { type = bool default = false @@ -84,21 +89,6 @@ variable "parent" { default = null } -variable "policy_boolean" { - type = map(bool) - default = {} -} - -variable "policy_list" { - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} -} - variable "prefix" { type = string default = null diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index 645db0dfe0..a9c4df68b3 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -12,47 +12,214 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_policy_boolean(plan_runner): "Test boolean org policy." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) - assert len(resources) == 7 - resources = [r for r in resources if r['type'] - == 'google_project_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', 'policy-b', 'policy-c' - ] - policy_values = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policy_values.append((policy,) + value[0].popitem()) - assert sorted(policy_values) == [ - ('boolean_policy', 'enforced', False), - ('boolean_policy', 'enforced', True), - ('restore_policy', 'default', True) - ] + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 6 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + assert all(x['values']['parent'] == 'projects/my-project' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } def test_policy_list(plan_runner): "Test list org policy." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) assert len(resources) == 7 - values = [r['values'] for r in resources if r['type'] - == 'google_project_organization_policy'] - assert [r['constraint'] for r in values] == [ - 'policy-a', 'policy-b', 'policy-c' - ] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 + assert all(x['values']['parent'] == 'projects/my-project' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + }