diff --git a/modules/folder/README.md b/modules/folder/README.md index d543004a..ee1e9e5b 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -311,8 +311,9 @@ module "folder" { | [logging_sinks](variables.tf#L105) | Logging sinks to create for this folder. | map(object({…})) | | {} | | [name](variables.tf#L126) | Folder name. | 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 | +| [org_policies_data_path](variables.tf#L172) | | string | | null | +| [parent](variables.tf#L178) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L188) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 8fc02140..3766005a 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -17,8 +17,57 @@ # tfdoc:file:description Folder-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { name = "${local.folder.name}/policies/${k}" parent = local.folder.name diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index a00e147f..ea6b1231 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -169,6 +169,12 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "" + type = string + default = null +} + variable "parent" { description = "Parent in folders/folder_id or organizations/org_id format." type = string diff --git a/modules/organization/README.md b/modules/organization/README.md index 3c57b743..5073a132 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -336,8 +336,9 @@ module "org" { | [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({…})) | | {} | | [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 | +| [org_policies_data_path](variables.tf#L200) | | string | | null | +| [tag_bindings](variables.tf#L206) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L212) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | ## Outputs diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 4dba1c85..fcde1658 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -17,8 +17,57 @@ # tfdoc:file:description Organization-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { name = "${var.organization_id}/policies/${k}" parent = var.organization_id diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 7499d6d6..5ace5b1e 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -197,6 +197,12 @@ variable "organization_id" { } } +variable "org_policies_data_path" { + description = "" + type = string + default = null +} + 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 dbb66fcd..4cbf49e6 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -407,21 +407,22 @@ output "compute_robot" { | [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) | | [] | | [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 | +| [org_policies_data_path](variables.tf#L176) | | string | | null | +| [oslogin](variables.tf#L182) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L188) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L196) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L203) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L213) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L219) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L225) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L237) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L244) | 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#L251) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L257) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L263) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L272) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L282) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L288) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 696bba75..d64dd956 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -17,8 +17,57 @@ # tfdoc:file:description Project-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { name = "projects/${local.project.project_id}/policies/${k}" parent = "projects/${local.project.project_id}" diff --git a/modules/project/variables.tf b/modules/project/variables.tf index a58affc9..4e3fec64 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -173,6 +173,12 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "" + type = string + default = null +} + variable "oslogin" { description = "Enable OS Login." type = bool diff --git a/tests/modules/folder/fixture/main.tf b/tests/modules/folder/fixture/main.tf index 8290f82e..a347f61b 100644 --- a/tests/modules/folder/fixture/main.tf +++ b/tests/modules/folder/fixture/main.tf @@ -27,4 +27,5 @@ module "test" { logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path } diff --git a/tests/modules/folder/fixture/variables.tf b/tests/modules/folder/fixture/variables.tf index 7c03e056..e2d7a293 100644 --- a/tests/modules/folder/fixture/variables.tf +++ b/tests/modules/folder/fixture/variables.tf @@ -58,3 +58,8 @@ variable "org_policies" { type = any default = {} } + +variable "org_policies_data_path" { + type = any + default = null +} diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index f84d50fb..0a6b9729 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -12,31 +12,106 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hcl2 +import yaml + +BOOLEAN_POLICIES = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } +}''' + +LIST_POLICIES = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = 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 + _, resources = plan_runner(org_policies=BOOLEAN_POLICIES) + validate_policy_boolean_resources(resources) + +def test_policy_list(plan_runner): + "Test list org policy." + _, resources = plan_runner(org_policies=LIST_POLICIES) + validate_policy_list_resources(resources) + + +def test_policy_boolean_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {BOOLEAN_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean_resources(resources) + + +def test_policy_list_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {LIST_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list_resources(resources) + + +def validate_policy_boolean_resources(resources): + assert len(resources) == 3 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] assert len(policies) == 2 @@ -76,7 +151,7 @@ def test_policy_boolean(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -86,46 +161,7 @@ def test_policy_boolean(plan_runner): } -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) +def validate_policy_list_resources(resources): assert len(resources) == 4 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] @@ -193,7 +229,7 @@ def test_policy_list(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -208,14 +244,10 @@ def test_policy_list(plan_runner): assert p3['rules'][2] == { 'allow_all': 'TRUE', 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' }], 'deny_all': None, 'enforce': None, diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 13f8e335..4f5df9e2 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -29,6 +29,7 @@ module "test" { logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path 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 f56e51dc..2508fb06 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -74,6 +74,11 @@ variable "org_policies" { default = {} } +variable "org_policies_data_path" { + type = any + default = null +} + variable "tag_bindings" { type = any default = null diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 33ee7e3f..0986e99d 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -15,35 +15,108 @@ import difflib from pathlib import Path +import hcl2 +import yaml + +BOOLEAN_POLICIES = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } +}''' + +LIST_POLICIES = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = 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) == 2 + _, resources = plan_runner(org_policies=BOOLEAN_POLICIES) + validate_policy_boolean_resources(resources) + +def test_policy_list(plan_runner): + "Test list org policy." + _, resources = plan_runner(org_policies=LIST_POLICIES) + validate_policy_list_resources(resources) + + +def test_policy_boolean_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {BOOLEAN_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean_resources(resources) + + +def test_policy_list_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {LIST_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list_resources(resources) + + +def validate_policy_boolean_resources(resources): + 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] @@ -81,7 +154,7 @@ def test_policy_boolean(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -91,52 +164,11 @@ def test_policy_boolean(plan_runner): } -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) +def validate_policy_list_resources(resources): 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] @@ -200,7 +232,7 @@ def test_policy_list(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -215,14 +247,10 @@ def test_policy_list(plan_runner): assert p3['rules'][2] == { 'allow_all': 'TRUE', 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' }], 'deny_all': None, 'enforce': None, @@ -244,7 +272,7 @@ def test_policy_implementation(plan_runner): assert list(diff1) == [ '--- \n', '+++ \n', - '@@ -14,14 +14,14 @@\n', + '@@ -14,7 +14,7 @@\n', ' * limitations under the License.\n', ' */\n', ' \n', @@ -252,8 +280,10 @@ def test_policy_implementation(plan_runner): '+# tfdoc:file:description Folder-level organization policies.\n', ' \n', ' locals {\n', + ' _factory_data_raw = (\n', + '@@ -69,8 +69,8 @@\n', ' org_policies = {\n', - ' for k, v in var.org_policies :\n', + ' for k, v in local._org_policies :\n', ' k => merge(v, {\n', '- name = "projects/${local.project.project_id}/policies/${k}"\n', '- parent = "projects/${local.project.project_id}"\n', @@ -268,7 +298,7 @@ def test_policy_implementation(plan_runner): assert list(diff2) == [ '--- \n', '+++ \n', - '@@ -14,14 +14,14 @@\n', + '@@ -14,7 +14,7 @@\n', ' * limitations under the License.\n', ' */\n', ' \n', @@ -276,8 +306,10 @@ def test_policy_implementation(plan_runner): '+# tfdoc:file:description Organization-level organization policies.\n', ' \n', ' locals {\n', + ' _factory_data_raw = (\n', + '@@ -69,8 +69,8 @@\n', ' org_policies = {\n', - ' for k, v in var.org_policies :\n', + ' for k, v in local._org_policies :\n', ' k => merge(v, {\n', '- name = "${local.folder.name}/policies/${k}"\n', '- parent = local.folder.name\n', @@ -286,7 +318,7 @@ def test_policy_implementation(plan_runner): ' \n', ' is_boolean_policy = v.allow == null && v.deny == null\n', ' has_values = (\n', - '@@ -94,4 +94,12 @@\n', + '@@ -143,4 +143,12 @@\n', ' }\n', ' }\n', ' }\n', diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf index 4c7441ac..08cf49dc 100644 --- a/tests/modules/project/fixture/main.tf +++ b/tests/modules/project/fixture/main.tf @@ -26,6 +26,7 @@ module "test" { labels = var.labels lien_reason = var.lien_reason org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path oslogin = var.oslogin oslogin_admins = var.oslogin_admins oslogin_users = var.oslogin_users diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf index 236cb69f..93843396 100644 --- a/tests/modules/project/fixture/variables.tf +++ b/tests/modules/project/fixture/variables.tf @@ -69,6 +69,11 @@ variable "org_policies" { default = {} } +variable "org_policies_data_path" { + type = any + default = null +} + variable "oslogin" { type = bool default = false diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index a9c4df68..28494467 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -12,31 +12,106 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hcl2 +import yaml + +BOOLEAN_POLICIES = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } +}''' + +LIST_POLICIES = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = 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) == 6 + _, resources = plan_runner(org_policies=BOOLEAN_POLICIES) + validate_policy_boolean_resources(resources) + +def test_policy_list(plan_runner): + "Test list org policy." + _, resources = plan_runner(org_policies=LIST_POLICIES) + validate_policy_list_resources(resources) + + +def test_policy_boolean_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {BOOLEAN_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean_resources(resources) + + +def test_policy_list_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {LIST_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list_resources(resources) + + +def validate_policy_boolean_resources(resources): + 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) @@ -77,7 +152,7 @@ def test_policy_boolean(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -87,46 +162,7 @@ def test_policy_boolean(plan_runner): } -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) +def validate_policy_list_resources(resources): assert len(resources) == 7 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] @@ -195,7 +231,7 @@ def test_policy_list(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -210,14 +246,10 @@ def test_policy_list(plan_runner): assert p3['rules'][2] == { 'allow_all': 'TRUE', 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' }], 'deny_all': None, 'enforce': None,