-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
tickets/DM-45523: walk redirects for Lab manually, extracting xsrf along the way #365
Changes from 10 commits
00095ed
e29b4df
c08bcd2
f13e459
e42a93b
9c71b81
5156794
35099a8
ec9b870
2711420
b8ab174
fbbaa83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,3 +25,6 @@ types-requests | |
|
||
# Documentation | ||
scriv[toml] | ||
|
||
# Not a dependency on Mac, but is on Linux | ||
greenlet |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -556,19 +556,20 @@ def __init__( | |
self._base_url = base_url | ||
self._logger = logger.bind(user=user.username) | ||
|
||
# Construct a connection pool to use for requets to JupyterHub. We | ||
# Construct a connection pool to use for requests to JupyterHub. We | ||
# have to create a separate connection pool for every monkey, since | ||
# each will get user-specific cookies set by JupyterHub. If we shared | ||
# connection pools, monkeys would overwrite each other's cookies and | ||
# get authentication failures from labs. | ||
headers = {"Authorization": f"Bearer {user.token}"} | ||
self._client = AsyncClient( | ||
headers=headers, | ||
follow_redirects=True, | ||
headers=headers, | ||
timeout=timeout.total_seconds(), | ||
) | ||
self._hub_xsrf: str | None = None | ||
self._lab_xsrf: str | None = None | ||
self._logger.debug("Created new NubladoClient") | ||
|
||
async def close(self) -> None: | ||
"""Close the underlying HTTP connection pool.""" | ||
|
@@ -591,11 +592,36 @@ async def auth_to_hub(self) -> None: | |
Raised if no ``_xsrf`` cookie was set in the reply from the lab. | ||
""" | ||
url = self._url_for("hub/home") | ||
r = await self._client.get(url) | ||
r = await self._client.get(url, follow_redirects=False) | ||
# As with auth_to_lab, manually extract from cookies at each | ||
# redirection, because httpx doesn't do that if following redirects | ||
# automatically. | ||
while r.is_redirect: | ||
self._logger.debug( | ||
"Following hub redirect looking for _xsrf cookies", | ||
method=r.request.method, | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
xsrf = self._extract_xsrf(r) | ||
if xsrf and xsrf != self._hub_xsrf: | ||
self._hub_xsrf = xsrf | ||
self._logger.debug( | ||
"Set _hub_xsrf", | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
next_url = urljoin(url, r.headers["Location"]) | ||
r = await self._client.get(next_url, follow_redirects=False) | ||
r.raise_for_status() | ||
xsrf = self._extract_xsrf(r) | ||
if xsrf: | ||
if xsrf and xsrf != self._hub_xsrf: | ||
self._hub_xsrf = xsrf | ||
self._logger.debug( | ||
"Set _hub_xsrf", | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
elif not self._hub_xsrf: | ||
msg = "No _xsrf cookie set in login reply from JupyterHub" | ||
raise JupyterProtocolError(msg) | ||
|
@@ -625,17 +651,33 @@ async def auth_to_lab(self) -> None: | |
url, headers=headers, follow_redirects=False | ||
) | ||
while r.is_redirect: | ||
self._logger.debug( | ||
"Following lab redirect looking for _xsrf cookies", | ||
method=r.request.method, | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
xsrf = self._extract_xsrf(r) | ||
if xsrf and xsrf != self._hub_xsrf: | ||
if xsrf and xsrf != self._lab_xsrf: | ||
self._lab_xsrf = xsrf | ||
self._logger.debug( | ||
"Set _lab_xsrf", | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
next_url = urljoin(url, r.headers["Location"]) | ||
r = await self._client.get( | ||
next_url, headers=headers, follow_redirects=False | ||
) | ||
r.raise_for_status() | ||
xsrf = self._extract_xsrf(r) | ||
if xsrf and xsrf != self._hub_xsrf: | ||
if xsrf and xsrf != self._lab_xsrf: | ||
self._lab_xsrf = xsrf | ||
self._logger.debug( | ||
"Set _lab_xsrf", | ||
url=r.url.copy_with(query=None, fragment=None), | ||
status_code=r.status_code, | ||
) | ||
if not self._lab_xsrf: | ||
msg = "No _xsrf cookie set in login reply from lab" | ||
raise JupyterProtocolError(msg) | ||
|
@@ -681,7 +723,7 @@ def open_lab_session( | |
|
||
Parameters | ||
---------- | ||
nobebook_name | ||
notebook_name | ||
Name of the notebook we will be running, which is passed to the | ||
session and might influence logging on the lab side. If set, the | ||
session type will be set to ``notebook``. If not set, the session | ||
|
@@ -802,7 +844,17 @@ def _extract_xsrf(self, response: Response) -> str | None: | |
""" | ||
cookies = Cookies() | ||
cookies.extract_cookies(response) | ||
return cookies.get("_xsrf") | ||
xsrf = cookies.get("_xsrf") | ||
if xsrf is not None: | ||
self._logger.debug( | ||
"Extracted _xsrf cookie", | ||
method=response.request.method, | ||
url=response.url.copy_with(query=None, fragment=None), | ||
status_code=response.status_code, | ||
# Logging the set-cookie header can be useful here but it | ||
# leaks secrets. Don't put that code in a release build. | ||
) | ||
Comment on lines
+848
to
+856
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we want to put this in each auth method so it would be easier to tell if it happened during the hub auth or the lab auth? It would be duplicated, but maybe more clear? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, if we end up using it, we log that (with whether it is lab or hub as well). I'm not sure we even want to log it in the extraction step. Maybe I'll just leave the comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh, I'm going to leave this alone for now. If it's too chatty if we need to turn on debug for real, we can change it then. |
||
return xsrf | ||
|
||
def _url_for(self, partial: str) -> str: | ||
"""Construct a JupyterHub or Jupyter lab URL from a partial URL. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about putting a
hub_login
call indelete_lab
? Tradeoff is that we may call it a few extra times.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer it be explicit.