Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add quota_project, credentials_file, and scopes support #1022

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 17 additions & 2 deletions googleapiclient/_auth.py
Expand Up @@ -38,12 +38,27 @@
HAS_OAUTH2CLIENT = False


def default_credentials():
def credentials_from_file(filename, scopes=None, quota_project_id=None):
"""Returns credentials loaded from a file."""
if HAS_GOOGLE_AUTH:
credentials, _ = google.auth.load_credentials_from_file(filename, scopes=scopes, quota_project_id=quota_project_id)
return credentials
else:
raise EnvironmentError(
"client_options.credentials_file is only supported in google-auth.")


def default_credentials(scopes=None, quota_project_id=None):
"""Returns Application Default Credentials."""
if HAS_GOOGLE_AUTH:
credentials, _ = google.auth.default()
credentials, _ = google.auth.default(scopes=scopes, quota_project_id=quota_project_id)
return credentials
elif HAS_OAUTH2CLIENT:
if scopes is not None or quota_project_id is not None:
raise EnvironmentError(
"client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
"Please install google-auth."
)
return oauth2client.client.GoogleCredentials.get_application_default()
else:
raise EnvironmentError(
Expand Down
68 changes: 51 additions & 17 deletions googleapiclient/discovery.py
Expand Up @@ -30,6 +30,7 @@
# Standard library imports
import copy
from collections import OrderedDict

try:
from email.generator import BytesGenerator
except ImportError:
Expand Down Expand Up @@ -260,14 +261,17 @@ def build(
else:
discovery_http = http

for discovery_url in \
_discovery_service_uri_options(discoveryServiceUrl, version):
for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
requested_url = uritemplate.expand(discovery_url, params)

try:
content = _retrieve_discovery_doc(
requested_url, discovery_http, cache_discovery, cache,
developerKey, num_retries=num_retries
requested_url,
discovery_http,
cache_discovery,
cache,
developerKey,
num_retries=num_retries,
)
return build_from_document(
content,
Expand Down Expand Up @@ -308,13 +312,15 @@ def _discovery_service_uri_options(discoveryServiceUrl, version):
# V1 Discovery won't work if the requested version is None
if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
logger.warning(
"Discovery V1 does not support empty versions. Defaulting to V2...")
"Discovery V1 does not support empty versions. Defaulting to V2..."
)
urls.pop(0)
return list(OrderedDict.fromkeys(urls))


def _retrieve_discovery_doc(url, http, cache_discovery,
cache=None, developerKey=None, num_retries=1):
def _retrieve_discovery_doc(
url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
):
"""Retrieves the discovery_doc from cache or the internet.

Args:
Expand Down Expand Up @@ -444,8 +450,20 @@ def build_from_document(
setting up mutual TLS channel.
"""

if http is not None and credentials is not None:
raise ValueError("Arguments http and credentials are mutually exclusive.")
if client_options is None:
client_options = google.api_core.client_options.ClientOptions()
if isinstance(client_options, six.moves.collections_abc.Mapping):
client_options = google.api_core.client_options.from_dict(client_options)

if http is not None:
# if http is passed, the user cannot provide credentials
banned_options = [
(credentials, "credentials"),
(client_options.credentials_file, "client_options.credentials_file"),
]
for option, name in banned_options:
if option is not None:
raise ValueError("Arguments http and {} are mutually exclusive".format(name))

if isinstance(service, six.string_types):
service = json.loads(service)
Expand All @@ -463,11 +481,8 @@ def build_from_document(

# If an API Endpoint is provided on client options, use that as the base URL
base = urljoin(service["rootUrl"], service["servicePath"])
if client_options:
if isinstance(client_options, six.moves.collections_abc.Mapping):
client_options = google.api_core.client_options.from_dict(client_options)
if client_options.api_endpoint:
base = client_options.api_endpoint
if client_options.api_endpoint:
base = client_options.api_endpoint

schema = Schemas(service)

Expand All @@ -483,13 +498,30 @@ def build_from_document(
# If so, then the we need to setup authentication if no developerKey is
# specified.
if scopes and not developerKey:
# Make sure the user didn't pass multiple credentials
if client_options.credentials_file and credentials:
raise google.api_core.exceptions.DuplicateCredentialArgs(
"client_options.credentials_file and credentials are mutually exclusive."
)
# Check for credentials file via client options
if client_options.credentials_file:
credentials = _auth.credentials_from_file(
client_options.credentials_file,
scopes=client_options.scopes,
quota_project_id=client_options.quota_project_id,
)
# If the user didn't pass in credentials, attempt to acquire application
# default credentials.
if credentials is None:
credentials = _auth.default_credentials()
credentials = _auth.default_credentials(
scopes=client_options.scopes,
quota_project_id=client_options.quota_project_id,
)

# The credentials need to be scoped.
credentials = _auth.with_scopes(credentials, scopes)
# If the user provided scopes via client_options don't override them
if not client_options.scopes:
credentials = _auth.with_scopes(credentials, scopes)

# If credentials are provided, create an authorized http instance;
# otherwise, skip authentication.
Expand Down Expand Up @@ -519,7 +551,9 @@ def build_from_document(
and client_options.client_encrypted_cert_source
):
client_cert_to_use = client_options.client_encrypted_cert_source
elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
elif (
adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
):
client_cert_to_use = mtls.default_client_encrypted_cert_source(
adc_cert_path, adc_key_path
)
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Expand Up @@ -44,7 +44,7 @@ def lint(session):
)


@nox.session(python=["2.7", "3.5", "3.6", "3.7"])
@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8"])
@nox.parametrize(
"oauth2client",
[
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -41,7 +41,7 @@
"httplib2>=0.9.2,<1dev",
"google-auth>=1.16.0",
"google-auth-httplib2>=0.0.3",
"google-api-core>=1.18.0,<2dev",
"google-api-core>=1.21.0,<2dev",
"six>=1.6.1,<2dev",
"uritemplate>=3.0.0,<4dev",
]
Expand Down
39 changes: 39 additions & 0 deletions tests/test__auth.py
Expand Up @@ -40,6 +40,35 @@ def test_default_credentials(self):

self.assertEqual(credentials, mock.sentinel.credentials)

def test_credentials_from_file(self):
with mock.patch(
"google.auth.load_credentials_from_file", autospec=True
) as default:
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)

credentials = _auth.credentials_from_file("credentials.json")

self.assertEqual(credentials, mock.sentinel.credentials)
default.assert_called_once_with(
"credentials.json", scopes=None, quota_project_id=None
)

def test_default_credentials_with_scopes(self):
with mock.patch("google.auth.default", autospec=True) as default:
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)
credentials = _auth.default_credentials(scopes=["1", "2"])

default.assert_called_once_with(scopes=["1", "2"], quota_project_id=None)
self.assertEqual(credentials, mock.sentinel.credentials)

def test_default_credentials_with_quota_project(self):
with mock.patch("google.auth.default", autospec=True) as default:
default.return_value = (mock.sentinel.credentials, mock.sentinel.project)
credentials = _auth.default_credentials(quota_project_id="my-project")

default.assert_called_once_with(scopes=None, quota_project_id="my-project")
self.assertEqual(credentials, mock.sentinel.credentials)

def test_with_scopes_non_scoped(self):
credentials = mock.Mock(spec=google.auth.credentials.Credentials)

Expand Down Expand Up @@ -95,6 +124,16 @@ def test_default_credentials(self):

self.assertEqual(credentials, mock.sentinel.credentials)

def test_credentials_from_file(self):
with self.assertRaises(EnvironmentError):
credentials = _auth.credentials_from_file("credentials.json")

def test_default_credentials_with_scopes_and_quota_project(self):
with self.assertRaises(EnvironmentError):
credentials = _auth.default_credentials(
scopes=["1", "2"], quota_project_id="my-project"
)

def test_with_scopes_non_scoped(self):
credentials = mock.Mock(spec=oauth2client.client.Credentials)

Expand Down