From 6610b79b6c02b98d6ec456d0564d56912a5a6c96 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 9 Nov 2020 11:29:08 +0100 Subject: [PATCH] Revert iam_additive behaviour (#160) * revert iam_additive format, add iam_additive_members * revert iam_additive format, add iam_additive_members * update CHANGELOG --- CHANGELOG.md | 1 + modules/organization/README.md | 1 + modules/organization/main.tf | 24 ++++--- modules/organization/variables.tf | 6 ++ modules/project/README.md | 1 + modules/project/main.tf | 24 ++++--- modules/project/variables.tf | 6 ++ tests/modules/organization/fixture/main.tf | 17 ++--- .../modules/organization/fixture/variables.tf | 5 ++ tests/modules/organization/test_plan.py | 16 +++++ tests/modules/project/fixture/main.tf | 35 +++++----- tests/modules/project/fixture/variables.tf | 5 ++ tests/modules/project/test_iam.py | 66 +++++++++++++++++++ 13 files changed, 166 insertions(+), 41 deletions(-) create mode 100644 tests/modules/project/test_iam.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dd8e34a3b..67b1b7c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - **incompatible change** rename prefix for node configuration variables in `gke-nodepool` module [#156] - add support for internally managed service account in `gke-nodepool` module [#156] - made examples in READMEs runnable and testable [#157] +- **incompatible change** `iam_additive` is now keyed by role to be more resilient with dynamic values, a new `iam_additive_members` variable has been added for backwards compatibility. ## [4.0.0] - 2020-11-06 diff --git a/modules/organization/README.md b/modules/organization/README.md index 9d12681c6..9547c67d4 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -39,6 +39,7 @@ module "org" { | *custom_roles* | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | *iam* | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | *iam_additive* | Non authoritative IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| *iam_additive_members* | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(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))) | | {} | | *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({...})) | | {} | diff --git a/modules/organization/main.tf b/modules/organization/main.tf index 6cf410173..e331d6ae3 100644 --- a/modules/organization/main.tf +++ b/modules/organization/main.tf @@ -16,13 +16,17 @@ locals { iam_additive_pairs = flatten([ - for member, roles in var.iam_additive : [ - for role in roles : - { role = role, member = member } + for role, members in var.iam_additive : [ + for member in members : { role = role, member = member } + ] + ]) + iam_additive_member_pairs = flatten([ + for member, roles in var.iam_additive_members : [ + for role in roles : { role = role, member = member } ] ]) iam_additive = { - for pair in local.iam_additive_pairs : + for pair in concat(local.iam_additive_pairs, local.iam_additive_member_pairs) : "${pair.role}-${pair.member}" => pair } } @@ -44,10 +48,14 @@ resource "google_organization_iam_binding" "authoritative" { } resource "google_organization_iam_member" "additive" { - for_each = length(var.iam_additive) > 0 ? local.iam_additive : {} - org_id = var.org_id - role = each.value.role - member = each.value.member + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + org_id = var.org_id + role = each.value.role + member = each.value.member } resource "google_organization_iam_audit_config" "config" { diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 293f01762..8c69fcbb8 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -32,6 +32,12 @@ variable "iam_additive" { default = {} } +variable "iam_additive_members" { + description = "IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values." + type = map(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))) diff --git a/modules/project/README.md b/modules/project/README.md index 99755a0e3..e69d3cf87 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -96,6 +96,7 @@ module "project" { | *custom_roles* | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | *iam* | IAM bindings in {ROLE => [MEMBERS]} format. | map(set(string)) | | {} | | *iam_additive* | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| *iam_additive_members* | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | | *labels* | Resource labels. | map(string) | | {} | | *lien_reason* | If non-empty, creates a project lien with this description. | string | | | | *oslogin* | Enable OS Login. | bool | | false | diff --git a/modules/project/main.tf b/modules/project/main.tf index f7cf5ddcf..9e5502ae6 100644 --- a/modules/project/main.tf +++ b/modules/project/main.tf @@ -16,13 +16,17 @@ locals { iam_additive_pairs = flatten([ - for member, roles in var.iam_additive : [ - for role in roles : - { role = role, member = member } + for role, members in var.iam_additive : [ + for member in members : { role = role, member = member } + ] + ]) + iam_additive_member_pairs = flatten([ + for member, roles in var.iam_additive_members : [ + for role in roles : { role = role, member = member } ] ]) iam_additive = { - for pair in local.iam_additive_pairs : + for pair in concat(local.iam_additive_pairs, local.iam_additive_member_pairs) : "${pair.role}-${pair.member}" => pair } parent_type = var.parent == null ? null : split("/", var.parent)[0] @@ -102,10 +106,14 @@ resource "google_project_iam_binding" "authoritative" { } resource "google_project_iam_member" "additive" { - for_each = length(var.iam_additive) > 0 ? local.iam_additive : {} - project = local.project.project_id - role = each.value.role - member = each.value.member + for_each = ( + length(var.iam_additive) + length(var.iam_additive_members) > 0 + ? local.iam_additive + : {} + ) + project = local.project.project_id + role = each.value.role + member = each.value.member depends_on = [ google_project_service.project_services, google_project_iam_custom_role.roles diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 760a91833..feb3b21ae 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -44,6 +44,12 @@ variable "iam_additive" { default = {} } +variable "iam_additive_members" { + description = "IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values." + type = map(list(string)) + default = {} +} + variable "labels" { description = "Resource labels." type = map(string) diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 6c5d0bcae..d1b0dd350 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -15,12 +15,13 @@ */ module "test" { - source = "../../../../modules/organization" - org_id = 1234567890 - custom_roles = var.custom_roles - iam = var.iam - iam_additive = var.iam_additive - iam_audit_config = var.iam_audit_config - policy_boolean = var.policy_boolean - policy_list = var.policy_list + source = "../../../../modules/organization" + org_id = 1234567890 + custom_roles = var.custom_roles + iam = var.iam + iam_additive = var.iam_additive + iam_additive_members = var.iam_additive_members + 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 index 887c33452..7a2ddfdb4 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -29,6 +29,11 @@ variable "iam_additive" { default = {} } +variable "iam_additive_members" { + type = map(list(string)) + default = {} +} + variable "iam_audit_config" { type = map(map(list(string))) default = {} diff --git a/tests/modules/organization/test_plan.py b/tests/modules/organization/test_plan.py index 493682d65..8d4152bac 100644 --- a/tests/modules/organization/test_plan.py +++ b/tests/modules/organization/test_plan.py @@ -30,6 +30,22 @@ def test_audit_config(plan_runner): assert log_types == set(['DATA_READ', 'DATA_WRITE']) +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"]}' + ) + _, resources = plan_runner(FIXTURES_DIR, 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}' diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf index e7a9fd0e2..acec2c3f0 100644 --- a/tests/modules/project/fixture/main.tf +++ b/tests/modules/project/fixture/main.tf @@ -15,21 +15,22 @@ */ module "test" { - source = "../../../../modules/project" - name = "my-project" - billing_account = "12345-12345-12345" - auto_create_network = var.auto_create_network - custom_roles = var.custom_roles - iam = var.iam - iam_additive = var.iam_additive - labels = var.labels - lien_reason = var.lien_reason - 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 - services = var.services + source = "../../../../modules/project" + name = "my-project" + billing_account = "12345-12345-12345" + auto_create_network = var.auto_create_network + custom_roles = var.custom_roles + iam = var.iam + iam_additive = var.iam_additive + iam_additive_members = var.iam_additive_members + labels = var.labels + lien_reason = var.lien_reason + 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 + services = var.services } diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf index 1a60f8567..3b759ddbf 100644 --- a/tests/modules/project/fixture/variables.tf +++ b/tests/modules/project/fixture/variables.tf @@ -34,6 +34,11 @@ variable "iam_additive" { default = {} } +variable "iam_additive_members" { + type = map(list(string)) + default = {} +} + variable "labels" { type = map(string) default = {} diff --git a/tests/modules/project/test_iam.py b/tests/modules/project/test_iam.py new file mode 100644 index 000000000..02e96faf5 --- /dev/null +++ b/tests/modules/project/test_iam.py @@ -0,0 +1,66 @@ +# 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_iam(plan_runner): + "Test IAM bindings." + iam = ( + '{"roles/owner" = ["user:one@example.org"],' + '"roles/viewer" = ["user:two@example.org", "user:three@example.org"]}' + ) + _, resources = plan_runner(FIXTURES_DIR, iam=iam) + roles = dict((r['values']['role'], r['values']['members']) + for r in resources if r['type'] == 'google_project_iam_binding') + assert roles == { + 'roles/owner': ['user:one@example.org'], + 'roles/viewer': ['user:three@example.org', 'user:two@example.org']} + + +def test_iam_additive(plan_runner): + "Test IAM additive bindings." + iam = ( + '{"roles/owner" = ["user:one@example.org"],' + '"roles/viewer" = ["user:two@example.org", "user:three@example.org"]}' + ) + _, resources = plan_runner(FIXTURES_DIR, iam_additive=iam) + roles = set((r['values']['role'], r['values']['member']) + for r in resources if r['type'] == 'google_project_iam_member') + assert roles == set([ + ('roles/owner', 'user:one@example.org'), + ('roles/viewer', 'user:three@example.org'), + ('roles/viewer', 'user:two@example.org') + ]) + + +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"]}' + ) + _, resources = plan_runner(FIXTURES_DIR, iam_additive_members=iam) + roles = set((r['values']['role'], r['values']['member']) + for r in resources if r['type'] == 'google_project_iam_member') + assert roles == set([ + ('roles/owner', 'user:one@example.org'), + ('roles/owner', 'user:two@example.org'), + ('roles/editor', 'user:two@example.org') + ])