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