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,63 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. 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.
"""Context managers related to credentials and authentication.
Context managers allow use of "with" syntax for managing credentials.
Example:
with CredentialProvidersManager():
# Task requiring credentials.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.iamcredentials import util as iamcred_util
from googlecloudsdk.core.credentials import store
class CredentialProvidersManager(object):
"""Context manager for handling credential provider registration."""
def __init__(self, credential_providers=None):
"""Initializes context manager with optional credential providers.
Args:
credential_providers (list[object]): List of provider classes like those
defined in core.credentials.store.py.
"""
self._credential_providers = credential_providers
def __enter__(self):
"""Registers sources for credentials and project for use by commands."""
self._credential_providers = self._credential_providers or [
store.GceCredentialProvider(),
]
for provider in self._credential_providers:
provider.Register()
# Register support for service account impersonation.
store.IMPERSONATION_TOKEN_PROVIDER = (
iamcred_util.ImpersonationAccessTokenProvider())
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
"""Cleans up credential providers."""
del [exc_type, exc_value, exc_traceback]
for provider in self._credential_providers:
provider.UnRegister()
store.IMPERSONATION_TOKEN_PROVIDER = None

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*- #
# Copyright 2014 Google LLC. 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.
"""Credentials for use with the developer shell."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.core.util import encoding
DEVSHELL_ENV = 'CLOUD_SHELL'
DEVSHELL_CLIENT_PORT = 'DEVSHELL_CLIENT_PORT'
def IsDevshellEnvironment():
return bool(encoding.GetEncodedValue(os.environ, DEVSHELL_ENV, False)) \
or HasDevshellAuth()
def HasDevshellAuth():
port = int(encoding.GetEncodedValue(os.environ, DEVSHELL_CLIENT_PORT, 0))
return port != 0

View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 Google LLC. 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.
"""Exceptions for authentications."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import textwrap
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
class Error(exceptions.Error):
"""Root error of this module."""
AUTH_LOGIN_COMMAND = 'gcloud auth login'
ADC_LOGIN_COMMAND = 'gcloud auth application-default login'
class InvalidCredentialsException(Error):
"""Exceptions to indicate that invalid credentials were found."""
class AuthenticationException(Error):
"""Exceptions that tell the users to re-login."""
def __init__(
self,
message,
for_adc=False,
should_relogin=True,
is_service_account=False,
):
if should_relogin:
if for_adc:
message = textwrap.dedent(
"""\
{message}
Please run:
$ {login_command}
to obtain new credentials.""".format(
message=message, login_command=ADC_LOGIN_COMMAND
)
)
elif is_service_account:
message = textwrap.dedent("""\
{message}
Please run:
$ gcloud auth activate-service-account --key-file=SERVICE_ACCOUNT_FILE_PATH
Or:
$ gcloud auth login --cred-file=SERVICE_ACCOUNT_FILE_PATH
to obtain new credentials.""".format(message=message))
else:
message = textwrap.dedent(
"""\
{message}
Please run:
$ {login_command}
to obtain new credentials.""".format(
message=message, login_command=AUTH_LOGIN_COMMAND
)
)
if not for_adc:
switch_account_msg = textwrap.dedent("""\
If you have already logged in with a different account, run:
$ gcloud config set account ACCOUNT
to select an already authenticated account to use.""")
message = '\n\n'.join([message, switch_account_msg])
super(AuthenticationException, self).__init__(message)
class NoActiveAccountException(AuthenticationException):
"""Exception for when there are no valid active credentials."""
def __init__(self, active_config_path=None):
if active_config_path:
if not os.path.exists(active_config_path):
log.warning('Could not open the configuration file: [%s].',
active_config_path)
super(
NoActiveAccountException,
self).__init__('You do not currently have an active account selected.')
class TokenRefreshError(AuthenticationException):
"""An exception raised when the auth tokens fail to refresh."""
def __init__(
self,
error,
for_adc=False,
should_relogin=True,
account=None,
is_service_account=False,
):
if account:
message = (
'There was a problem refreshing auth tokens for account {0}: {1}'
.format(account, error)
)
else:
message = (
'There was a problem refreshing your current auth tokens: {0}'.format(
error
)
)
super(TokenRefreshError, self).__init__(
message,
for_adc=for_adc,
should_relogin=should_relogin,
is_service_account=is_service_account,
)
class TokenRefreshDeniedByCAAError(TokenRefreshError):
"""Raises when token refresh is denied by context aware access policies."""
def __init__(self, error, for_adc=False):
# pylint: disable=g-import-not-at-top
from googlecloudsdk.core import context_aware
# pylint: enable=g-import-not-at-top
compiled_msg = '{}\n\n{}'.format(
error, context_aware.ContextAwareAccessError.Get())
super(TokenRefreshDeniedByCAAError, self).__init__(
compiled_msg, for_adc=for_adc, should_relogin=False)
class ReauthenticationException(Error):
"""Exceptions that tells the user to retry his command or run auth login."""
def __init__(self, message, for_adc=False):
login_command = ADC_LOGIN_COMMAND if for_adc else AUTH_LOGIN_COMMAND
super(ReauthenticationException, self).__init__(
textwrap.dedent("""\
{message}
Please retry your command or run:
$ {login_command}
to obtain new credentials.""".format(
message=message, login_command=login_command)))
class TokenRefreshReauthError(ReauthenticationException):
"""An exception raised when the auth tokens fail to refresh due to reauth."""
def __init__(self, error, for_adc=False):
message = ('There was a problem reauthenticating while refreshing your '
'current auth tokens: {0}').format(error)
super(TokenRefreshReauthError, self).__init__(message, for_adc=for_adc)
class WebLoginRequiredReauthError(Error):
"""An exception raised when login through browser is required for reauth.
This applies to SAML users who set password as their reauth method today.
Since SAML uers do not have knowledge of their Google password, we require
web login and allow users to be authenticated by their IDP.
"""
def __init__(self, for_adc=False):
login_command = ADC_LOGIN_COMMAND if for_adc else AUTH_LOGIN_COMMAND
super(WebLoginRequiredReauthError, self).__init__(
textwrap.dedent("""\
Please run:
$ {login_command}
to complete reauthentication.""".format(login_command=login_command)))

View File

@@ -0,0 +1,903 @@
# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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.
"""Run a web flow for oauth2."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
import contextlib
import os
import select
import socket
import sys
import webbrowser
import wsgiref
from google_auth_oauthlib import flow as google_auth_flow
from googlecloudsdk.core import config
from googlecloudsdk.core import exceptions as c_exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import requests
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.universe_descriptor import universe_descriptor
from googlecloudsdk.core.util import pkg_resources
from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors
from requests import exceptions as requests_exceptions
import six
from six.moves import input # pylint: disable=redefined-builtin
from six.moves.urllib import parse
_PORT_SEARCH_ERROR_MSG = (
'Failed to start a local webserver listening on any port '
'between {start_port} and {end_port}. Please check your '
'firewall settings or locally running programs that may be '
'blocking or using those ports.')
_PORT_SEARCH_START = 8085
_PORT_SEARCH_END = _PORT_SEARCH_START + 100
class Error(c_exceptions.Error):
"""Exceptions for the flow module."""
class AuthRequestRejectedError(Error):
"""Exception for when the authentication request was rejected."""
class AuthRequestFailedError(Error):
"""Exception for when the authentication request failed."""
class LocalServerCreationError(Error):
"""Exception for when a local server cannot be created."""
class LocalServerTimeoutError(Error):
"""Exception for when the local server timeout before receiving request."""
class WebBrowserInaccessible(Error):
"""Exception for when a web browser is required but not accessible."""
def RaiseProxyError(source_exc):
six.raise_from(
AuthRequestFailedError(
'Could not reach the login server. A potential cause of this could be'
' because you are behind a proxy. Please set the environment'
' variables HTTPS_PROXY and HTTP_PROXY to the address of the proxy in'
' the format "protocol://address:port" (without quotes) and try'
' again.\nExample: HTTPS_PROXY=http://192.168.0.1:8080'
),
source_exc,
)
def PromptForAuthCode(message, authorize_url, client_config=None):
ImportReadline(client_config)
log.err.Print(message.format(url=authorize_url))
return input(
'Once finished, enter the verification code provided in your browser: '
).strip()
@contextlib.contextmanager
def HandleOauth2FlowErrors():
"""Context manager for handling errors in the OAuth 2.0 flow."""
try:
yield
except requests_exceptions.ProxyError as e:
RaiseProxyError(e)
except (
rfc6749_errors.AccessDeniedError,
rfc6749_errors.InvalidGrantError,
) as e:
six.raise_from(AuthRequestRejectedError(e), e)
except rfc6749_errors.MissingTokenError:
# The real error is swallowed by the requests-oauthlib library. The
# exception we catch here just says "Missing access token parameter.". It's
# not helpful, so here we raise a new error to ask the user to run with
# --log-http to view the error response.
e = rfc6749_errors.MissingTokenError(
description=(
'Token is not returned from the token endpoint. Re-run the command'
' with --log-http to view the error response.'
)
)
raise six.raise_from(AuthRequestFailedError(e), e)
except ValueError as e:
raise six.raise_from(AuthRequestFailedError(e), e)
except rfc6749_errors.OAuth2Error as e:
raise six.raise_from(AuthRequestFailedError(e), e)
class WSGIServer(wsgiref.simple_server.WSGIServer):
"""WSGI server to handle more than one connections.
A normal WSGI server will handle connections one-by-one. When running a local
server to handle auth redirects, browser opens two connections. One connection
is used to send the authorization code. The other one is opened but not used.
Some browsers (i.e. Chrome) send data in the first connection. Other browsers
(i.e. Safari) send data in the second connection. To make the server working
for all these browsers, the server should be able to handle two connections
and smartly read data from the correct connection.
"""
# pylint: disable=invalid-name, follow the style of the base class.
def _conn_closed(self, conn):
"""Check if conn is closed at the client side."""
return not conn.recv(1024, socket.MSG_PEEK)
def _handle_closed_conn(self, closed_socket, sockets_to_read,
client_connections):
sockets_to_read.remove(closed_socket)
client_connections[:] = [
conn for conn in client_connections if conn[0] is not closed_socket
]
self.shutdown_request(closed_socket)
def _handle_new_client(self, listening_socket, socket_to_read,
client_connections):
request, client_address = listening_socket.accept()
client_connections.append((request, client_address))
socket_to_read.append(request)
def _handle_non_data_conn(self, data_conn, client_connections):
for request, _ in client_connections:
if request is not data_conn:
self.shutdown_request(request)
def _find_data_conn_with_client_address(self, data_conn, client_connections):
for request, client_address in client_connections:
if request is data_conn:
return request, client_address
def _find_data_conn(self):
"""Finds the connection which will be used to send data."""
sockets_to_read = [self.socket]
client_connections = []
while True:
sockets_ready_to_read, _, _ = select.select(sockets_to_read, [], [])
for s in sockets_ready_to_read:
# Listening socket is ready to accept client.
if s is self.socket:
self._handle_new_client(s, sockets_to_read, client_connections)
else:
if self._conn_closed(s):
self._handle_closed_conn(s, sockets_to_read, client_connections)
# Found the connection which will be used to send data.
else:
self._handle_non_data_conn(s, client_connections)
return self._find_data_conn_with_client_address(
s, client_connections)
# pylint: enable=invalid-name
def handle_request(self):
"""Handle one request."""
request, client_address = self._find_data_conn()
# The following section largely copies the
# socketserver.BaseSever._handle_request_noblock.
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except Exception: # pylint: disable=broad-except
self.handle_error(request, client_address)
self.shutdown_request(request)
except:
self.shutdown_request(request)
raise
else:
self.shutdown_request(request)
_LOCALHOST = 'localhost'
class InstalledAppFlow(
six.with_metaclass(abc.ABCMeta, google_auth_flow.InstalledAppFlow)):
"""Base class of authorization flow for installed app.
Attributes:
oauth2session: requests_oauthlib.OAuth2Session, The OAuth 2.0 session from
requests_oauthlib.
client_type: str, The client type, either "web" or "installed".
client_config: The client configuration in the Google client secrets format.
autogenerate_code_verifier: bool, If true, auto-generate a code verifier.
require_local_server: bool, True if this flow needs a local server to handle
redirect.
"""
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False,
require_local_server=False):
session = requests.GetSession(session=oauth2session)
super(InstalledAppFlow, self).__init__(
session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier)
self.original_client_config = client_config
if require_local_server:
self.host = _LOCALHOST
self.app = _RedirectWSGIApp()
self.server = CreateLocalServer(self.app, self.host, _PORT_SEARCH_START,
_PORT_SEARCH_END)
self.redirect_uri = 'http://{}:{}/'.format(self.host,
self.server.server_port)
elif redirect_uri:
self.redirect_uri = redirect_uri
else:
self.redirect_uri = self._OOB_REDIRECT_URI
# include_client_id should be set to True for 1P, and False for 3P.
self.include_client_id = self.client_config.get('3pi') is None
def Run(self, **kwargs):
with HandleOauth2FlowErrors():
return self._Run(**kwargs)
@abc.abstractmethod
def _Run(self, **kwargs):
pass
@property
def _for_adc(self):
"""If the flow is for application default credentials."""
return (
self.client_config.get('is_adc')
or self.client_config.get('client_id') != config.CLOUDSDK_CLIENT_ID
)
@property
def _target_command(self):
if self._for_adc:
return 'gcloud auth application-default login'
else:
return 'gcloud auth login'
@classmethod
def FromInstalledAppFlow(cls, source_flow):
"""Creates an instance of the current flow from an existing flow."""
return cls.from_client_config(
source_flow.original_client_config,
source_flow.oauth2session.scope,
autogenerate_code_verifier=source_flow.autogenerate_code_verifier)
class FullWebFlow(InstalledAppFlow):
"""The complete OAuth 2.0 authorization flow.
This class supports user account login using "gcloud auth login" with browser.
Specifically, it does the following:
1. Try to find an available port for the local server which handles the
redirect.
2. Create a WSGI app on the local server which can direct browser to
Google's confirmation pages for authentication.
"""
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False):
super(FullWebFlow, self).__init__(
oauth2session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier,
require_local_server=True)
def _Run(self, **kwargs):
"""Run the flow using the server strategy.
The server strategy instructs the user to open the authorization URL in
their browser and will attempt to automatically open the URL for them.
It will start a local web server to listen for the authorization
response. Once authorization is complete the authorization server will
redirect the user's browser to the local web server. The web server
will get the authorization code from the response and shutdown. The
code is then exchanged for a token.
Args:
**kwargs: Additional keyword arguments passed through to
"authorization_url".
Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.
Raises:
LocalServerTimeoutError: If the local server handling redirection timeout
before receiving the request.
AuthRequestFailedError: If the user did not consent to the required
cloud-platform scope.
"""
auth_url, _ = self.authorization_url(**kwargs)
webbrowser.open(auth_url, new=1, autoraise=True)
authorization_prompt_message = (
'Your browser has been opened to visit:\n\n {url}\n')
log.err.Print(authorization_prompt_message.format(url=auth_url))
self.server.handle_request()
self.server.server_close()
if not self.app.last_request_uri:
raise LocalServerTimeoutError(
'Local server timed out before receiving the redirection request.')
# Note: using https here because oauthlib requires that
# OAuth 2.0 should only occur over https.
authorization_response = self.app.last_request_uri.replace(
'http:', 'https:')
# TODO(b/204953716): Remove verify=None
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
self.fetch_token(
authorization_response=authorization_response,
include_client_id=self.include_client_id,
verify=None,
)
del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
self._CheckScopes()
return self.credentials
def _CheckScopes(self):
"""Checks requested scopes and granted scopes."""
orig_scope = list(self.oauth2session.scope)
granted_scope = self.oauth2session.token.scope.split(' ')
missing_scope = frozenset(orig_scope) - frozenset(granted_scope)
if 'https://www.googleapis.com/auth/cloud-platform' in missing_scope:
raise AuthRequestFailedError(
'https://www.googleapis.com/auth/cloud-platform scope is required but'
' not consented. Please run the login command again and consent in'
' the login page.'
)
if missing_scope:
log.status.write(
'You have consented to only few of the requested scopes, so'
' some features may not work as expected. If you would like to give'
' consent to all scopes, you can run the login command again.'
f' Requested scopes: {orig_scope}.\nScopes you consented for:'
f' {granted_scope}.\nMissing scopes: {list(missing_scope)}.'
)
# self.credentials' scope comes from self.oauth2session.scope, so here we
# update the oauth2session scope to the granted scope, so self.credentials
# will have the correct scope.
self.oauth2session.scope = granted_scope
# TODO(b/206804357): Remove OOB flow from gcloud.
class OobFlow(InstalledAppFlow):
"""Out-of-band flow.
This class supports user account login using "gcloud auth login" without
browser.
"""
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False):
super(OobFlow, self).__init__(
oauth2session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier,
require_local_server=False)
def _Run(self, **kwargs):
"""Run the flow using the console strategy.
The console strategy instructs the user to open the authorization URL
in their browser. Once the authorization is complete the authorization
server will give the user a code. The user then must copy & paste this
code into the application. The code is then exchanged for a token.
Args:
**kwargs: Additional keyword arguments passed through to
"authorization_url".
Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.
"""
kwargs.setdefault('prompt', 'consent')
auth_url, _ = self.authorization_url(**kwargs)
authorization_prompt_message = (
'Go to the following link in your browser:\n\n {url}\n')
code = PromptForAuthCode(authorization_prompt_message, auth_url)
# TODO(b/204953716): Remove verify=None
self.fetch_token(code=code, include_client_id=True, verify=None)
return self.credentials
class UrlManager(object):
"""A helper for url manipulation."""
def __init__(self, url):
self._parse_url = parse.urlparse(url)
self._scheme, self._netloc, self._path, self._query = (
self._parse_url.scheme, self._parse_url.netloc, self._parse_url.path,
self._parse_url.query)
self._parsed_query = parse.parse_qsl(self._query)
def UpdateQueryParams(self, query_params):
"""Updates query params in the url using query_params.
Args:
query_params: A list of two-element tuples. The first element in the
tuple is the query key and the second element is the query value.
"""
for key, value in query_params:
self._RemoveQueryParam(key)
self._parsed_query.append((key, value))
def RemoveQueryParams(self, query_keys):
"""Removes query params from the url.
Args:
query_keys: A list of query keys to remove.
"""
for p in query_keys:
self._RemoveQueryParam(p)
def _RemoveQueryParam(self, query_key):
self._parsed_query[:] = [p for p in self._parsed_query if p[0] != query_key]
def ContainQueryParams(self, query_keys):
"""If the url contains the query keys in query_key.
Args:
query_keys: A list of query keys to check in the url.
Returns:
True if all query keys in query_keys are contained in url. Otherwise,
return False.
"""
parsed_query_keys = {k for (k, v) in self._parsed_query}
return all([p in parsed_query_keys for p in query_keys])
def GetQueryParam(self, query_key):
"""Gets the value of the query_key.
Args:
query_key: str, A query key to get the value for.
Returns:
The value of the query_key. None if query_key does not exist in the url.
"""
for k, v in self._parsed_query:
if query_key == k:
return v
def GetUrl(self):
"""Gets the current url in the string format."""
encoded_query = parse.urlencode(self._parsed_query)
return parse.urlunparse(
(self._scheme, self._netloc, self._path, '', encoded_query, ''))
def GetPort(self):
try:
_, port = self._netloc.rsplit(':', 1)
return int(port)
except ValueError:
return None
_REQUIRED_QUERY_PARAMS_IN_AUTH_RESPONSE = ('state', 'code')
_AUTH_RESPONSE_ERR_MSG = (
'The provided authorization response is invalid. Expect a url '
'with query parameters of [{}].'.format(
', '.join(_REQUIRED_QUERY_PARAMS_IN_AUTH_RESPONSE)))
def _ValidateAuthResponse(auth_response):
if UrlManager(auth_response).ContainQueryParams(
_REQUIRED_QUERY_PARAMS_IN_AUTH_RESPONSE):
return
raise AuthRequestFailedError(_AUTH_RESPONSE_ERR_MSG)
def PromptForAuthResponse(helper_msg, prompt_msg, client_config=None):
ImportReadline(client_config)
log.err.Print(helper_msg)
log.err.Print('\n')
return input(prompt_msg).strip()
def ImportReadline(client_config):
if (
client_config is not None
and '3pi' in client_config
and (sys.platform.startswith('dar') or sys.platform.startswith('linux'))
):
# Importing readline alters the built-in input() method
# to use the GNU readline interface.
# The basic OSX input() has an input limit of 1024 characters,
# which is sometimes not enough for us.
import readline # pylint: disable=unused-import, g-import-not-at-top
class NoBrowserFlow(InstalledAppFlow):
"""Flow to authorize gcloud on a machine without access to web browsers.
Out-of-band flow (OobFlow) is deprecated. This flow together with the helper
flow NoBrowserHelperFlow is the replacement. gcloud in
environments without access to browsers (i.e. access via ssh) can use this
flow to authorize gcloud. This flow will print authorization parameters
which will be taken by the helper flow to build the final authorization
request. The helper flow (run by a gcloud instance
with access to browsers) will launch the browser and ask for user's
authorization. After the authorization, the helper flow will print the
authorization response to pass back to this flow to continue the process
(exchanging for the refresh/access tokens).
"""
# These _REQUIRED_VERSIONs are used in the --no-browser flows, which are
# used when interacting with the CLI and using a different machine.
# That other machine's version might be out of date.
_REQUIRED_GCLOUD_VERSION_FOR_TPC = '506.0.0'
_REQUIRED_GCLOUD_VERSION_FOR_BYOID = '420.0.0'
_REQUIRED_GCLOUD_VERSION = '372.0.0'
_HELPER_MSG = ('You are authorizing {target} without access to a web '
'browser. Please run the following command on a machine with '
'a web browser and copy its output back here. Make sure the '
'installed gcloud version is {version} or newer.\n\n'
'{command} --remote-bootstrap="{partial_url}"')
_PROMPT_MSG = 'Enter the output of the above command: '
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False):
super(NoBrowserFlow, self).__init__(
oauth2session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier,
require_local_server=False)
def _PromptForAuthResponse(self, partial_url):
if not self._for_adc:
target = 'gcloud CLI'
command = 'gcloud auth login'
else:
target = 'client libraries'
command = 'gcloud auth application-default login'
universe_domain_property = properties.VALUES.core.universe_domain
if (
universe_domain_property is not None
and properties.VALUES.core.universe_domain.Get()
!= universe_domain_property.default
):
required_gcloud_version = self._REQUIRED_GCLOUD_VERSION_FOR_TPC
elif self.client_config.get('3pi'):
required_gcloud_version = self._REQUIRED_GCLOUD_VERSION_FOR_BYOID
else:
required_gcloud_version = self._REQUIRED_GCLOUD_VERSION
helper_msg = self._HELPER_MSG.format(
target=target,
version=required_gcloud_version,
command=command,
partial_url=partial_url,
)
return PromptForAuthResponse(
helper_msg, self._PROMPT_MSG, self.client_config
)
def _Run(self, **kwargs):
auth_url, _ = self.authorization_url(**kwargs)
url_manager = UrlManager(auth_url)
# redirect_uri needs to be provided by the helper flow because the helper
# will dynamically select a port on its localhost to handle redirect.
url_manager.RemoveQueryParams(['redirect_uri'])
# token_usage=remote is to indicate that the authorization is to bootstrap a
# a different gcloud instance.
url_manager.UpdateQueryParams([('token_usage', 'remote')])
auth_response = self._PromptForAuthResponse(url_manager.GetUrl())
_ValidateAuthResponse(auth_response)
redirect_port = UrlManager(auth_response).GetPort()
# Even though we started the local service using "localhost" as host name,
# system may use a different name. So, the host name in the auth
# response may not be "localhost". However, we should ignore it and keep
# using "localhost" as the redirect_uri in token exchange because it is
# what was used during authorization.
self.redirect_uri = 'http://{}:{}/'.format(_LOCALHOST, redirect_port)
# include_client_id should be set to True for 1P, and False for 3P.
include_client_id = self.client_config.get('3pi') is None
# TODO(b/204953716): Remove verify=None
self.fetch_token(
authorization_response=auth_response,
include_client_id=include_client_id,
verify=None,
)
return self.credentials
class NoBrowserHelperFlow(InstalledAppFlow):
"""Helper flow for the NoBrowserFlow to help another gcloud to authorize.
This flow takes the authorization parameters (i.e. requested scopes) generated
by the NoBrowserFlow and launches the browser for users to authorize.
After users authorize, print the authorization response which will be taken
by NoBrowserFlow to continue the login process
(exchanging for refresh/access token).
"""
_COPY_AUTH_RESPONSE_INSTRUCTION = (
'Copy the following line back to the gcloud CLI waiting to continue '
'the login flow.')
_COPY_AUTH_RESPONSE_WARNING = (
'{bold}WARNING: The following line enables access to your Google Cloud '
'resources. Only copy it to the trusted machine that you ran the '
'`{command} --no-browser` command on earlier.{normal}')
_PROMPT_TO_CONTINUE_MSG = (
'DO NOT PROCEED UNLESS YOU ARE BOOTSTRAPPING GCLOUD '
'ON A TRUSTED MACHINE WITHOUT A WEB BROWSER AND THE ABOVE COMMAND WAS '
'THE OUTPUT OF `{command} --no-browser` FROM THE TRUSTED MACHINE.')
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False):
super(NoBrowserHelperFlow, self).__init__(
oauth2session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier,
require_local_server=True)
self.partial_auth_url = None
@property
def _for_adc(self):
client_id = UrlManager(self.partial_auth_url).GetQueryParam('client_id')
return client_id != config.CLOUDSDK_CLIENT_ID
def _PrintCopyInstruction(self, auth_response):
con = console_attr.GetConsoleAttr()
log.status.write(self._COPY_AUTH_RESPONSE_INSTRUCTION + ' ')
log.status.Print(
self._COPY_AUTH_RESPONSE_WARNING.format(
bold=con.GetFontCode(bold=True),
command=self._target_command,
normal=con.GetFontCode()))
log.status.write('\n')
log.status.Print(auth_response)
def _ShouldContinue(self):
"""Ask users to confirm before actually running the flow."""
return console_io.PromptContinue(
self._PROMPT_TO_CONTINUE_MSG.format(command=self._target_command),
prompt_string='Proceed',
default=False)
def _Run(self, **kwargs):
self.partial_auth_url = kwargs.pop('partial_auth_url')
auth_url_manager = UrlManager(self.partial_auth_url)
auth_url_manager.UpdateQueryParams([('redirect_uri', self.redirect_uri)] +
list(kwargs.items()))
auth_url = auth_url_manager.GetUrl()
if not self._ShouldContinue():
return
webbrowser.open(auth_url, new=1, autoraise=True)
authorization_prompt_message = (
'Your browser has been opened to visit:\n\n {url}\n')
log.err.Print(authorization_prompt_message.format(url=auth_url))
self.server.handle_request()
self.server.server_close()
if not self.app.last_request_uri:
raise LocalServerTimeoutError(
'Local server timed out before receiving the redirection request.')
# Note: using https here because oauthlib requires that
# OAuth 2.0 should only occur over https.
authorization_response = self.app.last_request_uri.replace(
'http:', 'https:')
self._PrintCopyInstruction(authorization_response)
class RemoteLoginWithAuthProxyFlow(InstalledAppFlow):
"""Flow to authorize gcloud on a machine without access to web browsers.
Out-of-band flow (OobFlow) is deprecated. gcloud in
environments without access to browsers (eg. access via ssh) can use this
flow to authorize gcloud. This flow will print a url which the user has to
copy to a browser in any machine and perform authorization. After the
authorization, the user is redirected to gcloud's auth proxy which displays
the auth code. User copies the auth code back to gcloud to continue the
process (exchanging auth code for the refresh/access tokens).
"""
def __init__(self,
oauth2session,
client_type,
client_config,
redirect_uri=None,
code_verifier=None,
autogenerate_code_verifier=False):
super(RemoteLoginWithAuthProxyFlow, self).__init__(
oauth2session,
client_type,
client_config,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
autogenerate_code_verifier=autogenerate_code_verifier,
require_local_server=False)
def _Run(self, **kwargs):
"""Run the flow using the console strategy.
The console strategy instructs the user to open the authorization URL
in their browser. Once the authorization is complete the authorization
server will give the user a code. The user then must copy & paste this
code into the application. The code is then exchanged for a token.
Args:
**kwargs: Additional keyword arguments passed through to
"authorization_url".
Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.
"""
kwargs.setdefault('prompt', 'consent')
# when the parameter token_usage=remote is present, the DUSI of the token is
# not attached to the local device whose browser is used to provide consent.
kwargs.setdefault('token_usage', 'remote')
auth_url, _ = self.authorization_url(**kwargs)
authorization_prompt_message = (
'Go to the following link in your browser, and complete the sign-in'
' prompts:\n\n {url}\n'
)
code = PromptForAuthCode(
authorization_prompt_message, auth_url, self.client_config
)
# TODO(b/204953716): Remove verify=None
self.fetch_token(
code=code, include_client_id=self.include_client_id, verify=None
)
return self.credentials
def CreateLocalServer(wsgi_app, host, search_start_port, search_end_port):
"""Creates a local wsgi server.
Finds an available port in the range of [search_start_port, search_end_point)
for the local server.
Args:
wsgi_app: A wsgi app running on the local server.
host: hostname of the server.
search_start_port: int, the port where the search starts.
search_end_port: int, the port where the search ends.
Raises:
LocalServerCreationError: If it cannot find an available port for
the local server.
Returns:
WSGISever, a wsgi server.
"""
port = search_start_port
local_server = None
while not local_server and port < search_end_port:
try:
local_server = wsgiref.simple_server.make_server(
host,
port,
wsgi_app,
server_class=WSGIServer,
handler_class=google_auth_flow._WSGIRequestHandler) # pylint:disable=protected-access
except (socket.error, OSError):
port += 1
if local_server:
return local_server
raise LocalServerCreationError(
_PORT_SEARCH_ERROR_MSG.format(
start_port=search_start_port, end_port=search_end_port - 1))
class _RedirectWSGIApp(object):
"""WSGI app to handle the authorization redirect.
Stores the request URI and responds with a confirmation page.
"""
def __init__(self):
self.last_request_uri = None
def __call__(self, environ, start_response):
"""WSGI Callable.
Args:
environ (Mapping[str, Any]): The WSGI environment.
start_response (Callable[str, list]): The WSGI start_response callable.
Returns:
Iterable[bytes]: The response body.
"""
start_response(
six.ensure_str('200 OK'),
[(six.ensure_str('Content-type'), six.ensure_str('text/html'))])
self.last_request_uri = wsgiref.util.request_uri(environ)
query = self.last_request_uri.split('?', 1)[-1]
query = dict(parse.parse_qsl(query))
if 'code' in query:
page = 'oauth2_landing.html'
else:
page = 'oauth2_landing_error.html'
page_string = pkg_resources.GetResource(__name__, page)
if not properties.IsDefaultUniverse():
# decode and replace cloud.google.com with the universe document domain
page_string = bytes(
bytes(page_string)
.decode('utf-8')
.replace(
'cloud.google.com',
universe_descriptor.GetUniverseDocumentDomain(),
),
'utf-8',
)
return [page_string]

View File

@@ -0,0 +1,342 @@
# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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.
"""Fetching GCE metadata."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import threading
from googlecloudsdk.core import properties
from googlecloudsdk.core.credentials import gce_cache
from googlecloudsdk.core.credentials import gce_read
from googlecloudsdk.core.util import retry
from six.moves import urllib
# Cloudtops are assigned a common service account in order to use some identity
# management APIs in GCE. Unfortunately, gcloud reports this account. The shared
# service account should be ignored. (see go/cloudtop-faq#miscellaneous)
CLOUDTOP_COMMON_SERVICE_ACCOUNT = 'insecure-cloudtop-shared-user@cloudtop-prod.google.com.iam.gserviceaccount.com'
class Error(Exception):
"""Exceptions for the gce module."""
class MetadataServerException(Error):
"""Exception for when the metadata server cannot be reached."""
class CannotConnectToMetadataServerException(MetadataServerException):
"""Exception for when the metadata server cannot be reached."""
class MissingAudienceForIdTokenError(Error):
"""Exception for when audience is missing from ID token minting call."""
@retry.RetryOnException(max_retrials=3)
def _ReadNoProxyWithCleanFailures(
uri,
http_errors_to_ignore=(),
timeout=properties.VALUES.compute.gce_metadata_read_timeout_sec.GetInt()):
"""Reads data from a URI with no proxy, yielding cloud-sdk exceptions."""
try:
return gce_read.ReadNoProxy(uri, timeout)
except urllib.error.HTTPError as e:
if e.code in http_errors_to_ignore:
return None
if e.code == 403:
raise MetadataServerException(
'The request is rejected. Please check if the metadata server is '
'concealed.\n'
'See https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#concealment '
'for more information about metadata server concealment.')
raise MetadataServerException(e)
except urllib.error.URLError as e:
raise CannotConnectToMetadataServerException(e)
def _HandleMissingMetadataServer(return_list=False):
"""Handles when the metadata server is missing and resets the caches.
If you move gcloud from one environment to another, it might still think it
in on GCE from a previous invocation (which would result in a crash).
Instead of crashing, we ignore the error and just update the cache.
Args:
return_list: True to return [] instead of None as the default empty answer.
Returns:
The value the underlying method would return.
"""
# pylint: disable=missing-docstring
def _Wrapper(f):
def Inner(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except CannotConnectToMetadataServerException:
with _metadata_lock:
self.connected = gce_cache.ForceCacheRefresh()
return [] if return_list else None
return Inner
return _Wrapper
class _GCEMetadata(object):
"""Class for fetching GCE metadata.
Attributes:
connected: bool, True if the metadata server is available.
"""
def __init__(self):
self.connected = gce_cache.GetOnGCE()
@_HandleMissingMetadataServer()
def DefaultAccount(self):
"""Get the default service account for the host GCE instance.
Fetches GOOGLE_GCE_METADATA_DEFAULT_ACCOUNT_URI and returns its contents.
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
Returns:
str, The email address for the default service account. None if not on a
GCE VM, or if there are no service accounts associated with this VM.
"""
if not self.connected:
return None
account = _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_DEFAULT_ACCOUNT_URI,
http_errors_to_ignore=(404,),
)
if account == CLOUDTOP_COMMON_SERVICE_ACCOUNT:
return None
return account
@_HandleMissingMetadataServer()
def Project(self):
"""Get the project that owns the current GCE instance.
Fetches GOOGLE_GCE_METADATA_PROJECT_URI and returns its contents.
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
Returns:
str, The project ID for the current active project. None if no project is
currently active.
"""
if not self.connected:
return None
project = _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_PROJECT_URI)
if project:
return project
return None
@_HandleMissingMetadataServer(return_list=True)
def Accounts(self):
"""Get the list of service accounts available from the metadata server.
Returns:
[str], The list of accounts. [] if not on a GCE VM.
Raises:
CannotConnectToMetadataServerException: If no metadata server is present.
MetadataServerException: If there is a problem communicating with the
metadata server.
"""
if not self.connected:
return []
accounts_listing = _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_ACCOUNTS_URI + '/')
accounts_lines = accounts_listing.split()
accounts = []
for account_line in accounts_lines:
account = account_line.strip('/')
if account == 'default' or account == CLOUDTOP_COMMON_SERVICE_ACCOUNT:
continue
accounts.append(account)
return accounts
@_HandleMissingMetadataServer()
def Zone(self):
"""Get the name of the zone containing the current GCE instance.
Fetches GOOGLE_GCE_METADATA_ZONE_URI, formats it, and returns its contents.
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
Returns:
str, The short name (e.g., us-central1-f) of the zone containing the
current instance.
None if not on a GCE VM.
"""
if not self.connected:
return None
# zone_path will be formatted as, for example,
# projects/123456789123/zones/us-central1-f
# and we want to return only the last component.
zone_path = _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_ZONE_URI,
http_errors_to_ignore=(404,))
if zone_path:
return zone_path.split('/')[-1]
return None
def Region(self):
"""Get the name of the region containing the current GCE instance.
Fetches GOOGLE_GCE_METADATA_ZONE_URI, extracts the region associated
with the zone, and returns it. Extraction is based property that
zone names have form <region>-<zone> (see https://cloud.google.com/
compute/docs/zones) and an assumption that <zone> contains no hyphens.
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
Returns:
str, The short name (e.g., us-central1) of the region containing the
current instance.
None if not on a GCE VM.
"""
if not self.connected:
return None
# Zone will be formatted as (e.g.) us-central1-a, and we want to return
# everything ahead of the last hyphen.
zone = self.Zone()
return '-'.join(zone.split('-')[:-1]) if zone else None
@_HandleMissingMetadataServer()
def GetIdToken(self,
audience,
token_format='standard',
include_license=False):
"""Get a valid identity token on the host GCE instance.
Fetches GOOGLE_GCE_METADATA_ID_TOKEN_URI and returns its contents.
Args:
audience: str, target audience for ID token.
token_format: str, Specifies whether or not the project and instance
details are included in the identity token. Choices are "standard",
"full".
include_license: bool, Specifies whether or not license codes for images
associated with GCE instance are included in their identity tokens
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
MissingAudienceForIdTokenError: If audience is missing.
Returns:
str, The id token or None if not on a CE VM, or if there are no
service accounts associated with this VM.
"""
if not self.connected:
return None
if not audience:
raise MissingAudienceForIdTokenError()
include_license = 'TRUE' if include_license else 'FALSE'
return _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_ID_TOKEN_URI.format(
audience=audience, format=token_format, licenses=include_license),
http_errors_to_ignore=(404,))
@_HandleMissingMetadataServer()
def UniverseDomain(self):
"""Get the universe domain of the current GCE instance.
If the GCE metadata server universe domain endpoint is not found, or the
endpoint returns an empty string, return the default universe domain
(googleapis.com); otherwise return the fetched universe domain value, or
raise an exception if the request fails.
Raises:
CannotConnectToMetadataServerException: If the metadata server
cannot be reached.
MetadataServerException: If there is a problem communicating with the
metadata server.
Returns:
str, The universe domain value from metadata server. None if not on GCE.
"""
if not self.connected:
return None
universe_domain = _ReadNoProxyWithCleanFailures(
gce_read.GOOGLE_GCE_METADATA_UNIVERSE_DOMAIN_URI,
http_errors_to_ignore=(404,),
)
if not universe_domain:
return properties.VALUES.core.universe_domain.default
return universe_domain
_metadata = None
_metadata_lock = threading.Lock()
def Metadata():
"""Get a singleton for the GCE metadata class.
Returns:
_GCEMetadata, An object used to collect information from the GCE metadata
server.
"""
with _metadata_lock:
global _metadata
if not _metadata:
_metadata = _GCEMetadata()
return _metadata

View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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.
"""Caching logic for checking if we're on GCE."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import socket
import threading
import time
from googlecloudsdk.core import config
from googlecloudsdk.core import properties
from googlecloudsdk.core.credentials import gce_read
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import retry
import six
from six.moves import http_client
from six.moves import urllib_error
SslCertificateError = None # pylint: disable=invalid-name
try:
import ssl # pylint: disable=g-import-not-at-top
except ImportError:
pass
if ssl is not None:
SslCertificateError = getattr(ssl, 'CertificateError', None)
_GCE_CACHE_MAX_AGE = 10 * 60 # 10 minutes
# Depending on how a firewall/ NAT behaves, we can have different
# exceptions at different levels in the networking stack when trying to
# access an address that we can't reach. Capture all these exceptions.
_POSSIBLE_ERRORS_GCE_METADATA_CONNECTION = (urllib_error.URLError, socket.error,
http_client.HTTPException,
SslCertificateError)
_DOMAIN_NAME_RESOLVE_ERROR_MSG = 'Name or service not known'
def _ShouldRetryMetadataServerConnection(exc_type, exc_value, exc_traceback,
state):
"""Decides if we need to retry the metadata server connection."""
del exc_type, exc_traceback, state
if not isinstance(exc_value, _POSSIBLE_ERRORS_GCE_METADATA_CONNECTION):
return False
# It means the domain name cannot be resolved, which happens when not on GCE.
if (isinstance(exc_value, urllib_error.URLError) and
_DOMAIN_NAME_RESOLVE_ERROR_MSG in six.text_type(exc_value)):
return False
return True
class _OnGCECache(object):
"""Logic to check if we're on GCE and cache the result to file or memory.
Checking if we are on GCE is done by issuing an HTTP request to a GCE server.
Since HTTP requests are slow, we cache this information. Because every run
of gcloud is a separate command, the cache is stored in a file in the user's
gcloud config dir. Because within a gcloud run we might check if we're on GCE
multiple times, we also cache this information in memory.
A user can move the gcloud instance to and from a GCE VM, and the GCE server
can sometimes not respond. Therefore the cache has an age and gets refreshed
if more than _GCE_CACHE_MAX_AGE passed since it was updated.
"""
def __init__(self, connected=None, expiration_time=None):
self.connected = connected
self.expiration_time = expiration_time
self.file_lock = threading.Lock()
def GetOnGCE(self, check_age=True):
"""Check if we are on a GCE machine.
Checks, in order:
* in-memory cache
* on-disk cache
* metadata server
If we read from one of these sources, update all of the caches above it in
the list.
If check_age is True, then update all caches if the information we have is
older than _GCE_CACHE_MAX_AGE. In most cases, age should be respected. It
was added for reporting metrics.
Args:
check_age: bool, determines if the cache should be refreshed if more than
_GCE_CACHE_MAX_AGE time passed since last update.
Returns:
bool, if we are on GCE or not.
"""
on_gce = self._CheckMemory(check_age=check_age)
if on_gce is not None:
return on_gce
self._WriteMemory(*self._CheckDisk())
on_gce = self._CheckMemory(check_age=check_age)
if on_gce is not None:
return on_gce
return self.CheckServerRefreshAllCaches()
def CheckServerRefreshAllCaches(self):
on_gce = self._CheckServerWithRetry()
self._WriteDisk(on_gce)
self._WriteMemory(on_gce, time.time() + _GCE_CACHE_MAX_AGE)
return on_gce
def _CheckMemory(self, check_age):
if not check_age:
return self.connected
if self.expiration_time and self.expiration_time >= time.time():
return self.connected
return None
def _WriteMemory(self, on_gce, expiration_time):
self.connected = on_gce
self.expiration_time = expiration_time
def _CheckDisk(self):
"""Reads cache from disk."""
gce_cache_path = config.Paths().GCECachePath()
with self.file_lock:
try:
mtime = os.stat(gce_cache_path).st_mtime
expiration_time = mtime + _GCE_CACHE_MAX_AGE
gcecache_file_value = files.ReadFileContents(gce_cache_path)
return gcecache_file_value == six.text_type(True), expiration_time
except (OSError, IOError, files.Error):
# Failed to read Google Compute Engine credential cache file.
# This could be due to permission reasons, or because it doesn't yet
# exist.
# Can't log here because the log module depends (indirectly) on this
# one.
return None, None
def _WriteDisk(self, on_gce):
"""Updates cache on disk."""
gce_cache_path = config.Paths().GCECachePath()
with self.file_lock:
try:
files.WriteFileContents(
gce_cache_path, six.text_type(on_gce), private=True)
except (OSError, IOError, files.Error):
# Failed to write Google Compute Engine credential cache file.
# This could be due to permission reasons, or because it doesn't yet
# exist.
# Can't log here because the log module depends (indirectly) on this
# one.
pass
def _CheckServerWithRetry(self):
try:
return self._CheckServer()
except _POSSIBLE_ERRORS_GCE_METADATA_CONNECTION: # pylint: disable=catching-non-exception
return False
@retry.RetryOnException(
max_retrials=3, should_retry_if=_ShouldRetryMetadataServerConnection)
def _CheckServer(self):
return gce_read.ReadNoProxy(
gce_read.GOOGLE_GCE_METADATA_NUMERIC_PROJECT_URI,
properties.VALUES.compute.gce_metadata_check_timeout_sec.GetInt(),
).isdigit()
# Since a module is initialized only once, this is effective a singleton
_SINGLETON_ON_GCE_CACHE = _OnGCECache()
def GetOnGCE(check_age=True):
"""Helper function to abstract the caching logic of if we're on GCE."""
return _SINGLETON_ON_GCE_CACHE.GetOnGCE(check_age)
def ForceCacheRefresh():
"""Force rechecking server status and refreshing of all the caches."""
return _SINGLETON_ON_GCE_CACHE.CheckServerRefreshAllCaches()

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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.
"""Utility functions for opening a GCE URL and getting contents."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import http_encoding
from six.moves import urllib
GOOGLE_GCE_METADATA_URI = 'http://{}/computeMetadata/v1'.format(
encoding.GetEncodedValue(os.environ, 'GCE_METADATA_ROOT',
'metadata.google.internal'))
GOOGLE_GCE_METADATA_DEFAULT_ACCOUNT_URI = (
GOOGLE_GCE_METADATA_URI + '/instance/service-accounts/default/email')
GOOGLE_GCE_METADATA_PROJECT_URI = (
GOOGLE_GCE_METADATA_URI + '/project/project-id')
GOOGLE_GCE_METADATA_NUMERIC_PROJECT_URI = (
GOOGLE_GCE_METADATA_URI + '/project/numeric-project-id')
GOOGLE_GCE_METADATA_ACCOUNTS_URI = (
GOOGLE_GCE_METADATA_URI + '/instance/service-accounts')
GOOGLE_GCE_METADATA_ACCOUNT_URI = (
GOOGLE_GCE_METADATA_ACCOUNTS_URI + '/{account}/email')
GOOGLE_GCE_METADATA_ZONE_URI = (GOOGLE_GCE_METADATA_URI + '/instance/zone')
GOOGLE_GCE_METADATA_UNIVERSE_DOMAIN_URI = (
GOOGLE_GCE_METADATA_URI + '/universe/universe-domain'
)
GOOGLE_GCE_METADATA_ID_TOKEN_URI = (
GOOGLE_GCE_METADATA_URI + '/instance/service-accounts/default/identity?'
'audience={audience}&format={format}&licenses={licenses}')
GOOGLE_GCE_METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
def ReadNoProxy(uri, timeout):
"""Opens a URI with metadata headers, without a proxy, and reads all data.."""
request = urllib.request.Request(uri, headers=GOOGLE_GCE_METADATA_HEADERS)
result = urllib.request.build_opener(urllib.request.ProxyHandler({})).open(
request, timeout=timeout).read()
return http_encoding.Decode(result)

View File

@@ -0,0 +1,380 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. 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.
"""Customizations of google auth credentials for gcloud."""
# TODO(b/151628904): Add reauth to google-auth.
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from google.auth import _helpers
from google.auth import credentials as google_auth_credentials
from google.auth import exceptions as google_auth_exceptions
from google.auth import external_account_authorized_user as google_auth_external_account_authorized_user
from google.oauth2 import _client as google_auth_client
from google.oauth2 import credentials
from google.oauth2 import reauth as google_auth_reauth
from googlecloudsdk.core import context_aware
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import http
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import retry
from oauth2client import client as oauth2client_client
from oauth2client.contrib import reauth
from pyu2f import errors as pyu2f_errors
import six
from six.moves import http_client
from six.moves import urllib
GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'
class Error(exceptions.Error):
"""Exceptions for the google_auth_credentials module."""
class ReauthRequiredError(Error, google_auth_exceptions.RefreshError):
"""Exceptions when reauth is required."""
class ContextAwareAccessDeniedError(Error, google_auth_exceptions.RefreshError):
"""Exceptions when access is denied."""
def __init__(self):
super(ContextAwareAccessDeniedError, self).__init__(
context_aware.ContextAwareAccessError.Get()
)
class TokenRevokeError(Error, google_auth_exceptions.GoogleAuthError):
"""Exceptions when revoking google auth user credentials fails."""
# In gcloud, Credentials should be used for user account credentials.
# Do not use its parent class credentials.Credentials because it
# does not support reauth.
class Credentials(credentials.Credentials):
"""Extends user credentials of the google auth library for reauth.
reauth is not supported by the google auth library. However, gcloud supports
reauth. This class is to override the refresh method to handle reauth.
"""
def __init__(self, *args, **kwargs):
if 'rapt_token' in kwargs:
self._rapt_token = kwargs['rapt_token']
del kwargs['rapt_token']
else:
self._rapt_token = None
super(Credentials, self).__init__(*args, **kwargs)
def __setstate__(self, d):
super(Credentials, self).__setstate__(d)
self._rapt_token = d.get('_rapt_token')
@property
def rapt_token(self):
"""Reauth proof token."""
return self._rapt_token
def refresh(self, request):
"""Refreshes the access token and handles reauth request when it is asked.
Args:
request: google.auth.transport.Request, a callable used to make HTTP
requests.
"""
try:
return self._Refresh(request)
except ReauthRequiredError:
if not console_io.CanPrompt():
raise google_auth_exceptions.ReauthFailError(
'cannot prompt during non-interactive execution.'
)
# When we clean up oauth2client code in the future, we can remove the else
# part.
if properties.VALUES.auth.reauth_use_google_auth.GetBool():
log.debug('using google-auth reauth')
try:
from pyu2f.convenience import customauthenticator # pylint: disable=g-import-not-at-top
# pyu2f has a hardcoded 5s timeout for the users to touch the security
# key. Here we extend it to 15s.
customauthenticator.U2F_SIGNATURE_TIMEOUT_SECONDS = 15
self._rapt_token = google_auth_reauth.get_rapt_token(
request,
self._client_id,
self._client_secret,
self._refresh_token,
self._token_uri,
list(self.scopes or []),
)
except pyu2f_errors.OsHidError as e:
# The device does not have a security key attached.
# Sometimes manually re-authenticating gets around this.
raise google_auth_exceptions.ReauthFailError(
'A security key reauthentication challenge was issued but no key'
' was found. Try manually reauthenticating.'
) from e
except KeyError:
# context: b/328663283
# pyu2f lib doesn't handle the timeout well. When timeout happens, the
# key challenge pair is ('', ''), which doesn't exist in the client
# data map and causes a key error, see
# https://github.com/google/pyu2f/blob/master/pyu2f/convenience/customauthenticator.py#L108
raise google_auth_exceptions.RefreshError(
'Failed to obtain reauth rapt token. Did you touch the security '
'key within the 15 second timeout window?'
)
else:
# reauth.GetRaptToken is implemented in oauth2client and it is built on
# httplib2. GetRaptToken does not work with
# google.auth.transport.Request.
log.debug('using oauth2client reauth')
response_encoding = None if six.PY2 else 'utf-8'
http_request = http.Http(response_encoding=response_encoding).request
self._rapt_token = reauth.GetRaptToken(
http_request,
self._client_id,
self._client_secret,
self._refresh_token,
self._token_uri,
list(self.scopes or []),
)
return self._Refresh(request)
def _Refresh(self, request):
if (self._refresh_token is None or self._token_uri is None or
self._client_id is None or self._client_secret is None):
raise google_auth_exceptions.RefreshError(
'The credentials do not contain the necessary fields need to '
'refresh the access token. You must specify refresh_token, '
'token_uri, client_id, and client_secret.')
rapt_token = getattr(self, '_rapt_token', None)
access_token, refresh_token, expiry, grant_response = _RefreshGrant(
request, self._token_uri, self._refresh_token, self._client_id,
self._client_secret, self._scopes, rapt_token)
self.token = access_token
self.expiry = expiry
self._refresh_token = refresh_token
self._id_token = grant_response.get('id_token')
# id_token in oauth2client creds is decoded and it uses id_tokenb64 to
# store the encoded copy. id_token in google-auth creds is encoded.
# Here, we add id_tokenb64 to google-auth creds for consistency.
self.id_tokenb64 = grant_response.get('id_token')
if self._scopes and 'scope' in grant_response:
requested_scopes = frozenset(self._scopes)
granted_scopes = frozenset(grant_response['scope'].split())
scopes_requested_but_not_granted = requested_scopes - granted_scopes
if scopes_requested_but_not_granted:
raise google_auth_exceptions.RefreshError(
'Not all requested scopes were granted by the '
'authorization server, missing scopes {}.'.format(
', '.join(scopes_requested_but_not_granted)))
def revoke(self, request):
query_params = {'token': self.refresh_token or self.token}
token_revoke_uri = _helpers.update_query(GOOGLE_REVOKE_URI, query_params)
headers = {
'content-type': google_auth_client._URLENCODED_CONTENT_TYPE, # pylint: disable=protected-access
}
response = request(token_revoke_uri, headers=headers, method='POST')
if response.status != http_client.OK:
response_data = six.ensure_text(response.data)
response_json = json.loads(response_data)
error = response_json.get('error')
error_description = response_json.get('error_description')
raise TokenRevokeError(error, error_description)
@classmethod
def FromGoogleAuthUserCredentials(cls, creds):
"""Creates an object from creds of google.oauth2.credentials.Credentials.
Args:
creds: Union[
google.oauth2.credentials.Credentials,
google.auth.external_account_authorized_user.Credentials
], The input credentials.
Returns:
Credentials of Credentials.
"""
if isinstance(creds, credentials.Credentials):
res = cls(
creds.token,
refresh_token=creds.refresh_token,
id_token=creds.id_token,
token_uri=creds.token_uri,
client_id=creds.client_id,
client_secret=creds.client_secret,
scopes=creds.scopes,
quota_project_id=creds.quota_project_id)
res.expiry = creds.expiry
return res
if isinstance(creds,
google_auth_external_account_authorized_user.Credentials):
return cls(
creds.token,
expiry=creds.expiry,
refresh_token=creds.refresh_token,
token_uri=creds.token_url,
client_id=creds.client_id,
client_secret=creds.client_secret,
scopes=creds.scopes,
quota_project_id=creds.quota_project_id)
raise exceptions.InvalidCredentials('Invalid Credentials')
def _RefreshGrant(request,
token_uri,
refresh_token,
client_id,
client_secret,
scopes=None,
rapt_token=None):
"""Prepares the request to send to auth server to refresh tokens."""
body = [
('grant_type', google_auth_client._REFRESH_GRANT_TYPE), # pylint: disable=protected-access
('client_id', client_id),
('client_secret', client_secret),
('refresh_token', refresh_token),
]
if scopes:
body.append(('scope', ' '.join(scopes)))
if rapt_token:
body.append(('rapt', rapt_token))
response_data = _TokenEndpointRequestWithRetry(request, token_uri, body)
try:
access_token = response_data['access_token']
except KeyError as caught_exc:
new_exc = google_auth_exceptions.RefreshError(
'No access token in response.', response_data)
six.raise_from(new_exc, caught_exc)
refresh_token = response_data.get('refresh_token', refresh_token)
expiry = google_auth_client._parse_expiry(response_data) # pylint: disable=protected-access
return access_token, refresh_token, expiry, response_data
def _ShouldRetryServerInternalError(exc_type, exc_value, exc_traceback, state):
"""Whether to retry the request when receive errors.
Do not retry reauth-related errors or context aware access errors.
Retrying won't help in those situations.
Args:
exc_type: type of the raised exception.
exc_value: the instance of the raise the exception.
exc_traceback: Traceback, traceback encapsulating the call stack at the the
point where the exception occurred.
state: RetryerState, state of the retryer.
Returns:
True if exception and is not due to reauth-related errors or context-aware
access restriction.
"""
del exc_value, exc_traceback, state
return (exc_type != ReauthRequiredError and
exc_type != ContextAwareAccessDeniedError)
@retry.RetryOnException(
max_retrials=1, should_retry_if=_ShouldRetryServerInternalError)
def _TokenEndpointRequestWithRetry(request, token_uri, body):
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
Args:
request: google.auth.transport.Request, A callable used to make HTTP
requests.
token_uri: str, The OAuth 2.0 authorizations server's token endpoint URI.
body: {str: str}, The parameters to send in the request body.
Returns:
The JSON-decoded response data.
"""
body = urllib.parse.urlencode(body)
headers = {
'content-type': google_auth_client._URLENCODED_CONTENT_TYPE, # pylint: disable=protected-access
}
response = request(method='POST', url=token_uri, headers=headers, body=body)
response_body = six.ensure_text(response.data)
if response.status != http_client.OK:
_HandleErrorResponse(response_body)
response_data = json.loads(response_body)
return response_data
def _HandleErrorResponse(response_body):
""""Translates an error response into an exception.
Args:
response_body: str, The decoded response data.
Raises:
google.auth.exceptions.RefreshError: If the token endpoint returned
an server internal error.
ContextAwareAccessDeniedError: if the error was due to a context aware
access restriction.
ReauthRequiredError: If reauth is required.
"""
error_data = json.loads(response_body)
error_code = error_data.get('error')
error_subtype = error_data.get('error_subtype')
if error_code == oauth2client_client.REAUTH_NEEDED_ERROR and (
error_subtype == oauth2client_client.REAUTH_NEEDED_ERROR_INVALID_RAPT or
error_subtype == oauth2client_client.REAUTH_NEEDED_ERROR_RAPT_REQUIRED):
raise ReauthRequiredError('reauth is required.')
try:
google_auth_client._handle_error_response(error_data, False) # pylint: disable=protected-access
except google_auth_exceptions.RefreshError as e:
if context_aware.IsContextAwareAccessDeniedError(e):
raise ContextAwareAccessDeniedError()
raise
class AccessTokenCredentials(google_auth_credentials.Credentials):
"""A credential represented by an access token."""
def __init__(self, token):
super(AccessTokenCredentials, self).__init__()
self.token = token
@property
def expired(self):
return False
def refresh(self, request):
del request # Unused
pass

View File

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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 to get a credentialed http object for making API calls."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from google.auth import external_account as google_auth_external_account
import google_auth_httplib2
from googlecloudsdk.calliope import base
from googlecloudsdk.core import http
from googlecloudsdk.core.credentials import creds as core_creds
from googlecloudsdk.core.credentials import store
from googlecloudsdk.core.credentials import transport
import six
def Http(timeout='unset',
response_encoding=None,
ca_certs=None,
enable_resource_quota=True,
allow_account_impersonation=True,
use_google_auth=None):
"""Get an httplib2.Http client for working with the Google API.
Args:
timeout: double, The timeout in seconds to pass to httplib2. This is the
socket level timeout. If timeout is None, timeout is infinite. If
default argument 'unset' is given, a sensible default is selected.
response_encoding: str, the encoding to use to decode the response.
ca_certs: str, absolute filename of a ca_certs file that overrides the
default
enable_resource_quota: bool, By default, we are going to tell APIs to use
the quota of the project being operated on. For some APIs we want to use
gcloud's quota, so you can explicitly disable that behavior by passing
False here.
allow_account_impersonation: bool, True to allow use of impersonated service
account credentials for calls made with this client. If False, the active
user credentials will always be used.
use_google_auth: bool, True if the calling command indicates to use
google-auth library for authentication. If False, authentication will
fallback to using the oauth2client library. If None, set the value based
on the configuration.
Returns:
1. A regular httplib2.Http object if no credentials are available;
2. Or a httplib2.Http client object authorized by oauth2client
credentials if use_google_auth==False;
3. Or a google_auth_httplib2.AuthorizedHttp client object authorized by
google-auth credentials.
Raises:
core.credentials.exceptions.Error: If an error loading the credentials
occurs.
"""
http_client = http.Http(timeout=timeout, response_encoding=response_encoding,
ca_certs=ca_certs)
if use_google_auth is None:
use_google_auth = True
request_wrapper = RequestWrapper()
credentials = store.LoadIfEnabled(
allow_account_impersonation, use_google_auth
)
http_client = request_wrapper.WrapQuota(
http_client,
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=credentials,
)
http_client = request_wrapper.WrapCredentials(
http_client,
allow_account_impersonation,
use_google_auth,
credentials=credentials,
)
if hasattr(http_client, '_googlecloudsdk_credentials'):
creds = http_client._googlecloudsdk_credentials # pylint: disable=protected-access
if core_creds.IsGoogleAuthCredentials(creds):
apitools_creds = _GoogleAuthApitoolsCredentials(creds)
else:
apitools_creds = creds
# apitools needs this attribute to do credential refreshes during batch API
# requests.
setattr(http_client.request, 'credentials', apitools_creds)
return http_client
class _GoogleAuthApitoolsCredentials():
"""Class of wrapping credentials."""
def __init__(self, credentials):
self.credentials = credentials
def refresh(self, http_client): # pylint: disable=invalid-name
del http_client # unused
if isinstance(
self.credentials,
google_auth_external_account.Credentials) and self.credentials.valid:
return
self.credentials.refresh(http.GoogleAuthRequest())
class RequestWrapper(transport.CredentialWrappingMixin,
transport.QuotaHandlerMixin, http.RequestWrapper):
"""Class for wrapping httplib.Httplib2 requests."""
def AuthorizeClient(self, http_client, creds):
"""Returns an http_client authorized with the given credentials."""
if core_creds.IsGoogleAuthCredentials(creds):
http_client = google_auth_httplib2.AuthorizedHttp(creds, http_client)
else:
http_client = creds.authorize(http_client)
return http_client
def WrapQuota(
self,
http_client,
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=None,
):
"""Returns an http_client with quota project handling."""
quota_project = self.QuotaProject(
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=credentials,
)
if not quota_project:
return http_client
orig_request = http_client.request
wrapped_request = self.QuotaWrappedRequest(
http_client, quota_project)
def RequestWithRetry(*args, **kwargs):
"""Retries the request after removing the quota project header.
Try the request with the X-Goog-User-Project header. If the account does
not have the permission to expense the quota of the user project in the
header, remove the header and retry.
Args:
*args: *args to send to httplib2.Http.request method.
**kwargs: **kwargs to send to httplib2.Http.request method.
Returns:
Response from httplib2.Http.request.
"""
response, content = wrapped_request(*args, **kwargs)
if response.status != 403:
return response, content
content_text = six.ensure_text(content)
try:
err_details = json.loads(content_text)['error']['details']
except (KeyError, json.JSONDecodeError):
return response, content
for err_detail in err_details:
if (err_detail.get('@type')
== 'type.googleapis.com/google.rpc.ErrorInfo' and
err_detail.get('reason') == transport.USER_PROJECT_ERROR_REASON and
err_detail.get('domain') == transport.USER_PROJECT_ERROR_DOMAIN):
return orig_request(*args, **kwargs)
return response, content
if base.UserProjectQuotaWithFallbackEnabled():
http_client.request = RequestWithRetry
else:
http_client.request = wrapped_request
return http_client

View File

@@ -0,0 +1,202 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 Google LLC. 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.
"""Provides utilities for token introspection."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import functools
import json
import logging
from google.auth import exceptions as google_auth_exceptions
from google.auth import external_account
from google.oauth2 import utils as oauth2_utils
from googlecloudsdk.core import config
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import properties
from six.moves import http_client
from six.moves import urllib
_ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'
_URLENCODED_HEADERS = {'Content-Type': 'application/x-www-form-urlencoded'}
_EXTERNAL_ACCT_TOKEN_INTROSPECT_ENDPOINT = (
'https://sts.googleapis.com/v1/introspect'
)
class Error(exceptions.Error):
"""A base exception for this module."""
class InactiveCredentialsError(Error):
"""Raised when the provided credentials are invalid or expired."""
class TokenIntrospectionError(Error):
"""Raised when an error is encountered while calling token introspection."""
class IntrospectionClient(oauth2_utils.OAuthClientAuthHandler):
"""Implements the OAuth 2.0 token introspection spec.
This is based on https://tools.ietf.org/html/rfc7662.
The implementation supports 3 types of client authentication when calling
the endpoints: no authentication, basic header authentication and POST body
authentication.
"""
def __init__(self, token_introspect_endpoint, client_authentication=None):
"""Initializes an OAuth introspection client instance.
Args:
token_introspect_endpoint (str): The token introspection endpoint.
client_authentication (Optional[oauth2_utils.ClientAuthentication]): The
optional OAuth client authentication credentials if available.
"""
super(IntrospectionClient, self).__init__(client_authentication)
self._token_introspect_endpoint = token_introspect_endpoint
def introspect(self, request, token, token_type_hint=_ACCESS_TOKEN_TYPE):
"""Returns the meta-information associated with an OAuth token.
Args:
request (google.auth.transport.Request): A callable that makes HTTP
requests.
token (str): The OAuth token whose meta-information are to be returned.
token_type_hint (Optional[str]): The optional token type. The default is
access_token.
Returns:
Mapping: The active token meta-information returned by the introspection
endpoint.
Raises:
InactiveCredentialsError: If the credentials are invalid or expired.
TokenIntrospectionError: If an error is encountered while calling the
token introspection endpoint.
"""
headers = _URLENCODED_HEADERS.copy()
request_body = {
'token': token,
'token_type_hint': token_type_hint,
}
# Apply OAuth client authentication.
self.apply_client_authentication_options(headers, request_body)
# Execute request.
response = request(
url=self._token_introspect_endpoint,
method='POST',
headers=headers,
body=urllib.parse.urlencode(request_body).encode('utf-8'),
)
response_body = (
response.data.decode('utf-8')
if hasattr(response.data, 'decode')
else response.data
)
# If non-200 response received, translate to TokenIntrospectionError.
if response.status != http_client.OK:
raise TokenIntrospectionError(response_body)
response_data = json.loads(response_body)
if response_data.get('active'):
return response_data
else:
raise InactiveCredentialsError(response_body)
def GetExternalAccountId(creds):
"""Returns the external account credentials' identifier.
This requires basic client authentication and only works with external
account credentials that have not been impersonated. The returned username
field is used for the account ID.
Args:
creds (google.auth.external_account.Credentials): The external account
credentials whose account ID is to be determined.
Returns:
Optional(str): The account ID string if determinable.
Raises:
InactiveCredentialsError: If the credentials are invalid or expired.
TokenIntrospectionError: If an error is encountered while calling the
token introspection endpoint.
"""
# pylint: disable=g-import-not-at-top
from googlecloudsdk.core import requests as core_requests
# pylint: enable=g-import-not-at-top
# Use basic client authentication.
client_authentication = oauth2_utils.ClientAuthentication(
oauth2_utils.ClientAuthType.basic,
config.CLOUDSDK_CLIENT_ID,
config.CLOUDSDK_CLIENT_NOTSOSECRET,
)
# Check if the introspection endpoint has been overridden,
# otherwise use default endpoint. Prioritize property override first then
# credential config.
token_introspection_endpoint = _EXTERNAL_ACCT_TOKEN_INTROSPECT_ENDPOINT
endpoint_override = properties.VALUES.auth.token_introspection_endpoint.Get()
property_override = creds.token_info_url
if endpoint_override or property_override:
token_introspection_endpoint = endpoint_override or property_override
oauth_introspection = IntrospectionClient(
token_introspect_endpoint=token_introspection_endpoint,
client_authentication=client_authentication,
)
# Create request with mTLS certificate injection for X.509 credentials.
# If mTLS is required but the certificate and key paths cannot be obtained, ``
# fall back to basic auth only.
request = core_requests.GoogleAuthRequest()
# Check for mTLS attributes. This is necessary because not all
# external_account.Credentials subclasses support mTLS, and there's no public
# interface to check for this capability.
if (
isinstance(creds, external_account.Credentials)
and hasattr(creds, '_mtls_required')
and callable(getattr(creds, '_mtls_required'))
and creds._mtls_required() # pylint: disable=protected-access
):
try:
cert_path, key_path = creds._get_mtls_cert_and_key_paths() # pylint: disable=protected-access
request = functools.partial(request, cert=(cert_path, key_path))
except (
AttributeError,
ValueError,
google_auth_exceptions.GoogleAuthError,
IOError,
OSError,
) as e:
# If mTLS is required but certificate and key paths are unavailable,
# log the error and fall back to basic auth only.
logging.debug('Could not get mTLS certificate and key paths: %s', e)
pass
if not creds.valid:
creds.refresh(request)
token_info = oauth_introspection.introspect(request, creds.token)
# User friendly identifier is stored in username.
return token_info.get('username')

View File

@@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/sdk/auth_success">
<script type="text/javascript">
window.location.href = "https://cloud.google.com/sdk/auth_success"
</script>
<title>Authentication Successful - Google Cloud SDK</title>
</head>
<body>
<!-- Show the static landing page in case the redirect fails. -->
<img width=600 src=http://storage.googleapis.com/cloudsdk%2Fassets%2Fgoogle-cloud-platform.png alt="Google Cloud Platform">
<p><font face=arial>
You are now authenticated with the Google Cloud SDK.
</font></p>
<p><font face=arial>
The authentication flow has completed. You may close this window, or check out the
<a href=https://developers.google.com/cloud/sdk/gettingstarted>Getting Started Guide</a> for
more information.
</font></p>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/sdk/auth_failure">
<script type="text/javascript">
window.location.href = "https://cloud.google.com/sdk/auth_failure"
</script>
<title>Authentication Failed - Google Cloud SDK</title>
</head>
<body>
<!-- Show the static landing page in case the redirect fails. -->
<img width=600 src=http://storage.googleapis.com/cloudsdk%2Fassets%2Fgoogle-cloud-platform.png alt="Google Cloud Platform">
<p><font face=arial>
The authentication flow did not complete successfully. To get valid credentials, please re-run:
</font></p>
<p><font face=courier>
$ gcloud auth login
</font></p>
<p><font face=arial>
You may close this window, or check out the
<a href=https://developers.google.com/cloud/sdk/gettingstarted>Getting Started Guide</a> for
more information.
</font></p>
</body>
</html>

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 Google LLC. 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.
"""google-auth p12 service account credentials."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from google.auth import _helpers
from google.auth.crypt import base as crypt_base
from google.oauth2 import service_account
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.util import encoding
_DEFAULT_PASSWORD = 'notasecret'
_PYCA_CRYPTOGRAPHY_MIN_VERSION = '2.5'
class Error(exceptions.Error):
"""Base Error class for this module."""
class MissingRequiredFieldsError(Error):
"""Error when required fields are missing to construct p12 credentials."""
class MissingDependencyError(Error):
"""Error when missing a dependency to use p12 credentials."""
class PKCS12Signer(crypt_base.Signer, crypt_base.FromServiceAccountMixin):
"""Signer for a p12 service account key based on pyca/cryptography."""
def __init__(self, key):
self._key = key
# Defined in the Signer interface, and is not useful for gcloud.
@property
def key_id(self):
return None
def sign(self, message):
message = _helpers.to_bytes(message)
from google.auth.crypt import _cryptography_rsa # pylint: disable=g-import-not-at-top
return self._key.sign(
message,
_cryptography_rsa._PADDING, # pylint: disable=protected-access
_cryptography_rsa._SHA256) # pylint: disable=protected-access
@classmethod
def from_string(cls, key_strings, key_id=None):
del key_id
key_string, password = (_helpers.to_bytes(k) for k in key_strings)
from cryptography.hazmat.primitives.serialization import pkcs12 # pylint: disable=g-import-not-at-top
from cryptography.hazmat import backends # pylint: disable=g-import-not-at-top
key, _, _ = pkcs12.load_key_and_certificates(
key_string, password, backend=backends.default_backend())
return cls(key)
class Credentials(service_account.Credentials):
"""google-auth service account credentials using p12 keys.
p12 keys are not supported by the google-auth service account credentials.
gcloud uses oauth2client to support p12 key users. Since oauth2client was
deprecated and bundling it is security concern, we decided to support p12
in gcloud codebase. We prefer not adding it to the google-auth library
because p12 is not supported from the beginning by google-auth. GCP strongly
suggests users to use the JSON format. gcloud has to support it to not
break users.
oauth2client uses PyOpenSSL to handle p12 keys. PyOpenSSL deprecated
p12 support from version 20.0.0 and encourages to use pyca/cryptography for
anything other than TLS connections.
"""
_REQUIRED_FIELDS = ('service_account_email', 'token_uri', 'scopes')
@property
def private_key_pkcs12(self):
return self._private_key_pkcs12
@property
def private_key_password(self):
return self._private_key_password
@classmethod
def from_service_account_pkcs12_keystring(cls,
key_string,
password=None,
**kwargs):
password = password or _DEFAULT_PASSWORD
signer = PKCS12Signer.from_string((key_string, password))
missing_fields = [f for f in cls._REQUIRED_FIELDS if f not in kwargs]
if missing_fields:
raise MissingRequiredFieldsError('Missing fields: {}.'.format(
', '.join(missing_fields)))
creds = cls(signer, **kwargs)
# saving key_string and password is necessary because gcloud caches
# credentials and re-construct it during runtime. Without them, we cannot
# re-construct it.
# pylint: disable=protected-access
creds._private_key_pkcs12 = key_string
creds._private_key_password = password
# pylint: enable=protected-access
return creds
def CreateP12ServiceAccount(key_string, password=None, **kwargs):
"""Creates a service account from a p12 key and handles import errors."""
log.warning('.p12 service account keys are not recommended unless it is '
'necessary for backwards compatibility. Please switch to '
'a newer .json service account key for this account.')
try:
return Credentials.from_service_account_pkcs12_keystring(
key_string, password, **kwargs)
except ImportError:
if not encoding.GetEncodedValue(os.environ, 'CLOUDSDK_PYTHON_SITEPACKAGES'):
raise MissingDependencyError(
('pyca/cryptography is not available. Please install or upgrade it '
'to a version >= {} and set the environment variable '
'CLOUDSDK_PYTHON_SITEPACKAGES to 1. If that does not work, see '
'https://developers.google.com/cloud/sdk/crypto for details '
'or consider using .json private key instead.'
).format(_PYCA_CRYPTOGRAPHY_MIN_VERSION))
else:
raise MissingDependencyError(
('pyca/cryptography is not available or the version is < {}. '
'Please install or upgrade it to a newer version. See '
'https://developers.google.com/cloud/sdk/crypto for details '
'or consider using .json private key instead.'
).format(_PYCA_CRYPTOGRAPHY_MIN_VERSION))

View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. 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 to get a credentialed http object for making API calls."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from google.auth import external_account as google_auth_external_account
from google.auth.transport import requests as google_auth_requests
from googlecloudsdk.calliope import base
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import requests
from googlecloudsdk.core import transport as core_transport
from googlecloudsdk.core.credentials import store
from googlecloudsdk.core.credentials import transport
REFRESH_STATUS_CODES = [401]
MAX_REFRESH_ATTEMPTS = 1
class Error(exceptions.Error):
"""Exceptions for this module."""
def GetSession(timeout='unset',
ca_certs=None,
enable_resource_quota=True,
allow_account_impersonation=True,
session=None,
streaming_response_body=False,
redact_request_body_reason=None):
"""Get requests.Session object for working with the Google API.
Args:
timeout: double, The timeout in seconds to pass to httplib2. This is the
socket level timeout. If timeout is None, timeout is infinite. If
default argument 'unset' is given, a sensible default is selected.
ca_certs: str, absolute filename of a ca_certs file that overrides the
default
enable_resource_quota: bool, By default, we are going to tell APIs to use
the quota of the project being operated on. For some APIs we want to use
gcloud's quota, so you can explicitly disable that behavior by passing
False here.
allow_account_impersonation: bool, True to allow use of impersonated service
account credentials for calls made with this client. If False, the
active user credentials will always be used.
session: requests.Session instance. Otherwise, a new requests.Session will
be initialized.
streaming_response_body: bool, True indicates that the response body will
be a streaming body.
redact_request_body_reason: str, the reason why the request body must be
redacted if --log-http is used. If None, the body is not redacted.
Returns:
1. A regular requests.Session object if no credentials are available;
2. Or an authorized requests.Session object authorized by google-auth
credentials.
Raises:
creds_exceptions.Error: If an error loading the credentials occurs.
"""
session = requests.GetSession(
timeout=timeout,
ca_certs=ca_certs,
session=session,
streaming_response_body=streaming_response_body,
redact_request_body_reason=redact_request_body_reason,
)
request_wrapper = RequestWrapper()
use_google_auth = True
credentials = store.LoadIfEnabled(
allow_account_impersonation, use_google_auth
)
session = request_wrapper.WrapQuota(
session,
enable_resource_quota,
allow_account_impersonation,
True,
credentials=credentials,
)
session = request_wrapper.WrapCredentials(
session, allow_account_impersonation, credentials=credentials
)
return session
class RequestWrapper(
transport.CredentialWrappingMixin,
transport.QuotaHandlerMixin,
requests.RequestWrapper,
):
"""Class for wrapping requests.Session requests."""
def AuthorizeClient(self, http_client, creds):
"""Returns an http_client authorized with the given credentials."""
orig_request = http_client.request
credential_refresh_state = {'attempt': 0}
auth_request = google_auth_requests.Request(http_client)
def WrappedRequest(method, url, data=None, headers=None, **kwargs):
wrapped_request = http_client.request
http_client.request = orig_request
creds.before_request(auth_request, method, url, headers)
http_client.request = wrapped_request
response = orig_request(
method, url, data=data, headers=headers or {}, **kwargs)
if (response.status_code in REFRESH_STATUS_CODES and
not (isinstance(creds, google_auth_external_account.Credentials) and
creds.valid) and
credential_refresh_state['attempt'] < MAX_REFRESH_ATTEMPTS):
credential_refresh_state['attempt'] += 1
creds.refresh(requests.GoogleAuthRequest())
response = orig_request(
method, url, data=data, headers=headers or {}, **kwargs)
return response
http_client.request = WrappedRequest
return http_client
def WrapQuota(
self,
http_client,
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=None,
):
"""Returns an http_client with quota project handling."""
quota_project = self.QuotaProject(
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=credentials,
)
if not quota_project:
return http_client
orig_request = http_client.request
wrapped_request = self.QuotaWrappedRequest(http_client, quota_project)
def RequestWithRetry(*args, **kwargs):
"""Retries the request after removing the quota project header.
Try the request with the X-Goog-User-Project header. If the account does
not have the permission to expense the quota of the user project in the
header, remove the header and retry.
Args:
*args: *args to send to requests.Session.request method.
**kwargs: **kwargs to send to requests.Session.request method.
Returns:
Response from requests.Session.request.
"""
response = wrapped_request(*args, **kwargs)
if response.status_code != 403:
return response
old_encoding = response.encoding
response.encoding = response.encoding or core_transport.ENCODING
try:
err_details = response.json()['error']['details']
except (KeyError, ValueError):
return response
finally:
response.encoding = old_encoding
for err_detail in err_details:
if (err_detail.get('@type')
== 'type.googleapis.com/google.rpc.ErrorInfo' and
err_detail.get('reason') == transport.USER_PROJECT_ERROR_REASON and
err_detail.get('domain') == transport.USER_PROJECT_ERROR_DOMAIN):
return orig_request(*args, **kwargs)
return response
if base.UserProjectQuotaWithFallbackEnabled():
http_client.request = RequestWithRetry
else:
http_client.request = wrapped_request
return http_client

View File

@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. 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.
"""Module for wrapping transports with credentials."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
from googlecloudsdk.core import context_aware
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import transport
from googlecloudsdk.core.credentials import creds as core_creds
from googlecloudsdk.core.credentials import exceptions as creds_exceptions
from googlecloudsdk.core.credentials import store
from googlecloudsdk.core.util import files
from oauth2client import client
import six
from google.auth import exceptions as google_auth_exceptions
class Error(exceptions.Error):
"""Exceptions for the credentials transport module."""
USER_PROJECT_ERROR_REASON = 'USER_PROJECT_DENIED'
USER_PROJECT_ERROR_DOMAIN = 'googleapis.com'
class QuotaHandlerMixin(object):
"""Mixin for handling quota project."""
def QuotaProject(
self,
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
credentials=None,
):
"""Returns None or the quota project for credentials."""
if not enable_resource_quota:
return None
if credentials is None:
credentials = store.LoadIfEnabled(
allow_account_impersonation, use_google_auth
)
return core_creds.GetQuotaProject(credentials)
def QuotaWrappedRequest(self, http_client, quota_project):
"""Returns a request method which adds the quota project header."""
handlers = [
transport.Handler(
transport.SetHeader('X-Goog-User-Project', quota_project)
)
]
self.WrapRequest(http_client, handlers)
return http_client.request
@abc.abstractmethod
def WrapQuota(
self,
http_client,
enable_resource_quota,
allow_account_impersonation,
use_google_auth,
):
"""Returns a http_client with quota project handling.
Args:
http_client: The http client to be wrapped.
enable_resource_quota: bool, By default, we are going to tell APIs to use
the quota of the project being operated on. For some APIs we want to use
gcloud's quota, so you can explicitly disable that behavior by passing
False here.
allow_account_impersonation: bool, True to allow use of impersonated
service account credentials for calls made with this client. If False,
the active user credentials will always be used.
use_google_auth: bool, True if the calling command indicates to use
google-auth library for authentication. If False, authentication will
fallback to using the oauth2client library. If None, set the value based
the configuration.
"""
class CredentialWrappingMixin(object):
"""Mixin for wrapping authorized http clients."""
def WrapCredentials(
self,
http_client,
allow_account_impersonation=True,
use_google_auth=None,
credentials=None,
):
"""Get an http client for working with Google APIs.
Args:
http_client: The http client to be wrapped.
allow_account_impersonation: bool, True to allow use of impersonated
service account credentials for calls made with this client. If False,
the active user credentials will always be used.
use_google_auth: bool, True if the calling command indicates to use
google-auth library for authentication. If False, authentication will
fallback to using the oauth2client library. If None, set the value based
the configuration.
credentials: google.auth.credentials.Credentials, The credentials to use.
Returns:
An authorized http client with exception handling.
Raises:
creds_exceptions.Error: If an error loading the credentials occurs.
"""
# Wrappers for IAM header injection.
authority_selector = properties.VALUES.auth.authority_selector.Get()
authorization_token_file = (
properties.VALUES.auth.authorization_token_file.Get()
)
handlers = _GetIAMAuthHandlers(authority_selector, authorization_token_file)
if use_google_auth is None:
use_google_auth = True
if credentials is None:
credentials = store.LoadIfEnabled(
allow_account_impersonation, use_google_auth
)
if credentials:
http_client = self.AuthorizeClient(http_client, credentials)
# Set this attribute so we can access it later, even after the http_client
# request method has been wrapped
setattr(http_client, '_googlecloudsdk_credentials', credentials)
self.WrapRequest(
http_client, handlers, _HandleAuthError,
(client.AccessTokenRefreshError, google_auth_exceptions.RefreshError))
return http_client
@abc.abstractmethod
def AuthorizeClient(self, http_client, credentials):
"""Returns an http_client authorized with the given credentials."""
def _GetIAMAuthHandlers(authority_selector, authorization_token_file):
"""Get the request handlers for IAM authority selctors and auth tokens..
Args:
authority_selector: str, The authority selector string we want to use for
the request or None.
authorization_token_file: str, The file that contains the authorization
token we want to use for the request or None.
Returns:
[transport Modifiers]: A list of request modifier functions to use to wrap
an http request.
"""
authorization_token = None
if authorization_token_file:
try:
authorization_token = files.ReadFileContents(authorization_token_file)
except files.Error as e:
raise Error(e)
handlers = []
if authority_selector:
handlers.append(
transport.Handler(
transport.SetHeader('x-goog-iam-authority-selector',
authority_selector)))
if authorization_token:
handlers.append(
transport.Handler(
transport.SetHeader('x-goog-iam-authorization-token',
authorization_token.strip())))
return handlers
def _HandleAuthError(e):
"""Handle a generic auth error and raise a nicer message.
Args:
e: The exception that was caught.
Raises:
creds_exceptions.TokenRefreshError: If an auth error occurs.
"""
msg = six.text_type(e)
log.debug('Exception caught during HTTP request: %s', msg,
exc_info=True)
if context_aware.IsContextAwareAccessDeniedError(e):
raise creds_exceptions.TokenRefreshDeniedByCAAError(msg)
raise creds_exceptions.TokenRefreshError(msg)

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. 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 to get a credentialed transport object for making API calls."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.core import properties
from googlecloudsdk.core import requests as core_requests
from googlecloudsdk.core.credentials import http
from googlecloudsdk.core.credentials import requests
def GetApitoolsTransport(timeout='unset',
enable_resource_quota=True,
response_encoding=None,
ca_certs=None,
allow_account_impersonation=True,
use_google_auth=None,
response_handler=None,
redact_request_body_reason=None):
"""Get an transport client for use with apitools.
Args:
timeout: double, The timeout in seconds to pass to httplib2. This is the
socket level timeout. If timeout is None, timeout is infinite. If
default argument 'unset' is given, a sensible default is selected.
enable_resource_quota: bool, By default, we are going to tell APIs to use
the quota of the project being operated on. For some APIs we want to use
gcloud's quota, so you can explicitly disable that behavior by passing
False here.
response_encoding: str, the encoding to use to decode the response.
ca_certs: str, absolute filename of a ca_certs file that overrides the
default
allow_account_impersonation: bool, True to allow use of impersonated service
account credentials for calls made with this client. If False, the
active user credentials will always be used.
use_google_auth: bool, True if the calling command indicates to use
google-auth library for authentication. If False, authentication will
fallback to using the oauth2client library.
response_handler: requests.ResponseHandler, handler that gets executed
before any other response handling.
redact_request_body_reason: str, the reason why the request body must be
redacted if --log-http is used. If None, the body is not redacted.
Returns:
1. A httplib2.Http-like object backed by httplib2 or requests.
"""
if base.UseRequests():
if response_handler:
if not isinstance(response_handler, core_requests.ResponseHandler):
raise ValueError('response_handler should be of type ResponseHandler.')
if (properties.VALUES.core.log_http.GetBool() and
properties.VALUES.core.log_http_streaming_body.GetBool()):
# We want to print the actual body instead of printing the placeholder.
# To achieve this, we need to set streaming_response_body as False.
# Not that the body will be empty if the response_handler has already
# consumed the stream.
streaming_response_body = False
else:
streaming_response_body = response_handler.use_stream
else:
streaming_response_body = False
session = requests.GetSession(
timeout=timeout,
enable_resource_quota=enable_resource_quota,
ca_certs=ca_certs,
allow_account_impersonation=allow_account_impersonation,
streaming_response_body=streaming_response_body,
redact_request_body_reason=redact_request_body_reason)
return core_requests.GetApitoolsRequests(session, response_handler,
response_encoding)
return http.Http(timeout=timeout,
enable_resource_quota=enable_resource_quota,
response_encoding=response_encoding,
ca_certs=ca_certs,
allow_account_impersonation=allow_account_impersonation,
use_google_auth=use_google_auth)