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}]