Repository: googleapis/google-cloud-python

Issue:
Storage: Provide credential-free access to public-access buckets
Per the [GCS authentication doc](https://cloud.google.com/storage/docs/authentication):

> Objects are anonymously accessible if the `allUsers` group has `READ` permission.

Add support for such access to the storage client.  Two strategies seem possible:

- Add a classmethod factory to the `Client` class which bypasses inferring credentials from the user environment.
- Add a custom `AnonymousCredentials` singleton which can be passed to the `Client` constructor.

We don't need to enforce any limits on the access to buckets / objects retrieved from the "anonymous" client:  the back-end will return `UNAUTHORIZED`.

System tests can use the `gcp-public-data-landsat` bucket, if desired (hopefully CI throttling won't be an issue).


Discussion:
Notes:

- The core `Client` class insists that the `credentials` passed to it be either a) `None` (in which case it infers a set from the environment), or b) an instance of `google.auth.credentials.Credentials` (or a subclass).
- We probably have to pass in a custom `_http` instance, which will otherwise default to a lazily-created `google.auth.transport.requests.AuthorizedSession` instance.
- OTOH, it looks as though the `AuthorizedSession` class just wants the `credentials` object to have a `before_request()` method, which we could stub out as a no-op for the anonymous case, and a `refresh()` method, which shouldn't be needed, and could just raise.
I'm totally open and +1 to the idea of "Null/Anonymous Credentials" in google.auth.

We could then make a nice helper constructor for storage:

```
storage_client = google.cloud.storage.Client.create_anonymous_client()
```
@jonparrott I'm working on the `AnonymousCredentials` bit right now.
Sweet.
@jonparrott Is the classmethod-factory the surface you'd prefer?  I had actually talked myself into making it just passing an instance of `google.auth.credentials.AnonymousCredentials` into `Client.__init__`.
passing in anonymouscredentials should work, ofc, but I think if this is a common enough use case we should either (a) explicitly document how to do this and/or (b) provide a helper constructor.
Awaiting release of (merged) https://github.com/GoogleCloudPlatform/google-auth-library-python/pull/206

Resolving patch:
diff --git a/storage/google/cloud/storage/client.py b/storage/google/cloud/storage/client.py
--- a/storage/google/cloud/storage/client.py
+++ b/storage/google/cloud/storage/client.py
@@ -15,6 +15,8 @@
 """Client for interacting with the Google Cloud Storage API."""
 
 
+from google.auth.credentials import AnonymousCredentials
+
 from google.api_core import page_iterator
 from google.cloud._helpers import _LocalStack
 from google.cloud.client import ClientWithProject
@@ -60,6 +62,22 @@ def __init__(self, project=None, credentials=None, _http=None):
         self._connection = Connection(self)
         self._batch_stack = _LocalStack()
 
+    @classmethod
+    def create_anonymous_client(cls):
+        """Factory: return client with anonymous credentials.
+
+        .. note::
+
+           Such a client has only limited access to "public" buckets:
+           listing their contents and downloading their blobs.
+
+        :rtype: :class:`google.cloud.storage.client.Client`
+        :returns: Instance w/ anonymous credentials and no project.
+        """
+        client = cls(project='<none>', credentials=AnonymousCredentials())
+        client.project = None
+        return client
+
     @property
     def _connection(self):
         """Get connection or batch on the client.
diff --git a/storage/setup.py b/storage/setup.py
--- a/storage/setup.py
+++ b/storage/setup.py
@@ -53,7 +53,7 @@
 REQUIREMENTS = [
     'google-cloud-core >= 0.28.0, < 0.29dev',
     'google-api-core >= 0.1.1, < 0.2.0dev',
-    'google-auth >= 1.0.0',
+    'google-auth >= 1.2.0',
     'google-resumable-media >= 0.3.1',
     'requests >= 2.18.0',
 ]


Test patch:
diff --git a/storage/tests/system.py b/storage/tests/system.py
--- a/storage/tests/system.py
+++ b/storage/tests/system.py
@@ -952,3 +952,15 @@ def test_notification_w_user_project(self):
             self.assertEqual(notifications[0].topic_name, self.TOPIC_NAME)
         finally:
             notification.delete()
+
+
+class TestAnonymousClient(unittest.TestCase):
+
+    PUBLIC_BUCKET = 'gcp-public-data-landsat'
+
+    def test_access_to_public_bucket(self):
+        anonymous = storage.Client.create_anonymous_client()
+        bucket = anonymous.bucket(self.PUBLIC_BUCKET)
+        blob, = bucket.list_blobs(max_results=1)
+        with tempfile.TemporaryFile() as stream:
+            blob.download_to_file(stream)
diff --git a/storage/tests/unit/test_client.py b/storage/tests/unit/test_client.py
--- a/storage/tests/unit/test_client.py
+++ b/storage/tests/unit/test_client.py
@@ -68,12 +68,25 @@ def test_ctor_connection_type(self):
         CREDENTIALS = _make_credentials()
 
         client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
+
         self.assertEqual(client.project, PROJECT)
         self.assertIsInstance(client._connection, Connection)
         self.assertIs(client._connection.credentials, CREDENTIALS)
         self.assertIsNone(client.current_batch)
         self.assertEqual(list(client._batch_stack), [])
 
+    def test_create_anonymous_client(self):
+        from google.auth.credentials import AnonymousCredentials
+        from google.cloud.storage._http import Connection
+
+        klass = self._get_target_class()
+        client = klass.create_anonymous_client()
+
+        self.assertIsNone(client.project)
+        self.assertIsInstance(client._connection, Connection)
+        self.assertIsInstance(
+            client._connection.credentials, AnonymousCredentials)
+
     def test__push_batch_and__pop_batch(self):
         from google.cloud.storage.batch import Batch