Skip to content

Commit

Permalink
Move read-only methods of 'Scoped' into new interface, 'ReadOnlyScope…
Browse files Browse the repository at this point in the history
…d'. (#195)

Not all subclasses of 'Scoped' can sanely implement 'with_scopes' (e.g, on
GCE the scopes are hard-wired in when creating the GCE node).

Make 'Scoped' derive from 'ReadOnlyScoped', adding the 'with_scopes' method.

Make GCE's 'credentials' class derive from 'ReadOnlyScoped'.

Closes #194.
  • Loading branch information
tseaver authored and Jon Wayne Parrott committed Sep 11, 2017
1 parent 9281ca0 commit 4246832
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 44 deletions.
16 changes: 1 addition & 15 deletions google/auth/compute_engine/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from google.auth.compute_engine import _metadata


class Credentials(credentials.Scoped, credentials.Credentials):
class Credentials(credentials.ReadOnnlyScoped, credentials.Credentials):
"""Compute Engine Credentials.
These credentials use the Google Compute Engine metadata server to obtain
Expand Down Expand Up @@ -105,17 +105,3 @@ def service_account_email(self):
def requires_scopes(self):
"""False: Compute Engine credentials can not be scoped."""
return False

def with_scopes(self, scopes):
"""Unavailable, Compute Engine credentials can not be scoped.
Scopes can only be set at Compute Engine instance creation time.
See the `Compute Engine authentication documentation`_ for details on
how to configure instance scopes.
.. _Compute Engine authentication documentation:
https://cloud.google.com/compute/docs/authentication#using
"""
raise NotImplementedError(
'Compute Engine credentials can not set scopes. Scopes must be '
'set when the Compute Engine instance is created.')
57 changes: 43 additions & 14 deletions google/auth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ def before_request(self, request, method, url, headers):


@six.add_metaclass(abc.ABCMeta)
class Scoped(object):
"""Interface for scoped credentials.
class ReadOnnlyScoped(object):
"""Interface for credentials whose scopes can be queried.
OAuth 2.0-based credentials allow limiting access using scopes as described
in `RFC6749 Section 3.3`_.
Expand Down Expand Up @@ -152,7 +152,7 @@ class Scoped(object):
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
"""
def __init__(self):
super(Scoped, self).__init__()
super(ReadOnnlyScoped, self).__init__()
self._scopes = None

@property
Expand All @@ -166,6 +166,46 @@ def requires_scopes(self):
"""
return False

def has_scopes(self, scopes):
"""Checks if the credentials have the given scopes.
.. warning: This method is not guaranteed 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 []))


class Scoped(ReadOnnlyScoped):
"""Interface for credentials whose scopes can be replaced while copying.
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
use scopes in their implementation.
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'])
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,
these credentials can be used as-is.
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
"""
@abc.abstractmethod
def with_scopes(self, scopes):
"""Create a copy of these credentials with the specified scopes.
Expand All @@ -180,17 +220,6 @@ def with_scopes(self, scopes):
"""
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 guaranteed 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 []))


def with_scopes_if_required(credentials, scopes):
"""Creates a copy of the credentials with scopes if scoping is required.
Expand Down
4 changes: 0 additions & 4 deletions tests/compute_engine/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,3 @@ def test_before_request_refreshes(self, get):

# Credentials should now be valid.
assert self.credentials.valid

def test_with_scopes(self):
with pytest.raises(NotImplementedError):
self.credentials.with_scopes(['one', 'two'])
20 changes: 9 additions & 11 deletions tests/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,20 @@ def test_before_request():
assert headers['authorization'] == 'Bearer token'


class ScopedCredentialsImpl(credentials.Scoped, CredentialsImpl):
class ReadOnnlyScopedCredentialsImpl(credentials.ReadOnnlyScoped,
CredentialsImpl):
@property
def requires_scopes(self):
return super(ScopedCredentialsImpl, self).requires_scopes

def with_scopes(self, scopes):
raise NotImplementedError
return super(ReadOnnlyScopedCredentialsImpl, self).requires_scopes


def test_scoped_credentials_constructor():
credentials = ScopedCredentialsImpl()
def test_readonly_scoped_credentials_constructor():
credentials = ReadOnnlyScopedCredentialsImpl()
assert credentials._scopes is None


def test_scoped_credentials_scopes():
credentials = ScopedCredentialsImpl()
def test_readonly_scoped_credentials_scopes():
credentials = ReadOnnlyScopedCredentialsImpl()
credentials._scopes = ['one', 'two']
assert credentials.scopes == ['one', 'two']
assert credentials.has_scopes(['one'])
Expand All @@ -101,8 +99,8 @@ def test_scoped_credentials_scopes():
assert not credentials.has_scopes(['three'])


def test_scoped_credentials_requires_scopes():
credentials = ScopedCredentialsImpl()
def test_readonly_scoped_credentials_requires_scopes():
credentials = ReadOnnlyScopedCredentialsImpl()
assert not credentials.requires_scopes


Expand Down

0 comments on commit 4246832

Please sign in to comment.