diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py index 69914c5b937a..4ef0aa040899 100644 --- a/gcloud/storage/acl.py +++ b/gcloud/storage/acl.py @@ -166,6 +166,21 @@ class ACL(object): """Container class representing a list of access controls.""" _URL_PATH_ELEM = 'acl' + _PREDEFINED_QUERY_PARAM = 'predefinedAcl' + + _PREDEFINED_ACLS = frozenset([ + 'private', + 'project-private', + 'public-read', + 'public-read-write', + 'authenticated-read', + 'bucket-owner-read', + 'bucket-owner-full-control', + ]) + """See: + https://cloud.google.com/storage/docs/access-control#predefined-acl + """ + loaded = False # Subclasses must override to provide these attributes (typically, @@ -385,6 +400,39 @@ def reload(self, client=None): for entry in found.get('items', ()): self.add_entity(self.entity_from_dict(entry)) + def _save(self, acl, predefined, client): + """Helper for :meth:`save` and :meth:`save_predefined`. + + :type acl: :class:`gcloud.storage.acl.ACL`, or a compatible list. + :param acl: The ACL object to save. If left blank, this will save + current entries. + + :type predefined: string or None + :param predefined: An identifier for a predefined ACL. Must be one + of the keys in :attr:`_PREDEFINED_ACLS` + 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. + """ + query_params = {'projection': 'full'} + if predefined is not None: + acl = [] + query_params[self._PREDEFINED_QUERY_PARAM] = predefined + + path = self.save_path + client = self._require_client(client) + result = client.connection.api_request( + method='PATCH', + path=path, + data={self._URL_PATH_ELEM: list(acl)}, + query_params=query_params) + self.entities.clear() + for entry in result.get(self._URL_PATH_ELEM, ()): + self.add_entity(self.entity_from_dict(entry)) + self.loaded = True + def save(self, acl=None, client=None): """Save this ACL for the current bucket. @@ -403,17 +451,24 @@ def save(self, acl=None, client=None): save_to_backend = True if save_to_backend: - path = self.save_path - client = self._require_client(client) - result = client.connection.api_request( - method='PATCH', - path=path, - data={self._URL_PATH_ELEM: list(acl)}, - query_params={'projection': 'full'}) - self.entities.clear() - for entry in result.get(self._URL_PATH_ELEM, ()): - self.add_entity(self.entity_from_dict(entry)) - self.loaded = True + self._save(acl, None, client) + + def save_predefined(self, predefined, client=None): + """Save this ACL for the current bucket using a predefined ACL. + + :type predefined: string + :param predefined: An identifier for a predefined ACL. Must be one + of the keys in :attr:`_PREDEFINED_ACLS` + 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: + raise ValueError("Invalid predefined ACL: %s" % (predefined,)) + + self._save(None, predefined, client) def clear(self, client=None): """Remove all ACL entries. @@ -461,6 +516,7 @@ class DefaultObjectACL(BucketACL): """A class representing the default object ACL for a bucket.""" _URL_PATH_ELEM = 'defaultObjectAcl' + _PREDEFINED_QUERY_PARAM = 'predefinedDefaultObjectAcl' class ObjectACL(ACL): diff --git a/gcloud/storage/test_acl.py b/gcloud/storage/test_acl.py index 08bf1aba8ea8..7ca42b6c1941 100644 --- a/gcloud/storage/test_acl.py +++ b/gcloud/storage/test_acl.py @@ -581,7 +581,7 @@ def test_save_existing_missing_none_passed(self): self.assertEqual(kw[0]['data'], {'acl': []}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) - def test_save_no_arg(self): + def test_save_no_acl(self): ROLE = 'role' AFTER = [{'entity': 'allUsers', 'role': ROLE}] connection = _Connection({'acl': AFTER}) @@ -599,7 +599,7 @@ def test_save_no_arg(self): self.assertEqual(kw[0]['data'], {'acl': AFTER}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) - def test_save_w_arg(self): + def test_save_w_acl(self): ROLE1 = 'role1' ROLE2 = 'role2' STICKY = {'entity': 'allUsers', 'role': ROLE2} @@ -621,6 +621,53 @@ def test_save_w_arg(self): self.assertEqual(kw[0]['data'], {'acl': new_acl}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_save_prefefined_invalid(self): + connection = _Connection() + client = _Client(connection) + acl = self._makeOne() + acl.save_path = '/testing' + acl.loaded = True + with self.assertRaises(ValueError): + acl.save_predefined('bogus', client=client) + + def test_save_predefined_valid(self): + PREDEFINED = 'private' + connection = _Connection({'acl': []}) + client = _Client(connection) + acl = self._makeOne() + acl.save_path = '/testing' + acl.loaded = True + acl.save_predefined(PREDEFINED, 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}) + + def test_save_predefined_valid_w_alternate_query_param(self): + # Cover case where subclass overrides _PREDEFINED_QUERY_PARAM + PREDEFINED = 'private' + connection = _Connection({'acl': []}) + client = _Client(connection) + acl = self._makeOne() + acl.save_path = '/testing' + acl.loaded = True + acl._PREDEFINED_QUERY_PARAM = 'alternate' + acl.save_predefined(PREDEFINED, 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', 'alternate': PREDEFINED}) + def test_clear(self): ROLE1 = 'role1' ROLE2 = 'role2'