Repository: googleapis/google-cloud-python

Issue:
blob.acl.save_predefined() only works for private
[From the documentation.](https://googlecloudplatform.github.io/gcloud-python/stable/storage-acl.html#gcloud.storage.acl.ACL.save_predefined)

> Must be one of the keys in [_PREDEFINED_ACLS](https://github.com/tseaver/gcloud-python/blob/1a210341811aff3b4185f830b07e87b22c1ef73d/gcloud/storage/acl.py#L171)

``` Python
    _PREDEFINED_ACLS = frozenset([
        "private",
        "project-private",
        "public-read",
        "public-read-write",
        "authenticated-read",
        "bucket-owner-read",
        "bucket-owner-full-control",
    ])
```

Let's try.

``` Python
>>> blob.acl.save_predefined('public-read')
...
gcloud.exceptions.BadRequest: 400 Invalid string value: 'public-read'. Allowed values: [authenticatedread, bucketownerfullcontrol, bucketownerread, private, projectprivate, publicread]
>>> blob.acl.save_predefined('publicread')
...
ValueError: Invalid predefined ACL: publicread
```

Coincidentally, the [tests](https://github.com/tseaver/gcloud-python/blob/1a210341811aff3b4185f830b07e87b22c1ef73d/gcloud/storage/test_acl.py#L633) only try _private_.



Discussion:
I have confirmed that editing `_PREDEFINED_ACLS` to use the suggested values makes it work. The bug is that it uses the XML API names rather than the JSON API names. I think both should be valid in `gcloud-python`, as `gsutil` uses the former.

PS. `public-read-write` only applies to buckets, so isn't listed in the allowed values.

Thanks for investigating!  Can you confirm my reading:  _we_ have the bug, because the values enumerated in `gcloud.storage.acl` are the XML API spellings, which aren't valid for use against the JSON API?

Begging the question, of course, of why the two APIs use different spelings for those constants. :(

Yes, that's the bug. I think both should be valid in `gcloud-python` though, as the XML API names are more commonly mentioned in docs and otherwise. And `gsutil` uses those names too, as mentioned. They could be easily mapped to each other.

Can you point me to the docs for the JSON predefined ACL constants?  The [ones I worked from](https://cloud.google.com/storage/docs/access-control#predefined-acl) weren't obviously tied to the XML API.

Oh, never mind:  I see that the first column is the JSON spellings.


Resolving patch:
diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py
--- a/gcloud/storage/acl.py
+++ b/gcloud/storage/acl.py
@@ -168,14 +168,24 @@ class ACL(object):
     _URL_PATH_ELEM = 'acl'
     _PREDEFINED_QUERY_PARAM = 'predefinedAcl'
 
-    _PREDEFINED_ACLS = frozenset([
+    PREDEFINED_XML_ACLS = {
+        # XML API name -> JSON API name
+        'project-private': 'projectPrivate',
+        'public-read': 'publicRead',
+        'public-read-write': 'publicReadWrite',
+        'authenticated-read': 'authenticatedRead',
+        'bucket-owner-read': 'bucketOwnerRead',
+        'bucket-owner-full-control': 'bucketOwnerFullControl',
+    }
+
+    PREDEFINED_JSON_ACLS = frozenset([
         'private',
-        'project-private',
-        'public-read',
-        'public-read-write',
-        'authenticated-read',
-        'bucket-owner-read',
-        'bucket-owner-full-control',
+        'projectPrivate',
+        'publicRead',
+        'publicReadWrite',
+        'authenticatedRead',
+        'bucketOwnerRead',
+        'bucketOwnerFullControl',
     ])
     """See:
     https://cloud.google.com/storage/docs/access-control#predefined-acl
@@ -409,7 +419,7 @@ def _save(self, acl, predefined, client):
 
         :type predefined: string or None
         :param predefined: An identifier for a predefined ACL.  Must be one
-                           of the keys in :attr:`_PREDEFINED_ACLS`
+                           of the keys in :attr:`PREDEFINED_JSON_ACLS`
                            If passed, `acl` must be None.
 
         :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
@@ -458,14 +468,18 @@ def save_predefined(self, predefined, client=None):
 
         :type predefined: string
         :param predefined: An identifier for a predefined ACL.  Must be one
-                           of the keys in :attr:`_PREDEFINED_ACLS`
+                           of the keys in :attr:`PREDEFINED_JSON_ACLS`
+                           or :attr:`PREDEFINED_XML_ACLS` (which will be
+                           aliased to the corresponding JSON name).
                            If passed, `acl` must be None.
 
         :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
         :param client: Optional. The client to use.  If not passed, falls back
                        to the ``client`` stored on the ACL's parent.
         """
-        if predefined not in self._PREDEFINED_ACLS:
+        predefined = self.PREDEFINED_XML_ACLS.get(predefined, predefined)
+
+        if predefined not in self.PREDEFINED_JSON_ACLS:
             raise ValueError("Invalid predefined ACL: %s" % (predefined,))
 
         self._save(None, predefined, client)


Test patch:
diff --git a/gcloud/storage/test_acl.py b/gcloud/storage/test_acl.py
--- a/gcloud/storage/test_acl.py
+++ b/gcloud/storage/test_acl.py
@@ -648,9 +648,29 @@ def test_save_predefined_valid(self):
         self.assertEqual(kw[0]['query_params'],
                          {'projection': 'full', 'predefinedAcl': PREDEFINED})
 
+    def test_save_predefined_w_XML_alias(self):
+        PREDEFINED_XML = 'project-private'
+        PREDEFINED_JSON = 'projectPrivate'
+        connection = _Connection({'acl': []})
+        client = _Client(connection)
+        acl = self._makeOne()
+        acl.save_path = '/testing'
+        acl.loaded = True
+        acl.save_predefined(PREDEFINED_XML, client=client)
+        entries = list(acl)
+        self.assertEqual(len(entries), 0)
+        kw = connection._requested
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'PATCH')
+        self.assertEqual(kw[0]['path'], '/testing')
+        self.assertEqual(kw[0]['data'], {'acl': []})
+        self.assertEqual(kw[0]['query_params'],
+                         {'projection': 'full',
+                          'predefinedAcl': PREDEFINED_JSON})
+
     def test_save_predefined_valid_w_alternate_query_param(self):
         # Cover case where subclass overrides _PREDEFINED_QUERY_PARAM
-        PREDEFINED = 'private'
+        PREDEFINED = 'publicRead'
         connection = _Connection({'acl': []})
         client = _Client(connection)
         acl = self._makeOne()