def _delete_data_from_registry(login_server, path, username, password, retry_times=3, retry_interval=5): for i in range(0, retry_times): errorMessage = None try: response = requests.delete( 'https://{}{}'.format(login_server, path), headers=_get_authorization_header(username, password), verify=(not should_disable_connection_verify()) ) log_registry_response(response) if response.status_code == 200 or response.status_code == 202: return elif response.status_code == 401: raise CLIError(_parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError(_parse_error_message('The requested data does not exist.', response)) else: raise Exception(_parse_error_message('Could not delete the requested data.', response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) if errorMessage: raise CLIError(errorMessage)
def load_images_from_aliases_doc(cli_ctx, publisher=None, offer=None, sku=None): import requests from azure.cli.core.cloud import CloudEndpointNotSetException from azure.cli.core.util import should_disable_connection_verify try: target_url = cli_ctx.cloud.endpoints.vm_image_alias_doc except CloudEndpointNotSetException: raise CLIError("'endpoint_vm_image_alias_doc' isn't configured. Please invoke 'az cloud update' to configure " "it or use '--all' to retrieve images from server") # under hack mode(say through proxies with unsigned cert), opt out the cert verification response = requests.get(target_url, verify=(not should_disable_connection_verify())) if response.status_code != 200: raise CLIError("Failed to retrieve image alias doc '{}'. Error: '{}'".format(target_url, response)) dic = json.loads(response.content.decode()) try: all_images = [] result = (dic['outputs']['aliases']['value']) for v in result.values(): # loop around os for alias, vv in v.items(): # loop around distros all_images.append({ 'urnAlias': alias, 'publisher': vv['publisher'], 'offer': vv['offer'], 'sku': vv['sku'], 'version': vv['version'] }) all_images = [i for i in all_images if (_matched(publisher, i['publisher']) and _matched(offer, i['offer']) and _matched(sku, i['sku']))] return all_images except KeyError: raise CLIError('Could not retrieve image list from {}'.format(target_url))
def _get_manifest_digest(login_server, repository, tag, username, password, retry_times=3, retry_interval=5): url = 'https://{}/v2/{}/manifests/{}'.format(login_server, repository, tag) headers = get_authorization_header(username, password) headers.update(MANIFEST_V2_HEADER) for i in range(0, retry_times): errorMessage = None try: response = requests.get( url=url, headers=headers, verify=(not should_disable_connection_verify()) ) log_registry_response(response) if response.status_code == 200 and response.headers and 'Docker-Content-Digest' in response.headers: return response.headers['Docker-Content-Digest'] elif response.status_code == 401: raise CLIError(parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError(parse_error_message('The manifest does not exist.', response)) else: raise Exception(parse_error_message('Could not get manifest digest.', response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) raise CLIError(errorMessage)
def _whl_download_from_url(url_parse_result, ext_file): from azure.cli.core.util import should_disable_connection_verify url = url_parse_result.geturl() r = requests.get(url, stream=True, verify=(not should_disable_connection_verify())) if r.status_code != 200: raise CLIError("Request to {} failed with {}".format(url, r.status_code)) with open(ext_file, 'wb') as f: for chunk in r.iter_content(chunk_size=1024): if chunk: # ignore keep-alive new chunks f.write(chunk)
def _obtain_data_from_registry(login_server, path, username, password, result_index, retry_times=3, retry_interval=5, pagination=20): resultList = [] executeNextHttpCall = True while executeNextHttpCall: executeNextHttpCall = False for i in range(0, retry_times): errorMessage = None try: response = requests.get( 'https://{}{}'.format(login_server, path), headers=_get_authorization_header(username, password), params=_get_pagination_params(pagination), verify=(not should_disable_connection_verify()) ) log_registry_response(response) if response.status_code == 200: result = response.json()[result_index] if result: resultList += response.json()[result_index] if 'link' in response.headers and response.headers['link']: linkHeader = response.headers['link'] # The registry is telling us there's more items in the list, # and another call is needed. The link header looks something # like `Link: </v2/_catalog?last=hello-world&n=1>; rel="next"` # we should follow the next path indicated in the link header path = linkHeader[(linkHeader.index('<') + 1):linkHeader.index('>')] executeNextHttpCall = True break elif response.status_code == 401: raise CLIError(_parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError(_parse_error_message('The requested data does not exist.', response)) else: raise Exception(_parse_error_message('Could not get the requested data.', response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) if errorMessage: raise CLIError(errorMessage) return resultList
def get_index(index_url=None): from azure.cli.core.util import should_disable_connection_verify index_url = index_url or DEFAULT_INDEX_URL try: response = requests.get(index_url, verify=(not should_disable_connection_verify())) if response.status_code == 200: return response.json() else: msg = ERR_TMPL_NON_200.format(response.status_code, index_url) raise CLIError(msg) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err: msg = ERR_TMPL_NO_NETWORK.format(str(err)) raise CLIError(msg) except ValueError as err: msg = ERR_TMPL_BAD_JSON.format(str(err)) raise CLIError(msg)
def get_index(index_url=None): from azure.cli.core.util import should_disable_connection_verify index_url = index_url or DEFAULT_INDEX_URL for try_number in range(TRIES): try: response = requests.get(index_url, verify=(not should_disable_connection_verify())) if response.status_code == 200: return response.json() msg = ERR_TMPL_NON_200.format(response.status_code, index_url) raise CLIError(msg) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err: msg = ERR_TMPL_NO_NETWORK.format(str(err)) raise CLIError(msg) except ValueError as err: # Indicates that url is not redirecting properly to intended index url, we stop retrying after TRIES calls if try_number == TRIES - 1: msg = ERR_TMPL_BAD_JSON.format(str(err)) raise CLIError(msg) import time time.sleep(0.5) continue
def _get_auth_token(self): profile = Profile(cli_ctx=self.cli_ctx) # Generate an Azure token with the VSTS resource app id auth_token, _, _ = profile.get_raw_token() content = { 'resourceId': self.remote_host, 'protocol': 'tcptunnel', 'workloadHostPort': self.remote_port, 'aztoken': auth_token[1], 'token': self.last_token, } if self.node_id: custom_header = {'X-Node-Id': self.node_id} else: custom_header = {} web_address = 'https://{}/api/tokens'.format(self.bastion.dns_name) response = requests.post( web_address, data=content, headers=custom_header, verify=(not should_disable_connection_verify())) response_json = None if response.content is not None: response_json = json.loads(response.content.decode("utf-8")) if response.status_code not in [200]: if response_json is not None and response_json[ "message"] is not None: exp = CloudError(response, error=response_json["message"]) else: exp = CloudError(response) raise exp self.last_token = response_json["authToken"] self.node_id = response_json["nodeId"] return self.last_token
def get_index(index_url=None, cli_ctx=None): import requests from azure.cli.core.util import should_disable_connection_verify index_url = index_url or get_index_url(cli_ctx=cli_ctx) for try_number in range(TRIES): try: response = requests.get( index_url, verify=(not should_disable_connection_verify())) if response.status_code == 200: return response.json() msg = ERR_TMPL_NON_200.format(response.status_code, index_url) raise CLIError(msg) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError, ValueError) as err: if try_number == TRIES - 1: # ValueError indicates that shortlink url is not redirecting properly to intended index url msg = ERR_TMPL_BAD_JSON.format(str(err)) if isinstance(err, ValueError) else \ ERR_TMPL_NO_NETWORK.format(str(err)) raise CLIError(msg) import time time.sleep(0.5) continue
def keyvault_data_plane_factory(cli_ctx, _): from azure.keyvault import KeyVaultAuthentication, KeyVaultClient from azure.cli.core.util import should_disable_connection_verify version = str(get_api_version(cli_ctx, ResourceType.DATA_KEYVAULT)) def get_token(server, resource, scope): # pylint: disable=unused-argument import adal try: return Profile(cli_ctx=cli_ctx).get_raw_token(resource)[0] except adal.AdalError as err: # pylint: disable=no-member if (hasattr(err, 'error_response') and ('error_description' in err.error_response) and ('AADSTS70008:' in err.error_response['error_description'])): raise CLIError( "Credentials have expired due to inactivity. Please run 'az login'" ) raise CLIError(err) client = KeyVaultClient(KeyVaultAuthentication(get_token), api_version=version) # HACK, work around the fact that KeyVault library does't take confiuration object on constructor # which could be used to turn off the verifiaction. Remove this once we migrate to new data plane library # pylint: disable=protected-access if hasattr(client, '_client') and hasattr(client._client, 'config'): verify = not should_disable_connection_verify() client._client.config.connection.verify = verify else: from knack.log import get_logger logger = get_logger(__name__) logger.info( 'Could not find the configuration object to turn off the verification if needed' ) return client
def _get_registry_status(login_server, registry_name, ignore_errors): import socket registry_ip = None try: registry_ip = socket.gethostbyname(login_server) except socket.gaierror: pass if not registry_ip: from ._errors import CONNECTIVITY_DNS_ERROR _handle_error(CONNECTIVITY_DNS_ERROR.format_error_message(login_server), ignore_errors) return False print_pass("DNS lookup to {} at IP {}".format(login_server, registry_ip)) import requests from requests.exceptions import SSLError, RequestException from azure.cli.core.util import should_disable_connection_verify try: challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) except SSLError: from ._errors import CONNECTIVITY_SSL_ERROR _handle_error(CONNECTIVITY_SSL_ERROR.format_error_message(login_server), ignore_errors) return False except RequestException: from ._errors import CONNECTIVITY_CHALLENGE_ERROR _handle_error(CONNECTIVITY_CHALLENGE_ERROR.format_error_message(login_server), ignore_errors) return False if challenge.status_code == 403: from ._errors import CONNECTIVITY_FORBIDDEN_ERROR _handle_error(CONNECTIVITY_FORBIDDEN_ERROR.format_error_message(login_server, registry_name), ignore_errors) return False return True
def get_pool_manager(url): proxies = urllib.request.getproxies() bypass_proxy = urllib.request.proxy_bypass( urllib.parse.urlparse(url).hostname) if 'https' in proxies and not bypass_proxy: proxy = urllib.parse.urlparse(proxies['https']) if proxy.username and proxy.password: proxy_headers = urllib3.util.make_headers( proxy_basic_auth='{0}:{1}'.format(proxy.username, proxy.password)) logger.debug('Setting proxy-authorization header for basic auth') else: proxy_headers = None logger.info('Using proxy for app service tunnel connection') http = urllib3.ProxyManager(proxy.geturl(), proxy_headers=proxy_headers) else: http = urllib3.PoolManager() if should_disable_connection_verify(): http.connection_pool_kw['cert_reqs'] = 'CERT_NONE' else: http.connection_pool_kw['cert_reqs'] = 'CERT_REQUIRED' if REQUESTS_CA_BUNDLE in os.environ: ca_bundle_file = os.environ[REQUESTS_CA_BUNDLE] logger.debug("Using CA bundle file at '%s'.", ca_bundle_file) if not os.path.isfile(ca_bundle_file): raise ValidationError( 'REQUESTS_CA_BUNDLE environment variable is specified with an invalid file path' ) else: ca_bundle_file = certifi.where() http.connection_pool_kw['ca_certs'] = ca_bundle_file return http
def _delete_data_from_registry(login_server, path, username, password, retry_times=3, retry_interval=5): for i in range(0, retry_times): errorMessage = None try: response = requests.delete( 'https://{}{}'.format(login_server, path), headers=_get_authorization_header(username, password), verify=(not should_disable_connection_verify())) log_registry_response(response) if response.status_code == 200 or response.status_code == 202: return elif response.status_code == 401: raise CLIError( _parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError( _parse_error_message('The requested data does not exist.', response)) else: raise Exception( _parse_error_message( 'Could not delete the requested data.', response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) raise CLIError(errorMessage)
def _get_aad_token_after_challenge(cli_ctx, token_params, login_server, only_refresh_token, repository, artifact_repository, permission, is_diagnostics_context): authurl = urlparse(token_params['realm']) authhost = urlunparse((authurl[0], authurl[1], '/oauth2/exchange', '', '', '')) from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cli_ctx) # this might be a cross tenant scenario, so pass subscription to get_raw_token subscription = get_subscription_id(cli_ctx) creds, _, tenant = profile.get_raw_token(subscription=subscription) headers = {'Content-Type': 'application/x-www-form-urlencoded'} content = { 'grant_type': 'access_token', 'service': token_params['service'], 'tenant': tenant, 'access_token': creds[1] } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: from ._errors import CONNECTIVITY_REFRESH_TOKEN_ERROR if is_diagnostics_context: return CONNECTIVITY_REFRESH_TOKEN_ERROR.format_error_message(login_server, response.status_code) raise CLIError(CONNECTIVITY_REFRESH_TOKEN_ERROR.format_error_message(login_server, response.status_code) .get_error_message()) refresh_token = loads(response.content.decode("utf-8"))["refresh_token"] if only_refresh_token: return refresh_token authhost = urlunparse((authurl[0], authurl[1], '/oauth2/token', '', '', '')) if repository: scope = 'repository:{}:{}'.format(repository, permission) elif artifact_repository: scope = 'artifact-repository:{}:{}'.format(artifact_repository, permission) else: # catalog only has * as permission, even for a read operation scope = 'registry:catalog:*' content = { 'grant_type': 'refresh_token', 'service': login_server, 'scope': scope, 'refresh_token': refresh_token } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: from ._errors import CONNECTIVITY_ACCESS_TOKEN_ERROR if is_diagnostics_context: return CONNECTIVITY_ACCESS_TOKEN_ERROR.format_error_message(login_server, response.status_code) raise CLIError(CONNECTIVITY_ACCESS_TOKEN_ERROR.format_error_message(login_server, response.status_code) .get_error_message()) return loads(response.content.decode("utf-8"))["access_token"]
def _get_aad_token(cli_ctx, login_server, only_refresh_token, repository=None, artifact_repository=None, permission=None): """Obtains refresh and access tokens for an AAD-enabled registry. :param str login_server: The registry login server URL to log in to :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ if repository and artifact_repository: raise ValueError( "Only one of repository and artifact_repository can be provided.") if (repository or artifact_repository) and permission not in ACCESS_TOKEN_PERMISSION: raise ValueError( "Permission is required for a repository or artifact_repository. Allowed access token permission: {}" .format(ACCESS_TOKEN_PERMISSION)) login_server = login_server.rstrip('/') challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) if challenge.status_code not in [ 401 ] or 'WWW-Authenticate' not in challenge.headers: raise CLIError( "Registry '{}' did not issue a challenge.".format(login_server)) authenticate = challenge.headers['WWW-Authenticate'] tokens = authenticate.split(' ', 2) if len(tokens) < 2 or tokens[0].lower() != 'bearer': raise CLIError( "Registry '{}' does not support AAD login.".format(login_server)) params = { y[0]: y[1].strip('"') for y in (x.strip().split('=', 2) for x in tokens[1].split(',')) } if 'realm' not in params or 'service' not in params: raise CLIError( "Registry '{}' does not support AAD login.".format(login_server)) authurl = urlparse(params['realm']) authhost = urlunparse( (authurl[0], authurl[1], '/oauth2/exchange', '', '', '')) from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cli_ctx) creds, _, tenant = profile.get_raw_token() headers = {'Content-Type': 'application/x-www-form-urlencoded'} content = { 'grant_type': 'access_token', 'service': params['service'], 'tenant': tenant, 'access_token': creds[1] } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: raise CLIError( "Access to registry '{}' was denied. Response code: {}.".format( login_server, response.status_code)) refresh_token = loads(response.content.decode("utf-8"))["refresh_token"] if only_refresh_token: return refresh_token authhost = urlunparse( (authurl[0], authurl[1], '/oauth2/token', '', '', '')) if repository: scope = 'repository:{}:{}'.format(repository, permission) elif artifact_repository: scope = 'artifact-repository:{}:{}'.format(artifact_repository, permission) else: # catalog only has * as permission, even for a read operation scope = 'registry:catalog:*' content = { 'grant_type': 'refresh_token', 'service': login_server, 'scope': scope, 'refresh_token': refresh_token } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: raise CLIError( "Access to registry '{}' was denied. Response code: {}.".format( login_server, response.status_code)) return loads(response.content.decode("utf-8"))["access_token"]
def _get_credentials(cli_ctx, registry_name, username, password, only_refresh_token, repository=None, artifact_repository=None, permission=None): """Try to get AAD authorization tokens or admin user credentials. :param str registry_name: The name of container registry :param str username: The username used to log into the container registry :param str password: The password used to log into the container registry :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ try: registry, resource_group_name = get_registry_by_name( cli_ctx, registry_name) login_server = registry.login_server except ResourceNotFound as e: # Try to use the pre-defined login server suffix to construct login server from registry name. login_server_suffix = get_login_server_suffix(cli_ctx) if not login_server_suffix: raise registry = None login_server = '{}{}'.format(registry_name, login_server_suffix).lower() resource_not_found = str(e) # Validate the login server is reachable try: requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) except RequestException: raise CLIError( "Could not connect to the registry '{}'. ".format(login_server) + "Please verify if the registry exists.") # 1. if username was specified, verify that password was also specified if username: if not password: try: password = prompt_pass(msg='Password: '******'Please specify both username and password in non-interactive mode.' ) return login_server, username, password # 2. if we don't yet have credentials, attempt to get a refresh token if not password and (not registry or registry.sku.name in MANAGED_REGISTRY_SKU): try: password = _get_aad_token(cli_ctx, login_server, only_refresh_token, repository, artifact_repository, permission) return login_server, EMPTY_GUID, password except CLIError as e: logger.warning( "Unable to get AAD authorization tokens with message: %s", str(e)) # 3. if we still don't have credentials, attempt to get the admin credentials (if enabled) if not password: error_message = "Unable to get admin user credentials with message" if registry: if registry.admin_user_enabled: try: cred = cf_acr_registries(cli_ctx).list_credentials( resource_group_name, registry_name) username = cred.username password = cred.passwords[0].value return login_server, username, password except CLIError as e: logger.warning("%s: %s", error_message, str(e)) else: logger.warning("%s: %s", error_message, "Admin user is disabled.") else: logger.warning("%s: %s", error_message, resource_not_found) # 4. if we still don't have credentials, prompt the user if not password: try: username = prompt('Username: '******'Password: '******'Unable to authenticate using AAD or admin login credentials. ' + 'Please specify both username and password in non-interactive mode.' ) return login_server, None, None
def _get_aad_token(cli_ctx, login_server, only_refresh_token, repository=None, permission='*'): """Obtains refresh and access tokens for an AAD-enabled registry. :param str login_server: The registry login server URL to log in to :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ login_server = login_server.rstrip('/') challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) if challenge.status_code not in [ 401 ] or 'WWW-Authenticate' not in challenge.headers: raise CLIError( "Registry '{}' did not issue a challenge.".format(login_server)) authenticate = challenge.headers['WWW-Authenticate'] tokens = authenticate.split(' ', 2) if len(tokens) < 2 or tokens[0].lower() != 'bearer': raise CLIError( "Registry '{}' does not support AAD login.".format(login_server)) params = { y[0]: y[1].strip('"') for y in (x.strip().split('=', 2) for x in tokens[1].split(',')) } if 'realm' not in params or 'service' not in params: raise CLIError( "Registry '{}' does not support AAD login.".format(login_server)) authurl = urlparse(params['realm']) authhost = urlunparse( (authurl[0], authurl[1], '/oauth2/exchange', '', '', '')) from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cli_ctx) sp_id, refresh, access, tenant = profile.get_refresh_token() headers = {'Content-Type': 'application/x-www-form-urlencoded'} if not sp_id: if not refresh: content = { 'grant_type': 'access_token', 'service': params['service'], 'tenant': tenant, 'access_token': access } else: content = { 'grant_type': 'access_token_refresh_token', 'service': params['service'], 'tenant': tenant, 'access_token': access, 'refresh_token': refresh } else: content = { 'grant_type': 'spn', 'service': params['service'], 'tenant': tenant, 'username': sp_id, 'password': refresh } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: raise CLIError( "Access to registry '{}' was denied. Response code: {}.".format( login_server, response.status_code)) refresh_token = loads(response.content.decode("utf-8"))["refresh_token"] if only_refresh_token: return refresh_token authhost = urlunparse( (authurl[0], authurl[1], '/oauth2/token', '', '', '')) if repository is None: scope = 'registry:catalog:*' else: scope = 'repository:{}:{}'.format(repository, permission) content = { 'grant_type': 'refresh_token', 'service': login_server, 'scope': scope, 'refresh_token': refresh_token } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) access_token = loads(response.content.decode("utf-8"))["access_token"] return access_token
def enable_one_deploy(cmd, resource_group_name, name, src, deploy_type=None, is_async=None, target_path=None, timeout=None, slot=None): import ntpath logger.info("Preparing for deployment") user_name, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot) try: scm_url = _get_scm_url(cmd, resource_group_name, name, slot) except ValueError: raise CLIError('Failed to fetch scm url for for app {}'.format(name)) # Interpret deployment type from the file extension if the type parameter is not passed if deploy_type is None: fileName = ntpath.basename(src) fileExtension = fileName.split(".", 1)[1] if fileExtension in ('war', 'jar', 'zip', 'ear'): deploy_type = fileExtension elif fileExtension in ('sh', 'bat'): deploy_type = 'startup' else: deploy_type = 'static' logger.warning( "Deployment type: %s. To override deloyment type, please specify the --type parameter. Possible values: static, zip, war, jar, ear, startup", deploy_type) deploy_url = scm_url + '/api/publish?type=' + deploy_type if is_async is not None: deploy_url = deploy_url + '&async=true' if target_path is not None: deploy_url = deploy_url + '&path=' + target_path deployment_status_url = scm_url + '/api/deployments/latest' from azure.cli.core.util import (should_disable_connection_verify, get_az_user_agent) import urllib3 authorization = urllib3.util.make_headers( basic_auth='{0}:{1}'.format(user_name, password)) headers = authorization headers['Content-Type'] = 'application/octet-stream' headers['Cache-Control'] = 'no-cache' headers['User-Agent'] = get_az_user_agent() import requests import os # Read file content with open(os.path.realpath(os.path.expanduser(src)), 'rb') as fs: artifact_content = fs.read() logger.warning("Starting deployment...") res = requests.post(deploy_url, data=artifact_content, headers=headers, verify=not should_disable_connection_verify()) # check if an error occured during deployment if res.status_code == 400: raise CLIError("An error occured durng deployment: {}".format( res.text)) # check if there's an ongoing process if res.status_code == 409: raise CLIError( "There may be an ongoing deployment or your app setting has WEBSITE_RUN_FROM_PACKAGE. " "Please track your deployment in {} and ensure the WEBSITE_RUN_FROM_PACKAGE app setting " "is removed.".format(deployment_status_url)) # check the status of async deployment if res.status_code == 202: logger.warning("Asynchronous deployment request has been recieved") response = _check_zip_deployment_status(cmd, resource_group_name, name, deployment_status_url, authorization, timeout) return response return logger.warning("Deployment has completed successfully")
def github_release_version_exists(version, repo, org='microsoft'): version_url = f'https://api.github.com/repos/{org}/{repo}/releases/tags/{version}' version_res = requests.get(version_url, verify=not should_disable_connection_verify()) return version_res.status_code < 400
def _get_aad_token(cli_ctx, login_server, only_refresh_token, repository=None, artifact_repository=None, permission=None): """Obtains refresh and access tokens for an AAD-enabled registry. :param str login_server: The registry login server URL to log in to :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ if repository and artifact_repository: raise ValueError("Only one of repository and artifact_repository can be provided.") if (repository or artifact_repository) and permission not in ACCESS_TOKEN_PERMISSION: raise ValueError( "Permission is required for a repository or artifact_repository. Allowed access token permission: {}" .format(ACCESS_TOKEN_PERMISSION)) login_server = login_server.rstrip('/') challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) if challenge.status_code not in [401] or 'WWW-Authenticate' not in challenge.headers: raise CLIError("Registry '{}' did not issue a challenge.".format(login_server)) authenticate = challenge.headers['WWW-Authenticate'] tokens = authenticate.split(' ', 2) if len(tokens) < 2 or tokens[0].lower() != 'bearer': raise CLIError("Registry '{}' does not support AAD login.".format(login_server)) params = {y[0]: y[1].strip('"') for y in (x.strip().split('=', 2) for x in tokens[1].split(','))} if 'realm' not in params or 'service' not in params: raise CLIError("Registry '{}' does not support AAD login.".format(login_server)) authurl = urlparse(params['realm']) authhost = urlunparse((authurl[0], authurl[1], '/oauth2/exchange', '', '', '')) from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cli_ctx) creds, _, tenant = profile.get_raw_token() headers = {'Content-Type': 'application/x-www-form-urlencoded'} content = { 'grant_type': 'access_token', 'service': params['service'], 'tenant': tenant, 'access_token': creds[1] } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: raise CLIError("Access to registry '{}' was denied. Response code: {}.".format( login_server, response.status_code)) refresh_token = loads(response.content.decode("utf-8"))["refresh_token"] if only_refresh_token: return refresh_token authhost = urlunparse((authurl[0], authurl[1], '/oauth2/token', '', '', '')) if repository: scope = 'repository:{}:{}'.format(repository, permission) elif artifact_repository: scope = 'artifact-repository:{}:{}'.format(artifact_repository, permission) else: # catalog only has * as permission, even for a read operation scope = 'registry:catalog:*' content = { 'grant_type': 'refresh_token', 'service': login_server, 'scope': scope, 'refresh_token': refresh_token } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code not in [200]: raise CLIError("Access to registry '{}' was denied. Response code: {}.".format( login_server, response.status_code)) return loads(response.content.decode("utf-8"))["access_token"]
def _get_credentials( cmd, # pylint: disable=too-many-statements registry_name, tenant_suffix, username, password, only_refresh_token, repository=None, artifact_repository=None, permission=None): """Try to get AAD authorization tokens or admin user credentials. :param str registry_name: The name of container registry :param str tenant_suffix: The registry login server tenant suffix :param str username: The username used to log into the container registry :param str password: The password used to log into the container registry :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ # Raise an error if password is specified but username isn't if not username and password: raise CLIError( 'Please also specify username if password is specified.') cli_ctx = cmd.cli_ctx resource_not_found, registry = None, None try: registry, resource_group_name = get_registry_by_name( cli_ctx, registry_name) login_server = registry.login_server if tenant_suffix: logger.warning( "Obtained registry login server '%s' from service. The specified suffix '%s' is ignored.", login_server, tenant_suffix) except (ResourceNotFound, CLIError) as e: resource_not_found = str(e) logger.debug("Could not get registry from service. Exception: %s", resource_not_found) if not isinstance(e, ResourceNotFound ) and _AZ_LOGIN_MESSAGE not in resource_not_found: raise # Try to use the pre-defined login server suffix to construct login server from registry name. login_server_suffix = get_login_server_suffix(cli_ctx) if not login_server_suffix: raise login_server = '{}{}{}'.format( registry_name, '-{}'.format(tenant_suffix) if tenant_suffix else '', login_server_suffix).lower() # Validate the login server is reachable challenge = 'https://' + login_server + '/v2/' try: requests.get(challenge, verify=(not should_disable_connection_verify())) except RequestException as e: logger.debug( "Could not connect to registry login server. Exception: %s", str(e)) if resource_not_found: logger.warning( "%s\nUsing '%s' as the default registry login server.", resource_not_found, login_server) raise CLIError("Could not connect to the registry login server '{}'. ". format(login_server) + "Please verify that the registry exists and " + "the URL '{}' is reachable from your environment.". format(challenge)) # 1. if username was specified, verify that password was also specified if username: if not password: try: password = prompt_pass(msg='Password: '******'Please specify both username and password in non-interactive mode.' ) return login_server, username, password # 2. if we don't yet have credentials, attempt to get a refresh token if not registry or registry.sku.name in get_managed_sku(cmd): try: return login_server, EMPTY_GUID, _get_aad_token( cli_ctx, login_server, only_refresh_token, repository, artifact_repository, permission) except CLIError as e: logger.warning("%s: %s", AAD_TOKEN_BASE_ERROR_MESSAGE, str(e)) # 3. if we still don't have credentials, attempt to get the admin credentials (if enabled) if registry: if registry.admin_user_enabled: try: cred = cf_acr_registries(cli_ctx).list_credentials( resource_group_name, registry_name) return login_server, cred.username, cred.passwords[0].value except CLIError as e: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, str(e)) else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, "Admin user is disabled.") else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, resource_not_found) # 4. if we still don't have credentials, prompt the user try: username = prompt('Username: '******'Password: '******'Unable to authenticate using AAD or admin login credentials. ' + 'Please specify both username and password in non-interactive mode.' ) return login_server, None, None
def request_data_from_registry(http_method, login_server, path, username, password, result_index=None, json_payload=None, file_payload=None, params=None, manifest_headers=False, raw=False, retry_times=3, retry_interval=5, timeout=300): if http_method not in ALLOWED_HTTP_METHOD: raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD)) if json_payload and file_payload: raise ValueError("One of json_payload and file_payload can be specified.") if http_method in ['get', 'delete'] and (json_payload or file_payload): raise ValueError("Empty payload is required for http method: {}".format(http_method)) if http_method in ['patch', 'put'] and not (json_payload or file_payload): raise ValueError("Non-empty payload is required for http method: {}".format(http_method)) url = 'https://{}{}'.format(login_server, path) if manifest_headers: headers = get_manifest_authorization_header(username, password) else: headers = get_authorization_header(username, password) for i in range(0, retry_times): errorMessage = None try: if file_payload: with open(file_payload, 'rb') as data_payload: response = requests.request( method=http_method, url=url, headers=headers, params=params, data=data_payload, timeout=timeout, verify=(not should_disable_connection_verify()) ) else: response = requests.request( method=http_method, url=url, headers=headers, params=params, json=json_payload, timeout=timeout, verify=(not should_disable_connection_verify()) ) log_registry_response(response) if manifest_headers and raw and response.status_code == 200: return response.content.decode('utf-8'), None if response.status_code == 200: result = response.json()[result_index] if result_index else response.json() next_link = response.headers['link'] if 'link' in response.headers else None return result, next_link if response.status_code == 201 or response.status_code == 202: result = None try: result = response.json()[result_index] if result_index else response.json() except ValueError as e: logger.debug('Response is empty or is not a valid json. Exception: %s', str(e)) return result, None if response.status_code == 204: return None, None if response.status_code == 401: raise RegistryException( parse_error_message('Authentication required.', response), response.status_code) if response.status_code == 404: raise RegistryException( parse_error_message('The requested data does not exist.', response), response.status_code) if response.status_code == 405: raise RegistryException( parse_error_message('This operation is not supported.', response), response.status_code) if response.status_code == 409: raise RegistryException( parse_error_message('Failed to request data due to a conflict.', response), response.status_code) raise Exception(parse_error_message('Could not {} the requested data.'.format(http_method), response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) raise CLIError(errorMessage)
def _get_token_with_username_and_password(login_server, username, password, repository=None, artifact_repository=None, permission=None, is_login_context=False, is_diagnostics_context=False): """Decides and obtains credentials for a registry using username and password. To be used for scoped access credentials. :param str login_server: The registry login server URL to log in to :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ if is_login_context: return username, password token_params = _handle_challenge_phase( login_server, repository, artifact_repository, permission, False, is_diagnostics_context ) from ._errors import ErrorClass if isinstance(token_params, ErrorClass): if is_diagnostics_context: return token_params raise CLIError(token_params.get_error_message()) if ALLOWS_BASIC_AUTH in token_params: return username, password if repository: scope = 'repository:{}:{}'.format(repository, permission) elif artifact_repository: scope = 'artifact-repository:{}:{}'.format(artifact_repository, permission) else: # catalog only has * as permission, even for a read operation scope = 'registry:catalog:*' authurl = urlparse(token_params['realm']) authhost = urlunparse((authurl[0], authurl[1], '/oauth2/token', '', '', '')) headers = {'Content-Type': 'application/x-www-form-urlencoded'} content = { 'service': token_params['service'], 'grant_type': 'password', 'username': username, 'password': password, 'scope': scope } response = requests.post(authhost, urlencode(content), headers=headers, verify=(not should_disable_connection_verify())) if response.status_code != 200: from ._errors import CONNECTIVITY_ACCESS_TOKEN_ERROR if is_diagnostics_context: return CONNECTIVITY_ACCESS_TOKEN_ERROR.format_error_message(login_server, response.status_code) raise CLIError(CONNECTIVITY_ACCESS_TOKEN_ERROR.format_error_message(login_server, response.status_code) .get_error_message()) access_token = loads(response.content.decode("utf-8"))["access_token"] return EMPTY_GUID, access_token
def _get_aad_token(cli_ctx, login_server, only_refresh_token, repository=None, artifact_repository=None, permission=None, is_diagnostics_context=False): """Obtains refresh and access tokens for an AAD-enabled registry. :param str login_server: The registry login server URL to log in to :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ if repository and artifact_repository: raise ValueError( "Only one of repository and artifact_repository can be provided.") if (repository or artifact_repository) and permission not in ACCESS_TOKEN_PERMISSION: raise ValueError( "Permission is required for a repository or artifact_repository. Allowed access token permission: {}" .format(ACCESS_TOKEN_PERMISSION)) login_server = login_server.rstrip('/') challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) if challenge.status_code not in [ 401 ] or 'WWW-Authenticate' not in challenge.headers: from ._errors import CONNECTIVITY_CHALLENGE_ERROR if is_diagnostics_context: return CONNECTIVITY_CHALLENGE_ERROR.format_error_message( login_server) raise CLIError( CONNECTIVITY_CHALLENGE_ERROR.format_error_message( login_server).get_error_message()) authenticate = challenge.headers['WWW-Authenticate'] tokens = authenticate.split(' ', 2) if len(tokens) < 2 or tokens[0].lower() != 'bearer': from ._errors import CONNECTIVITY_AAD_LOGIN_ERROR if is_diagnostics_context: return CONNECTIVITY_AAD_LOGIN_ERROR.format_error_message( login_server) raise CLIError( CONNECTIVITY_AAD_LOGIN_ERROR.format_error_message( login_server).get_error_message()) token_params = { y[0]: y[1].strip('"') for y in (x.strip().split('=', 2) for x in tokens[1].split(',')) } if 'realm' not in token_params or 'service' not in token_params: from ._errors import CONNECTIVITY_AAD_LOGIN_ERROR if is_diagnostics_context: return CONNECTIVITY_AAD_LOGIN_ERROR.format_error_message( login_server) raise CLIError( CONNECTIVITY_AAD_LOGIN_ERROR.format_error_message( login_server).get_error_message()) return _get_aad_token_after_challenge(cli_ctx, token_params, login_server, only_refresh_token, repository, artifact_repository, permission, is_diagnostics_context)
def _handle_challenge_phase(login_server, repository, artifact_repository, permission, is_aad_token=True, is_diagnostics_context=False): if repository and artifact_repository: raise ValueError( "Only one of repository and artifact_repository can be provided.") repo_permissions = { permission.value for permission in RepoAccessTokenPermission } if repository and permission not in repo_permissions: raise ValueError( "Permission is required for a repository. Allowed access token permission: {}" .format(repo_permissions)) helm_permissions = { permission.value for permission in HelmAccessTokenPermission } if artifact_repository and permission not in helm_permissions: raise ValueError( "Permission is required for an artifact_repository. Allowed access token permission: {}" .format(helm_permissions)) login_server = login_server.rstrip('/') challenge = requests.get('https://' + login_server + '/v2/', verify=(not should_disable_connection_verify())) if challenge.status_code != 401 or 'WWW-Authenticate' not in challenge.headers: from ._errors import CONNECTIVITY_CHALLENGE_ERROR if is_diagnostics_context: return CONNECTIVITY_CHALLENGE_ERROR.format_error_message( login_server) raise CLIError( CONNECTIVITY_CHALLENGE_ERROR.format_error_message( login_server).get_error_message()) authenticate = challenge.headers['WWW-Authenticate'] tokens = authenticate.split(' ', 2) if not is_aad_token and tokens[0].lower() == 'basic': return {ALLOWS_BASIC_AUTH: True} token_params = {y[0]: y[1].strip('"') for y in (x.strip().split('=', 2) for x in tokens[1].split(','))} \ if len(tokens) >= 2 and tokens[0].lower() == 'bearer' else None if not token_params or 'realm' not in token_params or 'service' not in token_params: from ._errors import CONNECTIVITY_AAD_LOGIN_ERROR, CONNECTIVITY_WWW_AUTHENTICATE_ERROR error = CONNECTIVITY_AAD_LOGIN_ERROR if is_aad_token else CONNECTIVITY_WWW_AUTHENTICATE_ERROR if is_diagnostics_context: return error.format_error_message(login_server) raise CLIError( error.format_error_message(login_server).get_error_message()) return token_params
def github_release_version_exists(cli_ctx, version, repo, org='microsoft'): import requests version_url = 'https://api.github.com/repos/{}/{}/releases/tags/{}'.format(org, repo, version) version_res = requests.get(version_url, verify=not should_disable_connection_verify()) return version_res.status_code < 400
def _obtain_data_from_registry(login_server, path, username, password, result_index, retry_times=3, retry_interval=5, top=None, orderby=None): resultList = [] executeNextHttpCall = True url = 'https://{}{}'.format(login_server, path) headers = _get_authorization_header(username, password) params = {'n': DEFAULT_PAGINATION, 'orderby': orderby} while executeNextHttpCall: executeNextHttpCall = False for i in range(0, retry_times): errorMessage = None try: # Override the default page size if top is provided if top is not None: params[ 'n'] = DEFAULT_PAGINATION if top > DEFAULT_PAGINATION else top top -= params['n'] response = requests.get( url=url, headers=headers, params=params, verify=(not should_disable_connection_verify())) log_registry_response(response) if response.status_code == 200: result = response.json()[result_index] if result: resultList += result if top is not None and top <= 0: break if 'link' in response.headers and response.headers['link']: linkHeader = response.headers['link'] # The registry is telling us there's more items in the list, # and another call is needed. The link header looks something # like `Link: </v2/_catalog?last=hello-world&n=1>; rel="next"` # we should follow the next path indicated in the link header path = linkHeader[(linkHeader.index('<') + 1):linkHeader.index('>')] tokens = path.split('?', 2) params = { y[0]: unquote(y[1]) for y in (x.split('=', 2) for x in tokens[1].split('&')) } executeNextHttpCall = True break elif response.status_code == 401: raise CLIError( _parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError( _parse_error_message( 'The requested data does not exist.', response)) else: raise Exception( _parse_error_message( 'Could not get the requested data.', response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) if errorMessage: raise CLIError(errorMessage) return resultList
def request_data_from_registry(http_method, login_server, path, username, password, result_index=None, json_payload=None, data_payload=None, params=None, retry_times=3, retry_interval=5): if http_method not in ALLOWED_HTTP_METHOD: raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD)) if json_payload and data_payload: raise ValueError( "One of json_payload and data_payload can be specified.") if http_method in ['get', 'delete'] and (json_payload or data_payload): raise ValueError( "Empty payload is required for http method: {}".format( http_method)) if http_method in ['patch', 'put'] and not (json_payload or data_payload): raise ValueError( "Non-empty payload is required for http method: {}".format( http_method)) url = 'https://{}{}'.format(login_server, path) headers = get_authorization_header(username, password) for i in range(0, retry_times): errorMessage = None try: response = requests.request( method=http_method, url=url, headers=headers, params=params, json=json_payload, data=data_payload, verify=(not should_disable_connection_verify())) log_registry_response(response) if response.status_code == 200: result = response.json( )[result_index] if result_index else response.json() next_link = response.headers[ 'link'] if 'link' in response.headers else None return result, next_link elif response.status_code == 201 or response.status_code == 202: result = None try: result = response.json( )[result_index] if result_index else response.json() except ValueError: logger.debug('Response is empty or is not a valid json.') return result, None elif response.status_code == 204: return None, None elif response.status_code == 401: raise CLIError( parse_error_message('Authentication required.', response)) elif response.status_code == 404: raise CLIError( parse_error_message('The requested data does not exist.', response)) else: raise Exception( parse_error_message( 'Could not {} the requested data.'.format(http_method), response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) raise CLIError(errorMessage)
def _get_credentials(cmd, # pylint: disable=too-many-statements registry_name, tenant_suffix, username, password, only_refresh_token, repository=None, artifact_repository=None, permission=None, is_login_context=False): """Try to get AAD authorization tokens or admin user credentials. :param str registry_name: The name of container registry :param str tenant_suffix: The registry login server tenant suffix :param str username: The username used to log into the container registry :param str password: The password used to log into the container registry :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ # Raise an error if password is specified but username isn't if not username and password: raise CLIError('Please also specify username if password is specified.') cli_ctx = cmd.cli_ctx resource_not_found, registry = None, None try: registry, resource_group_name = get_registry_by_name(cli_ctx, registry_name) login_server = registry.login_server if tenant_suffix: logger.warning( "Obtained registry login server '%s' from service. The specified suffix '%s' is ignored.", login_server, tenant_suffix) except (ResourceNotFound, CLIError) as e: resource_not_found = str(e) logger.debug("Could not get registry from service. Exception: %s", resource_not_found) if not isinstance(e, ResourceNotFound) and _AZ_LOGIN_MESSAGE not in resource_not_found: raise # Try to use the pre-defined login server suffix to construct login server from registry name. login_server_suffix = get_login_server_suffix(cli_ctx) if not login_server_suffix: raise login_server = '{}{}{}'.format( registry_name, '-{}'.format(tenant_suffix) if tenant_suffix else '', login_server_suffix).lower() # Validate the login server is reachable url = 'https://' + login_server + '/v2/' try: challenge = requests.get(url, verify=(not should_disable_connection_verify())) if challenge.status_code == 403: raise CLIError("Looks like you don't have access to registry '{}'. " "To see configured firewall rules, run 'az acr show --query networkRuleSet --name {}'. " "To see if public network access is enabled, run 'az acr show --query publicNetworkAccess'." "Please refer to https://aka.ms/acr/errors#connectivity_forbidden_error for more information." # pylint: disable=line-too-long .format(login_server, registry_name)) except RequestException as e: logger.debug("Could not connect to registry login server. Exception: %s", str(e)) if resource_not_found: logger.warning("%s\nUsing '%s' as the default registry login server.", resource_not_found, login_server) from .check_health import ACR_CHECK_HEALTH_MSG check_health_msg = ACR_CHECK_HEALTH_MSG.format(registry_name) raise CLIError("Could not connect to the registry login server '{}'. " "Please verify that the registry exists and the URL '{}' is reachable from your environment.\n{}" .format(login_server, url, check_health_msg)) # 1. if username was specified, verify that password was also specified if username: if not password: try: password = prompt_pass(msg='Password: '******'Please specify both username and password in non-interactive mode.') username, password = _get_token_with_username_and_password( login_server, username, password, repository, artifact_repository, permission, is_login_context ) return login_server, username, password # 2. if we don't yet have credentials, attempt to get a refresh token if not registry or registry.sku.name in get_managed_sku(cmd): logger.info("Attempting to retrieve AAD refresh token...") try: return login_server, EMPTY_GUID, _get_aad_token( cli_ctx, login_server, only_refresh_token, repository, artifact_repository, permission) except CLIError as e: logger.warning("%s: %s", AAD_TOKEN_BASE_ERROR_MESSAGE, str(e)) # 3. if we still don't have credentials, attempt to get the admin credentials (if enabled) if registry: if registry.admin_user_enabled: logger.info("Attempting with admin credentials...") try: cred = cf_acr_registries(cli_ctx).list_credentials(resource_group_name, registry_name) return login_server, cred.username, cred.passwords[0].value except CLIError as e: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, str(e)) else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, "Admin user is disabled.") else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, resource_not_found) # 4. if we still don't have credentials, prompt the user try: username = prompt('Username: '******'Password: '******'Unable to authenticate using AAD or admin login credentials. ' + 'Please specify both username and password in non-interactive mode.') return login_server, None, None
def _get_credentials(cmd, # pylint: disable=too-many-statements registry_name, tenant_suffix, username, password, only_refresh_token, repository=None, artifact_repository=None, permission=None): """Try to get AAD authorization tokens or admin user credentials. :param str registry_name: The name of container registry :param str tenant_suffix: The registry login server tenant suffix :param str username: The username used to log into the container registry :param str password: The password used to log into the container registry :param bool only_refresh_token: Whether to ask for only refresh token, or for both refresh and access tokens :param str repository: Repository for which the access token is requested :param str artifact_repository: Artifact repository for which the access token is requested :param str permission: The requested permission on the repository, '*' or 'pull' """ # Raise an error if password is specified but username isn't if not username and password: raise CLIError('Please also specify username if password is specified.') cli_ctx = cmd.cli_ctx resource_not_found, registry = None, None try: registry, resource_group_name = get_registry_by_name(cli_ctx, registry_name) login_server = registry.login_server if tenant_suffix: logger.warning( "Obtained registry login server '%s' from service. The specified suffix '%s' is ignored.", login_server, tenant_suffix) except (ResourceNotFound, CLIError) as e: resource_not_found = str(e) logger.debug("Could not get registry from service. Exception: %s", resource_not_found) if not isinstance(e, ResourceNotFound) and _AZ_LOGIN_MESSAGE not in resource_not_found: raise # Try to use the pre-defined login server suffix to construct login server from registry name. login_server_suffix = get_login_server_suffix(cli_ctx) if not login_server_suffix: raise login_server = '{}{}{}'.format( registry_name, '-{}'.format(tenant_suffix) if tenant_suffix else '', login_server_suffix).lower() # Validate the login server is reachable url = 'https://' + login_server + '/v2/' try: challenge = requests.get(url, verify=(not should_disable_connection_verify())) if challenge.status_code in [403]: raise CLIError("Looks like you don't have access to registry '{}'. " "Are firewalls and virtual networks enabled?".format(login_server)) except RequestException as e: logger.debug("Could not connect to registry login server. Exception: %s", str(e)) if resource_not_found: logger.warning("%s\nUsing '%s' as the default registry login server.", resource_not_found, login_server) raise CLIError("Could not connect to the registry login server '{}'. ".format(login_server) + "Please verify that the registry exists and " + "the URL '{}' is reachable from your environment.".format(url)) # 1. if username was specified, verify that password was also specified if username: if not password: try: password = prompt_pass(msg='Password: '******'Please specify both username and password in non-interactive mode.') return login_server, username, password # 2. if we don't yet have credentials, attempt to get a refresh token if not registry or registry.sku.name in get_managed_sku(cmd): try: return login_server, EMPTY_GUID, _get_aad_token( cli_ctx, login_server, only_refresh_token, repository, artifact_repository, permission) except CLIError as e: logger.warning("%s: %s", AAD_TOKEN_BASE_ERROR_MESSAGE, str(e)) # 3. if we still don't have credentials, attempt to get the admin credentials (if enabled) if registry: if registry.admin_user_enabled: try: cred = cf_acr_registries(cli_ctx).list_credentials(resource_group_name, registry_name) return login_server, cred.username, cred.passwords[0].value except CLIError as e: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, str(e)) else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, "Admin user is disabled.") else: logger.warning("%s: %s", ADMIN_USER_BASE_ERROR_MESSAGE, resource_not_found) # 4. if we still don't have credentials, prompt the user try: username = prompt('Username: '******'Password: '******'Unable to authenticate using AAD or admin login credentials. ' + 'Please specify both username and password in non-interactive mode.') return login_server, None, None
def test_send_raw_requests(self, send_mock, get_raw_token_mock): if 'AZURE_HTTP_USER_AGENT' in os.environ: del os.environ[ 'AZURE_HTTP_USER_AGENT'] # Clear env var possibly added by DevOps return_val = mock.MagicMock() return_val.is_ok = True send_mock.return_value = return_val get_raw_token_mock.return_value = ("Bearer", "eyJ0eXAiOiJKV1", None), None, None cli_ctx = DummyCli() cli_ctx.data = {'command': 'rest', 'safe_params': ['method', 'uri']} test_arm_active_directory_resource_id = 'https://management.core.windows.net/' test_arm_endpoint = 'https://management.azure.com/' subscription_id = '00000001-0000-0000-0000-000000000000' arm_resource_id = '/subscriptions/{}/resourcegroups/02?api-version=2019-07-01'.format( subscription_id) full_arm_rest_url = test_arm_endpoint.rstrip('/') + arm_resource_id test_body = '{"b1": "v1"}' expected_header = { 'User-Agent': get_az_user_agent(), 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'CommandName': 'rest', 'ParameterSetName': 'method uri', 'Content-Length': '12' } expected_header_with_auth = expected_header.copy() expected_header_with_auth['Authorization'] = 'Bearer eyJ0eXAiOiJKV1' # Test basic usage # Mock Put Blob https://docs.microsoft.com/en-us/rest/api/storageservices/put-blob # Authenticate with service SAS https://docs.microsoft.com/en-us/rest/api/storageservices/create-service-sas sas_token = ['sv=2019-02-02', '{"srt": "s"}', "{'ss': 'bf'}"] send_raw_request( cli_ctx, 'PUT', 'https://myaccount.blob.core.windows.net/mycontainer/myblob?timeout=30', uri_parameters=sas_token, body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_not_called() request = send_mock.call_args.args[1] self.assertEqual(request.method, 'PUT') self.assertEqual( request.url, 'https://myaccount.blob.core.windows.net/mycontainer/myblob?timeout=30&sv=2019-02-02&srt=s&ss=bf' ) self.assertEqual(request.body, '{"b1": "v1"}') # Verify no Authorization header self.assertDictEqual(dict(request.headers), expected_header) self.assertEqual(send_mock.call_args.kwargs["verify"], not should_disable_connection_verify()) # Test Authorization header is skipped send_raw_request(cli_ctx, 'GET', full_arm_rest_url, body=test_body, skip_authorization_header=True, generated_client_request_id_name=None) get_raw_token_mock.assert_not_called() request = send_mock.call_args.args[1] self.assertDictEqual(dict(request.headers), expected_header) # Test Authorization header is already provided send_raw_request(cli_ctx, 'GET', full_arm_rest_url, body=test_body, headers={'Authorization=Basic ABCDE'}, generated_client_request_id_name=None) get_raw_token_mock.assert_not_called() request = send_mock.call_args.args[1] self.assertDictEqual(dict(request.headers), { **expected_header, 'Authorization': 'Basic ABCDE' }) # Test Authorization header is auto appended send_raw_request(cli_ctx, 'GET', full_arm_rest_url, body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id, subscription=subscription_id) request = send_mock.call_args.args[1] self.assertDictEqual(dict(request.headers), expected_header_with_auth) # Test ARM Subscriptions - List # https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list # /subscriptions?api-version=2020-01-01 send_raw_request(cli_ctx, 'GET', '/subscriptions?api-version=2020-01-01', body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id) request = send_mock.call_args.args[1] self.assertEqual( request.url, test_arm_endpoint.rstrip('/') + '/subscriptions?api-version=2020-01-01') self.assertDictEqual(dict(request.headers), expected_header_with_auth) # Test ARM Tenants - List # https://docs.microsoft.com/en-us/rest/api/resources/tenants/list # /tenants?api-version=2020-01-01 send_raw_request(cli_ctx, 'GET', '/tenants?api-version=2020-01-01', body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id) request = send_mock.call_args.args[1] self.assertEqual( request.url, test_arm_endpoint.rstrip('/') + '/tenants?api-version=2020-01-01') self.assertDictEqual(dict(request.headers), expected_header_with_auth) # Test ARM resource ID # /subscriptions/00000001-0000-0000-0000-000000000000/resourcegroups/02?api-version=2019-07-01 send_raw_request(cli_ctx, 'GET', arm_resource_id, body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id, subscription=subscription_id) request = send_mock.call_args.args[1] self.assertEqual(request.url, full_arm_rest_url) self.assertDictEqual(dict(request.headers), expected_header_with_auth) # Test full ARM URL # https://management.azure.com/subscriptions/00000001-0000-0000-0000-000000000000/resourcegroups/02?api-version=2019-07-01 send_raw_request(cli_ctx, 'GET', full_arm_rest_url) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id, subscription=subscription_id) request = send_mock.call_args.args[1] self.assertEqual(request.url, full_arm_rest_url) # Test full ARM URL with port # https://management.azure.com:443/subscriptions/00000001-0000-0000-0000-000000000000/resourcegroups/02?api-version=2019-07-01 test_arm_endpoint_with_port = 'https://management.azure.com:443/' full_arm_rest_url_with_port = test_arm_endpoint_with_port.rstrip( '/') + arm_resource_id send_raw_request(cli_ctx, 'GET', full_arm_rest_url_with_port) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id, subscription=subscription_id) request = send_mock.call_args.args[1] self.assertEqual( request.url, 'https://management.azure.com:443/subscriptions/00000001-0000-0000-0000-000000000000/resourcegroups/02?api-version=2019-07-01' ) # Test non-ARM APIs # Test AD Graph API https://graph.windows.net/ url = 'https://graph.windows.net/00000002-0000-0000-0000-000000000000/applications/00000003-0000-0000-0000-000000000000?api-version=1.6' send_raw_request(cli_ctx, 'PATCH', url, body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with(mock.ANY, 'https://graph.windows.net/') request = send_mock.call_args.args[1] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.url, url) # Test MS Graph API https://graph.microsoft.com/beta/appRoleAssignments/01 url = 'https://graph.microsoft.com/beta/appRoleAssignments/01' send_raw_request(cli_ctx, 'PATCH', url, body=test_body, generated_client_request_id_name=None) get_raw_token_mock.assert_called_with(mock.ANY, 'https://graph.microsoft.com/') request = send_mock.call_args.args[1] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.url, url) # Test custom case-insensitive User-Agent with mock.patch.dict('os.environ', {'AZURE_HTTP_USER_AGENT': "env-ua"}): send_raw_request(cli_ctx, 'GET', full_arm_rest_url, headers={'user-agent=ARG-UA'}) get_raw_token_mock.assert_called_with( mock.ANY, test_arm_active_directory_resource_id, subscription=subscription_id) request = send_mock.call_args.args[1] self.assertEqual(request.headers['User-Agent'], get_az_user_agent() + ' env-ua ARG-UA')
def request_data_from_registry(http_method, login_server, path, username, password, result_index=None, json_payload=None, file_payload=None, params=None, retry_times=3, retry_interval=5): if http_method not in ALLOWED_HTTP_METHOD: raise ValueError("Allowed http method: {}".format(ALLOWED_HTTP_METHOD)) if json_payload and file_payload: raise ValueError("One of json_payload and file_payload can be specified.") if http_method in ['get', 'delete'] and (json_payload or file_payload): raise ValueError("Empty payload is required for http method: {}".format(http_method)) if http_method in ['patch', 'put'] and not (json_payload or file_payload): raise ValueError("Non-empty payload is required for http method: {}".format(http_method)) url = 'https://{}{}'.format(login_server, path) headers = get_authorization_header(username, password) for i in range(0, retry_times): errorMessage = None try: if file_payload: with open(file_payload, 'rb') as data_payload: response = requests.request( method=http_method, url=url, headers=headers, params=params, data=data_payload, verify=(not should_disable_connection_verify()) ) else: response = requests.request( method=http_method, url=url, headers=headers, params=params, json=json_payload, verify=(not should_disable_connection_verify()) ) log_registry_response(response) if response.status_code == 200: result = response.json()[result_index] if result_index else response.json() next_link = response.headers['link'] if 'link' in response.headers else None return result, next_link elif response.status_code == 201 or response.status_code == 202: result = None try: result = response.json()[result_index] if result_index else response.json() except ValueError as e: logger.debug('Response is empty or is not a valid json. Exception: %s', str(e)) return result, None elif response.status_code == 204: return None, None elif response.status_code == 401: raise RegistryException( parse_error_message('Authentication required.', response), response.status_code) elif response.status_code == 404: raise RegistryException( parse_error_message('The requested data does not exist.', response), response.status_code) elif response.status_code == 405: raise RegistryException( parse_error_message('This operation is not supported.', response), response.status_code) elif response.status_code == 409: raise RegistryException( parse_error_message('Failed to request data due to a conflict.', response), response.status_code) else: raise Exception(parse_error_message('Could not {} the requested data.'.format(http_method), response)) except CLIError: raise except Exception as e: # pylint: disable=broad-except errorMessage = str(e) logger.debug('Retrying %s with exception %s', i + 1, errorMessage) time.sleep(retry_interval) raise CLIError(errorMessage)