Repository: googleapis/google-cloud-python

Issue:
Storage: pass predefinedDefaultObjectAcl on bucket creation
 **Is your feature request related to a problem? Please describe.**
In the Bucket creation API it's allowed to pass a query parameter "predefinedDefaultObjectAcl" to set the default object ACL in the creation call of the bucket. In the current implementation of this library it's not possible to set this query parameter as it is hardcoded to only the (required) project.

https://github.com/googleapis/google-cloud-python/blob/master/storage/google/cloud/storage/bucket.py#L637

 **Describe the solution you'd like**
Expose the optional query parameters in this library.

 **Describe alternatives you've considered**
Updating the predefinedDefaultObjectAcl after bucket creation requires additional permissions on the bucket that should not be needed when setting this value directly on bucket creation.

 **Additional context**
API Reference for bucket/insert
https://cloud.google.com/storage/docs/json_api/v1/buckets/insert


Discussion:
@thoutenbos 
> Updating the predefinedDefaultObjectAcl after bucket creation requires additional permissions on the bucket that should not be needed when setting this value directly on bucket creation.

Can you please clarify?  I'm unaware of any case where creating a bucket would leave the creating owner without the `storage.buckets.update` permission required to update the default object ACL.
FWIW, if we do support passing the `predeinfedDefaultObjectAcl` query parameter, we should also support passing `predefinedAcl`.
Using a custom IAM role for a service account granting only the `storage.buckets.create` permission to create regular, time based, new buckets. If all needed bucket settings can be passed in the creation call then no additional permissions are needed for this service account. In the current implementation of this library the service account would need additional permissions to allow update to this (and other) buckets after creation.

The extra permissions needed to update this setting after bucket creation are:
- `storage.buckets.update`
- `storage.buckets.getIamPolicy`
- `storage.buckets.setIamPolicy`

From the least privilege principles granting these additional permissions is undesired when it can be avoided.

Resolving patch:
diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py
--- a/storage/google/cloud/storage/bucket.py
+++ b/storage/google/cloud/storage/bucket.py
@@ -595,7 +595,14 @@ def exists(self, client=None):
         except NotFound:
             return False
 
-    def create(self, client=None, project=None, location=None):
+    def create(
+        self,
+        client=None,
+        project=None,
+        location=None,
+        predefined_acl=None,
+        predefined_default_object_acl=None,
+    ):
         """Creates current bucket.
 
         If the bucket already exists, will raise
@@ -622,6 +629,16 @@ def create(self, client=None, project=None, location=None):
         :param location: Optional. The location of the bucket. If not passed,
                          the default location, US, will be used. See
                          https://cloud.google.com/storage/docs/bucket-locations
+
+        :type predefined_acl: str
+        :param predefined_acl:
+            Optional. Name of predefined ACL to apply to bucket. See:
+            https://cloud.google.com/storage/docs/access-control/lists#predefined-acl
+
+        :type predefined_default_object_acl: str
+        :param predefined_default_object_acl:
+            Optional. Name of predefined ACL to apply to bucket's objects. See:
+            https://cloud.google.com/storage/docs/access-control/lists#predefined-acl
         """
         if self.user_project is not None:
             raise ValueError("Cannot create bucket with 'user_project' set.")
@@ -635,6 +652,17 @@ def create(self, client=None, project=None, location=None):
             raise ValueError("Client project not set:  pass an explicit project.")
 
         query_params = {"project": project}
+
+        if predefined_acl is not None:
+            predefined_acl = BucketACL.validate_predefined(predefined_acl)
+            query_params["predefinedAcl"] = predefined_acl
+
+        if predefined_default_object_acl is not None:
+            predefined_default_object_acl = DefaultObjectACL.validate_predefined(
+                predefined_default_object_acl
+            )
+            query_params["predefinedDefaultObjectAcl"] = predefined_default_object_acl
+
         properties = {key: self._properties[key] for key in self._changes}
         properties["name"] = self.name
 


Test patch:
diff --git a/storage/tests/unit/test_bucket.py b/storage/tests/unit/test_bucket.py
--- a/storage/tests/unit/test_bucket.py
+++ b/storage/tests/unit/test_bucket.py
@@ -646,6 +646,60 @@ def test_create_w_extra_properties(self):
         self.assertEqual(kw["query_params"], {"project": PROJECT})
         self.assertEqual(kw["data"], DATA)
 
+    def test_create_w_predefined_acl_invalid(self):
+        PROJECT = "PROJECT"
+        BUCKET_NAME = "bucket-name"
+        DATA = {"name": BUCKET_NAME}
+        connection = _Connection(DATA)
+        client = _Client(connection, project=PROJECT)
+        bucket = self._make_one(client=client, name=BUCKET_NAME)
+
+        with self.assertRaises(ValueError):
+            bucket.create(predefined_acl="bogus")
+
+    def test_create_w_predefined_acl_valid(self):
+        PROJECT = "PROJECT"
+        BUCKET_NAME = "bucket-name"
+        DATA = {"name": BUCKET_NAME}
+        connection = _Connection(DATA)
+        client = _Client(connection, project=PROJECT)
+        bucket = self._make_one(client=client, name=BUCKET_NAME)
+        bucket.create(predefined_acl="publicRead")
+
+        kw, = connection._requested
+        self.assertEqual(kw["method"], "POST")
+        self.assertEqual(kw["path"], "/b")
+        expected_qp = {"project": PROJECT, "predefinedAcl": "publicRead"}
+        self.assertEqual(kw["query_params"], expected_qp)
+        self.assertEqual(kw["data"], DATA)
+
+    def test_create_w_predefined_default_object_acl_invalid(self):
+        PROJECT = "PROJECT"
+        BUCKET_NAME = "bucket-name"
+        DATA = {"name": BUCKET_NAME}
+        connection = _Connection(DATA)
+        client = _Client(connection, project=PROJECT)
+        bucket = self._make_one(client=client, name=BUCKET_NAME)
+
+        with self.assertRaises(ValueError):
+            bucket.create(predefined_default_object_acl="bogus")
+
+    def test_create_w_predefined_default_object_acl_valid(self):
+        PROJECT = "PROJECT"
+        BUCKET_NAME = "bucket-name"
+        DATA = {"name": BUCKET_NAME}
+        connection = _Connection(DATA)
+        client = _Client(connection, project=PROJECT)
+        bucket = self._make_one(client=client, name=BUCKET_NAME)
+        bucket.create(predefined_default_object_acl="publicRead")
+
+        kw, = connection._requested
+        self.assertEqual(kw["method"], "POST")
+        self.assertEqual(kw["path"], "/b")
+        expected_qp = {"project": PROJECT, "predefinedDefaultObjectAcl": "publicRead"}
+        self.assertEqual(kw["query_params"], expected_qp)
+        self.assertEqual(kw["data"], DATA)
+
     def test_acl_property(self):
         from google.cloud.storage.acl import BucketACL