286 lines
12 KiB
Python
286 lines
12 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2016 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 to install Application Default Credentials using a user account."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import textwrap
|
|
|
|
from googlecloudsdk.api_lib.auth import util as auth_util
|
|
from googlecloudsdk.calliope import actions
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions as c_exc
|
|
from googlecloudsdk.command_lib.auth import auth_util as command_auth_util
|
|
from googlecloudsdk.command_lib.auth import flags
|
|
from googlecloudsdk.command_lib.auth import workforce_login_config as workforce_login_config_util
|
|
from googlecloudsdk.core import config
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.console import console_io
|
|
from googlecloudsdk.core.credentials import creds as creds_module
|
|
from googlecloudsdk.core.credentials import gce as c_gce
|
|
from googlecloudsdk.core.credentials import store as c_store
|
|
from googlecloudsdk.core.util import files
|
|
|
|
|
|
@base.UniverseCompatible
|
|
class Login(base.Command):
|
|
r"""Acquire new user credentials to use for Application Default Credentials.
|
|
|
|
Obtains user access credentials via a web flow and puts them in the
|
|
well-known location for Application Default Credentials (ADC).
|
|
|
|
This command is useful when you are developing code that would normally
|
|
use a service account but need to run the code in a local development
|
|
environment where it's easier to provide user credentials. The credentials
|
|
will apply to all API calls that make use of the Application Default
|
|
Credentials client library. Do not set the `GOOGLE_APPLICATION_CREDENTIALS`
|
|
environment variable if you want to use the credentials generated by this
|
|
command in your local development. This command tries to find a quota
|
|
project from gcloud's context and write it to ADC so that Google client
|
|
libraries can use it for billing and quota. Alternatively, you can use
|
|
the `--client-id-file` flag. In this case, the project owning the client ID
|
|
will be used for billing and quota. You can create the client ID file
|
|
at https://console.cloud.google.com/apis/credentials.
|
|
|
|
This command has no effect on the user account(s) set up by the
|
|
`gcloud auth login` command.
|
|
|
|
Any credentials previously generated by
|
|
`gcloud auth application-default login` will be overwritten.
|
|
"""
|
|
detailed_help = {
|
|
'EXAMPLES':
|
|
"""\
|
|
If you want your local application to temporarily use your own user
|
|
credentials for API access, run:
|
|
|
|
$ {command}
|
|
|
|
If you'd like to login by passing in a file containing your own client
|
|
id, run:
|
|
|
|
$ {command} --client-id-file=clientid.json
|
|
"""
|
|
}
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
"""Set args for gcloud auth application-default login."""
|
|
parser.add_argument(
|
|
'--client-id-file',
|
|
help='A file containing your own client id to use to login. If '
|
|
'--client-id-file is specified, the quota project will not be '
|
|
'written to ADC.')
|
|
parser.add_argument(
|
|
'--scopes',
|
|
type=arg_parsers.ArgList(min_length=1),
|
|
metavar='SCOPE',
|
|
help='The names of the scopes to authorize for. By default '
|
|
'{0} scopes are used. '
|
|
'The list of possible scopes can be found at: '
|
|
'[](https://developers.google.com/identity/protocols/googlescopes). '
|
|
'To add scopes for applications outside of Google Cloud Platform, '
|
|
'such as Google Drive, [create an OAuth Client ID]'
|
|
'(https://support.google.com/cloud/answer/6158849) and provide it by '
|
|
'using the --client-id-file flag. '
|
|
.format(', '.join(auth_util.DEFAULT_SCOPES)))
|
|
parser.add_argument(
|
|
'--login-config',
|
|
help='Path to the login configuration file (workforce pool, '
|
|
'generated by the Cloud Console or '
|
|
'`gcloud iam workforce-pools create-login-config`)',
|
|
action=actions.StoreProperty(properties.VALUES.auth.login_config_file))
|
|
parser.add_argument(
|
|
'account',
|
|
nargs='?',
|
|
help=(
|
|
'User account used for authorization. When the account specified'
|
|
' has valid credentials in the local credential store these'
|
|
' credentials will be re-used. Otherwise new ones will be fetched'
|
|
' and replace any stored credential.'
|
|
' This caching behavior is only available for user credentials.'
|
|
),
|
|
)
|
|
flags.AddQuotaProjectFlags(parser)
|
|
flags.AddRemoteLoginFlags(parser, for_adc=True)
|
|
|
|
parser.display_info.AddFormat('none')
|
|
|
|
def Run(self, args):
|
|
"""Run the authentication command."""
|
|
# TODO(b/203102970): Remove this condition check after the bug is resolved
|
|
if properties.VALUES.auth.access_token_file.Get():
|
|
raise c_store.FlowError(
|
|
'auth/access_token_file or --access-token-file was set which is not '
|
|
'compatible with this command. Please unset the property and rerun '
|
|
'this command.'
|
|
)
|
|
|
|
if c_gce.Metadata().connected:
|
|
message = textwrap.dedent("""
|
|
You are running on a Google Compute Engine virtual machine.
|
|
The service credentials associated with this virtual machine
|
|
will automatically be used by Application Default
|
|
Credentials, so it is not necessary to use this command.
|
|
|
|
If you decide to proceed anyway, your user credentials may be visible
|
|
to others with access to this virtual machine. Are you sure you want
|
|
to authenticate with your personal account?
|
|
""")
|
|
console_io.PromptContinue(
|
|
message=message, throw_if_unattended=True, cancel_on_no=True)
|
|
|
|
command_auth_util.PromptIfADCEnvVarIsSet()
|
|
if args.client_id_file and not args.launch_browser:
|
|
raise c_exc.InvalidArgumentException(
|
|
'--no-launch-browser',
|
|
'`--no-launch-browser` flow no longer works with the '
|
|
'`--client-id-file`. Please replace `--no-launch-browser` with '
|
|
'`--no-browser`.'
|
|
)
|
|
|
|
# Currently the original scopes are not stored in the ADC. If they were, a
|
|
# change in scopes could also be used to determine a cache hit.
|
|
if args.account and not args.scopes:
|
|
if ShouldUseCachedCredentials(args.account):
|
|
log.warning(
|
|
'\nValid credentials already exist for {}. To force refresh the'
|
|
' existing credentials, omit the "ACCOUNT" positional field.'
|
|
.format(args.account)
|
|
)
|
|
return
|
|
|
|
scopes = args.scopes or auth_util.DEFAULT_SCOPES
|
|
|
|
target_impersonation_principal, delegates, target_scopes = None, None, None
|
|
impersonation_service_accounts = (
|
|
properties.VALUES.auth.impersonate_service_account.Get()
|
|
)
|
|
if impersonation_service_accounts:
|
|
(target_impersonation_principal, delegates
|
|
) = c_store.ParseImpersonationAccounts(impersonation_service_accounts)
|
|
# Only set target_scopes if user explicitly asked for something, if not
|
|
# we will let auth and client library decide on the scopes.
|
|
target_scopes = args.scopes or None
|
|
# Source credential just needs the scope needed to call the IAM API for
|
|
# impersonation. Asking user to consent any other scope is unnecessary.
|
|
scopes = [auth_util.CLOUD_PLATFORM_SCOPE]
|
|
|
|
flow_params = dict(
|
|
no_launch_browser=not args.launch_browser,
|
|
no_browser=not args.browser,
|
|
remote_bootstrap=args.remote_bootstrap,
|
|
)
|
|
|
|
# 1. Try the 3PI web flow with --no-browser:
|
|
# This could be a 3PI flow initiated via --no-browser.
|
|
# If provider_name is present, then this is the 3PI flow.
|
|
# We can start the flow as is as the remote_bootstrap value will be used.
|
|
if args.remote_bootstrap and 'provider_name' in args.remote_bootstrap:
|
|
auth_util.DoInstalledAppBrowserFlowGoogleAuth(
|
|
config.CLOUDSDK_EXTERNAL_ACCOUNT_SCOPES,
|
|
auth_proxy_redirect_uri=(
|
|
'https://sdk.cloud.google/applicationdefaultauthcode.html'
|
|
),
|
|
**flow_params
|
|
)
|
|
return
|
|
|
|
# 2. Try the 3PI web flow with a login configuration file.
|
|
login_config_file = workforce_login_config_util.GetWorkforceLoginConfig()
|
|
if login_config_file:
|
|
if args.client_id_file:
|
|
raise c_exc.ConflictingArgumentsException(
|
|
'--client-id-file is not currently supported for third party login '
|
|
'flows. ')
|
|
if args.scopes:
|
|
raise c_exc.ConflictingArgumentsException(
|
|
'--scopes is not currently supported for third party login flows.')
|
|
creds = workforce_login_config_util.DoWorkforceHeadfulLogin(
|
|
login_config_file,
|
|
True,
|
|
**flow_params
|
|
)
|
|
else:
|
|
# 3. Try the 1P web flow.
|
|
properties.VALUES.auth.client_id.Set(
|
|
auth_util.DEFAULT_CREDENTIALS_DEFAULT_CLIENT_ID)
|
|
properties.VALUES.auth.client_secret.Set(
|
|
auth_util.DEFAULT_CREDENTIALS_DEFAULT_CLIENT_SECRET)
|
|
if auth_util.CLOUD_PLATFORM_SCOPE not in scopes:
|
|
raise c_exc.InvalidArgumentException(
|
|
'--scopes',
|
|
'{} scope is required but not requested. Please include it in the'
|
|
' --scopes flag.'.format(auth_util.CLOUD_PLATFORM_SCOPE),
|
|
)
|
|
creds = auth_util.DoInstalledAppBrowserFlowGoogleAuth(
|
|
scopes,
|
|
client_id_file=args.client_id_file,
|
|
auth_proxy_redirect_uri=(
|
|
'https://sdk.cloud.google.com/applicationdefaultauthcode.html'
|
|
),
|
|
**flow_params
|
|
)
|
|
if not creds:
|
|
return
|
|
|
|
if args.account and hasattr(creds, 'with_account'):
|
|
_ = command_auth_util.ExtractAndValidateAccount(args.account, creds)
|
|
creds = creds.with_account(args.account)
|
|
|
|
if not target_impersonation_principal:
|
|
if args.IsSpecified('client_id_file'):
|
|
command_auth_util.DumpADC(creds, quota_project_disabled=False)
|
|
elif args.disable_quota_project:
|
|
command_auth_util.DumpADC(creds, quota_project_disabled=True)
|
|
else:
|
|
command_auth_util.DumpADCOptionalQuotaProject(creds)
|
|
else:
|
|
# TODO(b/184049366): Supports quota project with impersonated creds.
|
|
command_auth_util.DumpImpersonatedServiceAccountToADC(
|
|
creds,
|
|
target_principal=target_impersonation_principal,
|
|
delegates=delegates,
|
|
scopes=target_scopes)
|
|
return creds
|
|
|
|
|
|
def ShouldUseCachedCredentials(account):
|
|
"""Skip login if the existing ADC was provisioned for the requested account."""
|
|
try:
|
|
file_path = config.ADCFilePath()
|
|
data = files.ReadFileContents(file_path)
|
|
except files.Error:
|
|
return False
|
|
|
|
try:
|
|
credential = creds_module.FromJsonGoogleAuth(data)
|
|
except creds_module.UnknownCredentialsType:
|
|
return False
|
|
except creds_module.InvalidCredentialsError:
|
|
return False
|
|
|
|
cred_type = creds_module.CredentialTypeGoogleAuth.FromCredentials(credential)
|
|
if cred_type == creds_module.CredentialTypeGoogleAuth.USER_ACCOUNT:
|
|
if credential.account != account:
|
|
return False
|
|
|
|
return True
|