def request(method, url, is_success=_default_is_success, timeout=None, verify=None, **kwargs): """Sends an HTTP request. If the server responds with a 401, ask the user for their credentials, and try request again (up to 3 times). :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: Response """ toml_config = config.get_config() auth_token = config.get_config_val("core.dcos_acs_token", toml_config) dcos_url = urlparse(config.get_config_val("core.dcos_url", toml_config)) parsed_url = urlparse(url) # only request with DC/OS Auth if request is to DC/OS cluster # request should match scheme + netloc scheme_eq = parsed_url.scheme == dcos_url.scheme netloc_eq = parsed_url.netloc == dcos_url.netloc if auth_token and scheme_eq and netloc_eq: auth = DCOSAcsAuth(auth_token) else: auth = None response = _request(method, url, is_success, timeout, auth=auth, verify=verify, **kwargs) if is_success(response.status_code): return response elif response.status_code == 401: if auth_token is not None: msg = ("Your core.dcos_acs_token is invalid. " "Please run: `dcos auth login`") raise DCOSAuthenticationException(msg) else: raise DCOSAuthenticationException(response) elif response.status_code == 422: raise DCOSUnprocessableException(response) elif response.status_code == 403: raise DCOSAuthorizationException(response) elif response.status_code == 400: raise DCOSBadRequest(response) else: raise DCOSHTTPException(response)
def header_challenge_auth(dcos_url): """ Triggers authentication using scheme specified in www-authenticate header. Raises exception if authentication fails. :param dcos_url: url to cluster :type dcos_url: str :rtype: None """ # hit protected endpoint which will prompt for auth if cluster has auth endpoint = '/pkgpanda/active.buildinfo.full.json' url = urllib.parse.urljoin(dcos_url, endpoint) response = http._request('HEAD', url) auth_scheme = _get_auth_scheme(response) for _ in range(3): if response.status_code == 401: # this header claims the cluster is open DC/OS 1.7, 1.8 or 1.9 # and supports OIDC implicit auth if auth_scheme == "oauthjwt": token = _get_dcostoken_by_oidc_implicit_flow(dcos_url) # auth_scheme == "acsjwt" # this header claims the cluster is enterprise DC/OS 1.7, 1.8 or # 1.9 and supports username/pawword auth else: token = _get_dcostoken_by_dcos_uid_password_auth(dcos_url) if token is not None: break elif response.status_code == 200: break else: raise DCOSAuthenticationException(response)
def _request_with_auth(response, method, url, is_success=_default_is_success, timeout=None, verify=None, **kwargs): """Try request (3 times) with credentials if 401 returned from server :param response: requests.response :type response: requests.Response :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: requests.Response """ i = 0 while i < 3 and response.status_code == 401: parsed_url = urlparse(url) hostname = parsed_url.hostname auth_scheme, realm = get_auth_scheme(response) creds = (hostname, auth_scheme, realm) with lock: if creds not in AUTH_CREDS: auth = _get_http_auth(response, parsed_url, auth_scheme) else: auth = AUTH_CREDS[creds] # try request again, with auth response = _request(method, url, is_success, timeout, auth, verify, **kwargs) # only store credentials if they're valid with lock: if creds not in AUTH_CREDS and response.status_code == 200: AUTH_CREDS[creds] = auth # acs invalid token elif response.status_code == 401 and auth_scheme == "acsjwt": if util.get_config().get("core.dcos_acs_token") is not None: config.unset("core.dcos_acs_token") i += 1 if response.status_code == 401: raise DCOSAuthenticationException(response) return response
def request(method, url, is_success=_default_is_success, timeout=DEFAULT_TIMEOUT, verify=None, toml_config=None, **kwargs): """Sends an HTTP request. If the server responds with a 401, ask the user for their credentials, and try request again (up to 3 times). :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param toml_config: cluster config to use :type toml_config: Toml :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: Response """ if toml_config is None: toml_config = config.get_config() auth_token = config.get_config_val("core.dcos_acs_token", toml_config) prompt_login = config.get_config_val("core.prompt_login", toml_config) dcos_url = urlparse(config.get_config_val("core.dcos_url", toml_config)) # only request with DC/OS Auth if request is to DC/OS cluster if auth_token and _is_request_to_dcos(url): auth = DCOSAcsAuth(auth_token) else: auth = None response = _request(method, url, is_success, timeout, auth=auth, verify=verify, toml_config=toml_config, **kwargs) if is_success(response.status_code): return response elif response.status_code == 401: if prompt_login: # I don't like having imports that aren't at the top level, but # this is to resolve a circular import issue between dcos.http and # dcos.auth from dcos.auth import header_challenge_auth header_challenge_auth(dcos_url.geturl()) # if header_challenge_auth succeeded, then we auth-ed correctly and # thus can safely recursively call ourselves and not have to worry # about an infinite loop return request(method=method, url=url, is_success=is_success, timeout=timeout, verify=verify, **kwargs) else: if auth_token is not None: msg = ("Your core.dcos_acs_token is invalid. " "Please run: `dcos auth login`") raise DCOSAuthenticationException(response, msg) else: raise DCOSAuthenticationException(response) elif response.status_code == 422: raise DCOSUnprocessableException(response) elif response.status_code == 403: raise DCOSAuthorizationException(response) elif response.status_code == 400: raise DCOSBadRequest(response) else: raise DCOSHTTPException(response)
def request(method, url, is_success=_default_is_success, timeout=None, verify=None, **kwargs): """Sends an HTTP request. We first send a HEAD request for the supplied URL so that we can determine the type of authentication required (if any). If authentication is required then we again use a HEAD request asking the user for their credentials, and try request again (up to 3 times). Once authenticated, we issue the request passed in. We are careful to execute the request passed in just once given that it may be stateful e.g. any files object containing a stream may only be evaluated once. :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: Response """ if 'headers' not in kwargs: kwargs['headers'] = {'Accept': 'application/json'} verify = _verify_ssl(verify) # Silence 'Unverified HTTPS request' and 'SecurityWarning' for bad certs if verify is not None: silence_requests_warnings() response = _request('head', url, is_success, timeout, verify=verify) i = 0 while i < 3 and response.status_code == 401: auth_response = _request_with_auth(response, 'head', url, is_success, timeout, verify=verify) response.status_code = auth_response.status_code i += 1 if response.status_code == 401: raise DCOSAuthenticationException(response) response = _request_with_auth(response, method, url, is_success, timeout, verify, **kwargs) if is_success(response.status_code): return response elif response.status_code == 403: raise DCOSAuthorizationException(response) elif response.status_code == 400: raise DCOSBadRequest(response) else: raise DCOSHTTPException(response)