1# Copyright 2016 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16"""Interfaces for credentials."""
17
18import abc
19
20import six
21
22from google.auth import _helpers
23
24
25@six.add_metaclass(abc.ABCMeta)
26class Credentials(object):
27    """Base class for all credentials.
28
29    All credentials have a :attr:`token` that is used for authentication and
30    may also optionally set an :attr:`expiry` to indicate when the token will
31    no longer be valid.
32
33    Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
34    Credentials can do this automatically before the first HTTP request in
35    :meth:`before_request`.
36
37    Although the token and expiration will change as the credentials are
38    :meth:`refreshed <refresh>` and used, credentials should be considered
39    immutable. Various credentials will accept configuration such as private
40    keys, scopes, and other options. These options are not changeable after
41    construction. Some classes will provide mechanisms to copy the credentials
42    with modifications such as :meth:`ScopedCredentials.with_scopes`.
43    """
44
45    def __init__(self):
46        self.token = None
47        """str: The bearer token that can be used in HTTP headers to make
48        authenticated requests."""
49        self.expiry = None
50        """Optional[datetime]: When the token expires and is no longer valid.
51        If this is None, the token is assumed to never expire."""
52        self._quota_project_id = None
53        """Optional[str]: Project to use for quota and billing purposes."""
54
55    @property
56    def expired(self):
57        """Checks if the credentials are expired.
58
59        Note that credentials can be invalid but not expired because
60        Credentials with :attr:`expiry` set to None is considered to never
61        expire.
62        """
63        if not self.expiry:
64            return False
65
66        # Remove 10 seconds from expiry to err on the side of reporting
67        # expiration early so that we avoid the 401-refresh-retry loop.
68        skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
69        return _helpers.utcnow() >= skewed_expiry
70
71    @property
72    def valid(self):
73        """Checks the validity of the credentials.
74
75        This is True if the credentials have a :attr:`token` and the token
76        is not :attr:`expired`.
77        """
78        return self.token is not None and not self.expired
79
80    @property
81    def quota_project_id(self):
82        """Project to use for quota and billing purposes."""
83        return self._quota_project_id
84
85    @abc.abstractmethod
86    def refresh(self, request):
87        """Refreshes the access token.
88
89        Args:
90            request (google.auth.transport.Request): The object used to make
91                HTTP requests.
92
93        Raises:
94            google.auth.exceptions.RefreshError: If the credentials could
95                not be refreshed.
96        """
97        # pylint: disable=missing-raises-doc
98        # (pylint doesn't recognize that this is abstract)
99        raise NotImplementedError("Refresh must be implemented")
100
101    def apply(self, headers, token=None):
102        """Apply the token to the authentication header.
103
104        Args:
105            headers (Mapping): The HTTP request headers.
106            token (Optional[str]): If specified, overrides the current access
107                token.
108        """
109        headers["authorization"] = "Bearer {}".format(
110            _helpers.from_bytes(token or self.token)
111        )
112        if self.quota_project_id:
113            headers["x-goog-user-project"] = self.quota_project_id
114
115    def before_request(self, request, method, url, headers):
116        """Performs credential-specific before request logic.
117
118        Refreshes the credentials if necessary, then calls :meth:`apply` to
119        apply the token to the authentication header.
120
121        Args:
122            request (google.auth.transport.Request): The object used to make
123                HTTP requests.
124            method (str): The request's HTTP method or the RPC method being
125                invoked.
126            url (str): The request's URI or the RPC service's URI.
127            headers (Mapping): The request's headers.
128        """
129        # pylint: disable=unused-argument
130        # (Subclasses may use these arguments to ascertain information about
131        # the http request.)
132        if not self.valid:
133            self.refresh(request)
134        self.apply(headers)
135
136
137class CredentialsWithQuotaProject(Credentials):
138    """Abstract base for credentials supporting ``with_quota_project`` factory"""
139
140    def with_quota_project(self, quota_project_id):
141        """Returns a copy of these credentials with a modified quota project.
142
143        Args:
144            quota_project_id (str): The project to use for quota and
145                billing purposes
146
147        Returns:
148            google.oauth2.credentials.Credentials: A new credentials instance.
149        """
150        raise NotImplementedError("This credential does not support quota project.")
151
152
153class AnonymousCredentials(Credentials):
154    """Credentials that do not provide any authentication information.
155
156    These are useful in the case of services that support anonymous access or
157    local service emulators that do not use credentials.
158    """
159
160    @property
161    def expired(self):
162        """Returns `False`, anonymous credentials never expire."""
163        return False
164
165    @property
166    def valid(self):
167        """Returns `True`, anonymous credentials are always valid."""
168        return True
169
170    def refresh(self, request):
171        """Raises :class:`ValueError``, anonymous credentials cannot be
172        refreshed."""
173        raise ValueError("Anonymous credentials cannot be refreshed.")
174
175    def apply(self, headers, token=None):
176        """Anonymous credentials do nothing to the request.
177
178        The optional ``token`` argument is not supported.
179
180        Raises:
181            ValueError: If a token was specified.
182        """
183        if token is not None:
184            raise ValueError("Anonymous credentials don't support tokens.")
185
186    def before_request(self, request, method, url, headers):
187        """Anonymous credentials do nothing to the request."""
188
189
190@six.add_metaclass(abc.ABCMeta)
191class ReadOnlyScoped(object):
192    """Interface for credentials whose scopes can be queried.
193
194    OAuth 2.0-based credentials allow limiting access using scopes as described
195    in `RFC6749 Section 3.3`_.
196    If a credential class implements this interface then the credentials either
197    use scopes in their implementation.
198
199    Some credentials require scopes in order to obtain a token. You can check
200    if scoping is necessary with :attr:`requires_scopes`::
201
202        if credentials.requires_scopes:
203            # Scoping is required.
204            credentials = credentials.with_scopes(scopes=['one', 'two'])
205
206    Credentials that require scopes must either be constructed with scopes::
207
208        credentials = SomeScopedCredentials(scopes=['one', 'two'])
209
210    Or must copy an existing instance using :meth:`with_scopes`::
211
212        scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
213
214    Some credentials have scopes but do not allow or require scopes to be set,
215    these credentials can be used as-is.
216
217    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
218    """
219
220    def __init__(self):
221        super(ReadOnlyScoped, self).__init__()
222        self._scopes = None
223        self._default_scopes = None
224
225    @property
226    def scopes(self):
227        """Sequence[str]: the credentials' current set of scopes."""
228        return self._scopes
229
230    @property
231    def default_scopes(self):
232        """Sequence[str]: the credentials' current set of default scopes."""
233        return self._default_scopes
234
235    @abc.abstractproperty
236    def requires_scopes(self):
237        """True if these credentials require scopes to obtain an access token.
238        """
239        return False
240
241    def has_scopes(self, scopes):
242        """Checks if the credentials have the given scopes.
243
244        .. warning: This method is not guaranteed to be accurate if the
245            credentials are :attr:`~Credentials.invalid`.
246
247        Args:
248            scopes (Sequence[str]): The list of scopes to check.
249
250        Returns:
251            bool: True if the credentials have the given scopes.
252        """
253        credential_scopes = (
254            self._scopes if self._scopes is not None else self._default_scopes
255        )
256        return set(scopes).issubset(set(credential_scopes or []))
257
258
259class Scoped(ReadOnlyScoped):
260    """Interface for credentials whose scopes can be replaced while copying.
261
262    OAuth 2.0-based credentials allow limiting access using scopes as described
263    in `RFC6749 Section 3.3`_.
264    If a credential class implements this interface then the credentials either
265    use scopes in their implementation.
266
267    Some credentials require scopes in order to obtain a token. You can check
268    if scoping is necessary with :attr:`requires_scopes`::
269
270        if credentials.requires_scopes:
271            # Scoping is required.
272            credentials = credentials.create_scoped(['one', 'two'])
273
274    Credentials that require scopes must either be constructed with scopes::
275
276        credentials = SomeScopedCredentials(scopes=['one', 'two'])
277
278    Or must copy an existing instance using :meth:`with_scopes`::
279
280        scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
281
282    Some credentials have scopes but do not allow or require scopes to be set,
283    these credentials can be used as-is.
284
285    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
286    """
287
288    @abc.abstractmethod
289    def with_scopes(self, scopes, default_scopes=None):
290        """Create a copy of these credentials with the specified scopes.
291
292        Args:
293            scopes (Sequence[str]): The list of scopes to attach to the
294                current credentials.
295
296        Raises:
297            NotImplementedError: If the credentials' scopes can not be changed.
298                This can be avoided by checking :attr:`requires_scopes` before
299                calling this method.
300        """
301        raise NotImplementedError("This class does not require scoping.")
302
303
304def with_scopes_if_required(credentials, scopes, default_scopes=None):
305    """Creates a copy of the credentials with scopes if scoping is required.
306
307    This helper function is useful when you do not know (or care to know) the
308    specific type of credentials you are using (such as when you use
309    :func:`google.auth.default`). This function will call
310    :meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
311    the credentials require scoping. Otherwise, it will return the credentials
312    as-is.
313
314    Args:
315        credentials (google.auth.credentials.Credentials): The credentials to
316            scope if necessary.
317        scopes (Sequence[str]): The list of scopes to use.
318        default_scopes (Sequence[str]): Default scopes passed by a
319            Google client library. Use 'scopes' for user-defined scopes.
320
321    Returns:
322        google.auth.credentials.Credentials: Either a new set of scoped
323            credentials, or the passed in credentials instance if no scoping
324            was required.
325    """
326    if isinstance(credentials, Scoped) and credentials.requires_scopes:
327        return credentials.with_scopes(scopes, default_scopes=default_scopes)
328    else:
329        return credentials
330
331
332@six.add_metaclass(abc.ABCMeta)
333class Signing(object):
334    """Interface for credentials that can cryptographically sign messages."""
335
336    @abc.abstractmethod
337    def sign_bytes(self, message):
338        """Signs the given message.
339
340        Args:
341            message (bytes): The message to sign.
342
343        Returns:
344            bytes: The message's cryptographic signature.
345        """
346        # pylint: disable=missing-raises-doc,redundant-returns-doc
347        # (pylint doesn't recognize that this is abstract)
348        raise NotImplementedError("Sign bytes must be implemented.")
349
350    @abc.abstractproperty
351    def signer_email(self):
352        """Optional[str]: An email address that identifies the signer."""
353        # pylint: disable=missing-raises-doc
354        # (pylint doesn't recognize that this is abstract)
355        raise NotImplementedError("Signer email must be implemented.")
356
357    @abc.abstractproperty
358    def signer(self):
359        """google.auth.crypt.Signer: The signer used to sign bytes."""
360        # pylint: disable=missing-raises-doc
361        # (pylint doesn't recognize that this is abstract)
362        raise NotImplementedError("Signer must be implemented.")
363