diff --git a/.changelog/3729.txt b/.changelog/3729.txt new file mode 100644 index 00000000000..2c399019343 --- /dev/null +++ b/.changelog/3729.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +iam: made the `condition` block GA for all IAM resource and datasource types. +``` diff --git a/google/data_source_google_iam_policy.go b/google/data_source_google_iam_policy.go index 7a690305733..136137b20d4 100644 --- a/google/data_source_google_iam_policy.go +++ b/google/data_source_google_iam_policy.go @@ -48,6 +48,27 @@ func dataSourceGoogleIamPolicy() *schema.Resource { }, Set: schema.HashString, }, + "condition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expression": { + Type: schema.TypeString, + Required: true, + }, + "title": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, }, }, }, @@ -106,13 +127,15 @@ func dataSourceGoogleIamPolicyRead(d *schema.ResourceData, meta interface{}) err for i, v := range bset.List() { binding := v.(map[string]interface{}) members := convertStringSet(binding["members"].(*schema.Set)) + condition := expandIamCondition(binding["condition"]) // Sort members to get simpler diffs as it's what the API does sort.Strings(members) policy.Bindings[i] = &cloudresourcemanager.Binding{ - Role: binding["role"].(string), - Members: members, + Role: binding["role"].(string), + Members: members, + Condition: condition, } } diff --git a/google/resource_google_project_iam_binding_test.go b/google/resource_google_project_iam_binding_test.go index 917ccaf0101..e8723d4bf0c 100644 --- a/google/resource_google_project_iam_binding_test.go +++ b/google/resource_google_project_iam_binding_test.go @@ -216,6 +216,38 @@ func TestAccProjectIamBinding_noMembers(t *testing.T) { }) } +func TestAccProjectIamBinding_withCondition(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := fmt.Sprintf("tf-test-%d", randInt(t)) + role := "roles/compute.instanceAdmin" + conditionTitle := "expires_after_2019_12_31" + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(t, pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateBinding_withCondition(pid, pname, org, role, conditionTitle), + }, + { + ResourceName: "google_project_iam_binding.acceptance", + ImportStateId: fmt.Sprintf("%s %s %s", pid, role, conditionTitle), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccProjectAssociateBindingBasic(pid, name, org, role string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -301,3 +333,24 @@ resource "google_project_iam_binding" "acceptance" { } `, pid, name, org, role) } + +func testAccProjectAssociateBinding_withCondition(pid, name, org, role, conditionTitle string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_binding" "acceptance" { + project = google_project.acceptance.project_id + members = ["user:admin@hashicorptest.com"] + role = "%s" + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +`, pid, name, org, role, conditionTitle) +} diff --git a/google/resource_google_project_iam_member_test.go b/google/resource_google_project_iam_member_test.go index 96dc3f87ca5..3596ace8e7b 100644 --- a/google/resource_google_project_iam_member_test.go +++ b/google/resource_google_project_iam_member_test.go @@ -133,6 +133,40 @@ func TestAccProjectIamMember_remove(t *testing.T) { }) } +func TestAccProjectIamMember_withCondition(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := fmt.Sprintf("tf-test-%d", randInt(t)) + resourceName := "google_project_iam_member.acceptance" + role := "roles/compute.instanceAdmin" + member := "user:admin@hashicorptest.com" + conditionTitle := "expires_after_2019_12_31" + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(t, pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateMember_withCondition(pid, pname, org, role, member, conditionTitle), + }, + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s %s %s %s", pid, role, member, conditionTitle), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccProjectAssociateMemberBasic(pid, name, org, role, member string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -170,3 +204,24 @@ resource "google_project_iam_member" "multiple" { } `, pid, name, org, role, member, role2, member2) } + +func testAccProjectAssociateMember_withCondition(pid, name, org, role, member, conditionTitle string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_member" "acceptance" { + project = google_project.acceptance.project_id + role = "%s" + member = "%s" + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +`, pid, name, org, role, member, conditionTitle) +} diff --git a/google/resource_google_project_iam_policy_test.go b/google/resource_google_project_iam_policy_test.go index 29cabba0127..2f9d4ff711f 100644 --- a/google/resource_google_project_iam_policy_test.go +++ b/google/resource_google_project_iam_policy_test.go @@ -127,6 +127,35 @@ func TestAccProjectIamPolicy_expandedAuditConfig(t *testing.T) { }) } +func TestAccProjectIamPolicy_withCondition(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := fmt.Sprintf("tf-test-%d", randInt(t)) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(t, pid), + ), + }, + // Apply an IAM policy from a data source. The application + // merges policies, so we validate the expected state. + { + Config: testAccProjectAssociatePolicy_withCondition(pid, pname, org), + }, + { + ResourceName: "google_project_iam_policy.acceptance", + ImportState: true, + }, + }, + }) +} + func getStatePrimaryResource(s *terraform.State, res, expectedID string) (*terraform.InstanceState, error) { // Get the project resource resource, ok := s.RootModule().Resources[res] @@ -398,3 +427,39 @@ data "google_iam_policy" "expanded" { } `, pid, name, org) } + +func testAccProjectAssociatePolicy_withCondition(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_policy" "acceptance" { + project = google_project.acceptance.id + policy_data = data.google_iam_policy.admin.policy_data +} + +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + condition { + title = "expires_after_2019_12_31" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } + } +} +`, pid, name, org) +} diff --git a/google/resource_google_service_account_iam_test.go b/google/resource_google_service_account_iam_test.go index 23c563ccb60..111ba61ecd3 100644 --- a/google/resource_google_service_account_iam_test.go +++ b/google/resource_google_service_account_iam_test.go @@ -31,6 +31,62 @@ func TestAccServiceAccountIamBinding(t *testing.T) { }) } +func TestAccServiceAccountIamBinding_withCondition(t *testing.T) { + t.Parallel() + + account := fmt.Sprintf("tf-test-%d", randInt(t)) + conditionExpr := `request.time < timestamp(\"2020-01-01T00:00:00Z\")` + conditionTitle := "expires_after_2019_12_31" + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamBinding_withCondition(account, "user:admin@hashicorptest.com", conditionTitle, conditionExpr), + Check: testAccCheckGoogleServiceAccountIam(t, account, 1), + }, + { + ResourceName: "google_service_account_iam_binding.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", conditionTitle), + }, + }, + }) +} + +func TestAccServiceAccountIamBinding_withAndWithoutCondition(t *testing.T) { + t.Parallel() + + account := fmt.Sprintf("tf-test-%d", randInt(t)) + conditionExpr := `request.time < timestamp(\"2020-01-01T00:00:00Z\")` + conditionTitle := "expires_after_2019_12_31" + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamBinding_withAndWithoutCondition(account, "user:admin@hashicorptest.com", conditionTitle, conditionExpr), + Check: testAccCheckGoogleServiceAccountIam(t, account, 2), + }, + { + ResourceName: "google_service_account_iam_binding.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser"), + }, + { + ResourceName: "google_service_account_iam_binding.foo2", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", conditionTitle), + }, + }, + }) +} + func TestAccServiceAccountIamMember(t *testing.T) { t.Parallel() @@ -55,6 +111,62 @@ func TestAccServiceAccountIamMember(t *testing.T) { }) } +func TestAccServiceAccountIamMember_withCondition(t *testing.T) { + t.Parallel() + + account := fmt.Sprintf("tf-test-%d", randInt(t)) + identity := fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)) + conditionTitle := "expires_after_2019_12_31" + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamMember_withCondition(account, conditionTitle), + Check: testAccCheckGoogleServiceAccountIam(t, account, 1), + }, + { + ResourceName: "google_service_account_iam_member.foo", + ImportStateId: fmt.Sprintf("%s %s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", identity, conditionTitle), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccServiceAccountIamMember_withAndWithoutCondition(t *testing.T) { + t.Parallel() + + account := fmt.Sprintf("tf-test-%d", randInt(t)) + identity := fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)) + conditionTitle := "expires_after_2019_12_31" + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamMember_withAndWithoutCondition(account, conditionTitle), + Check: testAccCheckGoogleServiceAccountIam(t, account, 2), + }, + { + ResourceName: "google_service_account_iam_member.foo", + ImportStateId: fmt.Sprintf("%s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", identity), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_service_account_iam_member.foo2", + ImportStateId: fmt.Sprintf("%s %s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", identity, conditionTitle), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccServiceAccountIamPolicy(t *testing.T) { t.Parallel() @@ -77,6 +189,28 @@ func TestAccServiceAccountIamPolicy(t *testing.T) { }) } +func TestAccServiceAccountIamPolicy_withCondition(t *testing.T) { + t.Parallel() + + account := fmt.Sprintf("tf-test-%d", randInt(t)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamPolicy_withCondition(account), + }, + { + ResourceName: "google_service_account_iam_policy.foo", + ImportStateId: serviceAccountCanonicalId(account), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + // Ensure that our tests only create the expected number of bindings. // The content of the binding is tested in the import tests. func testAccCheckGoogleServiceAccountIam(t *testing.T, account string, numBindings int) resource.TestCheckFunc { @@ -118,6 +252,52 @@ resource "google_service_account_iam_binding" "foo" { `, account) } +func testAccServiceAccountIamBinding_withCondition(account, member, conditionTitle, conditionExpr string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Service Account Iam Testing Account" +} + +resource "google_service_account_iam_binding" "foo" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + members = ["%s"] + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "%s" + } +} +`, account, member, conditionTitle, conditionExpr) +} + +func testAccServiceAccountIamBinding_withAndWithoutCondition(account, member, conditionTitle, conditionExpr string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Service Account Iam Testing Account" +} + +resource "google_service_account_iam_binding" "foo" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + members = ["%s"] +} + +resource "google_service_account_iam_binding" "foo2" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + members = ["%s"] + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "%s" + } +} +`, account, member, member, conditionTitle, conditionExpr) +} + func testAccServiceAccountIamMember_basic(account string) string { return fmt.Sprintf(` resource "google_service_account" "test_account" { @@ -133,6 +313,52 @@ resource "google_service_account_iam_member" "foo" { `, account) } +func testAccServiceAccountIamMember_withCondition(account, conditionTitle string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Service Account Iam Testing Account" +} + +resource "google_service_account_iam_member" "foo" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_service_account.test_account.email}" + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +`, account, conditionTitle) +} + +func testAccServiceAccountIamMember_withAndWithoutCondition(account, conditionTitle string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Service Account Iam Testing Account" +} + +resource "google_service_account_iam_member" "foo" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_service_account.test_account.email}" +} + +resource "google_service_account_iam_member" "foo2" { + service_account_id = google_service_account.test_account.name + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_service_account.test_account.email}" + condition { + title = "%s" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +`, account, conditionTitle) +} + func testAccServiceAccountIamPolicy_basic(account string) string { return fmt.Sprintf(` resource "google_service_account" "test_account" { @@ -154,3 +380,30 @@ resource "google_service_account_iam_policy" "foo" { } `, account) } + +func testAccServiceAccountIamPolicy_withCondition(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Service Account Iam Testing Account" +} + +data "google_iam_policy" "foo" { + binding { + role = "roles/iam.serviceAccountUser" + + members = ["serviceAccount:${google_service_account.test_account.email}"] + condition { + title = "expires_after_2019_12_31" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } + } +} + +resource "google_service_account_iam_policy" "foo" { + service_account_id = google_service_account.test_account.name + policy_data = data.google_iam_policy.foo.policy_data +} +`, account) +} diff --git a/google/resource_iam_binding.go b/google/resource_iam_binding.go index d50527d7d90..06f865c9d9f 100644 --- a/google/resource_iam_binding.go +++ b/google/resource_iam_binding.go @@ -30,6 +30,31 @@ var iamBindingSchema = map[string]*schema.Schema{ return schema.HashString(strings.ToLower(v.(string))) }, }, + "condition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expression": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "title": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, "etag": { Type: schema.TypeString, Computed: true, @@ -81,6 +106,9 @@ func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc, en } d.SetId(updater.GetResourceId() + "/" + binding.Role) + if k := conditionKeyFromCondition(binding.Condition); !k.Empty() { + d.SetId(d.Id() + "/" + k.String()) + } return resourceIamBindingRead(newUpdaterFunc)(d, meta) } } @@ -119,6 +147,7 @@ func resourceIamBindingRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Rea } else { d.Set("role", binding.Role) d.Set("members", binding.Members) + d.Set("condition", flattenIamCondition(binding.Condition)) } d.Set("etag", p.Etag) return nil @@ -133,11 +162,18 @@ func iamBindingImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser config := m.(*Config) s := strings.Fields(d.Id()) var id, role string - if len(s) != 2 { + if len(s) < 2 { d.SetId("") - return nil, fmt.Errorf("Wrong number of parts to Binding id %s; expected 'resource_name role'.", s) + return nil, fmt.Errorf("Wrong number of parts to Binding id %s; expected 'resource_name role [condition_title]'.", s) + } + + var conditionTitle string + if len(s) == 2 { + id, role = s[0], s[1] + } else { + // condition titles can have any characters in them, so re-join the split string + id, role, conditionTitle = s[0], s[1], strings.Join(s[2:], " ") } - id, role = s[0], s[1] // Set the ID only to the first part so all IAM types can share the same resourceIdParserFunc. d.SetId(id) @@ -151,6 +187,35 @@ func iamBindingImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser // Use the current ID in case it changed in the resourceIdParserFunc. d.SetId(d.Id() + "/" + role) + // Since condition titles can have any character in them, we can't separate them from any other + // field the user might set in import (like the condition description and expression). So, we + // have the user just specify the title and then read the upstream policy to set the full + // condition. We can't rely on the read fn to do this for us because it looks for a match of the + // full condition. + updater, err := newUpdaterFunc(d, config) + if err != nil { + return nil, err + } + p, err := iamPolicyReadWithRetry(updater) + if err != nil { + return nil, err + } + var binding *cloudresourcemanager.Binding + for _, b := range p.Bindings { + if b.Role == role && conditionKeyFromCondition(b.Condition).Title == conditionTitle { + if binding != nil { + return nil, fmt.Errorf("Cannot import IAM member with condition title %q, it matches multiple conditions", conditionTitle) + } + binding = b + } + } + if binding != nil { + d.Set("condition", flattenIamCondition(binding.Condition)) + if k := conditionKeyFromCondition(binding.Condition); !k.Empty() { + d.SetId(d.Id() + "/" + k.String()) + } + } + // It is possible to return multiple bindings, since we can learn about all the bindings // for this resource here. Unfortunately, `terraform import` has some messy behavior here - // there's no way to know at this point which resource is being imported, so it's not possible @@ -199,5 +264,35 @@ func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding Members: convertStringArr(members), Role: d.Get("role").(string), } + if c := expandIamCondition(d.Get("condition")); c != nil { + b.Condition = c + } return b } + +func expandIamCondition(v interface{}) *cloudresourcemanager.Expr { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil + } + original := l[0].(map[string]interface{}) + return &cloudresourcemanager.Expr{ + Description: original["description"].(string), + Expression: original["expression"].(string), + Title: original["title"].(string), + ForceSendFields: []string{"Description", "Expression", "Title"}, + } +} + +func flattenIamCondition(condition *cloudresourcemanager.Expr) []map[string]interface{} { + if conditionKeyFromCondition(condition).Empty() { + return nil + } + return []map[string]interface{}{ + { + "expression": condition.Expression, + "title": condition.Title, + "description": condition.Description, + }, + } +} diff --git a/google/resource_iam_member.go b/google/resource_iam_member.go index 1d6b3051df6..33c36b93cf3 100644 --- a/google/resource_iam_member.go +++ b/google/resource_iam_member.go @@ -25,6 +25,31 @@ var IamMemberBaseSchema = map[string]*schema.Schema{ DiffSuppressFunc: caseDiffSuppress, ValidateFunc: validation.StringDoesNotMatch(regexp.MustCompile("^deleted:"), "Terraform does not support IAM members for deleted principals"), }, + "condition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expression": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "title": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, "etag": { Type: schema.TypeString, Computed: true, @@ -39,11 +64,18 @@ func iamMemberImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser config := m.(*Config) s := strings.Fields(d.Id()) var id, role, member string - if len(s) != 3 { + if len(s) < 3 { d.SetId("") - return nil, fmt.Errorf("Wrong number of parts to Member id %s; expected 'resource_name role member'.", s) + return nil, fmt.Errorf("Wrong number of parts to Member id %s; expected 'resource_name role member [condition_title]'.", s) + } + + var conditionTitle string + if len(s) == 3 { + id, role, member = s[0], s[1], s[2] + } else { + // condition titles can have any characters in them, so re-join the split string + id, role, member, conditionTitle = s[0], s[1], s[2], strings.Join(s[3:], " ") } - id, role, member = s[0], s[1], s[2] // Set the ID only to the first part so all IAM types can share the same resourceIdParserFunc. d.SetId(id) @@ -59,6 +91,43 @@ func iamMemberImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser // Use the current ID in case it changed in the resourceIdParserFunc. d.SetId(d.Id() + "/" + role + "/" + strings.ToLower(member)) + // Read the upstream policy so we can set the full condition. + updater, err := newUpdaterFunc(d, config) + if err != nil { + return nil, err + } + p, err := iamPolicyReadWithRetry(updater) + if err != nil { + return nil, err + } + var binding *cloudresourcemanager.Binding + for _, b := range p.Bindings { + if b.Role == role && conditionKeyFromCondition(b.Condition).Title == conditionTitle { + containsMember := false + for _, m := range b.Members { + if strings.ToLower(m) == strings.ToLower(member) { + containsMember = true + } + } + if !containsMember { + continue + } + + if binding != nil { + return nil, fmt.Errorf("Cannot import IAM member with condition title %q, it matches multiple conditions", conditionTitle) + } + binding = b + } + } + if binding == nil { + return nil, fmt.Errorf("Cannot find binding for %q with role %q, member %q, and condition title %q", updater.DescribeResource(), role, member, conditionTitle) + } + + d.Set("condition", flattenIamCondition(binding.Condition)) + if k := conditionKeyFromCondition(binding.Condition); !k.Empty() { + d.SetId(d.Id() + "/" + k.String()) + } + return []*schema.ResourceData{d}, nil } } @@ -84,6 +153,9 @@ func getResourceIamMember(d *schema.ResourceData) *cloudresourcemanager.Binding Members: []string{d.Get("member").(string)}, Role: d.Get("role").(string), } + if c := expandIamCondition(d.Get("condition")); c != nil { + b.Condition = c + } return b } @@ -112,6 +184,9 @@ func resourceIamMemberCreate(newUpdaterFunc newResourceIamUpdaterFunc, enableBat return err } d.SetId(updater.GetResourceId() + "/" + memberBind.Role + "/" + strings.ToLower(memberBind.Members[0])) + if k := conditionKeyFromCondition(memberBind.Condition); !k.Empty() { + d.SetId(d.Id() + "/" + k.String()) + } return resourceIamMemberRead(newUpdaterFunc)(d, meta) } } @@ -164,6 +239,7 @@ func resourceIamMemberRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Read d.Set("etag", p.Etag) d.Set("member", member) d.Set("role", binding.Role) + d.Set("condition", flattenIamCondition(binding.Condition)) return nil } } diff --git a/website/docs/d/iam_policy.html.markdown b/website/docs/d/iam_policy.html.markdown index d2a0a4db705..5b2ea7d1881 100644 --- a/website/docs/d/iam_policy.html.markdown +++ b/website/docs/d/iam_policy.html.markdown @@ -87,6 +87,15 @@ each accept the following arguments: * `log_type` (Required) Defines the logging level. `DATA_READ`, `DATA_WRITE` and `ADMIN_READ` capture different types of events. See [the audit configuration documentation](https://cloud.google.com/resource-manager/reference/rest/Shared.Types/AuditConfig) for more details. * `exempted_members` (Optional) Specifies the identities that are exempt from these types of logging operations. Follows the same format of the `members` array for `binding`. +* `condition` - (Optional) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. Structure is documented below. + +The `condition` block supports: + +* `expression` - (Required) Textual representation of an expression in Common Expression Language syntax. + +* `title` - (Required) A title for the expression, i.e. a short string describing its purpose. + +* `description` - (Optional) An optional description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI. ## Attributes Reference diff --git a/website/docs/r/google_project_iam.html.markdown b/website/docs/r/google_project_iam.html.markdown index e5cf01aa3d1..c8ea7086274 100644 --- a/website/docs/r/google_project_iam.html.markdown +++ b/website/docs/r/google_project_iam.html.markdown @@ -48,7 +48,7 @@ data "google_iam_policy" "admin" { } ``` -With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): +With IAM Conditions: ```hcl resource "google_project_iam_policy" "project" { @@ -88,7 +88,7 @@ resource "google_project_iam_binding" "project" { } ``` -With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): +With IAM Conditions: ```hcl resource "google_project_iam_binding" "project" { @@ -117,7 +117,7 @@ resource "google_project_iam_member" "project" { } ``` -With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): +With IAM Conditions: ```hcl resource "google_project_iam_member" "project" { @@ -183,7 +183,7 @@ will not be inferred from the provider. * `audit_log_config` - (Required only by google\_project\_iam\_audit\_config) The configuration for logging of each type of permission. This can be specified multiple times. Structure is documented below. -* `condition` - (Optional, [Beta](https://terraform.io/docs/providers/google/provider_versions.html)) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. +* `condition` - (Optional) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. Structure is documented below. --- diff --git a/website/docs/r/google_service_account_iam.html.markdown b/website/docs/r/google_service_account_iam.html.markdown index 8f87bd776ca..5bd5551aef2 100644 --- a/website/docs/r/google_service_account_iam.html.markdown +++ b/website/docs/r/google_service_account_iam.html.markdown @@ -63,7 +63,7 @@ resource "google_service_account_iam_binding" "admin-account-iam" { } ``` -With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): +With IAM Conditions: ```hcl resource "google_service_account" "sa" { @@ -112,7 +112,7 @@ resource "google_service_account_iam_member" "gce-default-account-iam" { } ``` -With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): +With IAM Conditions: ```hcl resource "google_service_account" "sa" { @@ -155,7 +155,7 @@ The following arguments are supported: * `policy_data` - (Required only by `google_service_account_iam_policy`) The policy data generated by a `google_iam_policy` data source. -* `condition` - (Optional, [Beta](https://terraform.io/docs/providers/google/provider_versions.html)) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. +* `condition` - (Optional) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. Structure is documented below. The `condition` block supports: