Repository: googleapis/google-cloud-python

Issue:
In storage.Key.get_acl, account for very long list of ACLs
From TODO:

``` python
    # TODO: This might be a VERY long list. Use the specific API endpoint.
```

See [`gcloud.storage.Key.get_acl`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/1f2aaf3606802f4f02eeb30e22e1082ef7f4594e/gcloud/storage/key.py#L388) for relevant code and #146 for some context.

In storage.Key.get_acl, account for very long list of ACLs
From TODO:

``` python
    # TODO: This might be a VERY long list. Use the specific API endpoint.
```

See [`gcloud.storage.Key.get_acl`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/1f2aaf3606802f4f02eeb30e22e1082ef7f4594e/gcloud/storage/key.py#L388) for relevant code and #146 for some context.



Discussion:
`git blame` output

```
b7f1b774 (JJ Geewax   2014-01-29 18:02:29 -0500 388)     # TODO: This might be a VERY long list. Use the specific API endpoint.
```

@jgeewax I'm guessing that this TODO was about using the ObjectAccessControls endpoints (https://cloud.google.com/storage/docs/json_api/v1/#ObjectAccessControls) to store and retrieve ACLs, rather than finding/saving them indirectly via Object endpoints for generic metadata (https://cloud.google.com/storage/docs/json_api/v1/#Objects).

I don't think we can efficiently use the ObjectAccessControls endpoints for updates:  we aren't tracking changes to the ACL to allow sending `delete`, `insert`, `patch`, or `update` requests for an individual ACE.  We should be able to fetch the whole ACL with the `list` API, though, which might be marginally more efficient than fetching all the metadata.  We are already storing the full ACL using the `patch` API of the Object endpoints.

`git blame` output

```
b7f1b774 (JJ Geewax   2014-01-29 18:02:29 -0500 388)     # TODO: This might be a VERY long list. Use the specific API endpoint.
```

@jgeewax I'm guessing that this TODO was about using the ObjectAccessControls endpoints (https://cloud.google.com/storage/docs/json_api/v1/#ObjectAccessControls) to store and retrieve ACLs, rather than finding/saving them indirectly via Object endpoints for generic metadata (https://cloud.google.com/storage/docs/json_api/v1/#Objects).

I don't think we can efficiently use the ObjectAccessControls endpoints for updates:  we aren't tracking changes to the ACL to allow sending `delete`, `insert`, `patch`, or `update` requests for an individual ACE.  We should be able to fetch the whole ACL with the `list` API, though, which might be marginally more efficient than fetching all the metadata.  We are already storing the full ACL using the `patch` API of the Object endpoints.


Resolving patch:
diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py
--- a/gcloud/storage/acl.py
+++ b/gcloud/storage/acl.py
@@ -167,9 +167,20 @@ def revoke_owner(self):
 class ACL(object):
     """Container class representing a list of access controls."""
 
+    loaded = False
+
     def __init__(self):
         self.entities = {}
 
+    def clear(self):
+        """Remove all entities from the ACL."""
+        self.entities.clear()
+
+    def reset(self):
+        """Remove all entities from the ACL, and clear the ``loaded`` flag."""
+        self.entities.clear()
+        self.loaded = False
+
     def __iter__(self):
         for entity in self.entities.itervalues():
             for role in entity.get_roles():
@@ -242,6 +253,7 @@ def add_entity(self, entity):
         :param entity: The entity to add to this ACL.
         """
         self.entities[str(entity)] = entity
+        self.loaded = True
 
     def entity(self, entity_type, identifier=None):
         """Factory method for creating an Entity.
diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py
--- a/gcloud/storage/bucket.py
+++ b/gcloud/storage/bucket.py
@@ -19,15 +19,27 @@ class Bucket(object):
     :type name: string
     :param name: The name of the bucket.
     """
+    # ACL rules are lazily retrieved.
+    _acl = _default_object_acl = None
 
     def __init__(self, connection=None, name=None, metadata=None):
         self.connection = connection
         self.name = name
         self.metadata = metadata
 
-        # ACL rules are lazily retrieved.
-        self.acl = None
-        self.default_object_acl = None
+    @property
+    def acl(self):
+        """Create our ACL on demand."""
+        if self._acl is None:
+            self._acl = BucketACL(self)
+        return self._acl
+
+    @property
+    def default_object_acl(self):
+        """Create our defaultObjectACL on demand."""
+        if self._default_object_acl is None:
+            self._default_object_acl = DefaultObjectACL(self)
+        return self._default_object_acl
 
     @classmethod
     def from_dict(cls, bucket_dict, connection=None):
@@ -313,17 +325,15 @@ def has_metadata(self, field=None):
         else:
             return True
 
-    def reload_metadata(self, full=False):
+    def reload_metadata(self):
         """Reload metadata from Cloud Storage.
 
-        :type full: bool
-        :param full: If True, loads all data (include ACL data).
-
         :rtype: :class:`Bucket`
         :returns: The bucket you just reloaded data for.
         """
-        projection = 'full' if full else 'noAcl'
-        query_params = {'projection': projection}
+        # Pass only '?projection=noAcl' here because 'acl'/'defaultObjectAcl'
+        # are handled via 'get_acl()'/'get_default_object_acl()'
+        query_params = {'projection': 'noAcl'}
         self.metadata = self.connection.api_request(
             method='GET', path=self.path, query_params=query_params)
         return self
@@ -344,9 +354,14 @@ def get_metadata(self, field=None, default=None):
         :rtype: dict or anything
         :returns: All metadata or the value of the specific field.
         """
+        if field == 'acl':
+            raise KeyError("Use 'get_acl()'")
+
+        if field == 'defaultObjectAcl':
+            raise KeyError("Use 'get_default_object_acl()'")
+
         if not self.has_metadata(field=field):
-            full = (field and field in ('acl', 'defaultObjectAcl'))
-            self.reload_metadata(full=full)
+            self.reload_metadata()
 
         if field:
             return self.metadata.get(field, default)
@@ -431,11 +446,15 @@ def reload_acl(self):
         :rtype: :class:`Bucket`
         :returns: The current bucket.
         """
-        self.acl = BucketACL(bucket=self)
+        self.acl.clear()
+
+        url_path = '%s/acl' % self.path
+        found = self.connection.api_request(method='GET', path=url_path)
+        for entry in found['items']:
+            self.acl.add_entity(self.acl.entity_from_dict(entry))
 
-        for entry in self.get_metadata('acl', []):
-            entity = self.acl.entity_from_dict(entry)
-            self.acl.add_entity(entity)
+        # Even if we fetch no entries, the ACL is still loaded.
+        self.acl.loaded = True
 
         return self
 
@@ -445,7 +464,7 @@ def get_acl(self):
         :rtype: :class:`gcloud.storage.acl.BucketACL`
         :returns: An ACL object for the current bucket.
         """
-        if not self.acl:
+        if not self.acl.loaded:
             self.reload_acl()
         return self.acl
 
@@ -487,12 +506,19 @@ def save_acl(self, acl=None):
         # both evaluate to False, but mean very different things.
         if acl is None:
             acl = self.acl
+            dirty = acl.loaded
+        else:
+            dirty = True
 
-        if acl is None:
-            return self
+        if dirty:
+            result = self.connection.api_request(
+                method='PATCH', path=self.path, data={'acl': list(acl)},
+                query_params={'projection': 'full'})
+            self.acl.clear()
+            for entry in result['acl']:
+                self.acl.entity(self.acl.entity_from_dict(entry))
+            self.acl.loaded = True
 
-        self.patch_metadata({'acl': list(acl)})
-        self.reload_acl()
         return self
 
     def clear_acl(self):
@@ -522,7 +548,9 @@ def clear_acl(self):
 
         At this point all the custom rules you created have been removed.
         """
-        return self.save_acl(acl=[])
+        # NOTE:  back-end makes some ACL entries sticky (they remain even
+        #        after the PATCH succeeds.
+        return self.save_acl([])
 
     def reload_default_object_acl(self):
         """Reload the Default Object ACL rules for this bucket.
@@ -530,11 +558,16 @@ def reload_default_object_acl(self):
         :rtype: :class:`Bucket`
         :returns: The current bucket.
         """
-        self.default_object_acl = DefaultObjectACL(bucket=self)
+        doa = self.default_object_acl
+        doa.clear()
 
-        for entry in self.get_metadata('defaultObjectAcl', []):
-            entity = self.default_object_acl.entity_from_dict(entry)
-            self.default_object_acl.add_entity(entity)
+        url_path = '%s/defaultObjectAcl' % self.path
+        found = self.connection.api_request(method='GET', path=url_path)
+        for entry in found['items']:
+            doa.add_entity(doa.entity_from_dict(entry))
+
+        # Even if we fetch no entries, the ACL is still loaded.
+        doa.loaded = True
 
         return self
 
@@ -547,7 +580,7 @@ def get_default_object_acl(self):
         :rtype: :class:`gcloud.storage.acl.DefaultObjectACL`
         :returns: A DefaultObjectACL object for this bucket.
         """
-        if not self.default_object_acl:
+        if not self.default_object_acl.loaded:
             self.reload_default_object_acl()
         return self.default_object_acl
 
@@ -562,18 +595,26 @@ def save_default_object_acl(self, acl=None):
         """
         if acl is None:
             acl = self.default_object_acl
+            dirty = acl.loaded
+        else:
+            dirty = True
+
+        if dirty:
+            result = self.connection.api_request(
+                method='PATCH', path=self.path,
+                data={'defaultObjectAcl': list(acl)},
+                query_params={'projection': 'full'})
+            doa = self.default_object_acl
+            doa.clear()
+            for entry in result['defaultObjectAcl']:
+                doa.entity(doa.entity_from_dict(entry))
+            doa.loaded = True
 
-        if acl is None:
-            return self
-
-        self.patch_metadata({'defaultObjectAcl': list(acl)})
-        self.reload_default_object_acl()
         return self
 
     def clear_default_object_acl(self):
         """Remove the Default Object ACL from this bucket."""
-
-        return self.save_default_object_acl(acl=[])
+        return self.save_default_object_acl([])
 
     def make_public(self, recursive=False, future=False):
         """Make a bucket public.
diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py
--- a/gcloud/storage/key.py
+++ b/gcloud/storage/key.py
@@ -17,6 +17,8 @@ class Key(object):
 
     This must be a multiple of 256 KB per the API specification.
     """
+    # ACL rules are lazily retrieved.
+    _acl = None
 
     def __init__(self, bucket=None, name=None, metadata=None):
         """Key constructor.
@@ -35,8 +37,12 @@ def __init__(self, bucket=None, name=None, metadata=None):
         self.name = name
         self.metadata = metadata or {}
 
-        # Lazily get the ACL information.
-        self.acl = None
+    @property
+    def acl(self):
+        """Create our ACL on demand."""
+        if self._acl is None:
+            self._acl = ObjectACL(self)
+        return self._acl
 
     @classmethod
     def from_dict(cls, key_dict, bucket=None):
@@ -316,17 +322,15 @@ def has_metadata(self, field=None):
         else:
             return True
 
-    def reload_metadata(self, full=False):
+    def reload_metadata(self):
         """Reload metadata from Cloud Storage.
 
-        :type full: bool
-        :param full: If True, loads all data (include ACL data).
-
         :rtype: :class:`Key`
         :returns: The key you just reloaded data for.
         """
-        projection = 'full' if full else 'noAcl'
-        query_params = {'projection': projection}
+        # Pass only '?projection=noAcl' here because 'acl' is handled via
+        # 'get_acl().
+        query_params = {'projection': 'noAcl'}
         self.metadata = self.connection.api_request(
             method='GET', path=self.path, query_params=query_params)
         return self
@@ -347,9 +351,12 @@ def get_metadata(self, field=None, default=None):
         :rtype: dict or anything
         :returns: All metadata or the value of the specific field.
         """
+        # We ignore 'acl' because it is meant to be handled via 'get_acl()'.
+        if field == 'acl':
+            raise KeyError("Use 'get_acl()'")
+
         if not self.has_metadata(field=field):
-            full = (field and field == 'acl')
-            self.reload_metadata(full=full)
+            self.reload_metadata()
 
         if field:
             return self.metadata.get(field, default)
@@ -382,11 +389,15 @@ def reload_acl(self):
         :rtype: :class:`Key`
         :returns: The current key.
         """
-        self.acl = ObjectACL(key=self)
+        self.acl.clear()
 
-        for entry in self.get_metadata('acl', []):
-            entity = self.acl.entity_from_dict(entry)
-            self.acl.add_entity(entity)
+        url_path = '%s/acl' % self.path
+        found = self.connection.api_request(method='GET', path=url_path)
+        for entry in found['items']:
+            self.acl.add_entity(self.acl.entity_from_dict(entry))
+
+        # Even if we fetch no entries, the ACL is still loaded.
+        self.acl.loaded = True
 
         return self
 
@@ -396,7 +407,7 @@ def get_acl(self):
         :rtype: :class:`gcloud.storage.acl.ObjectACL`
         :returns: An ACL object for the current key.
         """
-        if not self.acl:
+        if not self.acl.loaded:
             self.reload_acl()
         return self.acl
 
@@ -407,16 +418,21 @@ def save_acl(self, acl=None):
         :param acl: The ACL object to save.  If left blank, this will
                     save the ACL set locally on the key.
         """
-        # We do things in this weird way because [] and None
-        # both evaluate to False, but mean very different things.
         if acl is None:
             acl = self.acl
+            dirty = acl.loaded
+        else:
+            dirty = True
 
-        if acl is None:
-            return self
+        if dirty:
+            result = self.connection.api_request(
+                method='PATCH', path=self.path, data={'acl': list(acl)},
+                query_params={'projection': 'full'})
+            self.acl.clear()
+            for entry in result['acl']:
+                self.acl.entity(self.acl.entity_from_dict(entry))
+            self.acl.loaded = True
 
-        self.patch_metadata({'acl': list(acl)})
-        self.reload_acl()
         return self
 
     def clear_acl(self):
@@ -427,7 +443,7 @@ def clear_acl(self):
         have access to a key that you created even after you clear ACL
         rules with this method.
         """
-        return self.save_acl(acl=[])
+        return self.save_acl([])
 
     def make_public(self):
         """Make this key public giving all users read access.


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
@@ -125,6 +125,27 @@ def test_ctor(self):
         acl = self._makeOne()
         self.assertEqual(acl.entities, {})
         self.assertEqual(list(acl.get_entities()), [])
+        self.assertFalse(acl.loaded)
+
+    def test_clear(self):
+        TYPE = 'type'
+        ID = 'id'
+        acl = self._makeOne()
+        acl.entity(TYPE, ID)
+        acl.clear()
+        self.assertTrue(acl.loaded)
+        self.assertEqual(acl.entities, {})
+        self.assertEqual(list(acl.get_entities()), [])
+
+    def test_reset(self):
+        TYPE = 'type'
+        ID = 'id'
+        acl = self._makeOne()
+        acl.entity(TYPE, ID)
+        acl.reset()
+        self.assertFalse(acl.loaded)
+        self.assertEqual(acl.entities, {})
+        self.assertEqual(list(acl.get_entities()), [])
 
     def test___iter___empty(self):
         acl = self._makeOne()
@@ -272,6 +293,7 @@ def test_add_entity_miss(self):
         entity.grant(ROLE)
         acl = self._makeOne()
         acl.add_entity(entity)
+        self.assertTrue(acl.loaded)
         self.assertEqual(list(acl),
                          [{'entity': 'type-id', 'role': ROLE}])
         self.assertEqual(list(acl.get_entities()), [entity])
@@ -287,6 +309,7 @@ def test_add_entity_hit(self):
         acl = self._makeOne()
         before = acl.entity(TYPE, ID)
         acl.add_entity(entity)
+        self.assertTrue(acl.loaded)
         self.assertFalse(acl.get_entity(KEY) is before)
         self.assertTrue(acl.get_entity(KEY) is entity)
         self.assertEqual(list(acl),
@@ -299,6 +322,7 @@ def test_entity_miss(self):
         ROLE = 'role'
         acl = self._makeOne()
         entity = acl.entity(TYPE, ID)
+        self.assertTrue(acl.loaded)
         entity.grant(ROLE)
         self.assertEqual(list(acl),
                          [{'entity': 'type-id', 'role': ROLE}])
diff --git a/gcloud/storage/test_bucket.py b/gcloud/storage/test_bucket.py
--- a/gcloud/storage/test_bucket.py
+++ b/gcloud/storage/test_bucket.py
@@ -17,8 +17,8 @@ def test_ctor_defaults(self):
         self.assertEqual(bucket.connection, None)
         self.assertEqual(bucket.name, None)
         self.assertEqual(bucket.metadata, None)
-        self.assertEqual(bucket.acl, None)
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertTrue(bucket._acl is None)
+        self.assertTrue(bucket._default_object_acl is None)
 
     def test_ctor_explicit(self):
         NAME = 'name'
@@ -28,8 +28,8 @@ def test_ctor_explicit(self):
         self.assertTrue(bucket.connection is connection)
         self.assertEqual(bucket.name, NAME)
         self.assertEqual(bucket.metadata, metadata)
-        self.assertEqual(bucket.acl, None)
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertTrue(bucket._acl is None)
+        self.assertTrue(bucket._default_object_acl is None)
 
     def test_from_dict_defaults(self):
         NAME = 'name'
@@ -39,8 +39,8 @@ def test_from_dict_defaults(self):
         self.assertEqual(bucket.connection, None)
         self.assertEqual(bucket.name, NAME)
         self.assertEqual(bucket.metadata, metadata)
-        self.assertEqual(bucket.acl, None)
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertTrue(bucket._acl is None)
+        self.assertTrue(bucket._default_object_acl is None)
 
     def test_from_dict_explicit(self):
         NAME = 'name'
@@ -51,8 +51,22 @@ def test_from_dict_explicit(self):
         self.assertTrue(bucket.connection is connection)
         self.assertEqual(bucket.name, NAME)
         self.assertEqual(bucket.metadata, metadata)
-        self.assertEqual(bucket.acl, None)
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertTrue(bucket._acl is None)
+        self.assertTrue(bucket._default_object_acl is None)
+
+    def test_acl_property(self):
+        from gcloud.storage.acl import BucketACL
+        bucket = self._makeOne()
+        acl = bucket.acl
+        self.assertTrue(isinstance(acl, BucketACL))
+        self.assertTrue(acl is bucket._acl)
+
+    def test_default_object_acl_property(self):
+        from gcloud.storage.acl import DefaultObjectACL
+        bucket = self._makeOne()
+        acl = bucket.default_object_acl
+        self.assertTrue(isinstance(acl, DefaultObjectACL))
+        self.assertTrue(acl is bucket._default_object_acl)
 
     def test___iter___empty(self):
         NAME = 'name'
@@ -395,7 +409,7 @@ def test_has_metdata_hit(self):
         bucket = self._makeOne(metadata=metadata)
         self.assertTrue(bucket.has_metadata(KEY))
 
-    def test_reload_metadata_default(self):
+    def test_reload_metadata(self):
         NAME = 'name'
         before = {'foo': 'Foo'}
         after = {'bar': 'Bar'}
@@ -410,21 +424,6 @@ def test_reload_metadata_default(self):
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
         self.assertEqual(kw[0]['query_params'], {'projection': 'noAcl'})
 
-    def test_reload_metadata_explicit(self):
-        NAME = 'name'
-        before = {'foo': 'Foo'}
-        after = {'bar': 'Bar'}
-        connection = _Connection(after)
-        bucket = self._makeOne(connection, NAME, before)
-        found = bucket.reload_metadata(True)
-        self.assertTrue(found is bucket)
-        self.assertEqual(found.metadata, after)
-        kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
-
     def test_get_metadata_none_set_none_passed(self):
         NAME = 'name'
         after = {'bar': 'Bar'}
@@ -439,34 +438,40 @@ def test_get_metadata_none_set_none_passed(self):
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
         self.assertEqual(kw[0]['query_params'], {'projection': 'noAcl'})
 
-    def test_get_metadata_none_set_acl_hit(self):
+    def test_get_metadata_acl_no_default(self):
         NAME = 'name'
-        after = {'bar': 'Bar', 'acl': []}
-        connection = _Connection(after)
+        connection = _Connection()
         bucket = self._makeOne(connection, NAME)
-        found = bucket.get_metadata('acl')
-        self.assertEqual(found, [])
-        self.assertEqual(bucket.metadata, after)
+        self.assertRaises(KeyError, bucket.get_metadata, 'acl')
         kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(len(kw), 0)
+
+    def test_get_metadata_acl_w_default(self):
+        NAME = 'name'
+        connection = _Connection()
+        bucket = self._makeOne(connection, NAME)
+        default = object()
+        self.assertRaises(KeyError, bucket.get_metadata, 'acl', default)
+        kw = connection._requested
+        self.assertEqual(len(kw), 0)
+
+    def test_get_metadata_defaultObjectAcl_no_default(self):
+        NAME = 'name'
+        connection = _Connection()
+        bucket = self._makeOne(connection, NAME)
+        self.assertRaises(KeyError, bucket.get_metadata, 'defaultObjectAcl')
+        kw = connection._requested
+        self.assertEqual(len(kw), 0)
 
     def test_get_metadata_none_set_defaultObjectAcl_miss_clear_default(self):
         NAME = 'name'
-        after = {'bar': 'Bar'}
-        connection = _Connection(after)
+        connection = _Connection()
         bucket = self._makeOne(connection, NAME)
         default = object()
-        found = bucket.get_metadata('defaultObjectAcl', default)
-        self.assertTrue(found is default)
-        self.assertEqual(bucket.metadata, after)
+        self.assertRaises(KeyError, bucket.get_metadata, 'defaultObjectAcl',
+                          default)
         kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(len(kw), 0)
 
     def test_get_metadata_miss(self):
         NAME = 'name'
@@ -551,28 +556,42 @@ def test_disable_website(self):
 
     def test_reload_acl_eager_empty(self):
         from gcloud.storage.acl import BucketACL
-        metadata = {'acl': []}
-        bucket = self._makeOne(metadata=metadata)
+        NAME = 'name'
+        ROLE = 'role'
+        connection = _Connection({'items': []})
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.entity('allUsers', ROLE)
         self.assertTrue(bucket.reload_acl() is bucket)
         self.assertTrue(isinstance(bucket.acl, BucketACL))
         self.assertEqual(list(bucket.acl), [])
+        kw = connection._requested
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'GET')
+        self.assertEqual(kw[0]['path'], '/b/%s/acl' % NAME)
 
     def test_reload_acl_eager_nonempty(self):
         from gcloud.storage.acl import BucketACL
+        NAME = 'name'
         ROLE = 'role'
-        metadata = {'acl': [{'entity': 'allUsers', 'role': ROLE}]}
-        bucket = self._makeOne(metadata=metadata)
+        connection = _Connection(
+            {'items': [{'entity': 'allUsers', 'role': ROLE}]})
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
         self.assertTrue(bucket.reload_acl() is bucket)
         self.assertTrue(isinstance(bucket.acl, BucketACL))
         self.assertEqual(list(bucket.acl),
                          [{'entity': 'allUsers', 'role': ROLE}])
+        kw = connection._requested
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'GET')
+        self.assertEqual(kw[0]['path'], '/b/%s/acl' % NAME)
 
     def test_reload_acl_lazy(self):
         from gcloud.storage.acl import BucketACL
         NAME = 'name'
         ROLE = 'role'
-        after = {'acl': [{'entity': 'allUsers', 'role': ROLE}]}
-        connection = _Connection(after)
+        connection = _Connection(
+            {'items': [{'entity': 'allUsers', 'role': ROLE}]})
         bucket = self._makeOne(connection, NAME)
         self.assertTrue(bucket.reload_acl() is bucket)
         self.assertTrue(isinstance(bucket.acl, BucketACL))
@@ -581,26 +600,27 @@ def test_reload_acl_lazy(self):
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(kw[0]['path'], '/b/%s/acl' % NAME)
 
     def test_get_acl_lazy(self):
         from gcloud.storage.acl import BucketACL
-        metadata = {'acl': []}
-        connection = _Connection()
-        bucket = self._makeOne(metadata=metadata)
+        NAME = 'name'
+        connection = _Connection({'items': []})
+        bucket = self._makeOne(connection, NAME)
         acl = bucket.get_acl()
         self.assertTrue(acl is bucket.acl)
         self.assertTrue(isinstance(acl, BucketACL))
         self.assertEqual(list(bucket.acl), [])
         kw = connection._requested
-        self.assertEqual(len(kw), 0)
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'GET')
+        self.assertEqual(kw[0]['path'], '/b/%s/acl' % NAME)
 
     def test_get_acl_eager(self):
-        from gcloud.storage.acl import BucketACL
         connection = _Connection()
         bucket = self._makeOne()
-        preset = bucket.acl = BucketACL(bucket)
+        preset = bucket.acl  # Ensure it is assigned
+        preset.loaded = True
         acl = bucket.get_acl()
         self.assertTrue(acl is preset)
         kw = connection._requested
@@ -616,15 +636,14 @@ def test_save_acl_none_set_none_passed(self):
     def test_save_acl_existing_set_none_passed(self):
         NAME = 'name'
         connection = _Connection({'foo': 'Foo', 'acl': []})
-        metadata = {'acl': []}
-        bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_acl()
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
         self.assertTrue(bucket.save_acl() is bucket)
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['data'], metadata)
+        self.assertEqual(kw[0]['data'], {'acl': []})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
 
     def test_save_acl_existing_set_new_passed(self):
@@ -632,9 +651,8 @@ def test_save_acl_existing_set_new_passed(self):
         ROLE = 'role'
         new_acl = [{'entity': 'allUsers', 'role': ROLE}]
         connection = _Connection({'foo': 'Foo', 'acl': new_acl})
-        metadata = {'acl': []}
-        bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_acl()
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
         self.assertTrue(bucket.save_acl(new_acl) is bucket)
         self.assertEqual(list(bucket.acl), new_acl)
         kw = connection._requested
@@ -646,14 +664,17 @@ def test_save_acl_existing_set_new_passed(self):
 
     def test_clear_acl(self):
         NAME = 'name'
-        ROLE = 'role'
-        old_acl = [{'entity': 'allUsers', 'role': ROLE}]
-        connection = _Connection({'foo': 'Foo', 'acl': []})
-        metadata = {'acl': old_acl}
-        bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_acl()
+        ROLE1 = 'role1'
+        ROLE2 = 'role2'
+        STICKY = {'entity': 'allUsers', 'role': ROLE2}
+        connection = _Connection(
+            # Emulate back-end, which makes some entries "sticky".
+            {'foo': 'Foo', 'acl': [STICKY]},
+            )
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.entity('allUsers', ROLE1)
         self.assertTrue(bucket.clear_acl() is bucket)
-        self.assertEqual(list(bucket.acl), [])
+        self.assertEqual(list(bucket.acl), [STICKY])
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
@@ -662,57 +683,67 @@ def test_clear_acl(self):
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
 
     def test_reload_default_object_acl_eager_empty(self):
-        from gcloud.storage.acl import BucketACL
-        metadata = {'defaultObjectAcl': []}
-        bucket = self._makeOne(metadata=metadata)
+        from gcloud.storage.acl import DefaultObjectACL
+        NAME = 'name'
+        after = {'items': []}
+        connection = _Connection(after)
+        bucket = self._makeOne(connection, NAME)
+        bucket.default_object_acl.loaded = True
         self.assertTrue(bucket.reload_default_object_acl() is bucket)
-        self.assertTrue(isinstance(bucket.default_object_acl, BucketACL))
+        self.assertTrue(
+            isinstance(bucket.default_object_acl, DefaultObjectACL))
         self.assertEqual(list(bucket.default_object_acl), [])
 
     def test_reload_default_object_acl_eager_nonempty(self):
-        from gcloud.storage.acl import BucketACL
+        from gcloud.storage.acl import DefaultObjectACL
+        NAME = 'name'
         ROLE = 'role'
-        metadata = {'defaultObjectAcl': [{'entity': 'allUsers', 'role': ROLE}]}
-        bucket = self._makeOne(metadata=metadata)
+        after = {'items': [{'entity': 'allUsers', 'role': ROLE}]}
+        connection = _Connection(after)
+        bucket = self._makeOne(connection, NAME)
+        bucket.default_object_acl.entity('allUsers', 'OTHERROLE')
         self.assertTrue(bucket.reload_default_object_acl() is bucket)
-        self.assertTrue(isinstance(bucket.default_object_acl, BucketACL))
+        self.assertTrue(
+            isinstance(bucket.default_object_acl, DefaultObjectACL))
         self.assertEqual(list(bucket.default_object_acl),
                          [{'entity': 'allUsers', 'role': ROLE}])
 
     def test_reload_default_object_acl_lazy(self):
-        from gcloud.storage.acl import BucketACL
+        from gcloud.storage.acl import DefaultObjectACL
         NAME = 'name'
         ROLE = 'role'
-        after = {'defaultObjectAcl': [{'entity': 'allUsers', 'role': ROLE}]}
+        after = {'items': [{'entity': 'allUsers', 'role': ROLE}]}
         connection = _Connection(after)
         bucket = self._makeOne(connection, NAME)
         self.assertTrue(bucket.reload_default_object_acl() is bucket)
-        self.assertTrue(isinstance(bucket.default_object_acl, BucketACL))
+        self.assertTrue(
+            isinstance(bucket.default_object_acl, DefaultObjectACL))
         self.assertEqual(list(bucket.default_object_acl),
                          [{'entity': 'allUsers', 'role': ROLE}])
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(kw[0]['path'], '/b/%s/defaultObjectAcl' % NAME)
 
     def test_get_default_object_acl_lazy(self):
         from gcloud.storage.acl import BucketACL
-        metadata = {'defaultObjectAcl': []}
-        connection = _Connection()
-        bucket = self._makeOne(metadata=metadata)
+        NAME = 'name'
+        connection = _Connection({'items': []})
+        bucket = self._makeOne(connection, NAME)
         acl = bucket.get_default_object_acl()
         self.assertTrue(acl is bucket.default_object_acl)
         self.assertTrue(isinstance(acl, BucketACL))
         self.assertEqual(list(bucket.default_object_acl), [])
         kw = connection._requested
-        self.assertEqual(len(kw), 0)
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'GET')
+        self.assertEqual(kw[0]['path'], '/b/%s/defaultObjectAcl' % NAME)
 
     def test_get_default_object_acl_eager(self):
-        from gcloud.storage.acl import BucketACL
         connection = _Connection()
         bucket = self._makeOne()
-        preset = bucket.default_object_acl = BucketACL(bucket)
+        preset = bucket.default_object_acl  # ensure it is assigned
+        preset.loaded = True
         acl = bucket.get_default_object_acl()
         self.assertTrue(acl is preset)
         kw = connection._requested
@@ -727,83 +758,66 @@ def test_save_default_object_acl_none_set_none_passed(self):
 
     def test_save_default_object_acl_existing_set_none_passed(self):
         NAME = 'name'
-        connection = _Connection({'foo': 'Foo', 'acl': []})
         connection = _Connection(
-            {'foo': 'Foo', 'acl': []},
             {'foo': 'Foo', 'acl': [], 'defaultObjectAcl': []},
         )
-        metadata = {'defaultObjectAcl': []}
-        bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_default_object_acl()
+        bucket = self._makeOne(connection, NAME)
+        bucket.default_object_acl.loaded = True
         self.assertTrue(bucket.save_default_object_acl() is bucket)
         kw = connection._requested
-        self.assertEqual(len(kw), 2)
+        self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['data'], metadata)
+        self.assertEqual(kw[0]['data'], {'defaultObjectAcl': []})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
-        self.assertEqual(kw[1]['method'], 'GET')
-        self.assertEqual(kw[1]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[1]['query_params'], {'projection': 'full'})
 
     def test_save_default_object_acl_existing_set_new_passed(self):
         NAME = 'name'
         ROLE = 'role'
         new_acl = [{'entity': 'allUsers', 'role': ROLE}]
         connection = _Connection(
-            {'foo': 'Foo', 'acl': new_acl},
             {'foo': 'Foo', 'acl': new_acl, 'defaultObjectAcl': new_acl},
         )
         metadata = {'defaultObjectAcl': []}
         bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_default_object_acl()
+        bucket.default_object_acl.loaded = True
         self.assertTrue(bucket.save_default_object_acl(new_acl) is bucket)
         self.assertEqual(list(bucket.default_object_acl), new_acl)
         kw = connection._requested
-        self.assertEqual(len(kw), 2)
+        self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
         self.assertEqual(kw[0]['data'], {'defaultObjectAcl': new_acl})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
-        self.assertEqual(kw[1]['method'], 'GET')
-        self.assertEqual(kw[1]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[1]['query_params'], {'projection': 'full'})
 
     def test_clear_default_object_acl(self):
         NAME = 'name'
         ROLE = 'role'
-        old_acl = [{'entity': 'allUsers', 'role': ROLE}]
         connection = _Connection(
-            {'foo': 'Foo', 'acl': []},
             {'foo': 'Foo', 'acl': [], 'defaultObjectAcl': []},
         )
-        metadata = {'defaultObjectAcl': old_acl}
-        bucket = self._makeOne(connection, NAME, metadata)
-        bucket.reload_default_object_acl()
+        bucket = self._makeOne(connection, NAME)
+        bucket.default_object_acl.entity('allUsers', ROLE)
         self.assertTrue(bucket.clear_default_object_acl() is bucket)
         self.assertEqual(list(bucket.default_object_acl), [])
         kw = connection._requested
-        self.assertEqual(len(kw), 2)
+        self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
         self.assertEqual(kw[0]['data'], {'defaultObjectAcl': []})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
-        self.assertEqual(kw[1]['method'], 'GET')
-        self.assertEqual(kw[1]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[1]['query_params'], {'projection': 'full'})
 
     def test_make_public_defaults(self):
         from gcloud.storage.acl import _ACLEntity
         NAME = 'name'
-        before = {'acl': [], 'defaultObjectAcl': []}
         permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}]
         after = {'acl': permissive, 'defaultObjectAcl': []}
         connection = _Connection(after)
-        bucket = self._makeOne(connection, NAME, before)
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
         bucket.make_public()
-        self.assertEqual(bucket.metadata, after)
-        self.assertEqual(list(bucket.acl), after['acl'])
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertEqual(list(bucket.acl), permissive)
+        self.assertEqual(list(bucket.default_object_acl), [])
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
@@ -814,27 +828,25 @@ def test_make_public_defaults(self):
     def test_make_public_w_future(self):
         from gcloud.storage.acl import _ACLEntity
         NAME = 'name'
-        before = {'acl': [], 'defaultObjectAcl': []}
         permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}]
         after1 = {'acl': permissive, 'defaultObjectAcl': []}
         after2 = {'acl': permissive, 'defaultObjectAcl': permissive}
         connection = _Connection(after1, after2)
-        bucket = self._makeOne(connection, NAME, before)
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
+        bucket.default_object_acl.loaded = True
         bucket.make_public(future=True)
-        self.assertEqual(bucket.metadata, after2)
-        self.assertEqual(list(bucket.acl), after2['acl'])
-        self.assertEqual(list(bucket.default_object_acl),
-                         after2['defaultObjectAcl'])
+        self.assertEqual(list(bucket.acl), permissive)
+        self.assertEqual(list(bucket.default_object_acl), permissive)
         kw = connection._requested
         self.assertEqual(len(kw), 2)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['data'], {'acl': after1['acl']})
+        self.assertEqual(kw[0]['data'], {'acl': permissive})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
         self.assertEqual(kw[1]['method'], 'PATCH')
         self.assertEqual(kw[1]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[1]['data'], {'defaultObjectAcl':
-                                         after2['defaultObjectAcl']})
+        self.assertEqual(kw[1]['data'], {'defaultObjectAcl': permissive})
         self.assertEqual(kw[1]['query_params'], {'projection': 'full'})
 
     def test_make_public_recursive(self):
@@ -870,22 +882,21 @@ def get_items_from_response(self, response):
 
         NAME = 'name'
         KEY = 'key'
-        before = {'acl': [], 'defaultObjectAcl': []}
         permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}]
         after = {'acl': permissive, 'defaultObjectAcl': []}
         connection = _Connection(after, {'items': [{'name': KEY}]})
-        bucket = self._makeOne(connection, NAME, before)
+        bucket = self._makeOne(connection, NAME)
+        bucket.acl.loaded = True
         with _Monkey(MUT, _KeyIterator=_KeyIterator):
             bucket.make_public(recursive=True)
-        self.assertEqual(bucket.metadata, after)
-        self.assertEqual(list(bucket.acl), after['acl'])
-        self.assertEqual(bucket.default_object_acl, None)
+        self.assertEqual(list(bucket.acl), permissive)
+        self.assertEqual(list(bucket.default_object_acl), [])
         self.assertEqual(_saved, [(bucket, KEY, True)])
         kw = connection._requested
         self.assertEqual(len(kw), 2)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
-        self.assertEqual(kw[0]['data'], {'acl': after['acl']})
+        self.assertEqual(kw[0]['data'], {'acl': permissive})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
         self.assertEqual(kw[1]['method'], 'GET')
         self.assertEqual(kw[1]['path'], '/b/%s/o' % NAME)
diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py
--- a/gcloud/storage/test_key.py
+++ b/gcloud/storage/test_key.py
@@ -16,7 +16,7 @@ def test_ctor_defaults(self):
         self.assertEqual(key.connection, None)
         self.assertEqual(key.name, None)
         self.assertEqual(key.metadata, {})
-        self.assertEqual(key.acl, None)
+        self.assertTrue(key._acl is None)
 
     def test_ctor_explicit(self):
         KEY = 'key'
@@ -28,7 +28,7 @@ def test_ctor_explicit(self):
         self.assertTrue(key.connection is connection)
         self.assertEqual(key.name, KEY)
         self.assertEqual(key.metadata, metadata)
-        self.assertEqual(key.acl, None)
+        self.assertTrue(key._acl is None)
 
     def test_from_dict_defaults(self):
         KEY = 'key'
@@ -39,7 +39,7 @@ def test_from_dict_defaults(self):
         self.assertEqual(key.connection, None)
         self.assertEqual(key.name, KEY)
         self.assertEqual(key.metadata, metadata)
-        self.assertEqual(key.acl, None)
+        self.assertTrue(key._acl is None)
 
     def test_from_dict_explicit(self):
         KEY = 'key'
@@ -52,7 +52,14 @@ def test_from_dict_explicit(self):
         self.assertTrue(key.connection is connection)
         self.assertEqual(key.name, KEY)
         self.assertEqual(key.metadata, metadata)
-        self.assertEqual(key.acl, None)
+        self.assertTrue(key._acl is None)
+
+    def test_acl_property(self):
+        from gcloud.storage.acl import ObjectACL
+        key = self._makeOne()
+        acl = key.acl
+        self.assertTrue(isinstance(acl, ObjectACL))
+        self.assertTrue(acl is key._acl)
 
     def test_path_no_bucket(self):
         key = self._makeOne()
@@ -342,7 +349,7 @@ def test_has_metdata_hit(self):
         key = self._makeOne(metadata=metadata)
         self.assertTrue(key.has_metadata(KEY))
 
-    def test_reload_metadata_default(self):
+    def test_reload_metadata(self):
         KEY = 'key'
         before = {'foo': 'Foo'}
         after = {'bar': 'Bar'}
@@ -358,22 +365,6 @@ def test_reload_metadata_default(self):
         self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
         self.assertEqual(kw[0]['query_params'], {'projection': 'noAcl'})
 
-    def test_reload_metadata_explicit(self):
-        KEY = 'key'
-        before = {'foo': 'Foo'}
-        after = {'bar': 'Bar'}
-        connection = _Connection(after)
-        bucket = _Bucket(connection)
-        key = self._makeOne(bucket, KEY, before)
-        found = key.reload_metadata(True)
-        self.assertTrue(found is key)
-        self.assertEqual(found.metadata, after)
-        kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
-
     def test_get_metadata_none_set_none_passed(self):
         KEY = 'key'
         after = {'bar': 'Bar'}
@@ -389,36 +380,25 @@ def test_get_metadata_none_set_none_passed(self):
         self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
         self.assertEqual(kw[0]['query_params'], {'projection': 'noAcl'})
 
-    def test_get_metadata_none_set_acl_hit(self):
+    def test_get_metadata_acl_no_default(self):
         KEY = 'key'
-        after = {'bar': 'Bar', 'acl': []}
-        connection = _Connection(after)
+        connection = _Connection()
         bucket = _Bucket(connection)
         key = self._makeOne(bucket, KEY)
-        found = key.get_metadata('acl')
-        self.assertEqual(found, [])
-        self.assertEqual(key.metadata, after)
+        self.assertRaises(KeyError, key.get_metadata, 'acl')
         kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(len(kw), 0)
 
-    def test_get_metadata_none_set_acl_miss_explicit_default(self):
+    def test_get_metadata_acl_w_default(self):
         KEY = 'key'
         after = {'bar': 'Bar'}
         connection = _Connection(after)
         bucket = _Bucket(connection)
         key = self._makeOne(bucket, KEY)
         default = object()
-        found = key.get_metadata('acl', default)
-        self.assertTrue(found is default)
-        self.assertEqual(key.metadata, after)
+        self.assertRaises(KeyError, key.get_metadata, 'acl', default)
         kw = connection._requested
-        self.assertEqual(len(kw), 1)
-        self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(len(kw), 0)
 
     def test_get_metadata_miss(self):
         KEY = 'key'
@@ -461,27 +441,39 @@ def test_patch_metadata(self):
 
     def test_reload_acl_eager_empty(self):
         from gcloud.storage.acl import ObjectACL
-        metadata = {'acl': []}
-        key = self._makeOne(metadata=metadata)
+        KEY = 'key'
+        ROLE = 'role'
+        after = {'items': [{'entity': 'allUsers', 'role': ROLE}]}
+        connection = _Connection(after)
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        key.acl.loaded = True
         self.assertTrue(key.reload_acl() is key)
         self.assertTrue(isinstance(key.acl, ObjectACL))
-        self.assertEqual(list(key.acl), [])
+        self.assertEqual(list(key.acl), after['items'])
+        kw = connection._requested
+        self.assertEqual(len(kw), 1)
+        self.assertEqual(kw[0]['method'], 'GET')
+        self.assertEqual(kw[0]['path'], '/b/name/o/%s/acl' % KEY)
 
     def test_reload_acl_eager_nonempty(self):
         from gcloud.storage.acl import ObjectACL
+        KEY = 'key'
         ROLE = 'role'
-        metadata = {'acl': [{'entity': 'allUsers', 'role': ROLE}]}
-        key = self._makeOne(metadata=metadata)
+        after = {'items': []}
+        connection = _Connection(after)
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        key.acl.entity('allUsers', ROLE)
         self.assertTrue(key.reload_acl() is key)
         self.assertTrue(isinstance(key.acl, ObjectACL))
-        self.assertEqual(list(key.acl),
-                         [{'entity': 'allUsers', 'role': ROLE}])
+        self.assertEqual(list(key.acl), [])
 
     def test_reload_acl_lazy(self):
         from gcloud.storage.acl import ObjectACL
         KEY = 'key'
         ROLE = 'role'
-        after = {'acl': [{'entity': 'allUsers', 'role': ROLE}]}
+        after = {'items': [{'entity': 'allUsers', 'role': ROLE}]}
         connection = _Connection(after)
         bucket = _Bucket(connection)
         key = self._makeOne(bucket, KEY)
@@ -492,22 +484,23 @@ def test_reload_acl_lazy(self):
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'GET')
-        self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
+        self.assertEqual(kw[0]['path'], '/b/name/o/%s/acl' % KEY)
 
     def test_get_acl_lazy(self):
         from gcloud.storage.acl import ObjectACL
-        metadata = {'acl': []}
-        key = self._makeOne(metadata=metadata)
+        KEY = 'key'
+        connection = _Connection({'items': []})
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
         acl = key.get_acl()
         self.assertTrue(acl is key.acl)
         self.assertTrue(isinstance(acl, ObjectACL))
         self.assertEqual(list(key.acl), [])
 
     def test_get_acl_eager(self):
-        from gcloud.storage.acl import ObjectACL
         key = self._makeOne()
-        preset = key.acl = ObjectACL(key)
+        preset = key.acl
+        preset.loaded = True
         acl = key.get_acl()
         self.assertTrue(acl is preset)
 
@@ -524,15 +517,14 @@ def test_save_acl_existing_set_none_passed(self):
         KEY = 'key'
         connection = _Connection({'foo': 'Foo', 'acl': []})
         bucket = _Bucket(connection)
-        metadata = {'acl': []}
-        key = self._makeOne(bucket, KEY, metadata)
-        key.reload_acl()
+        key = self._makeOne(bucket, KEY)
+        key.acl.loaded = True
         self.assertTrue(key.save_acl() is key)
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['data'], metadata)
+        self.assertEqual(kw[0]['data'], {'acl': []})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
 
     def test_save_acl_existing_set_new_passed(self):
@@ -541,9 +533,8 @@ def test_save_acl_existing_set_new_passed(self):
         new_acl = [{'entity': 'allUsers', 'role': ROLE}]
         connection = _Connection({'foo': 'Foo', 'acl': new_acl})
         bucket = _Bucket(connection)
-        metadata = {'acl': []}
-        key = self._makeOne(bucket, KEY, metadata)
-        key.reload_acl()
+        key = self._makeOne(bucket, KEY)
+        key.acl.entity('allUsers', 'other-role')
         self.assertTrue(key.save_acl(new_acl) is key)
         self.assertEqual(list(key.acl), new_acl)
         kw = connection._requested
@@ -556,12 +547,10 @@ def test_save_acl_existing_set_new_passed(self):
     def test_clear_acl(self):
         KEY = 'key'
         ROLE = 'role'
-        old_acl = [{'entity': 'allUsers', 'role': ROLE}]
         connection = _Connection({'foo': 'Foo', 'acl': []})
         bucket = _Bucket(connection)
-        metadata = {'acl': old_acl}
-        key = self._makeOne(bucket, KEY, metadata)
-        key.reload_acl()
+        key = self._makeOne(bucket, KEY)
+        key.acl.entity('allUsers', ROLE)
         self.assertTrue(key.clear_acl() is key)
         self.assertEqual(list(key.acl), [])
         kw = connection._requested
@@ -574,20 +563,19 @@ def test_clear_acl(self):
     def test_make_public(self):
         from gcloud.storage.acl import _ACLEntity
         KEY = 'key'
-        before = {'acl': []}
         permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}]
         after = {'acl': permissive}
         connection = _Connection(after)
         bucket = _Bucket(connection)
-        key = self._makeOne(bucket, KEY, before)
+        key = self._makeOne(bucket, KEY)
+        key.acl.loaded = True
         key.make_public()
-        self.assertEqual(key.metadata, after)
-        self.assertEqual(list(key.acl), after['acl'])
+        self.assertEqual(list(key.acl), permissive)
         kw = connection._requested
         self.assertEqual(len(kw), 1)
         self.assertEqual(kw[0]['method'], 'PATCH')
         self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY)
-        self.assertEqual(kw[0]['data'], {'acl': after['acl']})
+        self.assertEqual(kw[0]['data'], {'acl': permissive})
         self.assertEqual(kw[0]['query_params'], {'projection': 'full'})