# -*- 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. """Anthos command library functions and utilities for the anthoscli binary.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import base64 import copy import json import os from googlecloudsdk.command_lib.anthos import flags from googlecloudsdk.command_lib.anthos.common import file_parsers from googlecloudsdk.command_lib.anthos.common import messages from googlecloudsdk.command_lib.util.anthos import binary_operations from googlecloudsdk.core import exceptions as c_except from googlecloudsdk.core import log from googlecloudsdk.core.console import console_io from googlecloudsdk.core.credentials import store as c_store from googlecloudsdk.core.util import files from googlecloudsdk.core.util import platforms import requests import six from six.moves import urllib DEFAULT_ENV_ARGS = {'COBRA_SILENCE_USAGE': 'true'} DEFAULT_LOGIN_CONFIG_PATH = { platforms.OperatingSystem.LINUX.id: '~/.config/google/anthos/kubectl-anthos-config.yaml', platforms.OperatingSystem.MACOSX.id: '~/Library/Preferences/google/anthos/kubectl-anthos-config.yaml', platforms.OperatingSystem.WINDOWS.id: os.path.join('%APPDATA%', 'google', 'anthos', 'kubectl-anthos-config.yaml') } def GetEnvArgsForCommand(extra_vars=None, exclude_vars=None): """Return an env dict to be passed on command invocation.""" env = copy.deepcopy(os.environ) env.update(DEFAULT_ENV_ARGS) if extra_vars: env.update(extra_vars) if exclude_vars: for k in exclude_vars: env.pop(k) return env class AnthosAuthException(c_except.Error): """Base Exception for auth issues raised by gcloud anthos surface.""" def RelativePkgPathFromFullPath(path): """Splits full path into relative(basename) path and parent dir.""" normpath = os.path.normpath(path) rel_path = os.path.basename(normpath) parent_dir = os.path.dirname(normpath) or rel_path return rel_path, parent_dir class AnthosCliWrapper(binary_operations.StreamingBinaryBackedOperation): """Binary operation wrapper for anthoscli commands.""" def __init__(self, **kwargs): custom_errors = { 'MISSING_EXEC': messages.MISSING_BINARY.format(binary='anthoscli') } super(AnthosCliWrapper, self).__init__( binary='anthoscli', custom_errors=custom_errors, **kwargs) def _ParseGetArgs(self, repo_uri, local_dest, file_pattern=None, **kwargs): del kwargs # Not Used Here exec_args = ['get', repo_uri, local_dest] if file_pattern: exec_args.extend(['--pattern', file_pattern]) return exec_args def _ParseUpdateArgs(self, local_dir, repo_uri=None, strategy=None, dry_run=False, verbose=False, **kwargs): del kwargs # Not Used here exec_args = ['update', local_dir] if repo_uri: exec_args.extend(['--repo', repo_uri]) if dry_run: exec_args.append('--dry-run') if strategy: exec_args.extend(['--strategy', strategy]) if verbose: exec_args.append('--verbose') return exec_args def _ParseDescribeArgs(self, local_dir, **kwargs): del kwargs # Not Used here return ['desc', local_dir] def _ParseTags(self, tags): return ','.join(['{}={}'.format(x, y) for x, y in six.iteritems(tags)]) def _ParseInitArgs(self, local_dir, description=None, name=None, tags=None, info_url=None, **kwargs): del kwargs # Not Used here package_path = local_dir if not package_path.endswith('/'): package_path += '/' exec_args = ['init', package_path] if description: exec_args.extend(['--description', description]) if name: exec_args.extend(['--name', name]) if tags: exec_args.extend(['--tag', self._ParseTags(tags)]) if info_url: exec_args.extend(['--url', info_url]) return exec_args def _ParseApplyArgs(self, apply_dir, project, **kwargs): del kwargs # Not Used Here exec_args = ['apply', '-f', apply_dir, '--project', project] return exec_args def _ParseExportArgs(self, cluster, project, location, output_dir, **kwargs): del kwargs # Not Used here exec_args = ['export', '-c', cluster, '--project', project] if location: exec_args.extend(['--location', location]) if output_dir: exec_args.extend(['--output-directory', output_dir]) return exec_args def _ParseArgsForCommand(self, command, **kwargs): if command == 'get': return self._ParseGetArgs(**kwargs) if command == 'update': return self._ParseUpdateArgs(**kwargs) if command == 'desc': return self._ParseDescribeArgs(**kwargs) if command == 'init': return self._ParseInitArgs(**kwargs) if command == 'apply': return self._ParseApplyArgs(**kwargs) if command == 'export': return self._ParseExportArgs(**kwargs) raise binary_operations.InvalidOperationForBinary( 'Invalid Operation [{}] for anthoscli'.format(command)) def GetAuthToken(account, operation, impersonated=False): """Generate a JSON object containing the current gcloud auth token.""" try: access_token = c_store.GetFreshAccessToken( account, allow_account_impersonation=impersonated) output = { 'auth_token': access_token, } except Exception as e: # pylint: disable=broad-except raise AnthosAuthException( 'Error retrieving auth credentials for {operation}: {error}. '.format( operation=operation, error=e)) return json.dumps(output, sort_keys=True) class AnthosAuthWrapper(binary_operations.StreamingBinaryBackedOperation): """Binary operation wrapper for anthoscli commands.""" def __init__(self, **kwargs): custom_errors = { 'MISSING_EXEC': messages.MISSING_AUTH_BINARY.format(binary='kubectl-anthos') } super(AnthosAuthWrapper, self).__init__( binary='kubectl-anthos', custom_errors=custom_errors, **kwargs) @property def default_config_path(self): return files.ExpandHomeAndVars( DEFAULT_LOGIN_CONFIG_PATH[platforms.OperatingSystem.Current().id]) def _ParseLoginArgs( self, cluster, kube_config=None, login_config=None, login_config_cert=None, user=None, ldap_user=None, ldap_pass=None, dry_run=None, preferred_auth=None, server_url=None, no_browser=None, remote_bootstrap=None, **kwargs ): del kwargs # Not Used Here exec_args = ['login'] if cluster: exec_args.extend(['--cluster', cluster]) if kube_config: exec_args.extend(['--kubeconfig', kube_config]) if login_config: exec_args.extend(['--login-config', login_config]) if login_config_cert: exec_args.extend(['--login-config-cert', login_config_cert]) if user: exec_args.extend(['--user', user]) if dry_run: exec_args.extend(['--dry-run']) if ldap_pass and ldap_user: exec_args.extend( ['--ldap-username', ldap_user, '--ldap-password', ldap_pass]) if preferred_auth: exec_args.extend(['--preferred-auth', preferred_auth]) if server_url: exec_args.extend(['--server', server_url]) if no_browser: exec_args.extend(['--remote-login']) if remote_bootstrap: exec_args.extend(['--remote-bootstrap', remote_bootstrap]) return exec_args def _ParseCreateLoginConfigArgs(self, kube_config, output_file=None, merge_from=None, **kwargs): del kwargs # Not Used Here exec_args = ['create-login-config'] exec_args.extend(['--kubeconfig', kube_config]) if output_file: exec_args.extend(['--output', output_file]) if merge_from: exec_args.extend(['--merge-from', merge_from]) return exec_args def _ParseTokenArgs(self, token_type, cluster, aws_sts_region, id_token, access_token, access_token_expiry, refresh_token, client_id, client_secret, idp_certificate_authority_data, idp_issuer_url, kubeconfig_path, user, **kwargs): del kwargs # Not Used Here exec_args = ['token'] if token_type: exec_args.extend(['--type', token_type]) if cluster: exec_args.extend(['--cluster', cluster]) if aws_sts_region: exec_args.extend(['--aws-sts-region', aws_sts_region]) if id_token: exec_args.extend(['--id-token', id_token]) if access_token: exec_args.extend(['--access-token', access_token]) if access_token_expiry: exec_args.extend(['--access-token-expiry', access_token_expiry]) if refresh_token: exec_args.extend(['--refresh-token', refresh_token]) if client_id: exec_args.extend(['--client-id', client_id]) if client_secret: exec_args.extend(['--client-secret', client_secret]) if idp_certificate_authority_data: exec_args.extend( ['--idp-certificate-authority-data', idp_certificate_authority_data]) if idp_issuer_url: exec_args.extend(['--idp-issuer-url', idp_issuer_url]) if kubeconfig_path: exec_args.extend(['--kubeconfig-path', kubeconfig_path]) if user: exec_args.extend(['--user', user]) return exec_args def _ParseArgsForCommand(self, command, **kwargs): if command == 'login': return self._ParseLoginArgs(**kwargs) elif command == 'create-login-config': return self._ParseCreateLoginConfigArgs(**kwargs) elif command == 'version': return ['version'] elif command == 'token': return self._ParseTokenArgs(**kwargs) else: raise binary_operations.InvalidOperationForBinary( 'Invalid Operation [{}] for kubectl-anthos'.format(command)) def _GetClusterConfig(all_configs, cluster): found_clusters = all_configs.FindMatchingItem( file_parsers.LoginConfigObject.CLUSTER_NAME_KEY, cluster) if len(found_clusters) != 1: raise AnthosAuthException( 'Cluster [{}] not found for config path [{}]'.format( cluster, all_configs.file_path)) return found_clusters.pop() def _Base64EncodeLdap(username, passwd): """Base64 Encode Ldap username and password.""" enc = lambda s: six.ensure_text(base64.b64encode(six.ensure_binary(s))) return enc(username), enc(passwd) def _GetLdapUserAndPass(cluster_config, auth_name, cluster): """Prompt User for Ldap Username and Password.""" ldap_user = None ldap_pass = None if not cluster_config.IsLdap(): return None, None # do the prompting user_message = ('Please enter the ldap user for ' '[{}] on cluster [{}]: '.format(auth_name, cluster)) pass_message = ('Please enter the ldap password for ' '[{}] on cluster [{}]: '.format(auth_name, cluster)) ldap_user = console_io.PromptWithValidator( validator=lambda x: len(x) > 1, error_message='Error: Invalid username, please try again.', prompt_string=user_message) ldap_pass = console_io.PromptPassword( pass_message, validation_callable=lambda x: len(x) > 1) return _Base64EncodeLdap(ldap_user, ldap_pass) def GetFileOrURL(cluster_config, certificate_file=True): """Parses config input to determine whether URL or File logic should execute. Determines whether the cluster_config is a file or URL. If it's a URL, it then pulls the contents of the file using a GET request. If it's a file, then it expands the file path and returns its contents. Args: cluster_config: str, A file path or URL for the login-config. certificate_file: str, Optional file path to the CA certificate to use with the GET request to the URL. Raises: AnthosAuthException: If the data could not be pulled from the URL. Returns: parsed_config_fileOrURL, config_contents, and is_url parsed_config_fileOrURL: str, returns either the URL that was passed or an expanded file path if a file was passed. config_contents: str, returns the contents of the file or URL. is_url: bool, True if the provided cluster_config input was a URL. """ if not cluster_config: return None, None, None # Handle if input is URL. config_url = urllib.parse.urlparse(cluster_config) is_url = config_url.scheme == 'http' or config_url.scheme == 'https' if is_url: response = requests.get(cluster_config, verify=certificate_file or True) if response.status_code != requests.codes.ok: raise AnthosAuthException('Request to login-config URL failed with' 'response code [{}] and text [{}]: '.format( response.status_code, response.text)) return cluster_config, response.text, is_url # Handle if input is file. expanded_config_path = flags.ExpandLocalDirAndVersion(cluster_config) contents = files.ReadFileContents(expanded_config_path) return expanded_config_path, contents, is_url def GetPreferredAuthForCluster(cluster, login_config, config_contents=None, force_update=False, is_url=False): """Get preferredAuthentication value for cluster.""" if not (cluster and login_config): return None, None, None configs = None # If URL, then pass contents directly. if is_url: if not config_contents: raise AnthosAuthException( 'Config contents were not passed with URL [{}]'.format(login_config)) configs = file_parsers.YamlConfigFile( file_contents=config_contents, item_type=file_parsers.LoginConfigObject) # If file, pass contents and location for updating. else: configs = file_parsers.YamlConfigFile( file_contents=config_contents, file_path=login_config, item_type=file_parsers.LoginConfigObject) cluster_config = _GetClusterConfig(configs, cluster) try: auth_method = cluster_config.GetPreferredAuth() except KeyError: auth_method = None except file_parsers.YamlConfigObjectFieldError: # gracefully quit for config versions older than v2alpha1 that # do not support 'preferredAuthentication' field. return None, None, None if not auth_method or force_update: providers = cluster_config.GetAuthProviders() if not providers: raise AnthosAuthException( 'No Authentication Providers found in [{}]'.format(login_config)) if len(providers) == 1: auth_method = providers.pop() else: # do the prompting prompt_message = ('Please select your preferred authentication option ' 'for cluster [{}]'.format(cluster)) override_warning = ('. Note: This will overwrite current preferred auth ' 'method [{}] in config file.') # Only print override warning in certain cases. if auth_method and force_update and not is_url: prompt_message = prompt_message + override_warning.format(auth_method) index = console_io.PromptChoice( providers, message=prompt_message, cancel_option=True) auth_method = providers[index] log.status.Print( 'Setting Preferred Authentication option to [{}]'.format(auth_method)) cluster_config.SetPreferredAuth(auth_method) # Only save to disk if file is specified. Don't want URL failure. if login_config and not is_url: configs.WriteToDisk() ldap_user, ldap_pass = _GetLdapUserAndPass(cluster_config, auth_method, cluster) return auth_method, ldap_user, ldap_pass def LoginResponseHandler(response, list_clusters_only=False): """Handle Login Responses.""" if response.stdout: log.status.Print(response.stdout) if response.stderr: log.status.Print(response.stderr) if response.failed: log.error(messages.LOGIN_CONFIG_FAILED_MESSAGE.format(response.stderr)) return None if not list_clusters_only: log.status.Print(messages.LOGIN_CONFIG_SUCCESS_MESSAGE) return response.stdout