diff --git a/google/iam_storage_bucket.go b/google/iam_storage_bucket.go new file mode 100644 index 00000000..524d35e9 --- /dev/null +++ b/google/iam_storage_bucket.go @@ -0,0 +1,84 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/storage/v1" +) + +var IamStorageBucketSchema = map[string]*schema.Schema{ + "bucket": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, +} + +type StorageBucketIamUpdater struct { + bucket string + Config *Config +} + +func NewStorageBucketIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) { + bucket := d.Get("bucket").(string) + + return &StorageBucketIamUpdater{ + bucket: bucket, + Config: config, + }, nil +} + +func (u *StorageBucketIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + p, err := u.Config.clientStorage.Buckets.GetIamPolicy(u.bucket).Do() + if err != nil { + return nil, fmt.Errorf("Error retrieving IAM policy for %s: %s", u.DescribeResource(), err) + } + + cloudResourcePolicy, err := storageToResourceManagerPolicy(p) + if err != nil { + return nil, fmt.Errorf("Invalid IAM policy for %s: %s", u.DescribeResource(), err) + } + + return cloudResourcePolicy, nil +} + +func (u *StorageBucketIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + storagePolicy, err := resourceManagerToStoragePolicy(policy) + + if err != nil { + return fmt.Errorf("Invalid IAM policy for %s: %s", u.DescribeResource(), err) + } + + _, err = u.Config.clientStorage.Buckets.SetIamPolicy(u.bucket, storagePolicy).Do() + + if err != nil { + return fmt.Errorf("Error setting IAM policy for %s: %s", u.DescribeResource(), err) + } + + return nil +} + +func (u *StorageBucketIamUpdater) GetResourceId() string { + return u.bucket +} + +func (u *StorageBucketIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-storage-bucket-%s", u.bucket) +} + +func (u *StorageBucketIamUpdater) DescribeResource() string { + return fmt.Sprintf("Storage Bucket %q", u.bucket) +} + +func resourceManagerToStoragePolicy(p *cloudresourcemanager.Policy) (policy *storage.Policy, err error) { + policy = &storage.Policy{} + err = Convert(p, policy) + return +} + +func storageToResourceManagerPolicy(p *storage.Policy) (policy *cloudresourcemanager.Policy, err error) { + policy = &cloudresourcemanager.Policy{} + err = Convert(p, policy) + return +} diff --git a/google/provider.go b/google/provider.go index a578ac35..0af0ff5a 100644 --- a/google/provider.go +++ b/google/provider.go @@ -164,8 +164,13 @@ func Provider() terraform.ResourceProvider { "google_service_account_key": resourceGoogleServiceAccountKey(), "google_storage_bucket": resourceStorageBucket(), "google_storage_bucket_acl": resourceStorageBucketAcl(), - "google_storage_bucket_object": resourceStorageBucketObject(), - "google_storage_object_acl": resourceStorageObjectAcl(), + // Legacy roles such as roles/storage.legacyBucketReader are automatically added + // when creating a bucket. For this reason, it is better not to add the authoritative + // google_storage_bucket_iam_policy resource. + "google_storage_bucket_iam_binding": ResourceIamBinding(IamStorageBucketSchema, NewStorageBucketIamUpdater), + "google_storage_bucket_iam_member": ResourceIamMember(IamStorageBucketSchema, NewStorageBucketIamUpdater), + "google_storage_bucket_object": resourceStorageBucketObject(), + "google_storage_object_acl": resourceStorageObjectAcl(), }, ConfigureFunc: providerConfigure, diff --git a/google/resource_iam_member.go b/google/resource_iam_member.go index 3fe927a0..ea51e782 100644 --- a/google/resource_iam_member.go +++ b/google/resource_iam_member.go @@ -144,7 +144,13 @@ func resourceIamMemberDelete(newUpdaterFunc newResourceIamUpdaterFunc) schema.De return nil } binding.Members = append(binding.Members[:memberToRemove], binding.Members[memberToRemove+1:]...) - p.Bindings[bindingToRemove] = binding + if len(binding.Members) == 0 { + // If there is no member left for the role, remove the binding altogether + p.Bindings = append(p.Bindings[:bindingToRemove], p.Bindings[bindingToRemove+1:]...) + } else { + p.Bindings[bindingToRemove] = binding + } + return nil }) if err != nil { diff --git a/google/resource_storage_bucket_iam_test.go b/google/resource_storage_bucket_iam_test.go new file mode 100644 index 00000000..717abf87 --- /dev/null +++ b/google/resource_storage_bucket_iam_test.go @@ -0,0 +1,153 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "reflect" + "sort" + "testing" +) + +func TestAccGoogleStorageBucketIamBinding(t *testing.T) { + t.Parallel() + + bucket := acctest.RandomWithPrefix("tf-test") + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test IAM Binding creation + Config: testAccGoogleStorageBucketIamBinding_basic(bucket, account), + Check: testAccCheckGoogleStorageBucketIam(bucket, "roles/storage.objectViewer", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + // Test IAM Binding update + Config: testAccGoogleStorageBucketIamBinding_update(bucket, account), + Check: testAccCheckGoogleStorageBucketIam(bucket, "roles/storage.objectViewer", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + fmt.Sprintf("serviceAccount:%s-2@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + }, + }) +} + +func TestAccGoogleStorageBucketIamMember(t *testing.T) { + t.Parallel() + + bucket := acctest.RandomWithPrefix("tf-test") + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccGoogleStorageBucketIamMember_basic(bucket, account), + Check: testAccCheckGoogleStorageBucketIam(bucket, "roles/storage.admin", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + }, + }) +} + +func testAccCheckGoogleStorageBucketIam(bucket, role string, members []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + p, err := config.clientStorage.Buckets.GetIamPolicy(bucket).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) + } + } + + return fmt.Errorf("No binding for role %q", role) + } +} + +func testAccGoogleStorageBucketIamBinding_basic(bucket, account string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_storage_bucket_iam_binding" "foo" { + bucket = "${google_storage_bucket.bucket.name}" + role = "roles/storage.objectViewer" + members = [ + "serviceAccount:${google_service_account.test-account-1.email}", + ] +} +`, bucket, account) +} + +func testAccGoogleStorageBucketIamBinding_update(bucket, account string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_service_account" "test-account-2" { + account_id = "%s-2" + display_name = "Iam Testing Account" +} + +resource "google_storage_bucket_iam_binding" "foo" { + bucket = "${google_storage_bucket.bucket.name}" + role = "roles/storage.objectViewer" + members = [ + "serviceAccount:${google_service_account.test-account-1.email}", + "serviceAccount:${google_service_account.test-account-2.email}", + ] +} +`, bucket, account, account) +} + +func testAccGoogleStorageBucketIamMember_basic(bucket, account string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_storage_bucket_iam_member" "foo" { + bucket = "${google_storage_bucket.bucket.name}" + role = "roles/storage.admin" + member = "serviceAccount:${google_service_account.test-account-1.email}" +} +`, bucket, account) +} diff --git a/website/docs/r/storage_bucket_iam.html.markdown b/website/docs/r/storage_bucket_iam.html.markdown new file mode 100644 index 00000000..46ee36be --- /dev/null +++ b/website/docs/r/storage_bucket_iam.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "google" +page_title: "Google: google_storage_bucket_iam" +sidebar_current: "docs-google-storage-bucket-iam" +description: |- + Collection of resources to manage IAM policy for a Google storage bucket. +--- + +# IAM policy for Google storage bucket + +Two different resources help you manage your IAM policy for storage bucket. Each of these resources serves a different use case: + +* `google_storage_bucket_iam_binding`: Authoritative for a given role. Updates the IAM policy to grant a role to a list of members. Other roles within the IAM policy for the storage bucket are preserved. +* `google_storage_bucket_iam_member`: Non-authoritative. Updates the IAM policy to grant a role to a new member. Other members for the role for the storage bucket are preserved. + +~> **Note:** `google_storage_bucket_iam_binding` resources **can be** used in conjunction with `google_storage_bucket_iam_member` resources **only if** they do not grant privilege to the same role. + +## google\_storage\_bucket\_iam\_binding + +```hcl +resource "google_storage_bucket_iam_binding" "binding" { + bucket = "your-bucket-name" + role = "roles/storage.objectViewer" + + members = [ + "user:jane@example.com", + ] +} +``` + +## google\_storage\_bucket\_iam\_member + +```hcl +resource "google_storage_bucket_iam_member" "member" { + bucket = "your-bucket-name" + role = "roles/storage.objectViewer" + member = "user:jane@example.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket it applies to. + +* `member/members` - (Required) Identities that will be granted the privilege in `role`. + 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. + * **allAuthenticatedUsers**: A special identifier that represents anyone who is authenticated with a Google account or a service account. + * **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. + * **group:{emailid}**: An email address that represents a Google group. For example, admins@example.com. + * **domain:{domain}**: A Google Apps domain name that represents all the users of that domain. For example, google.com or example.com. + +* `role` - (Required) The role that should be applied. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `etag` - (Computed) The etag of the storage bucket's IAM policy. \ No newline at end of file diff --git a/website/google.erb b/website/google.erb index d4281ba6..88ef9aef 100644 --- a/website/google.erb +++ b/website/google.erb @@ -434,6 +434,14 @@ google_storage_bucket_acl + > + google_storage_bucket_iam_binding + + + > + google_storage_bucket_iam_member + + > google_storage_bucket_object @@ -451,7 +459,13 @@ google_kms_key_ring > - google_kms_key_ring_iam_* + google_kms_key_ring_iam_binding + + > + google_kms_key_ring_iam_member + + > + google_kms_key_ring_iam_policy > google_kms_crypto_key