Skip to content

Commit 4862a9c

Browse files
authored
feat: add support for use_auth_w_custom_endpoint (#901)
* feat: add support for use_auth_w_custom_endpoint * update docstring * handle emulator cases * set default storage host as constant
1 parent 47a04ef commit 4862a9c

File tree

3 files changed

+77
-41
lines changed

3 files changed

+77
-41
lines changed

google/cloud/storage/_helpers.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,15 @@
3333
STORAGE_EMULATOR_ENV_VAR = "STORAGE_EMULATOR_HOST"
3434
"""Environment variable defining host for Storage emulator."""
3535

36-
_DEFAULT_STORAGE_HOST = os.getenv(
37-
"API_ENDPOINT_OVERRIDE", "https://storage.googleapis.com"
38-
)
36+
_BASE_STORAGE_URI = "https://storage.googleapis.com"
37+
"""Base request endpoint URI for JSON API."""
38+
39+
_DEFAULT_STORAGE_HOST = os.getenv("API_ENDPOINT_OVERRIDE", _BASE_STORAGE_URI)
3940
"""Default storage host for JSON API."""
4041

4142
_API_VERSION = os.getenv("API_VERSION_OVERRIDE", "v1")
4243
"""API version of the default storage host"""
4344

44-
_BASE_STORAGE_URI = "storage.googleapis.com"
45-
"""Base request endpoint URI for JSON API."""
46-
4745
# etag match parameters in snake case and equivalent header
4846
_ETAG_MATCH_PARAMETERS = (
4947
("if_etag_match", "If-Match"),

google/cloud/storage/client.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ class Client(ClientWithProject):
9696
:type client_options: :class:`~google.api_core.client_options.ClientOptions` or :class:`dict`
9797
:param client_options: (Optional) Client options used to set user options on the client.
9898
API Endpoint should be set through client_options.
99+
100+
:type use_auth_w_custom_endpoint: bool
101+
:param use_auth_w_custom_endpoint:
102+
(Optional) Whether authentication is required under custom endpoints.
103+
If false, uses AnonymousCredentials and bypasses authentication.
104+
Defaults to True. Note this is only used when a custom endpoint is set in conjunction.
99105
"""
100106

101107
SCOPE = (
@@ -112,6 +118,7 @@ def __init__(
112118
_http=None,
113119
client_info=None,
114120
client_options=None,
121+
use_auth_w_custom_endpoint=True,
115122
):
116123
self._base_connection = None
117124

@@ -132,7 +139,7 @@ def __init__(
132139
# then mTLS logic will be applied to decide which endpoint will be used.
133140
storage_host = _get_storage_host()
134141
kw_args["api_endpoint"] = (
135-
storage_host if storage_host != _DEFAULT_STORAGE_HOST else None
142+
storage_host if storage_host != _BASE_STORAGE_URI else None
136143
)
137144

138145
if client_options:
@@ -144,19 +151,23 @@ def __init__(
144151
api_endpoint = client_options.api_endpoint
145152
kw_args["api_endpoint"] = api_endpoint
146153

147-
# Use anonymous credentials and no project when
148-
# STORAGE_EMULATOR_HOST or a non-default api_endpoint is set.
149-
if (
150-
kw_args["api_endpoint"] is not None
151-
and _BASE_STORAGE_URI not in kw_args["api_endpoint"]
152-
):
153-
if credentials is None:
154-
credentials = AnonymousCredentials()
155-
if project is None:
156-
project = _get_environ_project()
157-
if project is None:
158-
no_project = True
159-
project = "<none>"
154+
# If a custom endpoint is set, the client checks for credentials
155+
# or finds the default credentials based on the current environment.
156+
# Authentication may be bypassed under certain conditions:
157+
# (1) STORAGE_EMULATOR_HOST is set (for backwards compatibility), OR
158+
# (2) use_auth_w_custom_endpoint is set to False.
159+
if kw_args["api_endpoint"] is not None:
160+
if (
161+
kw_args["api_endpoint"] == storage_host
162+
or not use_auth_w_custom_endpoint
163+
):
164+
if credentials is None:
165+
credentials = AnonymousCredentials()
166+
if project is None:
167+
project = _get_environ_project()
168+
if project is None:
169+
no_project = True
170+
project = "<none>"
160171

161172
super(Client, self).__init__(
162173
project=project,
@@ -897,7 +908,7 @@ def create_bucket(
897908
project = self.project
898909

899910
# Use no project if STORAGE_EMULATOR_HOST is set
900-
if _BASE_STORAGE_URI not in _get_storage_host():
911+
if _get_storage_host() != _DEFAULT_STORAGE_HOST:
901912
if project is None:
902913
project = _get_environ_project()
903914
if project is None:
@@ -1327,7 +1338,7 @@ def list_buckets(
13271338
project = self.project
13281339

13291340
# Use no project if STORAGE_EMULATOR_HOST is set
1330-
if _BASE_STORAGE_URI not in _get_storage_host():
1341+
if _get_storage_host() != _DEFAULT_STORAGE_HOST:
13311342
if project is None:
13321343
project = _get_environ_project()
13331344
if project is None:

tests/unit/test_client.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
from google.auth.credentials import AnonymousCredentials
2929
from google.oauth2.service_account import Credentials
3030

31+
from google.cloud.storage import _helpers
3132
from google.cloud.storage._helpers import STORAGE_EMULATOR_ENV_VAR
3233
from google.cloud.storage._helpers import _get_default_headers
33-
from google.cloud.storage import _helpers
34+
from google.cloud.storage._http import Connection
3435
from google.cloud.storage.retry import DEFAULT_RETRY
3536
from google.cloud.storage.retry import DEFAULT_RETRY_IF_GENERATION_SPECIFIED
3637
from tests.unit.test__helpers import GCCL_INVOCATION_TEST_CONST
@@ -119,7 +120,6 @@ def _make_one(self, *args, **kw):
119120

120121
def test_ctor_connection_type(self):
121122
from google.cloud._http import ClientInfo
122-
from google.cloud.storage._http import Connection
123123

124124
PROJECT = "PROJECT"
125125
credentials = _make_credentials()
@@ -179,8 +179,6 @@ def test_ctor_w_client_options_object(self):
179179
)
180180

181181
def test_ctor_wo_project(self):
182-
from google.cloud.storage._http import Connection
183-
184182
PROJECT = "PROJECT"
185183
credentials = _make_credentials(project=PROJECT)
186184

@@ -193,8 +191,6 @@ def test_ctor_wo_project(self):
193191
self.assertEqual(list(client._batch_stack), [])
194192

195193
def test_ctor_w_project_explicit_none(self):
196-
from google.cloud.storage._http import Connection
197-
198194
credentials = _make_credentials()
199195

200196
client = self._make_one(project=None, credentials=credentials)
@@ -207,7 +203,6 @@ def test_ctor_w_project_explicit_none(self):
207203

208204
def test_ctor_w_client_info(self):
209205
from google.cloud._http import ClientInfo
210-
from google.cloud.storage._http import Connection
211206

212207
credentials = _make_credentials()
213208
client_info = ClientInfo()
@@ -239,8 +234,40 @@ def test_ctor_mtls(self):
239234
self.assertEqual(client._connection.ALLOW_AUTO_SWITCH_TO_MTLS_URL, False)
240235
self.assertEqual(client._connection.API_BASE_URL, "http://foo")
241236

237+
def test_ctor_w_custom_endpoint_use_auth(self):
238+
custom_endpoint = "storage-example.p.googleapis.com"
239+
client = self._make_one(client_options={"api_endpoint": custom_endpoint})
240+
self.assertEqual(client._connection.API_BASE_URL, custom_endpoint)
241+
self.assertIsNotNone(client.project)
242+
self.assertIsInstance(client._connection, Connection)
243+
self.assertIsNotNone(client._connection.credentials)
244+
self.assertNotIsInstance(client._connection.credentials, AnonymousCredentials)
245+
246+
def test_ctor_w_custom_endpoint_bypass_auth(self):
247+
custom_endpoint = "storage-example.p.googleapis.com"
248+
client = self._make_one(
249+
client_options={"api_endpoint": custom_endpoint},
250+
use_auth_w_custom_endpoint=False,
251+
)
252+
self.assertEqual(client._connection.API_BASE_URL, custom_endpoint)
253+
self.assertEqual(client.project, None)
254+
self.assertIsInstance(client._connection, Connection)
255+
self.assertIsInstance(client._connection.credentials, AnonymousCredentials)
256+
257+
def test_ctor_w_custom_endpoint_w_credentials(self):
258+
PROJECT = "PROJECT"
259+
custom_endpoint = "storage-example.p.googleapis.com"
260+
credentials = _make_credentials(project=PROJECT)
261+
client = self._make_one(
262+
credentials=credentials, client_options={"api_endpoint": custom_endpoint}
263+
)
264+
self.assertEqual(client._connection.API_BASE_URL, custom_endpoint)
265+
self.assertEqual(client.project, PROJECT)
266+
self.assertIsInstance(client._connection, Connection)
267+
self.assertIs(client._connection.credentials, credentials)
268+
242269
def test_ctor_w_emulator_wo_project(self):
243-
# avoids authentication if STORAGE_EMULATOR_ENV_VAR is set
270+
# bypasses authentication if STORAGE_EMULATOR_ENV_VAR is set
244271
host = "http://localhost:8080"
245272
environ = {STORAGE_EMULATOR_ENV_VAR: host}
246273
with mock.patch("os.environ", environ):
@@ -250,16 +277,8 @@ def test_ctor_w_emulator_wo_project(self):
250277
self.assertEqual(client._connection.API_BASE_URL, host)
251278
self.assertIsInstance(client._connection.credentials, AnonymousCredentials)
252279

253-
# avoids authentication if storage emulator is set through api_endpoint
254-
client = self._make_one(
255-
client_options={"api_endpoint": "http://localhost:8080"}
256-
)
257-
self.assertIsNone(client.project)
258-
self.assertEqual(client._connection.API_BASE_URL, host)
259-
self.assertIsInstance(client._connection.credentials, AnonymousCredentials)
260-
261280
def test_ctor_w_emulator_w_environ_project(self):
262-
# avoids authentication and infers the project from the environment
281+
# bypasses authentication and infers the project from the environment
263282
host = "http://localhost:8080"
264283
environ_project = "environ-project"
265284
environ = {
@@ -289,9 +308,17 @@ def test_ctor_w_emulator_w_project_arg(self):
289308
self.assertEqual(client._connection.API_BASE_URL, host)
290309
self.assertIsInstance(client._connection.credentials, AnonymousCredentials)
291310

292-
def test_create_anonymous_client(self):
293-
from google.cloud.storage._http import Connection
311+
def test_ctor_w_emulator_w_credentials(self):
312+
host = "http://localhost:8080"
313+
environ = {STORAGE_EMULATOR_ENV_VAR: host}
314+
credentials = _make_credentials()
315+
with mock.patch("os.environ", environ):
316+
client = self._make_one(credentials=credentials)
294317

318+
self.assertEqual(client._connection.API_BASE_URL, host)
319+
self.assertIs(client._connection.credentials, credentials)
320+
321+
def test_create_anonymous_client(self):
295322
klass = self._get_target_class()
296323
client = klass.create_anonymous_client()
297324

0 commit comments

Comments
 (0)