Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cli/tests/integrations/test_marathon.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,12 @@ def _zero_instance_app():

@contextlib.contextmanager
def _zero_instance_app_through_http():
class JSONRequestHandler (BaseHTTPRequestHandler):
class JSONRequestHandler(BaseHTTPRequestHandler):

def do_HEAD(self): # noqa: N802
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()

def do_GET(self): # noqa: N802
self.send_response(200)
Expand Down
5 changes: 2 additions & 3 deletions cli/tests/unit/test_http_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,8 @@ def test_request_with_bad_auth_basic(mock, req_mock, auth_mock):

req_mock.return_value = mock

with pytest.raises(DCOSException) as e:
http._request_with_auth(mock, "method", mock.url)
assert e.exconly().split(':')[1].strip() == "Authentication failed"
response = http._request_with_auth(mock, "method", mock.url)
assert response.status_code == 401


@patch('requests.Response')
Expand Down
90 changes: 51 additions & 39 deletions dcos/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _request_with_auth(response,
timeout=None,
verify=None,
**kwargs):
"""Try request (3 times) with credentials if 401 returned from server
"""Request with credentials

:param response: requests.response
:type response: requests.Response
Expand All @@ -146,40 +146,32 @@ def _request_with_auth(response,
: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 in ["acsjwt", "oauthjwt"]:

if config.get_config_val("core.dcos_acs_token") is not None:
msg = ("Your core.dcos_acs_token is invalid. "
"Please run: `dcos auth login`")
raise DCOSException(msg)

i += 1
parsed_url = urlparse(url)
hostname = parsed_url.hostname
auth_scheme, realm = get_auth_scheme(response)
creds = (hostname, auth_scheme, realm)

if response.status_code == 401:
raise DCOSAuthenticationException(response)
with lock:
if creds not in AUTH_CREDS:
auth = _get_http_auth(response, parsed_url, auth_scheme)
else:
auth = AUTH_CREDS[creds]

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 in ["acsjwt", "oauthjwt"]:

if config.get_config_val("core.dcos_acs_token") is not None:
msg = ("Your core.dcos_acs_token is invalid. "
"Please run: `dcos auth login`")
raise DCOSException(msg)

return response

Expand All @@ -190,8 +182,14 @@ def request(method,
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).
"""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
Expand All @@ -218,12 +216,26 @@ def request(method,
if verify is not None:
silence_requests_warnings()

response = _request(method, url, is_success, timeout,
verify=verify, **kwargs)
dcos_url = config.get_config_val("core.dcos_url")
auth_url = urllib.parse.urljoin(dcos_url, 'exhibitor/')
response = _request(
'GET', auth_url, is_success, timeout, verify=verify, **kwargs)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still use a HEAD here - it is cheaper given the lack of content in reply. You'll still get the headers you need, and your service can potentially optimise in terms of handling HEAD.


if response.status_code == 401:
i = 0
while i < 3 and response.status_code == 401:
auth_response = _request_with_auth(
response, 'GET', auth_url, is_success, timeout,
Copy link
Author

@huntc huntc Nov 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, HEAD should be cheaper as mentioned. HEAD should always be interchangeable with GET.

verify=verify, **kwargs)
response.status_code = auth_response.status_code
i += 1

response = _request_with_auth(response, method, url, is_success,
timeout, verify, **kwargs)
timeout, verify=verify, **kwargs)
if response.status_code == 401:
raise DCOSAuthenticationException(response)
else:
response = _request(method, url, is_success, timeout, verify, **kwargs)

if is_success(response.status_code):
return response
Expand Down