From 67c66aad64f73b68bbf71551db3393ffb101b695 Mon Sep 17 00:00:00 2001 From: The Magician Date: Mon, 28 Oct 2019 10:05:22 -0700 Subject: [PATCH] Add IAM Conditions support; enable it in service account IAM (#1188) Signed-off-by: Modular Magician --- google-beta/data_source_google_iam_policy.go | 27 +- google-beta/iam.go | 77 +++-- google-beta/iam_service_account.go | 3 +- google-beta/iam_test.go | 283 ++++++++++++++-- ...esource_google_service_account_iam_test.go | 303 ++++++++++++++++-- google-beta/resource_iam_binding.go | 132 +++++++- google-beta/resource_iam_member.go | 111 ++++++- google-beta/resource_iam_policy.go | 1 + .../google_service_account_iam.html.markdown | 74 ++++- 9 files changed, 895 insertions(+), 116 deletions(-) diff --git a/google-beta/data_source_google_iam_policy.go b/google-beta/data_source_google_iam_policy.go index 637d77526d..ad141d7e51 100644 --- a/google-beta/data_source_google_iam_policy.go +++ b/google-beta/data_source_google_iam_policy.go @@ -41,6 +41,27 @@ func dataSourceGoogleIamPolicy() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, 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, + }, + }, + }, + }, }, }, }, @@ -99,13 +120,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-beta/iam.go b/google-beta/iam.go index 58ff369e1d..a7240b7af9 100644 --- a/google-beta/iam.go +++ b/google-beta/iam.go @@ -15,6 +15,7 @@ import ( ) const maxBackoffSeconds = 30 +const iamPolicyVersion = 3 // These types are implemented per GCP resource type and specify how to do per-resource IAM operations. // They are used in the generic Terraform IAM resource definitions @@ -152,26 +153,53 @@ func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModify return nil } -// Flattens AuditConfigs so each role has a single Binding with combined members +// Flattens a list of Bindings so each role+condition has a single Binding with combined members func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { bm := createIamBindingsMap(bindings) return listFromIamBindingMap(bm) } -// Flattens Bindings so each role has a single Binding with combined members -func removeAllBindingsWithRole(b []*cloudresourcemanager.Binding, role string) []*cloudresourcemanager.Binding { +type conditionKey struct { + Description string + Expression string + Title string +} + +func conditionKeyFromCondition(condition *cloudresourcemanager.Expr) conditionKey { + if condition == nil { + return conditionKey{} + } + return conditionKey{condition.Description, condition.Expression, condition.Title} +} + +func (k conditionKey) Empty() bool { + return k == conditionKey{} +} + +func (k conditionKey) String() string { + return fmt.Sprintf("%s/%s/%s", k.Title, k.Description, k.Expression) +} + +type iamBindingKey struct { + Role string + Condition conditionKey +} + +// Removes a single role+condition binding from a list of Bindings +func filterBindingsWithRoleAndCondition(b []*cloudresourcemanager.Binding, role string, condition *cloudresourcemanager.Expr) []*cloudresourcemanager.Binding { bMap := createIamBindingsMap(b) - delete(bMap, role) + key := iamBindingKey{role, conditionKeyFromCondition(condition)} + delete(bMap, key) return listFromIamBindingMap(bMap) } -// Removes given role/bound-member pairs from the given Bindings (i.e subtraction). +// Removes given role+condition/bound-member pairs from the given Bindings (i.e subtraction). func subtractFromBindings(bindings []*cloudresourcemanager.Binding, toRemove ...*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { currMap := createIamBindingsMap(bindings) toRemoveMap := createIamBindingsMap(toRemove) - for role, removeSet := range toRemoveMap { - members, ok := currMap[role] + for key, removeSet := range toRemoveMap { + members, ok := currMap[key] if !ok { continue } @@ -179,9 +207,9 @@ func subtractFromBindings(bindings []*cloudresourcemanager.Binding, toRemove ... for m := range removeSet { delete(members, m) } - // Remove role from bindings + // Remove role+condition from bindings if len(members) == 0 { - delete(currMap, role) + delete(currMap, key) } } @@ -189,14 +217,15 @@ func subtractFromBindings(bindings []*cloudresourcemanager.Binding, toRemove ... } // Construct map of role to set of members from list of bindings. -func createIamBindingsMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]struct{} { - bm := make(map[string]map[string]struct{}) +func createIamBindingsMap(bindings []*cloudresourcemanager.Binding) map[iamBindingKey]map[string]struct{} { + bm := make(map[iamBindingKey]map[string]struct{}) // Get each binding for _, b := range bindings { members := make(map[string]struct{}) + key := iamBindingKey{b.Role, conditionKeyFromCondition(b.Condition)} // Initialize members map - if _, ok := bm[b.Role]; ok { - members = bm[b.Role] + if _, ok := bm[key]; ok { + members = bm[key] } // Get each member (user/principal) for the binding for _, m := range b.Members { @@ -214,25 +243,33 @@ func createIamBindingsMap(bindings []*cloudresourcemanager.Binding) map[string]m members[m] = struct{}{} } if len(members) > 0 { - bm[b.Role] = members + bm[key] = members } else { - delete(bm, b.Role) + delete(bm, key) } } return bm } // Return list of Bindings for a map of role to member sets -func listFromIamBindingMap(bm map[string]map[string]struct{}) []*cloudresourcemanager.Binding { +func listFromIamBindingMap(bm map[iamBindingKey]map[string]struct{}) []*cloudresourcemanager.Binding { rb := make([]*cloudresourcemanager.Binding, 0, len(bm)) - for role, members := range bm { + for key, members := range bm { if len(members) == 0 { continue } - rb = append(rb, &cloudresourcemanager.Binding{ - Role: role, + b := &cloudresourcemanager.Binding{ + Role: key.Role, Members: stringSliceFromGolangSet(members), - }) + } + if !key.Condition.Empty() { + b.Condition = &cloudresourcemanager.Expr{ + Description: key.Condition.Description, + Expression: key.Condition.Expression, + Title: key.Condition.Title, + } + } + rb = append(rb, b) } return rb } diff --git a/google-beta/iam_service_account.go b/google-beta/iam_service_account.go index adee07eb30..49ed303dad 100644 --- a/google-beta/iam_service_account.go +++ b/google-beta/iam_service_account.go @@ -2,6 +2,7 @@ package google import ( "fmt" + "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "google.golang.org/api/cloudresourcemanager/v1" @@ -35,7 +36,7 @@ func ServiceAccountIdParseFunc(d *schema.ResourceData, _ *Config) error { } func (u *ServiceAccountIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { - p, err := u.Config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(u.serviceAccountId).Do() + p, err := u.Config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(u.serviceAccountId).OptionsRequestedPolicyVersion(iamPolicyVersion).Do() if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) diff --git a/google-beta/iam_test.go b/google-beta/iam_test.go index eed54b6257..ac1f400a37 100644 --- a/google-beta/iam_test.go +++ b/google-beta/iam_test.go @@ -2,9 +2,10 @@ package google import ( "encoding/json" - "google.golang.org/api/cloudresourcemanager/v1" "reflect" "testing" + + "google.golang.org/api/cloudresourcemanager/v1" ) func TestIamMergeBindings(t *testing.T) { @@ -152,6 +153,98 @@ func TestIamMergeBindings(t *testing.T) { }, }, }, + // Same role+members, different condition + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + }, + // Same role, same condition + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + { + Role: "role-1", + Members: []string{"member-3"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2", "member-3"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + }, + // Different roles, same condition + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + { + Role: "role-2", + Members: []string{"member-3"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + { + Role: "role-2", + Members: []string{"member-3"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + }, + }, + }, + }, } for _, tc := range testCases { @@ -163,11 +256,12 @@ func TestIamMergeBindings(t *testing.T) { } } -func TestIamRemoveAllBindingsWithRole(t *testing.T) { +func TestIamFilterBindingsWithRoleAndCondition(t *testing.T) { testCases := []struct { - input []*cloudresourcemanager.Binding - role string - expect []*cloudresourcemanager.Binding + input []*cloudresourcemanager.Binding + role string + conditionTitle string + expect []*cloudresourcemanager.Binding }{ // No-op { @@ -241,10 +335,32 @@ func TestIamRemoveAllBindingsWithRole(t *testing.T) { }, }, }, + // Remove one binding with condition + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + { + Role: "role-1", + Members: []string{"member-3", "member-4"}, + Condition: &cloudresourcemanager.Expr{Title: "condition-1"}, + }, + }, + role: "role-1", + conditionTitle: "condition-1", + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-2"}, + }, + }, + }, } for _, tc := range testCases { - got := removeAllBindingsWithRole(tc.input, tc.role) + got := filterBindingsWithRoleAndCondition(tc.input, tc.role, &cloudresourcemanager.Expr{Title: tc.conditionTitle}) if !compareBindings(got, tc.expect) { t.Errorf("Got unexpected value for removeAllBindingsWithRole(%s, %s).\nActual: %s\nExpected: %s", debugPrintBindings(tc.input), tc.role, debugPrintBindings(got), debugPrintBindings(tc.expect)) @@ -396,6 +512,57 @@ func TestIamSubtractFromBindings(t *testing.T) { }, }, }, + // With conditions + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-2", "member-3"}, + }, + { + Role: "role-2", + Members: []string{"member-1"}, + }, + { + Role: "role-1", + Members: []string{"member-1"}, + }, + { + Role: "role-3", + Members: []string{"member-1"}, + }, + { + Role: "role-2", + Members: []string{"member-1"}, + Condition: &cloudresourcemanager.Expr{Title: "condition-1"}, + }, + }, + remove: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-2", "member-4"}, + }, + { + Role: "role-2", + Members: []string{"member-1"}, + Condition: &cloudresourcemanager.Expr{Title: "condition-1"}, + }, + }, + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"member-1", "member-3"}, + }, + { + Role: "role-2", + Members: []string{"member-1"}, + }, + { + Role: "role-3", + Members: []string{"member-1"}, + }, + }, + }, } for _, tc := range testCases { @@ -410,11 +577,11 @@ func TestIamSubtractFromBindings(t *testing.T) { func TestIamCreateIamBindingsMap(t *testing.T) { testCases := []struct { input []*cloudresourcemanager.Binding - expect map[string]map[string]struct{} + expect map[iamBindingKey]map[string]struct{} }{ { input: []*cloudresourcemanager.Binding{}, - expect: map[string]map[string]struct{}{}, + expect: map[iamBindingKey]map[string]struct{}{}, }, { input: []*cloudresourcemanager.Binding{ @@ -423,8 +590,8 @@ func TestIamCreateIamBindingsMap(t *testing.T) { Members: []string{"user-1", "user-2"}, }, }, - expect: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}}, + expect: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}}, }, }, { @@ -438,8 +605,8 @@ func TestIamCreateIamBindingsMap(t *testing.T) { Members: []string{"user-3"}, }, }, - expect: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}, "user-3": {}}, + expect: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}, "user-3": {}}, }, }, { @@ -453,9 +620,9 @@ func TestIamCreateIamBindingsMap(t *testing.T) { Members: []string{"user-1"}, }, }, - expect: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}}, - "role-2": {"user-1": {}}, + expect: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}}, + {"role-2", conditionKey{}}: {"user-1": {}}, }, }, { @@ -481,10 +648,64 @@ func TestIamCreateIamBindingsMap(t *testing.T) { Members: []string{"user-3"}, }, }, - expect: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}, "user-3": {}}, - "role-2": {"user-1": {}, "user-2": {}}, - "role-3": {"user-3": {}}, + expect: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}, "user-3": {}}, + {"role-2", conditionKey{}}: {"user-1": {}, "user-2": {}}, + {"role-3", conditionKey{}}: {"user-3": {}}, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{"user-1", "user-2"}, + }, + { + Role: "role-2", + Members: []string{"user-1"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + Description: "condition-1-desc", + Expression: "condition-1-expr", + }, + }, + { + Role: "role-2", + Members: []string{"user-2"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-1", + Description: "condition-1-desc", + Expression: "condition-1-expr", + }, + }, + { + Role: "role-2", + Members: []string{"user-1"}, + Condition: &cloudresourcemanager.Expr{ + Title: "condition-2", + Description: "condition-2-desc", + Expression: "condition-2-expr", + }, + }, + }, + expect: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}}, + { + Role: "role-2", + Condition: conditionKey{ + Title: "condition-1", + Description: "condition-1-desc", + Expression: "condition-1-expr", + }, + }: {"user-1": {}, "user-2": {}}, + { + Role: "role-2", + Condition: conditionKey{ + Title: "condition-2", + Description: "condition-2-desc", + Expression: "condition-2-expr", + }, + }: {"user-1": {}}, }, }, } @@ -492,7 +713,7 @@ func TestIamCreateIamBindingsMap(t *testing.T) { for _, tc := range testCases { got := createIamBindingsMap(tc.input) if !reflect.DeepEqual(got, tc.expect) { - t.Errorf("Unexpected value for subtractFromBindings(%s).\nActual: %#v\nExpected: %#v\n", + t.Errorf("Unexpected value for createIamBindingsMap(%s).\nActual: %#v\nExpected: %#v\n", debugPrintBindings(tc.input), got, tc.expect) } } @@ -500,16 +721,16 @@ func TestIamCreateIamBindingsMap(t *testing.T) { func TestIamListFromIamBindingMap(t *testing.T) { testCases := []struct { - input map[string]map[string]struct{} + input map[iamBindingKey]map[string]struct{} expect []*cloudresourcemanager.Binding }{ { - input: map[string]map[string]struct{}{}, + input: map[iamBindingKey]map[string]struct{}{}, expect: []*cloudresourcemanager.Binding{}, }, { - input: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}}, + input: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}}, }, expect: []*cloudresourcemanager.Binding{ { @@ -519,9 +740,9 @@ func TestIamListFromIamBindingMap(t *testing.T) { }, }, { - input: map[string]map[string]struct{}{ - "role-1": {"user-1": {}}, - "role-2": {"user-1": {}, "user-2": {}}, + input: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}}, + {"role-2", conditionKey{}}: {"user-1": {}, "user-2": {}}, }, expect: []*cloudresourcemanager.Binding{ { @@ -535,9 +756,9 @@ func TestIamListFromIamBindingMap(t *testing.T) { }, }, { - input: map[string]map[string]struct{}{ - "role-1": {"user-1": {}, "user-2": {}}, - "role-2": {}, + input: map[iamBindingKey]map[string]struct{}{ + {"role-1", conditionKey{}}: {"user-1": {}, "user-2": {}}, + {"role-2", conditionKey{}}: {}, }, expect: []*cloudresourcemanager.Binding{ { @@ -551,7 +772,7 @@ func TestIamListFromIamBindingMap(t *testing.T) { for _, tc := range testCases { got := listFromIamBindingMap(tc.input) if !compareBindings(got, tc.expect) { - t.Errorf("Unexpected value for subtractFromBindings(%s).\nActual: %#v\nExpected: %#v\n", + t.Errorf("Unexpected value for subtractFromBindings(%v).\nActual: %#v\nExpected: %#v\n", tc.input, debugPrintBindings(got), debugPrintBindings(tc.expect)) } } diff --git a/google-beta/resource_google_service_account_iam_test.go b/google-beta/resource_google_service_account_iam_test.go index 5b31ce332d..d22910070d 100644 --- a/google-beta/resource_google_service_account_iam_test.go +++ b/google-beta/resource_google_service_account_iam_test.go @@ -2,8 +2,6 @@ package google import ( "fmt" - "reflect" - "sort" "testing" "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" @@ -22,15 +20,69 @@ func TestAccServiceAccountIamBinding(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccServiceAccountIamBinding_basic(account), - Check: testAccCheckGoogleServiceAccountIam(account, "roles/viewer", []string{ - fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)), - }), + Check: testAccCheckGoogleServiceAccountIam(account, 1), }, { ResourceName: "google_service_account_iam_binding.foo", - ImportStateId: fmt.Sprintf("%s %s", serviceAccountCanonicalId(account), "roles/viewer"), ImportState: true, ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser"), + }, + }, + }) +} + +func TestAccServiceAccountIamBinding_withCondition(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + conditionExpr := `request.time < timestamp(\"2020-01-01T00:00:00Z\")` + conditionTitle := "expires_after_2019_12_31" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamBinding_withCondition(account, "user:admin@hashicorptest.com", conditionTitle, conditionExpr), + Check: testAccCheckGoogleServiceAccountIam(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 := acctest.RandomWithPrefix("tf-test") + conditionExpr := `request.time < timestamp(\"2020-01-01T00:00:00Z\")` + conditionTitle := "expires_after_2019_12_31" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamBinding_withAndWithoutCondition(account, "user:admin@hashicorptest.com", conditionTitle, conditionExpr), + Check: testAccCheckGoogleServiceAccountIam(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), }, }, }) @@ -48,11 +100,67 @@ func TestAccServiceAccountIamMember(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccServiceAccountIamMember_basic(account), - Check: testAccCheckGoogleServiceAccountIam(account, "roles/editor", []string{identity}), + Check: testAccCheckGoogleServiceAccountIam(account, 1), + }, + { + ResourceName: "google_service_account_iam_member.foo", + ImportStateId: fmt.Sprintf("%s %s %s", serviceAccountCanonicalId(account), "roles/iam.serviceAccountUser", identity), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccServiceAccountIamMember_withCondition(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + identity := fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)) + conditionTitle := "expires_after_2019_12_31" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamMember_withCondition(account, conditionTitle), + Check: testAccCheckGoogleServiceAccountIam(account, 1), }, { ResourceName: "google_service_account_iam_member.foo", - ImportStateId: fmt.Sprintf("%s %s %s", serviceAccountCanonicalId(account), "roles/editor", identity), + 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 := acctest.RandomWithPrefix("tf-test") + identity := fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)) + conditionTitle := "expires_after_2019_12_31" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamMember_withAndWithoutCondition(account, conditionTitle), + Check: testAccCheckGoogleServiceAccountIam(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, }, @@ -71,9 +179,6 @@ func TestAccServiceAccountIamPolicy(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccServiceAccountIamPolicy_basic(account), - Check: testAccCheckGoogleServiceAccountIam(account, "roles/owner", []string{ - fmt.Sprintf("serviceAccount:%s", serviceAccountCanonicalEmail(account)), - }), }, { ResourceName: "google_service_account_iam_policy.foo", @@ -85,28 +190,43 @@ func TestAccServiceAccountIamPolicy(t *testing.T) { }) } -func testAccCheckGoogleServiceAccountIam(account, role string, members []string) resource.TestCheckFunc { +func TestAccServiceAccountIamPolicy_withCondition(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(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(account string, numBindings int) resource.TestCheckFunc { return func(s *terraform.State) error { config := testAccProvider.Meta().(*Config) - p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(serviceAccountCanonicalId(account)).Do() + p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(serviceAccountCanonicalId(account)).OptionsRequestedPolicyVersion(iamPolicyVersion).Do() if err != nil { return err } - for _, binding := range p.Bindings { - if binding.Role == role { - sort.Strings(members) - sort.Strings(binding.Members) - - if reflect.DeepEqual(members, binding.Members) { - return nil - } - - return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members) - } + if len(p.Bindings) != numBindings { + return fmt.Errorf("Expected exactly %d binding(s) for account %q, was %d", numBindings, account, len(p.Bindings)) } - return fmt.Errorf("No binding for role %q", role) + return nil } } @@ -127,12 +247,58 @@ resource "google_service_account" "test_account" { resource "google_service_account_iam_binding" "foo" { service_account_id = "${google_service_account.test_account.name}" - role = "roles/viewer" - members = ["serviceAccount:${google_service_account.test_account.email}"] + role = "roles/iam.serviceAccountUser" + members = ["user:admin@hashicorptest.com"] } `, 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" { @@ -142,12 +308,58 @@ resource "google_service_account" "test_account" { resource "google_service_account_iam_member" "foo" { service_account_id = "${google_service_account.test_account.name}" - role = "roles/editor" + role = "roles/iam.serviceAccountUser" member = "serviceAccount:${google_service_account.test_account.email}" } `, 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" { @@ -156,11 +368,38 @@ resource "google_service_account" "test_account" { } data "google_iam_policy" "foo" { - binding { - role = "roles/owner" + binding { + role = "roles/iam.serviceAccountUser" - members = ["serviceAccount:${google_service_account.test_account.email}"] - } + members = ["serviceAccount:${google_service_account.test_account.email}"] + } +} + +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) +} + +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" { diff --git a/google-beta/resource_iam_binding.go b/google-beta/resource_iam_binding.go index 3c10db54c9..b6e605536e 100644 --- a/google-beta/resource_iam_binding.go +++ b/google-beta/resource_iam_binding.go @@ -27,6 +27,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, @@ -46,7 +71,7 @@ func ResourceIamBindingWithBatching(parentSpecificSchema map[string]*schema.Sche Delete: resourceIamBindingDelete(newUpdaterFunc, enableBatching), Schema: mergeSchemas(iamBindingSchema, parentSpecificSchema), Importer: &schema.ResourceImporter{ - State: iamBindingImport(resourceIdParser), + State: iamBindingImport(newUpdaterFunc, resourceIdParser), }, } } @@ -61,8 +86,9 @@ func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc, en binding := getResourceIamBinding(d) modifyF := func(ep *cloudresourcemanager.Policy) error { - cleaned := removeAllBindingsWithRole(ep.Bindings, binding.Role) + cleaned := filterBindingsWithRoleAndCondition(ep.Bindings, binding.Role, binding.Condition) ep.Bindings = append(cleaned, binding) + ep.Version = iamPolicyVersion return nil } @@ -75,7 +101,11 @@ func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc, en if err != nil { return err } + d.SetId(updater.GetResourceId() + "/" + binding.Role) + if k := conditionKeyFromCondition(binding.Condition); !k.Empty() { + d.SetId(d.Id() + "/" + k.String()) + } return resourceIamBindingRead(newUpdaterFunc)(d, meta) } } @@ -89,46 +119,57 @@ func resourceIamBindingRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Rea } eBinding := getResourceIamBinding(d) + eCondition := conditionKeyFromCondition(eBinding.Condition) p, err := iamPolicyReadWithRetry(updater) if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("Resource %q with IAM Binding (Role %q)", updater.DescribeResource(), eBinding.Role)) } - log.Printf("[DEBUG]: Retrieved policy for %s: %+v", updater.DescribeResource(), p) + log.Printf("[DEBUG] Retrieved policy for %s: %+v", updater.DescribeResource(), p) + log.Printf("[DEBUG] Looking for binding with role %q and condition %+v", eBinding.Role, eCondition) var binding *cloudresourcemanager.Binding for _, b := range p.Bindings { - if b.Role != eBinding.Role { - continue + if b.Role == eBinding.Role && conditionKeyFromCondition(b.Condition) == eCondition { + binding = b + break } - binding = b - break } + if binding == nil { - log.Printf("[DEBUG]: Binding for role %q not found in policy for %s, assuming it has no members.", eBinding.Role, updater.DescribeResource()) + log.Printf("[DEBUG] Binding for role %q and condition %+v not found in policy for %s, assuming it has no members.", eBinding.Role, eCondition, updater.DescribeResource()) d.Set("role", eBinding.Role) d.Set("members", nil) return nil } else { d.Set("role", binding.Role) d.Set("members", binding.Members) + d.Set("condition", flattenIamCondition(binding.Condition)) } d.Set("etag", p.Etag) return nil } } -func iamBindingImport(resourceIdParser resourceIdParserFunc) schema.StateFunc { +func iamBindingImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser resourceIdParserFunc) schema.StateFunc { return func(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { if resourceIdParser == nil { return nil, errors.New("Import not supported for this IAM resource.") } config := m.(*Config) s := strings.Fields(d.Id()) - if len(s) != 2 { + var id, role string + 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) @@ -141,6 +182,38 @@ func iamBindingImport(resourceIdParser resourceIdParserFunc) schema.StateFunc { // Set the ID again so that the ID matches the ID it would have if it had been created via TF. // 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 { + return nil, fmt.Errorf("Cannot find binding for %q with role %q and condition title %q", updater.DescribeResource(), role, conditionTitle) + } + + 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 @@ -165,7 +238,7 @@ func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc, enableBa binding := getResourceIamBinding(d) modifyF := func(p *cloudresourcemanager.Policy) error { - p.Bindings = removeAllBindingsWithRole(p.Bindings, binding.Role) + p.Bindings = filterBindingsWithRoleAndCondition(p.Bindings, binding.Role, binding.Condition) return nil } @@ -185,8 +258,39 @@ func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc, enableBa func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding { members := d.Get("members").(*schema.Set).List() - return &cloudresourcemanager.Binding{ + b := &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-beta/resource_iam_member.go b/google-beta/resource_iam_member.go index ae3f1ed472..31b3f19a0a 100644 --- a/google-beta/resource_iam_member.go +++ b/google-beta/resource_iam_member.go @@ -22,29 +22,63 @@ var IamMemberBaseSchema = map[string]*schema.Schema{ ForceNew: true, DiffSuppressFunc: caseDiffSuppress, }, + "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, }, } -func iamMemberImport(resourceIdParser resourceIdParserFunc) schema.StateFunc { +func iamMemberImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser resourceIdParserFunc) schema.StateFunc { return func(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { if resourceIdParser == nil { return nil, errors.New("Import not supported for this IAM resource.") } config := m.(*Config) s := strings.Fields(d.Id()) - if len(s) != 3 { + var id, role, member string + 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) d.Set("role", role) d.Set("member", strings.ToLower(member)) + err := resourceIdParser(d, config) if err != nil { return nil, err @@ -53,6 +87,44 @@ func iamMemberImport(resourceIdParser resourceIdParserFunc) schema.StateFunc { // Set the ID again so that the ID matches the ID it would have if it had been created via TF. // 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 } } @@ -68,16 +140,20 @@ func ResourceIamMemberWithBatching(parentSpecificSchema map[string]*schema.Schem Delete: resourceIamMemberDelete(newUpdaterFunc, enableBatching), Schema: mergeSchemas(IamMemberBaseSchema, parentSpecificSchema), Importer: &schema.ResourceImporter{ - State: iamMemberImport(resourceIdParser), + State: iamMemberImport(newUpdaterFunc, resourceIdParser), }, } } func getResourceIamMember(d *schema.ResourceData) *cloudresourcemanager.Binding { - return &cloudresourcemanager.Binding{ + b := &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 } func resourceIamMemberCreate(newUpdaterFunc newResourceIamUpdaterFunc, enableBatching bool) schema.CreateFunc { @@ -92,6 +168,7 @@ func resourceIamMemberCreate(newUpdaterFunc newResourceIamUpdaterFunc, enableBat modifyF := func(ep *cloudresourcemanager.Policy) error { // Merge the bindings together ep.Bindings = mergeBindings(append(ep.Bindings, memberBind)) + ep.Version = iamPolicyVersion return nil } if enableBatching { @@ -104,6 +181,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) } } @@ -117,39 +197,46 @@ func resourceIamMemberRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Read } eMember := getResourceIamMember(d) + eCondition := conditionKeyFromCondition(eMember.Condition) p, err := iamPolicyReadWithRetry(updater) if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("Resource %q with IAM Member: Role %q Member %q", updater.DescribeResource(), eMember.Role, eMember.Members[0])) } log.Printf("[DEBUG]: Retrieved policy for %s: %+v\n", updater.DescribeResource(), p) + log.Printf("[DEBUG]: Looking for binding with role %q and condition %+v", eMember.Role, eCondition) var binding *cloudresourcemanager.Binding for _, b := range p.Bindings { - if b.Role != eMember.Role { - continue + if b.Role == eMember.Role && conditionKeyFromCondition(b.Condition) == eCondition { + binding = b + break } - binding = b - break } + if binding == nil { - log.Printf("[DEBUG]: Binding for role %q does not exist in policy of %s, removing member %q from state.", eMember.Role, updater.DescribeResource(), eMember.Members[0]) + log.Printf("[DEBUG]: Binding for role %q with condition %+v does not exist in policy of %s, removing member %q from state.", eMember.Role, eCondition, updater.DescribeResource(), eMember.Members[0]) d.SetId("") return nil } + + log.Printf("[DEBUG]: Looking for member %q in found binding", eMember.Members[0]) var member string for _, m := range binding.Members { if strings.ToLower(m) == strings.ToLower(eMember.Members[0]) { member = m } } + if member == "" { - log.Printf("[DEBUG]: Member %q for binding for role %q does not exist in policy of %s, removing from state.", eMember.Members[0], eMember.Role, updater.DescribeResource()) + log.Printf("[DEBUG]: Member %q for binding for role %q with condition %+v does not exist in policy of %s, removing from state.", eMember.Members[0], eMember.Role, eCondition, updater.DescribeResource()) d.SetId("") return nil } + 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/google-beta/resource_iam_policy.go b/google-beta/resource_iam_policy.go index 5d11da3aae..a07eeced2d 100644 --- a/google-beta/resource_iam_policy.go +++ b/google-beta/resource_iam_policy.go @@ -129,6 +129,7 @@ func setIamPolicyData(d *schema.ResourceData, updater ResourceIamUpdater) error if err != nil { return fmt.Errorf("'policy_data' is not valid for %s: %s", updater.DescribeResource(), err) } + policy.Version = iamPolicyVersion err = updater.SetResourceIamPolicy(policy) if err != nil { diff --git a/website/docs/r/google_service_account_iam.html.markdown b/website/docs/r/google_service_account_iam.html.markdown index 033532c2db..16f26505bc 100644 --- a/website/docs/r/google_service_account_iam.html.markdown +++ b/website/docs/r/google_service_account_iam.html.markdown @@ -63,6 +63,30 @@ resource "google_service_account_iam_binding" "admin-account-iam" { } ``` +With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): + +```hcl +resource "google_service_account" "sa" { + account_id = "my-service-account" + display_name = "A service account that only Jane can use" +} + +resource "google_service_account_iam_binding" "admin-account-iam" { + service_account_id = "${google_service_account.sa.name}" + role = "roles/iam.serviceAccountUser" + + members = [ + "user:jane@example.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\")" + } +} +``` + ## google\_service\_account\_iam\_member ```hcl @@ -87,6 +111,27 @@ resource "google_service_account_iam_member" "gce-default-account-iam" { } ``` +With IAM Conditions ([beta](https://terraform.io/docs/providers/google/provider_versions.html)): + +```hcl +resource "google_service_account" "sa" { + account_id = "my-service-account" + display_name = "A service account that Jane can use" +} + +resource "google_service_account_iam_member" "admin-account-iam" { + service_account_id = "${google_service_account.sa.name}" + role = "roles/iam.serviceAccountUser" + member = "user:jane@example.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\")" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -109,6 +154,21 @@ 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. + 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. + +~> **Warning:** Terraform considers the `role` and condition contents (`title`+`description`+`expression`) as the + identifier for the binding. This means that if any part of the condition is changed out-of-band, Terraform will + consider it to be an entirely different resource and will treat it as such. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are @@ -118,12 +178,18 @@ exported: ## Import -Service account IAM resources can be imported using the project, service account email, role and member identity. +Service account IAM resources can be imported using the project, service account email, role, member identity, and condition (beta). ``` $ terraform import google_service_account_iam_policy.admin-account-iam projects/{your-project-id}/serviceAccounts/{your-service-account-email} -$ terraform import google_service_account_iam_binding.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} roles/editor" +$ terraform import google_service_account_iam_binding.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} iam.serviceAccountUser" + +$ terraform import google_service_account_iam_member.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} iam.serviceAccountUser user:foo@example.com" +``` + +With conditions: +``` +$ terraform import -provider=google-beta google_service_account_iam_binding.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} iam.serviceAccountUser expires_after_2019_12_31" -$ terraform import google_service_account_iam_member.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} roles/editor user:foo@example.com" -``` \ No newline at end of file +$ terraform import -provider=google-beta google_service_account_iam_member.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} iam.serviceAccountUser user:foo@example.com expires_after_2019_12_31" \ No newline at end of file