264 lines
9.5 KiB
Python
264 lines
9.5 KiB
Python
# -*- 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)
|