feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
Dependent Modules
=================
This code has the following dependencies
above and beyond the Python standard library:
oauth2client - Apache License 2.0
pyu2f - Apache License 2.0

View File

@@ -0,0 +1 @@
#!/usr/bin/env python

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python
# Copyright 2017 Google Inc. All rights reserved.
#
# 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 getpass
import sys
def get_user_password(text):
"""Get password from user.
Override this function with a different logic if you are using this library
outside a CLI.
Args:
text: message for the password prompt.
Returns: password string.
"""
return getpass.getpass(text)
def is_interactive():
"""Check if we are in an interractive environment.
If the rapt token needs refreshing, the user needs to answer the
challenges.
If the user is not in an interractive environment, the challenges can not
be answered and we just wait for timeout for no reason.
Returns: True if is interactive environment, False otherwise.
"""
return sys.stdin.isatty()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python
# Copyright 2018 Google Inc. All rights reserved.
#
# 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.
"""Client for interacting with the Reauth HTTP API.
This module provides the ability to do the following with the API:
1. Get a list of challenges needed to obtain additional authorization.
2. Send the result of the challenge to obtain a rapt token.
3. A modified version of the standard OAuth2.0 refresh grant that takes a rapt
token.
"""
import json
from six.moves import urllib
from google_reauth import errors
_REAUTH_API = 'https://reauth.googleapis.com/v2/sessions'
def _handle_errors(msg):
"""Raise an exception if msg has errors.
Args:
msg: parsed json from http response.
Returns: input response.
Raises: ReauthAPIError
"""
if 'error' in msg:
raise errors.ReauthAPIError(msg['error']['message'])
return msg
def _endpoint_request(http_request, path, body, access_token):
_, content = http_request(
uri='{0}{1}'.format(_REAUTH_API, path),
method='POST',
body=json.dumps(body),
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)
response = json.loads(content)
_handle_errors(response)
return response
def get_challenges(
http_request, supported_challenge_types, access_token,
requested_scopes=None):
"""Does initial request to reauth API to get the challenges.
Args:
http_request (Callable): callable to run http requests. Accepts uri,
method, body and headers. Returns a tuple: (response, content)
supported_challenge_types (Sequence[str]): list of challenge names
supported by the manager.
access_token (str): Access token with reauth scopes.
requested_scopes (list[str]): Authorized scopes for the credentials.
Returns:
dict: The response from the reauth API.
"""
body = {'supportedChallengeTypes': supported_challenge_types}
if requested_scopes:
body['oauthScopesForDomainPolicyLookup'] = requested_scopes
return _endpoint_request(
http_request, ':start', body, access_token)
def send_challenge_result(
http_request, session_id, challenge_id, client_input, access_token):
"""Attempt to refresh access token by sending next challenge result.
Args:
http_request (Callable): callable to run http requests. Accepts uri,
method, body and headers. Returns a tuple: (response, content)
session_id (str): session id returned by the initial reauth call.
challenge_id (str): challenge id returned by the initial reauth call.
client_input: dict with a challenge-specific client input. For example:
``{'credential': password}`` for password challenge.
access_token (str): Access token with reauth scopes.
Returns:
dict: The response from the reauth API.
"""
body = {
'sessionId': session_id,
'challengeId': challenge_id,
'action': 'RESPOND',
'proposalResponse': client_input,
}
return _endpoint_request(
http_request, '/{0}:continue'.format(session_id), body, access_token)
def refresh_grant(
http_request, client_id, client_secret, refresh_token,
token_uri, scopes=None, rapt=None, headers={}):
"""Implements the OAuth 2.0 Refresh Grant with the addition of the reauth
token.
Args:
http_request (Callable): callable to run http requests. Accepts uri,
method, body and headers. Returns a tuple: (response, content)
client_id (str): client id to get access token for reauth scope.
client_secret (str): client secret for the client_id
refresh_token (str): refresh token to refresh access token
token_uri (str): uri to refresh access token
scopes (str): scopes required by the client application as a
comma-joined list.
rapt (str): RAPT token
headers (dict): headers for http request
Returns:
Tuple[str, dict]: http response and parsed response content.
"""
parameters = {
'grant_type': 'refresh_token',
'client_id': client_id,
'client_secret': client_secret,
'refresh_token': refresh_token,
}
if scopes:
parameters['scope'] = scopes
if rapt:
parameters['rapt'] = rapt
body = urllib.parse.urlencode(parameters)
response, content = http_request(
uri=token_uri,
method='POST',
body=body,
headers=headers)
return response, content

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python
"""Run all the test for google_reauth."""
import unittest
from google_reauth.tests.test_challenges import ChallengesTest
from google_reauth.tests.test_reauth import ReauthTest
from google_reauth.tests.test_reauth_creds import ReauthCredsTest
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python
# Copyright 2017 Google Inc. All rights reserved.
#
# 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 abc
import base64
import sys
import pyu2f.convenience.authenticator
import pyu2f.errors
import pyu2f.model
import six
from google_reauth import _helpers, errors
REAUTH_ORIGIN = 'https://accounts.google.com'
@six.add_metaclass(abc.ABCMeta)
class ReauthChallenge(object):
"""Base class for reauth challenges."""
@property
@abc.abstractmethod
def name(self):
"""Returns the name of the challenge."""
pass
@property
@abc.abstractmethod
def is_locally_eligible(self):
"""Returns true if a challenge is supported locally on this machine."""
pass
@abc.abstractmethod
def obtain_challenge_input(self, metadata):
"""Performs logic required to obtain credentials and returns it.
Args:
metadata: challenge metadata returned in the 'challenges' field in
the initial reauth request. Includes the 'challengeType' field
and other challenge-specific fields.
Returns:
response that will be send to the reauth service as the content of
the 'proposalResponse' field in the request body. Usually a dict
with the keys specific to the challenge. For example,
{'credential': password} for password challenge.
"""
pass
class PasswordChallenge(ReauthChallenge):
"""Challenge that asks for user's password."""
@property
def name(self):
return 'PASSWORD'
@property
def is_locally_eligible(self):
return True
def obtain_challenge_input(self, unused_metadata):
passwd = _helpers.get_user_password('Please enter your password:')
if not passwd:
passwd = ' ' # avoid the server crashing in case of no password :D
return {'credential': passwd}
class SecurityKeyChallenge(ReauthChallenge):
"""Challenge that asks for user's security key touch."""
@property
def name(self):
return 'SECURITY_KEY'
@property
def is_locally_eligible(self):
return True
def obtain_challenge_input(self, metadata):
sk = metadata['securityKey']
challenges = sk['challenges']
app_id = sk['applicationId']
challenge_data = []
for c in challenges:
kh = c['keyHandle'].encode('ascii')
key = pyu2f.model.RegisteredKey(
bytearray(base64.urlsafe_b64decode(kh)))
challenge = c['challenge'].encode('ascii')
challenge = base64.urlsafe_b64decode(challenge)
challenge_data.append({'key': key, 'challenge': challenge})
try:
api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
REAUTH_ORIGIN)
response = api.Authenticate(app_id, challenge_data,
print_callback=sys.stderr.write)
return {'securityKey': response}
except pyu2f.errors.U2FError as e:
if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
sys.stderr.write('Ineligible security key.\n')
elif e.code == pyu2f.errors.U2FError.TIMEOUT:
sys.stderr.write(
'Timed out while waiting for security key touch.\n')
else:
raise e
except pyu2f.errors.NoDeviceFoundError:
sys.stderr.write('No security key found.\n')
return None
class SamlChallenge(ReauthChallenge):
"""Challenge that asks the users to browse to their ID Providers."""
@property
def name(self):
return 'SAML'
@property
def is_locally_eligible(self):
return True
def obtain_challenge_input(self, metadata):
# Magic Arch does not fully support returning a proper redirect URL
# for programmatic SAML users today. So we error out here and request
# users to complete a web login.
raise errors.ReauthSamlLoginRequiredError()
AVAILABLE_CHALLENGES = {
challenge.name: challenge
for challenge in [
SecurityKeyChallenge(),
PasswordChallenge(),
SamlChallenge()
]
}

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
# Copyright 2017 Google Inc. All rights reserved.
#
# 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.
"""A module that provides rapt authentication errors."""
class ReauthError(Exception):
"""Base exception for reauthentication."""
pass
class HttpAccessTokenRefreshError(Exception):
"""Error (with HTTP status) trying to refresh an expired access token."""
def __init__(self, message, status=None):
super(HttpAccessTokenRefreshError, self).__init__(message)
self.status = status
class ReauthUnattendedError(ReauthError):
"""An exception for when reauth cannot be answered."""
def __init__(self):
super(ReauthUnattendedError, self).__init__(
'Reauthentication challenge could not be answered because you are '
'not in an interactive session.')
class ReauthFailError(ReauthError):
"""An exception for when reauth failed."""
def __init__(self, message=None):
super(ReauthFailError, self).__init__(
'Reauthentication challenge failed. {0}'.format(message))
class ReauthAPIError(ReauthError):
"""An exception for when reauth API returned something we can't handle."""
def __init__(self, api_error):
super(ReauthAPIError, self).__init__(
'Reauthentication challenge failed due to API error: {0}.'.format(
api_error))
class ReauthAccessTokenRefreshError(ReauthError):
"""An exception for when we can't get an access token for reauth."""
def __init__(self, message=None, status=None):
super(ReauthAccessTokenRefreshError, self).__init__(
'Failed to get an access token for reauthentication. {0}'.format(
message))
self.status = status
class ReauthSamlLoginRequiredError(ReauthError):
"""An exception for when web login is required to complete reauth.
This applies to SAML users who are required to login through their IDP to
complete reauth.
"""
def __init__(self):
super(ReauthSamlLoginRequiredError, self).__init__(
'SAML login is required for the current account to complete '
'reauthentication.')

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python
# Copyright 2017 Google Inc. All rights reserved.
#
# 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.
"""A module that provides functions for handling rapt authentication.
Reauth is a process of obtaining additional authentication (such as password,
security token, etc.) while refreshing OAuth 2.0 credentials for a user.
Credentials that use the Reauth flow must have the reauth scope,
``https://www.googleapis.com/auth/accounts.reauth``.
This module provides a high-level function for executing the Reauth process,
:func:`refresh_access_token`, and lower-level helpers for doing the individual
steps of the reauth process.
Those steps are:
1. Obtaining a list of challenges from the reauth server.
2. Running through each challenge and sending the result back to the reauth
server.
3. Refreshing the access token using the returned rapt token.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import json
import sys
from google_reauth import challenges
from google_reauth import errors
from google_reauth import _helpers
from google_reauth import _reauth_client
from six.moves import http_client
from six.moves import range
_REAUTH_SCOPE = 'https://www.googleapis.com/auth/accounts.reauth'
_REAUTH_NEEDED_ERROR = 'invalid_grant'
_REAUTH_NEEDED_ERROR_INVALID_RAPT = 'invalid_rapt'
_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = 'rapt_required'
_AUTHENTICATED = 'AUTHENTICATED'
_CHALLENGE_REQUIRED = 'CHALLENGE_REQUIRED'
_CHALLENGE_PENDING = 'CHALLENGE_PENDING'
def _run_next_challenge(msg, http_request, access_token):
"""Get the next challenge from msg and run it.
Args:
msg: Reauth API response body (either from the initial request to
https://reauth.googleapis.com/v2/sessions:start or from sending the
previous challenge response to
https://reauth.googleapis.com/v2/sessions/id:continue)
http_request: callable to run http requests. Accepts uri, method, body
and headers. Returns a tuple: (response, content)
access_token: reauth access token
Returns: rapt token.
Raises:
errors.ReauthError if reauth failed
"""
for challenge in msg['challenges']:
if challenge['status'] != 'READY':
# Skip non-activated challneges.
continue
c = challenges.AVAILABLE_CHALLENGES.get(
challenge['challengeType'], None)
if not c:
raise errors.ReauthFailError(
'Unsupported challenge type {0}. Supported types: {1}'
.format(challenge['challengeType'],
','.join(list(challenges.AVAILABLE_CHALLENGES.keys())))
)
if not c.is_locally_eligible:
raise errors.ReauthFailError(
'Challenge {0} is not locally eligible'
.format(challenge['challengeType']))
client_input = c.obtain_challenge_input(challenge)
if not client_input:
return None
return _reauth_client.send_challenge_result(
http_request,
msg['sessionId'],
challenge['challengeId'],
client_input,
access_token)
return None
def _obtain_rapt(http_request, access_token, requested_scopes, rounds_num=5):
"""Given an http request method and reauth access token, get rapt token.
Args:
http_request: callable to run http requests. Accepts uri, method, body
and headers. Returns a tuple: (response, content)
access_token: reauth access token
requested_scopes: scopes required by the client application
rounds_num: max number of attempts to get a rapt after the next
challenge, before failing the reauth. This defines total number of
challenges + number of additional retries if the chalenge input
wasn't accepted.
Returns: rapt token.
Raises:
errors.ReauthError if reauth failed
"""
msg = None
for _ in range(0, rounds_num):
if not msg:
msg = _reauth_client.get_challenges(
http_request,
list(challenges.AVAILABLE_CHALLENGES.keys()),
access_token,
requested_scopes)
if msg['status'] == _AUTHENTICATED:
return msg['encodedProofOfReauthToken']
if not (msg['status'] == _CHALLENGE_REQUIRED or
msg['status'] == _CHALLENGE_PENDING):
raise errors.ReauthAPIError(
'Challenge status {0}'.format(msg['status']))
if not _helpers.is_interactive():
raise errors.ReauthUnattendedError()
msg = _run_next_challenge(msg, http_request, access_token)
# If we got here it means we didn't get authenticated.
raise errors.ReauthFailError()
def get_rapt_token(http_request, client_id, client_secret, refresh_token,
token_uri, scopes=None):
"""Given an http request method and refresh_token, get rapt token.
Args:
http_request: callable to run http requests. Accepts uri, method, body
and headers. Returns a tuple: (response, content)
client_id: client id to get access token for reauth scope.
client_secret: client secret for the client_id
refresh_token: refresh token to refresh access token
token_uri: uri to refresh access token
scopes: scopes required by the client application
Returns: rapt token.
Raises:
errors.ReauthError if reauth failed
"""
sys.stderr.write('Reauthentication required.\n')
# Get access token for reauth.
response, content = _reauth_client.refresh_grant(
http_request=http_request,
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
token_uri=token_uri,
scopes=_REAUTH_SCOPE,
headers={'Content-Type': 'application/x-www-form-urlencoded'})
try:
content = json.loads(content)
except (TypeError, ValueError):
raise errors.ReauthAccessTokenRefreshError(
'Invalid response {0}'.format(_substr_for_error_message(content)))
if response.status != http_client.OK:
raise errors.ReauthAccessTokenRefreshError(
_get_refresh_error_message(content), response.status)
if 'access_token' not in content:
raise errors.ReauthAccessTokenRefreshError(
'Access token missing from the response')
# Get rapt token from reauth API.
rapt_token = _obtain_rapt(
http_request,
content['access_token'],
requested_scopes=scopes)
return rapt_token
def _rapt_refresh_required(content):
"""Checks if the rapt refresh is required.
Args:
content: refresh response content
Returns:
True if rapt refresh is required.
"""
try:
content = json.loads(content)
except (TypeError, ValueError):
return False
return (
content.get('error') == _REAUTH_NEEDED_ERROR and
(content.get('error_subtype') == _REAUTH_NEEDED_ERROR_INVALID_RAPT or
content.get('error_subtype') == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED))
def _get_refresh_error_message(content):
"""Constructs an error from the http response.
Args:
response: http response
content: parsed response content
Returns:
error message to show
"""
error_msg = 'Invalid response.'
if 'error' in content:
error_msg = content['error']
if 'error_description' in content:
error_msg += ': ' + content['error_description']
return error_msg
def _substr_for_error_message(content):
"""Returns content string to include in the error message"""
return content if len(content) <= 100 else content[0:97] + "..."
def refresh_access_token(
http_request, client_id, client_secret, refresh_token,
token_uri, rapt=None, scopes=None, headers=None):
"""Refresh the access_token using the refresh_token.
Args:
http_request: callable to run http requests. Accepts uri, method, body
and headers. Returns a tuple: (response, content)
client_id: client id to get access token for reauth scope.
client_secret: client secret for the client_id
refresh_token: refresh token to refresh access token
token_uri: uri to refresh access token
scopes: scopes required by the client application
Returns:
Tuple[str, str, str, Optional[str], Optional[str], Optional[str]]: The
rapt token, the access token, new refresh token, expiration,
token id and response content returned by the token endpoint.
Raises:
errors.ReauthError if reauth failed
errors.HttpAccessTokenRefreshError it access token refresh failed
"""
response, content = _reauth_client.refresh_grant(
http_request=http_request,
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
token_uri=token_uri,
rapt=rapt,
headers=headers)
if response.status != http_client.OK:
# Check if we need a rapt token or if the rapt token is invalid.
# Once we refresh the rapt token, retry the access token refresh.
# If we did refresh the rapt token and still got an error, then the
# refresh token is expired or revoked.
if (_rapt_refresh_required(content)):
rapt = get_rapt_token(
http_request,
client_id,
client_secret,
refresh_token,
token_uri,
scopes=scopes,
)
# retry with refreshed rapt
response, content = _reauth_client.refresh_grant(
http_request=http_request,
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
token_uri=token_uri,
rapt=rapt,
headers=headers)
try:
content = json.loads(content)
except (TypeError, ValueError):
raise errors.HttpAccessTokenRefreshError(
'Invalid response {0}'.format(_substr_for_error_message(content)),
response.status)
if response.status != http_client.OK:
raise errors.HttpAccessTokenRefreshError(
_get_refresh_error_message(content), response.status)
access_token = content['access_token']
refresh_token = content.get('refresh_token', None)
expires_in = content.get('expires_in', None)
id_token = content.get('id_token', None)
return rapt, content, access_token, refresh_token, expires_in, id_token

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python
# Copyright 2017 Google Inc. All rights reserved.
#
# 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.
"""Two factor Oauth2Credentials."""
import datetime
import json
import logging
from oauth2client_4_0 import _helpers
from oauth2client_4_0 import client
from oauth2client_4_0 import transport
from google_reauth import errors
from google_reauth import reauth
_LOGGER = logging.getLogger(__name__)
class Oauth2WithReauthCredentials(client.OAuth2Credentials):
"""Credentials object that extends OAuth2Credentials with reauth support.
This class provides the same functionality as OAuth2Credentials, but adds
the support for reauthentication and rapt tokens. These credentials should
behave the same as OAuth2Credentials when the credentials don't use rauth.
"""
def __init__(self, *args, **kwargs):
"""Create an instance of Oauth2WithReauthCredentials.
A Oauth2WithReauthCredentials has an extra rapt_token."""
self.rapt_token = kwargs.pop('rapt_token', None)
super(Oauth2WithReauthCredentials, self).__init__(*args, **kwargs)
@classmethod
def from_json(cls, json_data):
"""Overrides."""
data = json.loads(_helpers._from_bytes(json_data))
if ((data.get('token_expiry')
and not isinstance(data['token_expiry'], datetime.datetime))):
try:
data['token_expiry'] = datetime.datetime.strptime(
data['token_expiry'], client.EXPIRY_FORMAT)
except ValueError:
data['token_expiry'] = None
kwargs = {}
for param in ('revoke_uri', 'id_token', 'id_token_jwt',
'token_response', 'scopes', 'token_info_uri',
'rapt_token'):
value = data.get(param, None)
if value is not None:
kwargs[param] = value
retval = cls(
data['access_token'],
data['client_id'],
data['client_secret'],
data['refresh_token'],
data['token_expiry'],
data['token_uri'],
data['user_agent'],
**kwargs
)
retval.invalid = data['invalid']
return retval
@classmethod
def from_OAuth2Credentials(cls, original):
"""Instantiate a Oauth2WithReauthCredentials from OAuth2Credentials."""
json = original.to_json()
return cls.from_json(json)
def _do_refresh_request(self, http):
"""Refresh the access_token using the refresh_token.
Args:
http: An object to be used to make HTTP requests.
rapt_refreshed: If we did or did not already refreshed the rapt
token.
Raises:
oauth2client_4_0.client.HttpAccessTokenRefreshError: if the refresh
fails.
"""
headers = self._generate_refresh_request_headers()
_LOGGER.info('Refreshing access_token')
def http_request(uri, method, body, headers):
response, content = transport.request(
http, uri, method=method,
body=body, headers=headers)
content = _helpers._from_bytes(content)
return response, content
try:
self._update(*reauth.refresh_access_token(
http_request,
self.client_id,
self.client_secret,
self.refresh_token,
self.token_uri,
rapt=self.rapt_token,
scopes=list(self.scopes),
headers=headers))
except (errors.ReauthAccessTokenRefreshError,
errors.HttpAccessTokenRefreshError) as e:
self.invalid = True
if self.store:
self.store.locked_put(self)
raise client.HttpAccessTokenRefreshError(e, status=e.status)
def _update(self, rapt, content, access_token, refresh_token=None,
expires_in=None, id_token=None):
if rapt:
self.rapt_token = rapt
self.token_response = content
self.access_token = access_token
self.refresh_token = (
refresh_token if refresh_token else self.refresh_token)
if expires_in:
delta = datetime.timedelta(seconds=int(expires_in))
self.token_expiry = delta + client._UTCNOW()
else:
self.token_expiry = None
self.id_token_jwt = id_token
self.id_token = (
client._extract_id_token(id_token) if id_token else None)
self.invalid = False
if self.store:
self.store.locked_put(self)