diff --git a/google/iam.go b/google/iam.go index e80d907c..4c16988b 100644 --- a/google/iam.go +++ b/google/iam.go @@ -123,6 +123,25 @@ func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModify return nil } +// Takes a single binding and will either overwrite the same role in a list or append it to the end +func overwriteBinding(bindings []*cloudresourcemanager.Binding, overwrite *cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { + var found bool + + for i, b := range bindings { + if b.Role == overwrite.Role { + bindings[i] = overwrite + found = true + break + } + } + + if !found { + bindings = append(bindings, overwrite) + } + + return bindings +} + // Merge multiple Bindings such that Bindings with the same Role result in // a single Binding with combined Members func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { diff --git a/google/resource_google_project_iam_policy_test.go b/google/resource_google_project_iam_policy_test.go index 5d572406..c7180c6e 100644 --- a/google/resource_google_project_iam_policy_test.go +++ b/google/resource_google_project_iam_policy_test.go @@ -169,6 +169,62 @@ func testAccCheckGoogleProjectIamPolicyExists(projectRes, policyRes, pid string) } } +func TestIamOverwriteBinding(t *testing.T) { + table := []struct { + input []*cloudresourcemanager.Binding + override cloudresourcemanager.Binding + expect []cloudresourcemanager.Binding + }{ + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + }, + override: cloudresourcemanager.Binding{ + Role: "role-1", + Members: []string{"new-member"}, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"new-member"}, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + }, + override: cloudresourcemanager.Binding{ + Role: "role-2", + Members: []string{"member-3"}, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + { + Role: "role-2", + Members: []string{"member-3"}, + }, + }, + }, + } + + for _, test := range table { + got := overwriteBinding(test.input, &test.override) + if !reflect.DeepEqual(derefBindings(got), test.expect) { + t.Errorf("OverwriteIamBinding failed.\nGot %+v\nWant %+v", derefBindings(got), test.expect) + } + } +} + func TestIamMergeBindings(t *testing.T) { table := []struct { input []*cloudresourcemanager.Binding @@ -177,95 +233,61 @@ func TestIamMergeBindings(t *testing.T) { { input: []*cloudresourcemanager.Binding{ { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - }, + Role: "role-1", + Members: []string{"member-1", "member-2"}, }, { - Role: "role-1", - Members: []string{ - "member-3", - }, + Role: "role-1", + Members: []string{"member-3"}, }, }, expect: []cloudresourcemanager.Binding{ { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - "member-3", - }, + Role: "role-1", + Members: []string{"member-1", "member-2", "member-3"}, }, }, }, { input: []*cloudresourcemanager.Binding{ { - Role: "role-1", - Members: []string{ - "member-3", - "member-4", - }, + Role: "role-1", + Members: []string{"member-3", "member-4"}, }, { - Role: "role-1", - Members: []string{ - "member-2", - "member-1", - }, + Role: "role-1", + Members: []string{"member-2", "member-1"}, }, { - Role: "role-2", - Members: []string{ - "member-1", - }, + Role: "role-2", + Members: []string{"member-1"}, }, { - Role: "role-1", - Members: []string{ - "member-5", - }, + Role: "role-1", + Members: []string{"member-5"}, }, { - Role: "role-3", - Members: []string{ - "member-1", - }, + Role: "role-3", + Members: []string{"member-1"}, }, { - Role: "role-2", - Members: []string{ - "member-2", - }, + Role: "role-2", + Members: []string{"member-2"}, }, {Role: "empty-role", Members: []string{}}, }, expect: []cloudresourcemanager.Binding{ { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - "member-3", - "member-4", - "member-5", - }, + Role: "role-1", + Members: []string{"member-1", "member-2", "member-3", "member-4", "member-5"}, }, { - Role: "role-2", - Members: []string{ - "member-1", - "member-2", - }, + Role: "role-2", + Members: []string{"member-1", "member-2"}, }, { - Role: "role-3", - Members: []string{ - "member-1", - }, + Role: "role-3", + Members: []string{"member-1"}, }, }, }, @@ -416,7 +438,7 @@ data "google_iam_policy" "expanded" { "user:paddy@carvers.co", ] } - + binding { role = "roles/viewer" members = [ diff --git a/google/resource_iam_binding.go b/google/resource_iam_binding.go index 6be57651..5a8d072a 100644 --- a/google/resource_iam_binding.go +++ b/google/resource_iam_binding.go @@ -31,9 +31,9 @@ var iamBindingSchema = map[string]*schema.Schema{ func ResourceIamBinding(parentSpecificSchema map[string]*schema.Schema, newUpdaterFunc newResourceIamUpdaterFunc) *schema.Resource { return &schema.Resource{ - Create: resourceIamBindingCreate(newUpdaterFunc), + Create: resourceIamBindingCreateUpdate(newUpdaterFunc), Read: resourceIamBindingRead(newUpdaterFunc), - Update: resourceIamBindingUpdate(newUpdaterFunc), + Update: resourceIamBindingCreateUpdate(newUpdaterFunc), Delete: resourceIamBindingDelete(newUpdaterFunc), Schema: mergeSchemas(iamBindingSchema, parentSpecificSchema), } @@ -47,7 +47,7 @@ func ResourceIamBindingWithImport(parentSpecificSchema map[string]*schema.Schema return r } -func resourceIamBindingCreate(newUpdaterFunc newResourceIamUpdaterFunc) schema.CreateFunc { +func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc) func(*schema.ResourceData, interface{}) error { return func(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) updater, err := newUpdaterFunc(d, config) @@ -57,11 +57,7 @@ func resourceIamBindingCreate(newUpdaterFunc newResourceIamUpdaterFunc) schema.C p := getResourceIamBinding(d) err = iamPolicyReadModifyWrite(updater, func(ep *cloudresourcemanager.Policy) error { - // Creating a binding does not remove existing members if they are not in the provided members list. - // This prevents removing existing permission without the user's knowledge. - // Instead, a diff is shown in that case after creation. Subsequent calls to update will remove any - // existing members not present in the provided list. - ep.Bindings = mergeBindings(append(ep.Bindings, p)) + ep.Bindings = overwriteBinding(ep.Bindings, p) return nil }) if err != nil { @@ -151,38 +147,6 @@ func iamBindingImport(resourceIdParser resourceIdParserFunc) schema.StateFunc { } } -func resourceIamBindingUpdate(newUpdaterFunc newResourceIamUpdaterFunc) schema.UpdateFunc { - return func(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - updater, err := newUpdaterFunc(d, config) - if err != nil { - return err - } - - binding := getResourceIamBinding(d) - err = iamPolicyReadModifyWrite(updater, func(p *cloudresourcemanager.Policy) error { - var found bool - for pos, b := range p.Bindings { - if b.Role != binding.Role { - continue - } - found = true - p.Bindings[pos] = binding - break - } - if !found { - p.Bindings = append(p.Bindings, binding) - } - return nil - }) - if err != nil { - return err - } - - return resourceIamBindingRead(newUpdaterFunc)(d, meta) - } -} - func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc) schema.DeleteFunc { return func(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) diff --git a/website/docs/d/google_iam_policy.html.markdown b/website/docs/d/google_iam_policy.html.markdown index f3a454ef..a3f14c1c 100644 --- a/website/docs/d/google_iam_policy.html.markdown +++ b/website/docs/d/google_iam_policy.html.markdown @@ -26,10 +26,10 @@ data "google_iam_policy" "admin" { role = "roles/storage.objectViewer" members = [ - "user:jane@example.com", + "user:alice@gmail.com", ] } - + audit_config { service = "cloudkms.googleapis.com" audit_log_configs = [ @@ -73,11 +73,11 @@ each accept the following arguments: See the [IAM Roles](https://cloud.google.com/compute/docs/access/iam) documentation for a complete list of roles. Note that custom roles must be of the format `[projects|organizations]/{parent-name}/roles/{role-name}`. -* `members` (Required) - An array of identites that will be granted the privilege in the `role`. +* `members` (Required) - An array of identites that will be granted the privilege in the `role`. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding Each entry can have one of the following values: * **allUsers**: A special identifier that represents anyone who is on the internet; with or without a Google account. It **can't** be used with the `google_project` resource. * **allAuthenticatedUsers**: A special identifier that represents anyone who is authenticated with a Google account or a service account. It **can't** be used with the `google_project` resource. - * **user:{emailid}**: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com. + * **user:{emailid}**: An email address that represents a specific Google account. For example, alice@gmail.com. * **serviceAccount:{emailid}**: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com. * **group:{emailid}**: An email address that represents a Google group. For example, admins@example.com. * **domain:{domain}**: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com. diff --git a/website/docs/r/google_billing_account_iam_binding.md b/website/docs/r/google_billing_account_iam_binding.md index 3a64364e..eac0bd93 100644 --- a/website/docs/r/google_billing_account_iam_binding.md +++ b/website/docs/r/google_billing_account_iam_binding.md @@ -15,6 +15,10 @@ an existing Google Cloud Platform Billing Account. `google_billing_account_iam_member` for the __same role__ or they will fight over what your policy should be. +~> **Note:** On create, this resource will overwrite members of any existing roles. + Use `terraform import` and inspect the `terraform plan` output to ensure + your existing members are preserved. + ## Example Usage ```hcl @@ -23,7 +27,7 @@ resource "google_billing_account_iam_binding" "binding" { role = "roles/billing.viewer" members = [ - "user:jane@example.com", + "user:alice@gmail.com", ] } ``` @@ -36,7 +40,7 @@ The following arguments are supported: * `role` - (Required) The role that should be applied. -* `members` - (Required) A list of users that the role should apply to. +* `members` - (Required) A list of users that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding ## Attributes Reference diff --git a/website/docs/r/google_billing_account_iam_member.md b/website/docs/r/google_billing_account_iam_member.md index 30e4a0d4..dfe973fe 100644 --- a/website/docs/r/google_billing_account_iam_member.md +++ b/website/docs/r/google_billing_account_iam_member.md @@ -21,7 +21,7 @@ the IAM policy for an existing Google Cloud Platform Billing Account. resource "google_billing_account_iam_member" "binding" { billing_account_id = "00AA00-000AAA-00AA0A" role = "roles/billing.viewer" - member = "user:jane@example.com" + member = "user:alice@gmail.com" } ``` @@ -33,8 +33,8 @@ The following arguments are supported: * `role` - (Required) The role that should be applied. -* `member` - (Required) The user that the role should apply to. - +* `member` - (Required) The user that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are diff --git a/website/docs/r/google_folder_iam_binding.html.markdown b/website/docs/r/google_folder_iam_binding.html.markdown index a9226115..9589e1ac 100644 --- a/website/docs/r/google_folder_iam_binding.html.markdown +++ b/website/docs/r/google_folder_iam_binding.html.markdown @@ -15,6 +15,10 @@ an existing Google Cloud Platform folder. `google_folder_iam_policy` or they will fight over what your policy should be. +~> **Note:** On create, this resource will overwrite members of any existing roles. + Use `terraform import` and inspect the `terraform plan` output to ensure + your existing members are preserved. + ## Example Usage ```hcl @@ -28,7 +32,7 @@ resource "google_folder_iam_binding" "admin" { role = "roles/editor" members = [ - "user:jane@example.com", + "user:alice@gmail.com", ] } ``` @@ -41,10 +45,11 @@ The following arguments are supported: * `members` (Required) - An array of identites that will be granted the privilege in the `role`. Each entry can have one of the following values: - * **user:{emailid}**: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com. + * **user:{emailid}**: An email address that is associated with a specific Google account. For example, alice@gmail.com. * **serviceAccount:{emailid}**: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com. * **group:{emailid}**: An email address that represents a Google group. For example, admins@example.com. * **domain:{domain}**: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com. + * For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding * `role` - (Required) The role that should be applied. Only one `google_folder_iam_binding` can be used per role. Note that custom roles must be of the format diff --git a/website/docs/r/google_folder_iam_member.html.markdown b/website/docs/r/google_folder_iam_member.html.markdown index 3ac0088c..3deb8e53 100644 --- a/website/docs/r/google_folder_iam_member.html.markdown +++ b/website/docs/r/google_folder_iam_member.html.markdown @@ -27,7 +27,7 @@ resource "google_folder" "department1" { resource "google_folder_iam_member" "admin" { folder = "${google_folder.department1.name}" role = "roles/editor" - member = "user:jane@example.com" + member = "user:alice@gmail.com" } ``` @@ -37,7 +37,7 @@ The following arguments are supported: * `folder` - (Required) The resource name of the folder the policy is attached to. Its format is folders/{folder_id}. -* `member` - (Required) The identity that will be granted the privilege in the `role`. +* `member` - (Required) The identity that will be granted the privilege in the `role`. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding This field can have one of the following values: * **user:{emailid}**: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com. * **serviceAccount:{emailid}**: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com. diff --git a/website/docs/r/google_kms_crypto_key_iam_binding.html.markdown b/website/docs/r/google_kms_crypto_key_iam_binding.html.markdown index cf7f3012..f0782cce 100644 --- a/website/docs/r/google_kms_crypto_key_iam_binding.html.markdown +++ b/website/docs/r/google_kms_crypto_key_iam_binding.html.markdown @@ -11,6 +11,10 @@ description: |- Allows creation and management of a single binding within IAM policy for an existing Google Cloud KMS crypto key. +~> **Note:** On create, this resource will overwrite members of any existing roles. + Use `terraform import` and inspect the `terraform plan` output to ensure + your existing members are preserved. + ## Example Usage ```hcl @@ -19,7 +23,7 @@ resource "google_kms_crypto_key_iam_binding" "crypto_key" { role = "roles/editor" members = [ - "user:jane@example.com", + "user:alice@gmail.com", ] } ``` @@ -28,7 +32,7 @@ resource "google_kms_crypto_key_iam_binding" "crypto_key" { The following arguments are supported: -* `members` - (Required) A list of users that the role should apply to. +* `members` - (Required) A list of users that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding * `role` - (Required) The role that should be applied. Only one `google_kms_crypto_key_iam_binding` can be used per role. Note that custom roles must be of the format diff --git a/website/docs/r/google_kms_crypto_key_iam_member.html.markdown b/website/docs/r/google_kms_crypto_key_iam_member.html.markdown index 6e8f820f..c73b892a 100644 --- a/website/docs/r/google_kms_crypto_key_iam_member.html.markdown +++ b/website/docs/r/google_kms_crypto_key_iam_member.html.markdown @@ -22,7 +22,7 @@ the IAM policy for an existing Google Cloud KMS crypto key. resource "google_kms_crypto_key_iam_member" "crypto_key" { crypto_key_id = "your-crypto-key-id" role = "roles/editor" - member = "user:jane@example.com" + member = "user:alice@gmail.com" } ``` @@ -30,7 +30,7 @@ resource "google_kms_crypto_key_iam_member" "crypto_key" { The following arguments are supported: -* `member` - (Required) The user that the role should apply to. +* `member` - (Required) The user that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding * `role` - (Required) The role that should be applied. Note that custom roles must be of the format `[projects|organizations]/{parent-name}/roles/{role-name}`. diff --git a/website/docs/r/google_organization_iam_binding.md b/website/docs/r/google_organization_iam_binding.md index a381f59e..be14a12a 100644 --- a/website/docs/r/google_organization_iam_binding.md +++ b/website/docs/r/google_organization_iam_binding.md @@ -15,6 +15,10 @@ an existing Google Cloud Platform Organization. `google_organization_iam_member` for the __same role__ or they will fight over what your policy should be. +~> **Note:** On create, this resource will overwrite members of any existing roles. + Use `terraform import` and inspect the `terraform plan` output to ensure + your existing members are preserved. + ## Example Usage ```hcl @@ -23,7 +27,7 @@ resource "google_organization_iam_binding" "binding" { role = "roles/browser" members = [ - "user:jane@example.com", + "user:alice@gmail.com", ] } ``` @@ -38,7 +42,7 @@ The following arguments are supported: `google_organization_iam_binding` can be used per role. Note that custom roles must be of the format `[projects|organizations]/{parent-name}/roles/{role-name}`. -* `members` - (Required) A list of users that the role should apply to. +* `members` - (Required) A list of users that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding ## Attributes Reference diff --git a/website/docs/r/google_organization_iam_member.md b/website/docs/r/google_organization_iam_member.md index 5bfd2ee7..77f6db8a 100644 --- a/website/docs/r/google_organization_iam_member.md +++ b/website/docs/r/google_organization_iam_member.md @@ -21,7 +21,7 @@ the IAM policy for an existing Google Cloud Platform Organization. resource "google_organization_iam_member" "binding" { org_id = "0123456789" role = "roles/editor" - member = "user:jane@example.com" + member = "user:alice@gmail.com" } ``` @@ -34,8 +34,8 @@ The following arguments are supported: * `role` - (Required) The role that should be applied. Note that custom roles must be of the format `[projects|organizations]/{parent-name}/roles/{role-name}`. -* `member` - (Required) The user that the role should apply to. - +* `member` - (Required) The user that the role should apply to. For more details on format and restrictions see https://cloud.google.com/billing/reference/rest/v1/Policy#Binding + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are diff --git a/website/docs/version_2_upgrade.html.markdown b/website/docs/version_2_upgrade.html.markdown index 2eb2d3a3..916a61be 100644 --- a/website/docs/version_2_upgrade.html.markdown +++ b/website/docs/version_2_upgrade.html.markdown @@ -54,6 +54,7 @@ Upgrade topics: - [Resource: `google_sql_database_instance`](#resource-google_sql_database_instance) - [Resource: `google_storage_default_object_acl`](#resource-google_storage_default_object_acl) - [Resource: `google_storage_object_acl`](#resource-google_storage_object_acl) +- [Resource: `google_*_iam_binding`](#google_*_iam_binding) @@ -663,3 +664,31 @@ values that were added outside of Terraform should be added to the config. Terraform will remove values not explicitly set in this field. Any `role_entity` values that were added outside of Terraform should be added to the config. For fine-grained management, use `google_storage_object_access_control`. + +## Resource: `google_*_iam_binding` + +### Create is now authoritative + +Every `iam_binding` resource will overwrite the existing member list for a given +role on Create. Running `terraform plan` for the first time will not show members +that have been added via other tools. *To ensure existing `members` are preserved +use `terraform import` instead of creating the resource.* + +Previous versions of `google_*_iam_binding` resources would merge the existing +members of a role with the members defined in the terraform config. If there was +a difference between the members defined in the config and the existing members +defined for an existing role it would show a diff if `terraform plan` was run +immediately after create had succeeded. + +Affected resources: +* `google_billing_account_iam_binding` +* `google_folder_iam_binding` +* `google_kms_key_ring_iam_binding` +* `google_kms_crypto_key_iam_binding` +* `google_spanner_instance_iam_binding` +* `google_spanner_database_iam_binding` +* `google_organization_iam_binding` +* `google_project_iam_binding` +* `google_pubsub_topic_iam_binding` +* `google_pubsub_subscription_iam_binding` +* `google_service_account_iam_binding`