Skip to content

Commit

Permalink
feat: infer project from explicit service account creds (#51)
Browse files Browse the repository at this point in the history
Closes #27

Co-authored-by: Christopher Wilcox <crwilcox@google.com>
  • Loading branch information
tseaver and crwilcox committed Jan 19, 2021
1 parent d1d4943 commit 2bd41a1
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 8 deletions.
42 changes: 34 additions & 8 deletions google/cloud/client.py
Expand Up @@ -16,13 +16,15 @@

import io
import json
import os
from pickle import PicklingError

import six

import google.api_core.client_options
import google.api_core.exceptions
import google.auth
from google.auth import environment_vars
import google.auth.credentials
import google.auth.transport.requests
from google.cloud._helpers import _determine_default_project
Expand Down Expand Up @@ -188,26 +190,50 @@ class _ClientProjectMixin(object):
"""Mixin to allow setting the project on the client.
:type project: str
:param project: the project which the client acts on behalf of. If not
passed falls back to the default inferred from the
environment.
:param project:
(Optional) the project which the client acts on behalf of. If not
passed, falls back to the default inferred from the environment.
:type credentials: :class:`google.auth.credentials.Credentials`
:param credentials:
(Optional) credentials used to discover a project, if not passed.
:raises: :class:`EnvironmentError` if the project is neither passed in nor
set in the environment. :class:`ValueError` if the project value
is invalid.
set on the credentials or in the environment. :class:`ValueError`
if the project value is invalid.
"""

def __init__(self, project=None):
project = self._determine_default(project)
def __init__(self, project=None, credentials=None):
# This test duplicates the one from `google.auth.default`, but earlier,
# for backward compatibility: we want the environment variable to
# override any project set on the credentials. See:
# https://github.com/googleapis/python-cloud-core/issues/27
if project is None:
project = os.getenv(
environment_vars.PROJECT,
os.getenv(environment_vars.LEGACY_PROJECT),
)

# Project set on explicit credentials overrides discovery from
# SDK / GAE / GCE.
if project is None and credentials is not None:
project = getattr(credentials, "project_id", None)

if project is None:
project = self._determine_default(project)

if project is None:
raise EnvironmentError(
"Project was not passed and could not be "
"determined from the environment."
)

if isinstance(project, six.binary_type):
project = project.decode("utf-8")

if not isinstance(project, six.string_types):
raise ValueError("Project must be a string.")

self.project = project

@staticmethod
Expand Down Expand Up @@ -246,5 +272,5 @@ class ClientWithProject(Client, _ClientProjectMixin):
_SET_PROJECT = True # Used by from_service_account_json()

def __init__(self, project=None, credentials=None, client_options=None, _http=None):
_ClientProjectMixin.__init__(self, project=project)
_ClientProjectMixin.__init__(self, project=project, credentials=credentials)
Client.__init__(self, credentials=credentials, client_options=client_options, _http=_http)
126 changes: 126 additions & 0 deletions tests/unit/test_client.py
Expand Up @@ -174,6 +174,132 @@ def test_from_service_account_json_bad_args(self):
)


class Test_ClientProjectMixin(unittest.TestCase):
@staticmethod
def _get_target_class():
from google.cloud.client import _ClientProjectMixin

return _ClientProjectMixin

def _make_one(self, *args, **kw):
return self._get_target_class()(*args, **kw)

def test_ctor_defaults_wo_envvar(self):
environ = {}
patch_env = mock.patch("os.environ", new=environ)
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=None,
)
with patch_env:
with patch_default as patched:
with self.assertRaises(EnvironmentError):
self._make_one()

patched.assert_called_once_with(None)

def test_ctor_defaults_w_envvar(self):
from google.auth.environment_vars import PROJECT

project = "some-project-123"
environ = {PROJECT: project}
patch_env = mock.patch("os.environ", new=environ)
with patch_env:
client = self._make_one()

self.assertEqual(client.project, project)

def test_ctor_defaults_w_legacy_envvar(self):
from google.auth.environment_vars import LEGACY_PROJECT

project = "some-project-123"
environ = {LEGACY_PROJECT: project}
patch_env = mock.patch("os.environ", new=environ)
with patch_env:
client = self._make_one()

self.assertEqual(client.project, project)

def test_ctor_w_explicit_project(self):
explicit_project = "explicit-project-456"
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=None,
)
with patch_default as patched:
client = self._make_one(project=explicit_project)

self.assertEqual(client.project, explicit_project)

patched.assert_not_called()

def test_ctor_w_explicit_project_bytes(self):
explicit_project = b"explicit-project-456"
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=None,
)
with patch_default as patched:
client = self._make_one(project=explicit_project)

self.assertEqual(client.project, explicit_project.decode("utf-8"))

patched.assert_not_called()

def test_ctor_w_explicit_project_invalid(self):
explicit_project = object()
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=None,
)
with patch_default as patched:
with self.assertRaises(ValueError):
self._make_one(project=explicit_project)

patched.assert_not_called()

@staticmethod
def _make_credentials(**kw):
from google.auth.credentials import Credentials

class _Credentials(Credentials):
def __init__(self, **kw):
self.__dict__.update(kw)

def refresh(self): # pragma: NO COVER
pass

return _Credentials(**kw)

def test_ctor_w_explicit_credentials_wo_project(self):
default_project = "default-project-123"
credentials = self._make_credentials()
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=default_project,
)
with patch_default as patched:
client = self._make_one(credentials=credentials)

self.assertEqual(client.project, default_project)

patched.assert_called_once_with(None)

def test_ctor_w_explicit_credentials_w_project(self):
project = "credentials-project-456"
credentials = self._make_credentials(project_id=project)
patch_default = mock.patch(
"google.cloud.client._determine_default_project",
return_value=None,
)
with patch_default as patched:
client = self._make_one(credentials=credentials)

self.assertEqual(client.project, project)

patched.assert_not_called()


class TestClientWithProject(unittest.TestCase):
@staticmethod
def _get_target_class():
Expand Down

0 comments on commit 2bd41a1

Please sign in to comment.