diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6fa49ff..419941ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- add organization policy module + ## [1.2.0] - 2020-04-06 - add squid container to the `cloud-config-container` module @@ -20,7 +22,7 @@ All notable changes to this project will be documented in this file. - merge development branch with suite of new modules and end-to-end examples -[Unreleased]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.2...HEAD [1.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.1...v1.2 [1.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.0...v1.1 [1.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v0.1...v1.0 diff --git a/modules/organization/README.md b/modules/organization/README.md new file mode 100644 index 000000000..53ac2cbb5 --- /dev/null +++ b/modules/organization/README.md @@ -0,0 +1,53 @@ +# Organization Module + +This module allows managing several organization properties: + +- IAM bindings, both authoritative and additive +- custom IAM roles +- audit logging configuration for services +- organization policies + +## Example + +```hcl +module "org" { + source = "./modules/organization" + org_id = 1234567890 + iam_roles = ["roles/projectCreator"] + iam_members = { "roles/projectCreator" = ["group:cloud-admins@example.org"] } + policy_boolean = { + "constraints/compute.disableGuestAttributesAccess" = true + "constraints/compute.skipDefaultNetworkCreation" = true + } + policy_list = { + "constraints/compute.trustedImageProjects" = { + inherit_from_parent = null + suggested_value = null + status = true + values = ["projects/my-project"] + } + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| org_id | Organization id in nnnnnn format. | number | ✓ | | +| *custom_roles* | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| *iam_additive_members* | Map of member lists used to set non authoritative bindings, keyed by role. | map(list(string)) | | {} | +| *iam_additive_roles* | List of roles used to set non authoritative bindings. | list(string) | | [] | +| *iam_audit_config* | Service audit logging configuration. Service as key, map of log permission (eg DATA_READ) and excluded members as value for each service. | map(map(list(string))) | | {} | +| *iam_members* | Map of member lists used to set authoritative bindings, keyed by role. | map(list(string)) | | {} | +| *iam_roles* | List of roles used to set authoritative bindings. | list(string) | | [] | +| *policy_boolean* | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| *policy_list* | 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({...})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| org_id | Organization id dependent on module resources. | | + diff --git a/modules/organization/main.tf b/modules/organization/main.tf new file mode 100644 index 000000000..91e01b5fe --- /dev/null +++ b/modules/organization/main.tf @@ -0,0 +1,138 @@ +/** + * Copyright 2020 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. + */ + +locals { + iam_additive_pairs = flatten([ + for role in var.iam_additive_roles : [ + for member in lookup(var.iam_additive_members, role, []) : + { role = role, member = member } + ] + ]) + iam_additive = { + for pair in local.iam_additive_pairs : + "${pair.role}-${pair.member}" => pair + } +} + +resource "google_organization_iam_custom_role" "roles" { + for_each = var.custom_roles + org_id = var.org_id + role_id = each.key + title = "Custom role ${each.key}" + description = "Terraform-managed" + permissions = each.value +} + +resource "google_organization_iam_binding" "authoritative" { + for_each = toset(var.iam_roles) + org_id = var.org_id + role = each.value + members = lookup(var.iam_members, each.value, []) +} + +resource "google_organization_iam_member" "additive" { + for_each = length(var.iam_additive_roles) > 0 ? local.iam_additive : {} + org_id = var.org_id + role = each.value.role + member = each.value.member +} + +resource "google_organization_iam_audit_config" "config" { + for_each = var.iam_audit_config + org_id = var.org_id + service = each.key + dynamic audit_log_config { + for_each = each.value + iterator = config + content { + log_type = config.key + exempted_members = config.value + } + } +} + +resource "google_organization_policy" "boolean" { + for_each = var.policy_boolean + org_id = var.org_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 + } + } +} + +resource "google_organization_policy" "list" { + for_each = var.policy_list + org_id = var.org_id + constraint = each.key + + 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 ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic restore_policy { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf new file mode 100644 index 000000000..2a829c4dd --- /dev/null +++ b/modules/organization/outputs.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2020 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. + */ + +output "org_id" { + description = "Organization id dependent on module resources." + value = var.org_id + depends_on = [ + google_organization_iam_audit_config, + google_organization_iam_binding.authoritative, + google_organization_iam_custom_role.roles, + google_organization_iam_member.additive, + google_organization_policy.boolean, + google_organization_policy.list + ] +} diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf new file mode 100644 index 000000000..05d636bce --- /dev/null +++ b/modules/organization/variables.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2020 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. + */ + +variable "custom_roles" { + description = "Map of role name => list of permissions to create in this project." + type = map(list(string)) + default = {} +} + +variable "iam_members" { + description = "Map of member lists used to set authoritative bindings, keyed by role." + type = map(list(string)) + default = {} +} + +variable "iam_roles" { + description = "List of roles used to set authoritative bindings." + type = list(string) + default = [] +} + +variable "iam_additive_members" { + description = "Map of member lists used to set non authoritative bindings, keyed by role." + type = map(list(string)) + default = {} +} + +variable "iam_additive_roles" { + description = "List of roles used to set non authoritative bindings." + type = list(string) + default = [] +} + +variable "iam_audit_config" { + description = "Service audit logging configuration. Service as key, map of log permission (eg DATA_READ) and excluded members as value for each service." + type = map(map(list(string))) + default = {} + # default = { + # allServices = { + # DATA_READ = ["user:me@example.org"] + # } + # } +} + +variable "org_id" { + description = "Organization id in nnnnnn format." + type = number +} + +variable "policy_boolean" { + description = "Map of boolean org policies and enforcement value, set value to null for policy restore." + type = map(bool) + default = {} +} + +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 = {} +} diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf new file mode 100644 index 000000000..ce6918e09 --- /dev/null +++ b/modules/organization/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 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. + */ + +terraform { + required_version = ">= 0.12.6" +} diff --git a/tests/modules/organization/__init__.py b/tests/modules/organization/__init__.py new file mode 100644 index 000000000..6913f02e3 --- /dev/null +++ b/tests/modules/organization/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 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. diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf new file mode 100644 index 000000000..20786b5e5 --- /dev/null +++ b/tests/modules/organization/fixture/main.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2020 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. + */ + +module "test" { + source = "../../../../modules/organization" + org_id = 1234567890 + custom_roles = var.custom_roles + iam_members = var.iam_members + iam_roles = var.iam_roles + iam_additive_members = var.iam_additive_members + iam_additive_roles = var.iam_additive_roles + iam_audit_config = var.iam_audit_config + policy_boolean = var.policy_boolean + policy_list = var.policy_list +} diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf new file mode 100644 index 000000000..561b446c0 --- /dev/null +++ b/tests/modules/organization/fixture/variables.tf @@ -0,0 +1,60 @@ +/** + * Copyright 2020 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. + */ + +variable "custom_roles" { + type = map(list(string)) + default = {} +} + +variable "iam_members" { + type = map(list(string)) + default = {} +} + +variable "iam_roles" { + type = list(string) + default = [] +} + +variable "iam_additive_members" { + type = map(list(string)) + default = {} +} + +variable "iam_additive_roles" { + type = list(string) + default = [] +} + +variable "iam_audit_config" { + type = map(map(list(string))) + default = {} +} + +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 = {} +} diff --git a/tests/modules/organization/test_plan.py b/tests/modules/organization/test_plan.py new file mode 100644 index 000000000..493682d65 --- /dev/null +++ b/tests/modules/organization/test_plan.py @@ -0,0 +1,72 @@ +# Copyright 2020 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. + + +import os +import pytest + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + + +def test_audit_config(plan_runner): + "Test audit config." + iam_audit_config = '{allServices={DATA_READ=[], DATA_WRITE=["user:me@example.org"]}}' + _, resources = plan_runner(FIXTURES_DIR, 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']) + assert log_types == set(['DATA_READ', 'DATA_WRITE']) + + +def test_policy_boolean(plan_runner): + "Test boolean org policy." + policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' + _, resources = plan_runner(FIXTURES_DIR, 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(FIXTURES_DIR, policy_list=policy_list) + # from pprint import pprint + # pprint(resources) + 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}]