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,21 @@
# -*- 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.
"""Libraries to support the auth command surface."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

View File

@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Support library for the auth command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
import os
import textwrap
from google.auth import jwt
from googlecloudsdk.api_lib.auth import exceptions as auth_exceptions
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.iamcredentials import util as impersonation_util
from googlecloudsdk.calliope import exceptions as c_exc
from googlecloudsdk.command_lib.projects import util as project_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 c_creds
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms
from oauth2client import client
from oauth2client import service_account
from oauth2client.contrib import gce as oauth2client_gce
SERVICEUSAGE_PERMISSION = 'serviceusage.services.use'
class MissingPermissionOnQuotaProjectError(c_creds.ADCError):
"""An error when ADC does not have permission to bill a quota project."""
class AddQuotaProjectError(c_creds.ADCError):
"""An error when quota project ID is added to creds that don't support it."""
def IsGceAccountCredentials(cred):
"""Checks if the credential is a Compute Engine service account credential."""
# Import only when necessary to decrease the startup time. Move it to
# global once google-auth is ready to replace oauth2client.
# pylint: disable=g-import-not-at-top
import google.auth.compute_engine as google_auth_gce
return (isinstance(cred, oauth2client_gce.AppAssertionCredentials) or
isinstance(cred, google_auth_gce.credentials.Credentials))
def IsServiceAccountCredential(cred):
"""Checks if the credential is a service account credential."""
# Import only when necessary to decrease the startup time. Move it to
# global once google-auth is ready to replace oauth2client.
# pylint: disable=g-import-not-at-top
import google.oauth2.service_account as google_auth_service_account
return (isinstance(cred, service_account.ServiceAccountCredentials) or
isinstance(cred, google_auth_service_account.Credentials))
def IsImpersonationCredential(cred):
"""Checks if the credential is an impersonated service account credential."""
return (impersonation_util.
ImpersonationAccessTokenProvider.IsImpersonationCredential(cred))
def ValidIdTokenCredential(cred):
return (IsImpersonationCredential(cred) or
IsServiceAccountCredential(cred) or
IsGceAccountCredentials(cred))
def PromptIfADCEnvVarIsSet():
"""Warns users if ADC environment variable is set."""
override_file = config.ADCEnvVariable()
if override_file:
message = textwrap.dedent("""
The environment variable [{envvar}] is set to:
[{override_file}]
Credentials will still be generated to the default location:
[{default_file}]
To use these credentials, unset this environment variable before
running your application.
""".format(
envvar=client.GOOGLE_APPLICATION_CREDENTIALS,
override_file=override_file,
default_file=config.ADCFilePath()))
console_io.PromptContinue(
message=message, throw_if_unattended=True, cancel_on_no=True)
def WriteGcloudCredentialsToADC(creds, add_quota_project=False):
"""Writes gclouds's credential from auth login to ADC json."""
# TODO(b/190114370): We will also support writing service account creds.
if (not c_creds.IsUserAccountCredentials(creds) and
not c_creds.IsExternalAccountCredentials(creds)):
log.warning('Credentials cannot be written to application default '
'credentials because it is not a user or external account '
'credential.')
return
# Quota project ID should not be added to non-user credentials.
if c_creds.IsExternalAccountCredentials(creds) and add_quota_project:
raise AddQuotaProjectError(
'The application default credentials are external account credentials, '
'quota project cannot be added.')
PromptIfADCEnvVarIsSet()
if add_quota_project:
DumpADCOptionalQuotaProject(creds)
else:
c_creds.ADC(creds).DumpADCToFile()
def GetADCAsJson():
"""Reads ADC from disk and converts it to a json object."""
if not os.path.isfile(config.ADCFilePath()):
return None
with files.FileReader(config.ADCFilePath()) as f:
return json.load(f)
def GetQuotaProjectFromADC():
"""Reads the quota project ID from ADC json file and return it."""
adc_json = GetADCAsJson()
try:
return adc_json['quota_project_id']
except (KeyError, TypeError):
return None
def AssertADCExists():
adc_path = config.ADCFilePath()
if not os.path.isfile(adc_path):
raise c_exc.BadFileException(
'Application default credentials have not been set up. '
'Run $ gcloud auth application-default login to set it up first.')
def ADCIsUserAccount():
"""Returns whether the ADC credentials correspond to a user account or not."""
cred_file = config.ADCFilePath()
creds, _ = c_creds.GetGoogleAuthDefault().load_credentials_from_file(
cred_file)
return (c_creds.IsUserAccountCredentials(creds) or
c_creds.IsExternalAccountUserCredentials(creds))
def AdcHasGivenPermissionOnProject(project_id, permissions):
AssertADCExists()
project_ref = project_util.ParseProject(project_id)
return _AdcHasGivenPermissionOnProjectHelper(project_ref, permissions)
def _AdcHasGivenPermissionOnProjectHelper(project_ref, permissions):
cred_file_override_old = properties.VALUES.auth.credential_file_override.Get()
try:
properties.VALUES.auth.credential_file_override.Set(config.ADCFilePath())
granted_permissions = projects_api.TestIamPermissions(
project_ref, permissions).permissions
return set(permissions) == set(granted_permissions)
finally:
properties.VALUES.auth.credential_file_override.Set(cred_file_override_old)
# Create this function to ease unit test mocks.
def GetAdcRealPath(adc_path):
return os.path.realpath(adc_path)
def LogADCIsWritten(adc_path):
"""Prints the confirmation when ADC file was successfully written."""
real_path = adc_path
if platforms.OperatingSystem.Current() == platforms.OperatingSystem.WINDOWS:
real_path = GetAdcRealPath(adc_path)
log.status.Print('\nCredentials saved to file: [{}]'.format(real_path))
log.status.Print(
'\nThese credentials will be used by any library that requests '
'Application Default Credentials (ADC).')
# See https://bugs.python.org/issue40377 for the issue with python
# from Microsoft store. The ADC file is transparently redirected to a
# different path which client libraries do not expect, so cannot locate.
if real_path != adc_path:
log.warning('You may be running gcloud with a python interpreter installed '
'from Microsoft Store which is not supported by this command. '
'Run `gcloud topic startup` for instructions to select a '
'different python interpreter. Otherwise, you have to '
'set the environment variable `GOOGLE_APPLICATION_CREDENTIALS` '
'to the file path `{}`. See '
'https://cloud.google.com/docs/authentication/'
'getting-started#setting_the_environment_variable '
'for more information.'.format(real_path))
def LogQuotaProjectAdded(quota_project):
log.status.Print(
'\nQuota project "{}" was added to ADC which can be used by Google '
'client libraries for billing and quota. Note that some services may '
'still bill the project owning the resource.'.format(quota_project))
def LogQuotaProjectNotFound():
log.warning('\nCannot find a quota project to add to ADC. You might receive '
'a "quota exceeded" or "API not enabled" error. Run $ gcloud '
'auth application-default set-quota-project to add '
'a quota project.')
def LogMissingPermissionOnQuotaProject(quota_project):
log.warning(
'\nCannot add the project "{}" to ADC as the quota project because the '
'account in ADC does not have the "{}" permission on this project. '
'You might receive a "quota_exceeded" or "API not enabled" error. '
'Run $ gcloud auth application-default set-quota-project to add a quota '
'project.'.format(quota_project, SERVICEUSAGE_PERMISSION))
def LogQuotaProjectDisabled():
log.warning(
'\nQuota project is disabled. You might receive a "quota exceeded" or '
'"API not enabled" error. Run $ gcloud auth application-default '
'set-quota-project to add a quota project.')
def DumpADC(credentials, quota_project_disabled=False):
"""Dumps the given credentials to ADC file.
Args:
credentials: a credentials from oauth2client or google-auth libraries, the
credentials to dump.
quota_project_disabled: bool, If quota project is explicitly disabled by
users using flags.
"""
adc_path = c_creds.ADC(credentials).DumpADCToFile()
LogADCIsWritten(adc_path)
if quota_project_disabled:
LogQuotaProjectDisabled()
def DumpADCOptionalQuotaProject(credentials):
"""Dumps the given credentials to ADC file with an optional quota project.
Loads quota project from gcloud's context and writes it to application default
credentials file if the credentials has the "serviceusage.services.use"
permission on the quota project..
Args:
credentials: a credentials from oauth2client or google-auth libraries, the
credentials to dump.
"""
adc_path = c_creds.ADC(credentials).DumpADCToFile()
LogADCIsWritten(adc_path)
quota_project = c_creds.GetQuotaProject(
credentials, force_resource_quota=True)
if not quota_project:
LogQuotaProjectNotFound()
elif AdcHasGivenPermissionOnProject(
quota_project, permissions=[SERVICEUSAGE_PERMISSION]):
c_creds.ADC(credentials).DumpExtendedADCToFile(quota_project=quota_project)
LogQuotaProjectAdded(quota_project)
else:
LogMissingPermissionOnQuotaProject(quota_project)
def AddQuotaProjectToADC(quota_project):
"""Adds the quota project to the existing ADC file.
Quota project is only added to ADC when the credentials have the
"serviceusage.services.use" permission on the project.
Args:
quota_project: str, The project id of a valid GCP project to add to ADC.
Raises:
MissingPermissionOnQuotaProjectError: If the credentials do not have the
"serviceusage.services.use" permission.
"""
AssertADCExists()
if not ADCIsUserAccount():
raise c_exc.BadFileException(
'The application default credentials are not user credentials, quota '
'project cannot be added.'
)
credentials, _ = c_creds.GetGoogleAuthDefault().load_credentials_from_file(
config.ADCFilePath()
)
previous_quota_project = credentials.quota_project_id
# we call the cloudresourcemanager api under the quota of gcloud's quota
# project to check if the user is granted the usage permission. Given the
# current project can have this api disabled. So removing the quota project
# from ADC so that check can be done regardless if cloudresourcemanager api
# is enabled or disabled on the project.
c_creds.ADC(credentials).DumpADCToFile()
if not AdcHasGivenPermissionOnProject(
quota_project, permissions=[SERVICEUSAGE_PERMISSION]
):
c_creds.ADC(credentials).DumpExtendedADCToFile(
quota_project=previous_quota_project
)
raise MissingPermissionOnQuotaProjectError(
'Cannot add the project "{}" to application default credentials (ADC) '
'as a quota project because the account in ADC does not have the '
'"{}" permission on this project.'.format(
quota_project, SERVICEUSAGE_PERMISSION
)
)
adc_path = c_creds.ADC(credentials).DumpExtendedADCToFile(
quota_project=quota_project
)
LogADCIsWritten(adc_path)
LogQuotaProjectAdded(quota_project)
def DumpImpersonatedServiceAccountToADC(credentials,
target_principal,
delegates,
scopes=None):
adc_path = c_creds.ADC(credentials, target_principal,
delegates, scopes).DumpADCToFile()
LogADCIsWritten(adc_path)
def ExtractAndValidateAccount(account, creds):
"""Extracts account from creds and validates it against account."""
decoded_id_token = jwt.decode(creds.id_token, verify=False)
web_flow_account = decoded_id_token['email']
if account and account.lower() != web_flow_account.lower():
raise auth_exceptions.WrongAccountError(
'You attempted to log in as account [{account}] but the received '
'credentials were for account [{web_flow_account}].\n\n'
'Please check that your browser is logged in as account [{account}] '
'and that you are using the correct browser profile.'.format(
account=account, web_flow_account=web_flow_account
)
)
return web_flow_account

View File

@@ -0,0 +1,422 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Create ECP configurations."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
import json
import os
from googlecloudsdk.core import config
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms
RESOURCE_TYPE = 'enterprise-certificate-proxy configuration file'
def get_platform_folder():
sdk_root = config.Paths().sdk_root
if not sdk_root:
raise ECPConfigError(
'Unable to find the SDK root path. The gcloud installation may be'
' corrupted.'
)
return os.path.join(sdk_root, 'platform', 'enterprise_cert')
def get_bin_folder():
sdk_bin_path = config.Paths().sdk_bin_path
if not sdk_bin_path:
raise ECPConfigError(
'Unable to find the SDK bin path. The gcloud installation may be'
' corrupted.'
)
return sdk_bin_path
def get_config_path(output_file):
if output_file:
return output_file
return config.CertConfigDefaultFilePath()
def platform_to_config(platform):
if not platform:
platform = platforms.Platform.Current()
if platform.operating_system == platforms.OperatingSystem.MACOSX:
return ConfigType.KEYCHAIN
elif platform.operating_system == platforms.OperatingSystem.LINUX:
return ConfigType.PKCS11
elif platform.operating_system == platforms.OperatingSystem.WINDOWS:
return ConfigType.MYSTORE
else:
raise ECPConfigError(
(
'Unsupported platform {}. Enterprise Certificate Proxy currently'
' only supports OSX, Windows, and Linux.'
).format(platform.operating_system)
)
class ConfigType(enum.Enum):
PKCS11 = 1
KEYCHAIN = 2
MYSTORE = 3
WORKLOAD = 4
class WindowsBinaryPathConfig(object):
"""Configuration for the paths to the ECP binaries on Windows.
Attributes:
ecp: Path to the ECP binary.
ecp_http_proxy: Path to the ECP HTTP proxy binary.
ecp_client: Path to the ECP client library.
tls_offload: Path to the TLS offload library.
"""
def __init__(self, ecp, ecp_client, tls_offload, ecp_http_proxy):
self.ecp = ecp if ecp else os.path.join(get_bin_folder(), 'ecp.exe')
self.ecp_http_proxy = (
ecp_http_proxy
if ecp_http_proxy
else os.path.join(get_bin_folder(), 'ecp_http_proxy.exe')
)
self.ecp_client = (
ecp_client
if ecp_client
else os.path.join(get_platform_folder(), 'libecp.dll')
)
self.tls_offload = (
tls_offload
if tls_offload
else os.path.join(get_platform_folder(), 'libtls_offload.dll')
)
class LinuxPathConfig(object):
"""Configuration for the paths to the ECP binaries on Linux.
Attributes:
ecp: Path to the ECP binary.
ecp_http_proxy: Path to the ECP HTTP proxy binary.
ecp_client: Path to the ECP client library.
tls_offload: Path to the TLS offload library.
"""
def __init__(self, ecp, ecp_client, tls_offload, ecp_http_proxy):
self.ecp = ecp if ecp else os.path.join(get_bin_folder(), 'ecp')
self.ecp_http_proxy = (
ecp_http_proxy
if ecp_http_proxy
else os.path.join(get_bin_folder(), 'ecp_http_proxy')
)
self.ecp_client = (
ecp_client
if ecp_client
else os.path.join(get_platform_folder(), 'libecp.so')
)
self.tls_offload = (
tls_offload
if tls_offload
else os.path.join(get_platform_folder(), 'libtls_offload.so')
)
class MacOSBinaryPathConfig(object):
"""Configuration for the paths to the ECP binaries on MacOS.
Attributes:
ecp: Path to the ECP binary.
ecp_http_proxy: Path to the ECP HTTP proxy binary.
ecp_client: Path to the ECP client library.
tls_offload: Path to the TLS offload library.
"""
def __init__(self, ecp, ecp_client, tls_offload, ecp_http_proxy):
self.ecp = ecp if ecp else os.path.join(get_bin_folder(), 'ecp')
self.ecp_http_proxy = (
ecp_http_proxy
if ecp_http_proxy
else os.path.join(get_bin_folder(), 'ecp_http_proxy')
)
self.ecp_client = (
ecp_client
if ecp_client
else os.path.join(get_platform_folder(), 'libecp.dylib')
)
self.tls_offload = (
tls_offload
if tls_offload
else os.path.join(get_platform_folder(), 'libtls_offload.dylib')
)
class PKCS11Config(object):
def __init__(self, module, slot, label, user_pin):
self.module = module
self.slot = slot
self.label = label
if user_pin:
self.user_pin = user_pin
class KeyChainConfig(object):
def __init__(self, issuer, keychain_type):
self.issuer = issuer
self.keychain_type = keychain_type
class MyStoreConfig(object):
def __init__(self, issuer, store, provider):
self.issuer = issuer
self.store = store
self.provider = provider
class WorkloadConfig(object):
def __init__(self, cert_path, key_path):
self.cert_path = cert_path
self.key_path = key_path
def create_linux_config(base_config, **kwargs):
"""Creates a Linux ECP Config.
Args:
base_config: Optional parameter to use as a fallback for parameters that are
not set in kwargs.
**kwargs: Linux config parameters. See go/enterprise-cert-config for valid
variables.
Returns:
A dictionary object containing the ECP config.
"""
if base_config:
base_linux_config = base_config.get('cert_configs', {}).get('pkcs11', {})
base_libs_config = base_config.get('libs', {})
else:
base_linux_config = {}
base_libs_config = {}
ecp_config = PKCS11Config(
kwargs.get('module', None) or base_linux_config.get('module', None),
kwargs.get('slot', None) or base_linux_config.get('slot', 0),
kwargs.get('label', None) or base_linux_config.get('label', None),
kwargs.get('user_pin', None) or base_linux_config.get('user_pin', None),
)
lib_config = LinuxPathConfig(
kwargs.get('ecp', None) or base_libs_config.get('ecp', None),
kwargs.get('ecp_client', None)
or base_libs_config.get('ecp_client', None),
kwargs.get('tls_offload', None)
or base_libs_config.get('tls_offload', None),
kwargs.get('ecp_http_proxy', None)
or base_libs_config.get('ecp_http_proxy', None),
)
return {'pkcs11': vars(ecp_config)}, {'libs': vars(lib_config)}
def create_macos_config(base_config, **kwargs):
"""Creates a MacOS ECP Config.
Args:
base_config: Optional parameter to use as a fallback for parameters that are
not set in kwargs.
**kwargs: MacOS config parameters. See go/enterprise-cert-config for valid
variables.
Returns:
A dictionary object containing the ECP config.
"""
if base_config:
base_macos_config = base_config['cert_configs']['macos_keychain']
base_libs_config = base_config['libs']
else:
base_macos_config = {}
base_libs_config = {}
ecp_config = KeyChainConfig(
kwargs.get('issuer', None) or base_macos_config.get('issuer', None),
kwargs.get('keychain_type', 'all')
or base_macos_config.get('keychain_type', 'all'),
)
lib_config = MacOSBinaryPathConfig(
kwargs.get('ecp', None) or base_libs_config.get('ecp', None),
kwargs.get('ecp_client', None)
or base_libs_config.get('ecp_client', None),
kwargs.get('tls_offload', None)
or base_libs_config.get('tls_offload', None),
kwargs.get('ecp_http_proxy', None)
or base_libs_config.get('ecp_http_proxy', None),
)
return {'macos_keychain': vars(ecp_config)}, {'libs': vars(lib_config)}
def create_windows_config(base_config, **kwargs):
"""Creates a Windows ECP Config.
Args:
base_config: Optional parameter to use as a fallback for parameters that are
not set in kwargs.
**kwargs: Windows config parameters. See go/enterprise-cert-config for valid
variables.
Returns:
A dictionary object containing the ECP config.
"""
if base_config:
base_windows_config = base_config['cert_configs']['windows_store']
base_libs_config = base_config['libs']
else:
base_windows_config = {}
base_libs_config = {}
ecp_config = MyStoreConfig(
kwargs.get('issuer', None) or base_windows_config.get('issuer', None),
kwargs.get('store', None) or base_windows_config.get('store', None),
kwargs.get('provider', None) or base_windows_config.get('provider', None),
)
lib_config = WindowsBinaryPathConfig(
kwargs.get('ecp', None) or base_libs_config.get('ecp', None),
kwargs.get('ecp_client', None)
or base_libs_config.get('ecp_client', None),
kwargs.get('tls_offload', None)
or base_libs_config.get('tls_offload', None),
kwargs.get('ecp_http_proxy', None)
or base_libs_config.get('ecp_http_proxy', None),
)
return {'windows_store': vars(ecp_config)}, {'libs': vars(lib_config)}
def create_workload_config(base_config, **kwargs):
"""Creates a Workload ECP Config.
Args:
base_config: Optional parameter to use as a fallback for parameters that are
not set in kwargs.
**kwargs: Workload config parameters. See go/enterprise-cert-config for
valid variables.
Returns:
A dictionary object containing the ECP config.
"""
if base_config:
base_workload_config = base_config['cert_configs']['workload']
else:
base_workload_config = {}
workload_config = WorkloadConfig(
kwargs.get('cert_path', None)
or base_workload_config.get('cert_path', None),
kwargs.get('key_path', None)
or base_workload_config.get('key_path', None),
)
return {'workload': vars(workload_config)}, {}
def create_ecp_config(config_type, base_config=None, **kwargs):
"""Creates an ECP Config.
Args:
config_type: An ConfigType Enum that describes the type of ECP config.
base_config: Optional parameter to use as a fallback for parameters that are
not set in kwargs.
**kwargs: config parameters. See go/enterprise-cert-config for valid
variables.
Returns:
A dictionary object containing the ECP config.
Raises:
ECPConfigError: No valid config_type is specified.
"""
if config_type == ConfigType.PKCS11:
ecp_config, libs_config = create_linux_config(base_config, **kwargs)
elif config_type == ConfigType.KEYCHAIN:
ecp_config, libs_config = create_macos_config(base_config, **kwargs)
elif config_type == ConfigType.MYSTORE:
ecp_config, libs_config = create_windows_config(base_config, **kwargs)
elif config_type == ConfigType.WORKLOAD:
ecp_config, libs_config = create_workload_config(base_config, **kwargs)
else:
raise ECPConfigError(
(
'Unknown config_type {} passed to create enterprise certificate'
' configuration. Valid options are: [PKCS11, KEYCHAIN, MYSTORE]'
).format(config_type)
)
# TODO(b/459858373): remove gating for ECP HTTP Proxy on internal user check.
if (
not (
properties.VALUES.context_aware.use_ecp_http_proxy.GetBool()
and properties.IsInternalUserCheck()
)
and 'libs' in libs_config
and 'ecp_http_proxy' in libs_config['libs']
):
del libs_config['libs']['ecp_http_proxy']
return {'cert_configs': ecp_config, **libs_config}
def create_config(config_type, **kwargs):
"""Creates the ECP config based on the passed in CLI arguments."""
output = create_ecp_config(config_type, None, **kwargs)
config_path = get_config_path(kwargs.get('output_file', None))
files.WriteFileContents(config_path, json.dumps(output, indent=2))
log.CreatedResource(config_path, RESOURCE_TYPE)
def update_config(config_type, **kwargs):
"""Updates the ECP config based on the passed in CLI arguments.
Args:
config_type: An ConfigType Enum that describes the type of ECP config.
**kwargs: config parameters that will be updated. See
go/enterprise-cert-config for valid variables.
Only explicit args will overwrite existing values
"""
config_path = get_config_path(kwargs.get('output_file', None))
data = files.ReadFileContents(config_path)
active_config = json.loads(data)
output = create_ecp_config(config_type, active_config, **kwargs)
files.WriteFileContents(config_path, json.dumps(output, indent=2))
log.CreatedResource(config_path, RESOURCE_TYPE)
class ECPConfigError(Exception):
def __init__(self, message):
super(ECPConfigError, self).__init__()
self.message = message

View File

@@ -0,0 +1,29 @@
# -*- 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 the auth commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import exceptions
class Error(exceptions.Error):
"""Errors raised by this module."""
class CredentialsNotFound(Error):
"""Raised when credentials could not be located."""

View File

@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Defines arguments for gcloud auth."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
def AddAccountArg(parser):
parser.add_argument(
'account',
nargs='?',
help=('Account to print the identity token for. If not specified, '
'the current active account will be used.'))
def AddAudienceArg(parser):
parser.add_argument(
'--audiences',
type=str,
metavar='AUDIENCES',
help='Intended recipient of the token. '
'Currently, only one audience can be specified.')
def AddIncludeEmailArg(parser):
parser.add_argument(
'--include-email',
action='store_true',
help=('Specify whether or not service account email is included in the '
"identity token. If specified, the token will contain 'email' and "
"'email_verified' claims. This flag should only be used for "
'impersonate service account.'))
def AddGCESpecificArgs(parser):
"""Add GCE specific arguments to parser."""
gce_arg_group = parser.add_argument_group(
help='Parameters for Google Compute Engine instance identity tokens.')
gce_arg_group.add_argument(
'--token-format',
choices=['standard', 'full'],
default='standard',
help='Specify whether or not the project and instance details are '
'included in the identity token payload. This flag only applies to '
'Google Compute Engine instance identity tokens. '
'See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#token_format '
'for more details on token format.')
gce_arg_group.add_argument(
'--include-license',
action='store_true',
help='Specify whether or not license codes for images associated with '
'this instance are included in the identity token payload. Default '
'is False. This flag does not have effect unless '
'`--token-format=full`.')
def _AddDisableQuotaProject(parser):
parser.add_argument(
'--disable-quota-project',
default=False,
action='store_true',
help="""\
By default, the project in billing/quota_project or core/project will
be written to application default credentials (ADC) as the quota project.
When both are set, billing/quota_project takes precedence.
You can use --billing-project to overwrite the value in
billing/quota_project. Similarly, you can use --project to overwrite
the value in core/project. Client libraries will send it to services
and use it for quota and billing. To be able to use a project as the
quota project, the account in ADC must have the serviceusage.services.use
permission on the project. This permission is granted to the
project editor and project owner. You can create custom roles to
include this permission.
Note that some cloud services may ignore this quota project and still
bill the project owning the resources.
In the following situations, you may use this flag to skip setting the
quota project:
* The account in ADC cannot be granted the project editor or owner
role or any role with the serviceusage.services.use permission.
* You always want to bill the project owning the resources.
"""
)
def AddQuotaProjectFlags(parser):
_AddDisableQuotaProject(parser)
def AddNoBrowserFlag(parser, auth_target, auth_command):
parser.add_argument(
'--browser',
default=True,
action='store_true',
help="""\
If you want to authorize the {0} on a machine that doesn't
have a browser and you can install the gcloud CLI on another machine
with a browser, use the `--no-browser` flag.
1. To initiate authorization, enter the following command:
```
{1} --no-browser
```
2. Copy the long command that begins with `{1} --remote-bootstrap="`.
3. Paste and run this command on the command line of a different,
trusted machine that has local installations of both a web browser
and the gcloud CLI tool version 372.0 or later.
4. Copy the long URL output from the machine with the web browser.
5. Paste the long URL back to the first machine under the prompt,
"Enter the output of the above command", and press Enter to complete
the authorization.
""".format(auth_target, auth_command),
)
def AddRemoteBootstrapFlag(parser):
parser.add_argument(
'--remote-bootstrap',
# Never un-hide this flag because it should not be used directly by users.
hidden=True,
default=None,
help='Use this flag to pass login parameters to a gcloud instance '
'which will help this gcloud to login. '
'This flag is reserved for bootstrapping remote workstation without '
'access to web browsers, which should be initiated by using '
'the --no-browser. Users should not use this flag directly.')
def AddNoBrowserArgGroup(parser, auth_target, auth_command):
group = parser.add_mutually_exclusive_group()
AddNoBrowserFlag(group, auth_target, auth_command)
AddRemoteBootstrapFlag(group)
def AddRemoteLoginFlags(parser, for_adc=False):
if for_adc:
auth_command = 'gcloud auth application-default login'
auth_target = 'client libraries'
else:
auth_command = 'gcloud auth login'
auth_target = 'gcloud CLI'
AddNoBrowserArgGroup(parser, auth_target, auth_command)
AddNoLaunchBrowserFlag(parser, auth_target, auth_command)
def AddNoLaunchBrowserFlag(parser, auth_target, auth_command):
parser.add_argument(
'--launch-browser',
default=True,
dest='launch_browser',
help="""\
Launch a browser for authorization. If not enabled or if it
is not possible to launch a browser, prints a URL to standard output
to be copied.
If you want to authorize the {0} on a
machine that doesn't have a browser and you cannot install the
gcloud CLI on another machine with a browser, use the
`--no-launch-browser` flag.
The `--no-launch-browser` flag prevents the command from automatically
opening a web browser.
1. To initiate authorization, enter the following command:
```
{1} --no-launch-browser
```
2. Copy the long URL that begins with
`https://accounts.google.com/o/oauth2/auth... `
3. Paste this URL into the browser of a different, trusted machine that
has a web browser.
4. Copy the authorization code from the machine with the web browser.
5. Paste the authorization code back to the first machine at the prompt,
"Enter authorization code", and press Enter
to complete the authorization.
""".format(auth_target, auth_command),
action='store_true'
)
def AddCommonEnterpriseCertConfigFlags(parser):
"""Common ECP configuration flags."""
parser.add_argument(
'--ecp',
default=None,
help=(
'Provide a custom path to the enterprise-certificate-proxy binary.'
' This flag must be the full path to the binary.'
),
)
parser.add_argument(
'--ecp-client',
default=None,
help=(
'Provide a custom path to the enterprise-certificate-proxy shared'
' client library. This flag must be the full path to the shared'
' library.'
),
)
parser.add_argument(
'--tls-offload',
default=None,
help=(
'Provide a custom path to the enterprise-certificate-proxy shared tls'
' offload library. This flag must be the full path to the shared'
' library.'
),
)
parser.add_argument(
'--output-file',
default=None,
help=(
'Override the file path that the enterprise-certificate-proxy'
' configuration is written to.'
),
)

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Support library for the login-config auth commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from google.auth import external_account_authorized_user
from googlecloudsdk.api_lib.auth import util as auth_util
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.core import config
from googlecloudsdk.core import properties
CLOUDSDK_AUTH_LOGIN_CONFIG_FILE = 'CLOUDSDK_AUTH_LOGIN_CONFIG_FILE'
GOOGLE_DEFAULT_CLOUD_WEB_DOMAIN = 'cloud.google'
AUTH_PROXY_URL_GDU = 'https://sdk.cloud.google/authcode.html'
AUTH_PROXY_URL_TEMPLATE_NON_GDU = 'https://sdk.{}/authcode'
ADC_AUTH_PROXY_URL_GDU = (
'https://sdk.cloud.google/applicationdefaultauthcode.html'
)
ADC_AUTH_PROXY_URL_TEMPLATE_NON_GDU = (
'https://sdk.{}/applicationdefaultauthcode'
)
def DoWorkforceHeadfulLogin(login_config_file, is_adc=False, **kwargs):
"""DoWorkforceHeadfulLogin attempts to log in with appropriate login configuration.
It will return the account and credentials of the user if it succeeds
Args:
login_config_file (str): The path to the workforce headful login
configuration file.
is_adc (str): Whether the flow is initiated via application-default login.
**kwargs (Mapping): Extra Arguments to pass to the method creating the flow.
Returns:
(google.auth.credentials.Credentials): The account and
credentials of the user who logged in
"""
login_config_data = auth_util.GetCredentialsConfigFromFile(login_config_file)
if login_config_data.get(
'type', None) != 'external_account_authorized_user_login_config':
raise calliope_exceptions.BadFileException(
'Only external account authorized user login config JSON credential '
'file types are supported for Workforce Identity Federation login '
'configurations.')
client_config = _MakeThirdPartyClientConfig(login_config_data, is_adc)
audience = login_config_data['audience']
path_start = audience.find('/locations/')
provider_name = None
if path_start != -1:
# If the audience is "//iam.googleapis.com/locations/global/workforcePools/
# <workforce-pool-id>/providers/<provider-id>",
# Then the provider_name is "locations/global/workforcePools/
# <workforce-pool-id>/providers/<provider-id>".
provider_name = audience[path_start + 1:]
# Figure out the correct auth_proxy_redirect_uri if not provided by the user.
auth_proxy_redirect_uri = kwargs.get('auth_proxy_redirect_uri', None)
if not auth_proxy_redirect_uri:
universe_cloud_web_domain = login_config_data.get(
'universe_cloud_web_domain', None
)
if (
not universe_cloud_web_domain
or universe_cloud_web_domain == GOOGLE_DEFAULT_CLOUD_WEB_DOMAIN
):
if is_adc:
kwargs['auth_proxy_redirect_uri'] = ADC_AUTH_PROXY_URL_GDU
else:
kwargs['auth_proxy_redirect_uri'] = AUTH_PROXY_URL_GDU
else:
if is_adc:
template = ADC_AUTH_PROXY_URL_TEMPLATE_NON_GDU
else:
template = AUTH_PROXY_URL_TEMPLATE_NON_GDU
kwargs['auth_proxy_redirect_uri'] = template.format(
universe_cloud_web_domain
)
creds = auth_util.DoInstalledAppBrowserFlowGoogleAuth(
config.CLOUDSDK_EXTERNAL_ACCOUNT_SCOPES,
client_config=client_config,
query_params={
# For 3PI, we pass the provider_name
# to be included in the query params.
'provider_name': provider_name
},
**kwargs)
if isinstance(creds, external_account_authorized_user.Credentials):
universe_domain_from_config = login_config_data.get('universe_domain', None)
# TODO: b/314826985 - Use the public with_universe_domain method instead of
# _universe_domain once google-auth lib is updated in gcloud.
creds._universe_domain = ( # pylint: disable=protected-access
universe_domain_from_config
or properties.VALUES.core.universe_domain.Get()
)
# TODO(b/260741921): Once google-oauthlib sets the audience, remove this.
if not creds.audience:
creds._audience = audience # pylint: disable=protected-access
return creds
def GetWorkforceLoginConfig():
"""_GetWorkforceLoginConfig gets the correct Credential Configuration.
It will first check from the supplied argument if present, then from an
environment variable if present, and finally from the project settings, if
present.
Returns:
Optional[str]: The name of the Credential Configuration File to use.
"""
# PropertyFile.Get() first checks any StoredProperty (which we do during
# parsing), then the environment variable, then the project settings.
return properties.VALUES.auth.login_config_file.Get()
def _MakeThirdPartyClientConfig(login_config_data, is_adc):
client_id = config.CLOUDSDK_CLIENT_ID
client_secret = config.CLOUDSDK_CLIENT_NOTSOSECRET
return {
'installed': {
'client_id': client_id,
'client_secret': client_secret,
'auth_uri': login_config_data['auth_url'],
'token_uri': login_config_data['token_url'],
'token_info_url': login_config_data['token_info_url'],
# TODO(b/260741921): Have google-oauthlib use this
# audience field during credential creation.
'audience': login_config_data['audience'],
'3pi': True,
'is_adc': is_adc,
}
}