From 902f7610a1d8f708056849ca3e6f3eaae9d0ca3f Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 4 Oct 2016 14:42:59 -0700 Subject: [PATCH 1/8] Add google.auth.credentials - the public interfaces for credentials --- docs/reference/google.auth.credentials.rst | 7 + docs/reference/google.auth.rst | 1 + google/auth/_helpers.py | 34 +++- google/auth/credentials.py | 208 +++++++++++++++++++++ tests/test__helpers.py | 30 +++ tests/test_credentials.py | 100 ++++++++++ 6 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 docs/reference/google.auth.credentials.rst create mode 100644 google/auth/credentials.py create mode 100644 tests/test_credentials.py diff --git a/docs/reference/google.auth.credentials.rst b/docs/reference/google.auth.credentials.rst new file mode 100644 index 000000000..646fa7bb3 --- /dev/null +++ b/docs/reference/google.auth.credentials.rst @@ -0,0 +1,7 @@ +google.auth.credentials module +============================== + +.. automodule:: google.auth.credentials + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/google.auth.rst b/docs/reference/google.auth.rst index ef0d68981..78f778680 100644 --- a/docs/reference/google.auth.rst +++ b/docs/reference/google.auth.rst @@ -19,6 +19,7 @@ Submodules .. toctree:: + google.auth.credentials google.auth.crypt google.auth.exceptions google.auth.jwt diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index d4bf8f1b4..54c6e65bb 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -14,7 +14,6 @@ """Helper functions for commonly used utilities.""" - import calendar import datetime @@ -135,3 +134,36 @@ def update_query(url, params, remove=None): # Unsplit the url. new_parts = parts._replace(query=new_query) return urllib.parse.urlunparse(new_parts) + + +def scopes_to_string(scopes): + """Converts scope value to a string. + If scopes is a string then it is simply passed through. If scopes is an + iterable then a string is returned that is all the individual scopes + concatenated with spaces. + Args: + scopes (Union[Sequence, str]) + Returns: + str: The scopes formatted as a single string. + """ + if isinstance(scopes, six.string_types): + return scopes + else: + return ' '.join(scopes) + + +def string_to_scopes(scopes): + """Converts stringifed scope value to a list. + If scopes is a list then it is simply passed through. If scopes is an + string then a list of each individual scope is returned. + Args: + scopes (Union[Sequence, str]) + Returns: + list: The scopes in a list. + """ + if not scopes: + return [] + elif isinstance(scopes, six.string_types): + return scopes.split(' ') + else: + return scopes diff --git a/google/auth/credentials.py b/google/auth/credentials.py new file mode 100644 index 000000000..162b101d0 --- /dev/null +++ b/google/auth/credentials.py @@ -0,0 +1,208 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Interfaces for credentials.""" + +import abc + +import six + +from google.auth import _helpers + + +@six.add_metaclass(abc.ABCMeta) +class Credentials(object): + """Base class for all credentials. + + All credentials have a :attr:`token` that is used for authentication and + may also optionally set an :attr:`expiry` to indicate when the token will + no longer be valid. + + Most credentials will be :attr:`invalid` until :meth:`refresh` is called. + Credentials can do this automatically before the first HTTP request in + :meth:`before_request`. + + Although the token and expiration will change as the credentials are + :meth:`refreshed ` and used, credentials should be considered + immutable. Various credentials will accept configuration such as private + keys, scopes, and other options. These options are not changeable after + construction. Some classes will provide mechanisms to copy the credentials + with modifications such as :meth:`ScopedCredentials.with_scopes`. + """ + def __init__(self): + self.token = None + """str: The bearer token that can be used in HTTP headers to make + authenticated requests.""" + self.expiry = None + """Optional[datetime]: When the token expires and is no longer valid. + If this is None, the token is assumed to never expire.""" + + @property + def valid(self): + """Checks the validity of the credentials. + + This is True if the credentials have a :attr:`token` and the token + is not :attr:`expired`. + """ + return self.token is not None and not self.expired + + @property + def expired(self): + """Checks if the credentials are expired. + + Note that credentials can be invalid but not expired becaue Credentials + with :attr:`expiry` set to None is considered to never expire. + """ + now = _helpers.now() + if self.expiry is None or self.expiry > now: + return False + else: + return True + + @abc.abstractmethod + def refresh(self, request): + """Refreshes the access token. + + Args: + request (google.auth.transport.Request): A callable used to make + HTTP requests. + + Raises: + google.auth.exceptions.RefreshError: If the credentials could + not be refreshed. + """ + # pylint: disable=missing-raises-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError('Refresh must be implemented') + + def apply(self, headers, token=None): + """Apply the token to the authentication header. + + Args: + headers (Mapping): The HTTP request headers. + token (Optional[str]): If specified, overrides the current access + token. + """ + headers[b'authorization'] = 'Bearer {}'.format( + _helpers.from_bytes(token or self.token)) + + def before_request(self, request, method, url, headers): + """Performs credential-specific before request logic. + + Refreshes the credentials if necessary, then calls :meth:`apply` to + apply the token to the authentication header. + + Args: + request (google.auth.transport.Request): A callable used to make + HTTP requests. + method (str): The request's HTTP method. + url (str): The request's URI. + headers (Mapping): The request's headers. + """ + # pylint: disable=unused-argument + # (Subclasses may use these arguments to ascertain information about + # the http request.) + if not self.valid: + self.refresh(request) + self.apply(headers) + + +@six.add_metaclass(abc.ABCMeta) +class ScopedCredentials(object): + """Interface for scoped credentials. + + OAuth 2.0-based credentials allow limiting access using scopes as described + in `RFC6749 `__. + If a credential class implements this interface then the credentials either + require or use scopes in their implementation. + + Credentials that require scopes to obtain access tokens must either be + constructed with scopes:: + + credentials = SomeScopedCredentials(scopes=['one', 'two']) + + Or must copy an existing instance using :meth:`with_scopes`:: + + scoped_credentials = credentials.with_scopes(scopes=['one', 'two']) + + Some credentials have scopes but do not allow or require scopes to be set. + You can check if scoping is necessary with :attr:`requires_scopes`:: + + if credentials.requires_scopes: + credentials = credentials.create_scoped(['one', 'two']) + + """ + def __init__(self): + super(ScopedCredentials, self).__init__() + self.__scopes = None + + @property + def _scopes(self): + return self.__scopes + + @_scopes.setter + def _scopes(self, value): + self.__scopes = _helpers.string_to_scopes(value) + + @abc.abstractproperty + def requires_scopes(self): + """True if these credentials require scopes to obtain an access token. + """ + return False + + @abc.abstractmethod + def with_scopes(self, scopes): + """Create a copy of these credentials with the specified scopes. + + Args: + scopes (Union[str, Sequence]): The scope or list of scopes to + request. + + Raises: + NotImplementedError: If the credentials' scopes can not be changed. + This can be avoided by checking :attr:`requires_scopes` before + calling this method. + """ + raise NotImplementedError('This class does not require scoping.') + + def has_scopes(self, scopes): + """Checks if the credentials have the given scopes. + + .. warning: This method is not guarenteed to be accurate if the + credentials are :attr:`~Credentials.invalid`. + + Returns: + bool: True if the credentials have the given scopes. + """ + return set(scopes).issubset(set(self._scopes or [])) + + +@six.add_metaclass(abc.ABCMeta) +class SigningCredentials(object): + """Interface for credentials that can cryptographically sign messages.""" + + @abc.abstractmethod + def sign_bytes(self, message): + """Signs the given message. + + Args: + message (bytes): The message to sign. + + Returns: + bytes: The messages cryptographic signature. + """ + # pylint: disable=missing-raises-doc,redundant-returns-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError('Sign bytes must be implemented.') diff --git a/tests/test__helpers.py b/tests/test__helpers.py index d475fc4bc..e7841ee8a 100644 --- a/tests/test__helpers.py +++ b/tests/test__helpers.py @@ -93,3 +93,33 @@ def test_update_query_remove_param(): uri = base_uri + '?x=a' updated = _helpers.update_query(uri, {'y': 'c'}, remove=['x']) _assert_query(updated, {'y': ['c']}) + + +def test_scopes_to_string(): + cases = [ + ('', ''), + ('', ()), + ('', []), + ('', ('',)), + ('', ['', ]), + ('a', ('a',)), + ('b', ['b', ]), + ('a b', ['a', 'b']), + ('a b', ('a', 'b')), + ('a b', 'a b'), + ('a b', (s for s in ['a', 'b'])), + ] + for expected, case in cases: + assert _helpers.scopes_to_string(case) == expected + + +def test_string_to_scopes(): + cases = [ + (['a', 'b'], ['a', 'b']), + ('', []), + ('a', ['a']), + ('a b c d e f', ['a', 'b', 'c', 'd', 'e', 'f']), + ] + + for case, expected in cases: + assert _helpers.string_to_scopes(case) == expected diff --git a/tests/test_credentials.py b/tests/test_credentials.py new file mode 100644 index 000000000..87ecc0452 --- /dev/null +++ b/tests/test_credentials.py @@ -0,0 +1,100 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +from google.auth import credentials + + +class CredentialsImpl(credentials.Credentials): + def refresh(self, request): + self.token = request + + +def test_credentials_constructor(): + credentials = CredentialsImpl() + assert not credentials.token + assert not credentials.expiry + assert not credentials.expired + assert not credentials.valid + + +def test_expired_and_valid(): + credentials = CredentialsImpl() + credentials.token = 'token' + + assert credentials.valid + assert not credentials.expired + + credentials.expiry = ( + datetime.datetime.utcnow() - datetime.timedelta(seconds=60)) + + assert not credentials.valid + assert credentials.expired + + +def test_before_request(): + credentials = CredentialsImpl() + request = 'token' + headers = {} + + # First call should call refresh, setting the token. + credentials.before_request(request, 'http://example.com', 'GET', headers) + assert credentials.valid + assert credentials.token == 'token' + assert headers[b'authorization'] == 'Bearer token' + + request = 'token2' + headers = {} + + # Second call shouldn't call refresh. + credentials.before_request(request, 'http://example.com', 'GET', headers) + assert credentials.valid + assert credentials.token == 'token' + assert headers[b'authorization'] == 'Bearer token' + + +class ScopedCredentialsImpl(credentials.ScopedCredentials, CredentialsImpl): + @property + def requires_scopes(self): + return super(ScopedCredentialsImpl, self).requires_scopes + + def with_scopes(self, scopes): + raise NotImplementedError + + +def test_scoped_credentials_constructor(): + credentials = ScopedCredentialsImpl() + assert credentials._scopes is None + + +def test_scoped_credentials_scopes(): + credentials = ScopedCredentialsImpl() + + credentials._scopes = ['one', 'two'] + assert credentials.has_scopes(['one']) + assert credentials.has_scopes(['two']) + assert credentials.has_scopes(['one', 'two']) + assert not credentials.has_scopes(['three']) + + credentials._scopes = 'one two' + assert credentials.has_scopes(['one']) + assert credentials.has_scopes(['two']) + assert credentials.has_scopes(['one', 'two']) + assert not credentials.has_scopes(['three']) + + +def test_scoped_credentials_requires_scopes(): + credentials = ScopedCredentialsImpl() + assert not credentials.requires_scopes From db9aa4c4253b6cee31c0a34c7ff7ed1e6717ed98 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 4 Oct 2016 14:51:10 -0700 Subject: [PATCH 2/8] Fix inline hyperlink --- google/auth/credentials.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 162b101d0..828fecbc2 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -124,7 +124,7 @@ class ScopedCredentials(object): """Interface for scoped credentials. OAuth 2.0-based credentials allow limiting access using scopes as described - in `RFC6749 `__. + in `RFC6749 Section 3.3`_. If a credential class implements this interface then the credentials either require or use scopes in their implementation. @@ -143,6 +143,7 @@ class ScopedCredentials(object): if credentials.requires_scopes: credentials = credentials.create_scoped(['one', 'two']) + .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3 """ def __init__(self): super(ScopedCredentials, self).__init__() From 78c615c01586b0395ce1ad3d093a9b03004b9307 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Mon, 10 Oct 2016 13:14:13 -0700 Subject: [PATCH 3/8] Fix utcnow usage --- google/auth/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 828fecbc2..2403c0d34 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -65,7 +65,7 @@ def expired(self): Note that credentials can be invalid but not expired becaue Credentials with :attr:`expiry` set to None is considered to never expire. """ - now = _helpers.now() + now = _helpers.utcnow() if self.expiry is None or self.expiry > now: return False else: From cd526b97cb071d99f6dedbb96b408bbc8426c627 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Mon, 10 Oct 2016 16:52:34 -0700 Subject: [PATCH 4/8] Address review comments --- google/auth/credentials.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 2403c0d34..b6693aa8c 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -49,15 +49,6 @@ def __init__(self): """Optional[datetime]: When the token expires and is no longer valid. If this is None, the token is assumed to never expire.""" - @property - def valid(self): - """Checks the validity of the credentials. - - This is True if the credentials have a :attr:`token` and the token - is not :attr:`expired`. - """ - return self.token is not None and not self.expired - @property def expired(self): """Checks if the credentials are expired. @@ -66,17 +57,23 @@ def expired(self): with :attr:`expiry` set to None is considered to never expire. """ now = _helpers.utcnow() - if self.expiry is None or self.expiry > now: - return False - else: - return True + return self.expiry is not None and self.expiry <= now + + @property + def valid(self): + """Checks the validity of the credentials. + + This is True if the credentials have a :attr:`token` and the token + is not :attr:`expired`. + """ + return self.token is not None and not self.expired @abc.abstractmethod def refresh(self, request): """Refreshes the access token. Args: - request (google.auth.transport.Request): A callable used to make + request (google.auth.transport.Request): The object used to make HTTP requests. Raises: @@ -105,7 +102,7 @@ def before_request(self, request, method, url, headers): apply the token to the authentication header. Args: - request (google.auth.transport.Request): A callable used to make + request (google.auth.transport.Request): the object used to make HTTP requests. method (str): The request's HTTP method. url (str): The request's URI. @@ -181,7 +178,7 @@ def with_scopes(self, scopes): def has_scopes(self, scopes): """Checks if the credentials have the given scopes. - .. warning: This method is not guarenteed to be accurate if the + .. warning: This method is not guaranteed to be accurate if the credentials are :attr:`~Credentials.invalid`. Returns: @@ -202,7 +199,7 @@ def sign_bytes(self, message): message (bytes): The message to sign. Returns: - bytes: The messages cryptographic signature. + bytes: The message's cryptographic signature. """ # pylint: disable=missing-raises-doc,redundant-returns-doc # (pylint doesn't recognize that this is abstract) From 2aa80a6ad9b3540a2fe649c8e32b5e7cbccdf488 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 11 Oct 2016 14:25:22 -0700 Subject: [PATCH 5/8] Make scopes public read-only, restrict type to sequence --- google/auth/credentials.py | 14 +++++--------- tests/test_credentials.py | 8 +------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index b6693aa8c..88eaaa433 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -144,15 +144,12 @@ class ScopedCredentials(object): """ def __init__(self): super(ScopedCredentials, self).__init__() - self.__scopes = None + self._scopes = None @property - def _scopes(self): - return self.__scopes - - @_scopes.setter - def _scopes(self, value): - self.__scopes = _helpers.string_to_scopes(value) + def scopes(self): + """Sequence[str]: the credentials' current set of scopes.""" + return self._scopes @abc.abstractproperty def requires_scopes(self): @@ -165,8 +162,7 @@ def with_scopes(self, scopes): """Create a copy of these credentials with the specified scopes. Args: - scopes (Union[str, Sequence]): The scope or list of scopes to - request. + scopes (Sequence[str]): The list of scopes to request. Raises: NotImplementedError: If the credentials' scopes can not be changed. diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 87ecc0452..bb28f377a 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -81,14 +81,8 @@ def test_scoped_credentials_constructor(): def test_scoped_credentials_scopes(): credentials = ScopedCredentialsImpl() - credentials._scopes = ['one', 'two'] - assert credentials.has_scopes(['one']) - assert credentials.has_scopes(['two']) - assert credentials.has_scopes(['one', 'two']) - assert not credentials.has_scopes(['three']) - - credentials._scopes = 'one two' + assert credentials.scopes == ['one', 'two'] assert credentials.has_scopes(['one']) assert credentials.has_scopes(['two']) assert credentials.has_scopes(['one', 'two']) From 2b83967082d246e5f3a6c8cdce7e17b366321cce Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 14 Oct 2016 12:59:00 -0700 Subject: [PATCH 6/8] Rename mixin classes --- google/auth/credentials.py | 6 +++--- tests/test_credentials.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 88eaaa433..4091df074 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -117,7 +117,7 @@ def before_request(self, request, method, url, headers): @six.add_metaclass(abc.ABCMeta) -class ScopedCredentials(object): +class Scoped(object): """Interface for scoped credentials. OAuth 2.0-based credentials allow limiting access using scopes as described @@ -143,7 +143,7 @@ class ScopedCredentials(object): .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3 """ def __init__(self): - super(ScopedCredentials, self).__init__() + super(Scoped, self).__init__() self._scopes = None @property @@ -184,7 +184,7 @@ def has_scopes(self, scopes): @six.add_metaclass(abc.ABCMeta) -class SigningCredentials(object): +class Signing(object): """Interface for credentials that can cryptographically sign messages.""" @abc.abstractmethod diff --git a/tests/test_credentials.py b/tests/test_credentials.py index bb28f377a..7aa41b7fd 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -65,7 +65,7 @@ def test_before_request(): assert headers[b'authorization'] == 'Bearer token' -class ScopedCredentialsImpl(credentials.ScopedCredentials, CredentialsImpl): +class ScopedCredentialsImpl(credentials.Scoped, CredentialsImpl): @property def requires_scopes(self): return super(ScopedCredentialsImpl, self).requires_scopes From 602be6750e8d922fd9f819dc138351d83afb9a79 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 14 Oct 2016 13:15:34 -0700 Subject: [PATCH 7/8] Remove unneeded use of byte strings --- google/auth/credentials.py | 2 +- tests/test_credentials.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 4091df074..91ed45843 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -92,7 +92,7 @@ def apply(self, headers, token=None): token (Optional[str]): If specified, overrides the current access token. """ - headers[b'authorization'] = 'Bearer {}'.format( + headers['authorization'] = 'Bearer {}'.format( _helpers.from_bytes(token or self.token)) def before_request(self, request, method, url, headers): diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 7aa41b7fd..d78c554d5 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -53,7 +53,7 @@ def test_before_request(): credentials.before_request(request, 'http://example.com', 'GET', headers) assert credentials.valid assert credentials.token == 'token' - assert headers[b'authorization'] == 'Bearer token' + assert headers['authorization'] == 'Bearer token' request = 'token2' headers = {} @@ -62,7 +62,7 @@ def test_before_request(): credentials.before_request(request, 'http://example.com', 'GET', headers) assert credentials.valid assert credentials.token == 'token' - assert headers[b'authorization'] == 'Bearer token' + assert headers['authorization'] == 'Bearer token' class ScopedCredentialsImpl(credentials.Scoped, CredentialsImpl): From f522fc427708ab6a16b4adbae92b982623e6f62b Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 14 Oct 2016 14:06:19 -0700 Subject: [PATCH 8/8] Address offline review comments --- google/auth/_helpers.py | 31 +++++++++++++------------------ google/auth/credentials.py | 19 +++++++++++-------- tests/test__helpers.py | 3 --- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index 54c6e65bb..9c49e06f4 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -137,33 +137,28 @@ def update_query(url, params, remove=None): def scopes_to_string(scopes): - """Converts scope value to a string. - If scopes is a string then it is simply passed through. If scopes is an - iterable then a string is returned that is all the individual scopes - concatenated with spaces. + """Converts scope value to a string suitable for sending to OAuth 2.0 + authorization servers. + Args: - scopes (Union[Sequence, str]) + scopes (Sequence[str]): The sequence of scopes to convert. + Returns: str: The scopes formatted as a single string. """ - if isinstance(scopes, six.string_types): - return scopes - else: - return ' '.join(scopes) + return ' '.join(scopes) def string_to_scopes(scopes): - """Converts stringifed scope value to a list. - If scopes is a list then it is simply passed through. If scopes is an - string then a list of each individual scope is returned. + """Converts stringifed scopes value to a list. + Args: - scopes (Union[Sequence, str]) + scopes (Union[Sequence, str]): The string of space-separated scopes + to convert. Returns: - list: The scopes in a list. + Sequence(str): The separated scopes. """ if not scopes: return [] - elif isinstance(scopes, six.string_types): - return scopes.split(' ') - else: - return scopes + + return scopes.split(' ') diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 91ed45843..b10e63e14 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -123,10 +123,16 @@ class Scoped(object): OAuth 2.0-based credentials allow limiting access using scopes as described in `RFC6749 Section 3.3`_. If a credential class implements this interface then the credentials either - require or use scopes in their implementation. + use scopes in their implementation. - Credentials that require scopes to obtain access tokens must either be - constructed with scopes:: + Some credentials require scopes in order to obtain a token. You can check + if scoping is necessary with :attr:`requires_scopes`:: + + if credentials.requires_scopes: + # Scoping is required. + credentials = credentials.create_scoped(['one', 'two']) + + Credentials that require scopes must either be constructed with scopes:: credentials = SomeScopedCredentials(scopes=['one', 'two']) @@ -134,11 +140,8 @@ class Scoped(object): scoped_credentials = credentials.with_scopes(scopes=['one', 'two']) - Some credentials have scopes but do not allow or require scopes to be set. - You can check if scoping is necessary with :attr:`requires_scopes`:: - - if credentials.requires_scopes: - credentials = credentials.create_scoped(['one', 'two']) + Some credentials have scopes but do not allow or require scopes to be set, + these credentials can be used as-is. .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3 """ diff --git a/tests/test__helpers.py b/tests/test__helpers.py index e7841ee8a..86cacefef 100644 --- a/tests/test__helpers.py +++ b/tests/test__helpers.py @@ -97,7 +97,6 @@ def test_update_query_remove_param(): def test_scopes_to_string(): cases = [ - ('', ''), ('', ()), ('', []), ('', ('',)), @@ -106,7 +105,6 @@ def test_scopes_to_string(): ('b', ['b', ]), ('a b', ['a', 'b']), ('a b', ('a', 'b')), - ('a b', 'a b'), ('a b', (s for s in ['a', 'b'])), ] for expected, case in cases: @@ -115,7 +113,6 @@ def test_scopes_to_string(): def test_string_to_scopes(): cases = [ - (['a', 'b'], ['a', 'b']), ('', []), ('a', ['a']), ('a b c d e f', ['a', 'b', 'c', 'd', 'e', 'f']),