# -*- 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 command that prints access tokens.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import textwrap import time from google.auth import credentials from google.auth import exceptions as google_auth_exceptions from googlecloudsdk.api_lib.auth import exceptions as auth_exceptions from googlecloudsdk.api_lib.auth import util as auth_util from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import base from googlecloudsdk.calliope import exceptions as c_exc from googlecloudsdk.core import config from googlecloudsdk.core import log from googlecloudsdk.core import properties from googlecloudsdk.core.credentials import creds as c_creds from googlecloudsdk.core.credentials import store as c_store from oauth2client import client # bq needs this as part of its auth flow to support the Drive scope. _TRUSTED_SCOPES = list(config.CLOUDSDK_SCOPES) + [auth_util.GOOGLE_DRIVE_SCOPE] _MTLS_WARNING_INTERVAL_SECONDS = 24 * 60 * 60 # Seconds in a day _MTLS_WARNING_LAST_SHOWN_CONFIG_KEY = 'mtls_warning_last_shown' class FakeCredentials(object): """An access token container. oauth2client and google-auth are both supported by gcloud as the auth library. credentials in oauth2client store the access token in the "access_token" filed. google-auth stores it in the "token" filed. We use this fake credentials class to unify them. """ def __init__(self, token): self.token = token def _ShowMTLSWarningOnceDaily(): """Checks if mTLS warning should be shown and shows it if needed. The warning is shown at most once per day. """ current_time = time.time() should_show_warning = True active_config_store = config.GetConfigStore() if active_config_store: last_shown_str = active_config_store.Get( _MTLS_WARNING_LAST_SHOWN_CONFIG_KEY ) if last_shown_str: try: last_shown = float(last_shown_str) if (current_time - last_shown) < _MTLS_WARNING_INTERVAL_SECONDS: should_show_warning = False except ValueError: # If the stored value is not a valid float, # treat it as if the warning has never been shown. log.debug( 'Could not parse float from [%s] for %s.', last_shown_str, _MTLS_WARNING_LAST_SHOWN_CONFIG_KEY, ) if should_show_warning: log.warning(textwrap.dedent("""\ Warning: This access token is for the gcloud CLI tool itself and is \ bound to its specific client ID, which requires Certificate Based Access (CBA). \ This means it can only be used with CBA-enabled API endpoints. If you need a token for your own application code or scripts, you \ should use Application Default Credentials (ADC). Obtain an ADC \ token by running: 1. gcloud auth application-default login (if you haven't already) 2. gcloud auth application-default print-access-token ADC tokens are intended for local development and do not have the \ same CBA restrictions.""")) # Update the config store with the current timestamp if active_config_store: active_config_store.Set( _MTLS_WARNING_LAST_SHOWN_CONFIG_KEY, str(current_time) ) @base.UniverseCompatible class AccessToken(base.Command): """Print an access token for the specified account.""" detailed_help = { 'DESCRIPTION': """\ {description} See [RFC6749](https://tools.ietf.org/html/rfc6749) for more information about access tokens. Note that token itself may not be enough to access some services. If you use the token with curl or similar tools, you may see permission errors similar to "API has not been used in project 32555940559 before or it is disabled.". If it happens, you may need to provide a quota project in the "X-Goog-User-Project" header. For example, $ curl -H "X-Goog-User-Project: your-project" -H "Authorization: Bearer $(gcloud auth print-access-token)" foo.googleapis.com The identity that granted the token must have the serviceusage.services.use permission on the provided project. See https://cloud.google.com/apis/docs/system-parameters for more information. """, 'EXAMPLES': """\ To print access tokens: $ {command} """, } @staticmethod def Args(parser): parser.add_argument( 'account', nargs='?', help=( 'Account to get the access token for. If not specified, ' 'the current active account will be used.' ), ) parser.add_argument( '--lifetime', type=arg_parsers.Duration(upper_bound='43200s'), help=( 'Access token lifetime. The default access token ' 'lifetime is 3600 seconds, but you can use this flag to reduce ' 'the lifetime or extend it up to 43200 seconds (12 hours). The ' 'org policy constraint ' '`constraints/iam.allowServiceAccountCredentialLifetimeExtension`' ' must be set if you want to extend the lifetime beyond 3600 ' 'seconds. Note that this flag is for service account ' 'impersonation only, so it must be used together with the ' '`--impersonate-service-account` flag.' ), ) parser.add_argument( '--scopes', hidden=True, type=arg_parsers.ArgList(min_length=1), metavar='SCOPE', help=( 'The scopes to authorize for. This flag is supported for user' ' accounts and service accounts only. The list of possible scopes' ' can be found at:' ' https://developers.google.com/identity/protocols/googlescopes.\n\nFor' ' end-user accounts the provided scopes must from [{0}]'.format( _TRUSTED_SCOPES ) ), ) parser.display_info.AddFormat('value(token)') @c_exc.RaiseErrorInsteadOf( auth_exceptions.AuthenticationError, client.Error, google_auth_exceptions.GoogleAuthError, ) def Run(self, args): """Run the helper command.""" if ( properties.VALUES.context_aware.use_client_certificate.GetBool() and properties.IsInternalUserCheck() ): _ShowMTLSWarningOnceDaily() if args.lifetime and not args.impersonate_service_account: raise c_exc.InvalidArgumentException( '--lifetime', 'Lifetime flag is for service account impersonation only. It must be ' 'used together with the --impersonate-service-account flag.', ) # Do not auto cache the custom scoped access token. Otherwise, it'll # affect other gcloud CLIs that depends on cloud-platform scopes. cache_only_rapt = True if args.scopes else False cred = c_store.Load( args.account, allow_account_impersonation=True, use_google_auth=True, cache_only_rapt=cache_only_rapt, ) # c_store.Load already refreshed the cred, so we don't need to refresh the # cred unless we need to alter the cred in the code below, for example, # changing the scopes. should_refresh_again = False if args.scopes: # refresh again due to altered scopes should_refresh_again = True cred_type = c_creds.CredentialTypeGoogleAuth.FromCredentials(cred) if cred_type not in [ c_creds.CredentialTypeGoogleAuth.USER_ACCOUNT, c_creds.CredentialTypeGoogleAuth.SERVICE_ACCOUNT, ]: # TODO(b/223649175): Add support for other credential types(e.g GCE). log.warning( '`--scopes` flag may not work as expected and will be ignored ' 'for account type {}.'.format(cred_type.key) ) scopes = args.scopes + [auth_util.OPENID, auth_util.USER_EMAIL_SCOPE] # non user account credential types if isinstance(cred, credentials.Scoped): cred = cred.with_scopes(scopes) else: requested_scopes = set(args.scopes) trusted_scopes = set(_TRUSTED_SCOPES) if not requested_scopes.issubset(trusted_scopes): raise c_exc.InvalidArgumentException( '--scopes', 'Invalid scopes value. Please make sure the scopes are from [{0}]' .format(config.CLOUDSDK_SCOPES), ) # pylint:disable=protected-access cred._scopes = scopes if c_creds.IsImpersonatedAccountCredentials(cred) and args.lifetime: # refresh again due to altered lifetime should_refresh_again = True cred._lifetime = args.lifetime # pylint: disable=protected-access if should_refresh_again: c_store.Refresh(cred) if c_creds.IsOauth2ClientCredentials(cred): token = cred.access_token else: token = cred.token if not token: raise auth_exceptions.InvalidCredentialsError( 'No access token could be obtained from the current credentials.' ) return FakeCredentials(token)