-
Notifications
You must be signed in to change notification settings - Fork 102
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
Fixing cookie based authentication #1091
base: main
Are you sure you want to change the base?
Changes from 2 commits
9acfabf
b675a0b
150d0b6
02adced
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 |
---|---|---|
|
@@ -256,7 +256,8 @@ async def connect( | |
specified_facades=None, | ||
proxy=None, | ||
debug_log_conn=None, | ||
debug_log_params={} | ||
debug_log_params={}, | ||
cookie_domain=None, | ||
): | ||
"""Connect to the websocket. | ||
|
||
|
@@ -285,8 +286,10 @@ async def connect( | |
to prevent using the conservative client pinning with in the client. | ||
:param TextIOWrapper debug_log_conn: target if this is a debug log connection | ||
:param dict debug_log_params: filtering parameters for the debug-log output | ||
:param str cookie_domain: Which domain the controller uses for cookies. | ||
""" | ||
self = cls() | ||
self.cookie_domain = cookie_domain | ||
if endpoint is None: | ||
raise ValueError('no endpoint provided') | ||
if not isinstance(endpoint, str) and not isinstance(endpoint, list): | ||
|
@@ -301,7 +304,7 @@ async def connect( | |
if password is not None: | ||
raise errors.JujuAuthError('cannot log in as external ' | ||
'user with a password') | ||
username = None | ||
|
||
self.usertag = tag.user(username) | ||
self.password = password | ||
|
||
|
@@ -762,6 +765,7 @@ def connect_params(self): | |
'bakery_client': self.bakery_client, | ||
'max_frame_size': self.max_frame_size, | ||
'proxy': self.proxy, | ||
'cookie_domain': self.cookie_domain, | ||
} | ||
|
||
async def controller(self): | ||
|
@@ -774,6 +778,7 @@ async def controller(self): | |
cacert=self.cacert, | ||
bakery_client=self.bakery_client, | ||
max_frame_size=self.max_frame_size, | ||
cookie_domain=self.cookie_domain, | ||
) | ||
|
||
async def reconnect(self): | ||
|
@@ -871,7 +876,7 @@ async def _connect_with_login(self, endpoints): | |
# a few times. | ||
for i in range(0, 2): | ||
result = (await self.login())['response'] | ||
macaroonJSON = result.get('discharge-required') | ||
macaroonJSON = result.get('bakery-discharge-required') | ||
if macaroonJSON is None: | ||
self.info = result | ||
success = True | ||
|
@@ -962,7 +967,7 @@ async def login(self): | |
params['credentials'] = self.password | ||
else: | ||
macaroons = _macaroons_for_domain(self.bakery_client.cookies, | ||
self.endpoint) | ||
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. I'm not sure I fully get the intent behind this from inspection. What is the difference here between the endpoint and the cookie domain? 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. This is because when you are using macaroons from the bakery client, you are actually using cookies that were originally put in the cookie jar by the juju client. The juju client does not use the endpoint as the cookie domain. Instead it uses either the controller's UUID (if the controller is self signed) or its SNIHostName. If we don't pick the correct cookie domain then pylibjuju will not find the cookie and it will fail to authenticate with the controller. I believe this is because if you have a self signed HA controller (for example) then there will be three different potential endpoints, so if you use that for your cookie domain, then your coookie will only work with one of the endpoints. As an example: you might have an endpoint which is 192.168.3.14, but the cookie in the jar has the domain "791fa163-16bd-4726-9c58-7c360cc133b2". 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. Ah fair, good work on the digging! Seems like a bug that this wasn't implemented the same way then. 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. Correct, the juju code is
where PreferredHost is either the SNIHostName or controller UUID. |
||
self.cookie_domain) | ||
params['macaroons'] = [[bakery.macaroon_to_dict(m) for m in ms] | ||
for ms in macaroons] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,46 @@ | |
import http.cookiejar as cookiejar | ||
import json | ||
import time | ||
|
||
import pyrfc3339 | ||
|
||
|
||
# There are certain subtle differences between the Go and Python | ||
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. Rather than monkey patching here, would it be possible to get the inherit from the DefaultCookiePolicy, override the relevent methods then pass it as a policy kwarg to I believe this should achive the same effect without monkey patching the code. 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. There was some complication which I have forgotten. I can look into it again later today. |
||
# cookie implementations that make them incompatible. Particularly, | ||
# Juju relies on domains being permitted to be arbitrary strings, | ||
# while Python will try to convert these internally to local domains. | ||
# The only reasonably economical solution to this problem is to monkey | ||
# patch the affected class in anticipation of a good solution. | ||
|
||
def better_return_ok_domain(): | ||
old_method = cookiejar.DefaultCookiePolicy.return_ok_domain | ||
|
||
def inner(self, cookie, request): | ||
'''This is a monkey patch for the DefaultCookiePolicy class | ||
that allows arbitrary domains to be used in cookies.''' | ||
if cookie.domain == request.host: | ||
return True | ||
else: | ||
return old_method(self, cookie, request) | ||
return inner | ||
|
||
|
||
def better_domain_return_ok(): | ||
old_method = cookiejar.DefaultCookiePolicy.domain_return_ok | ||
|
||
def inner(self, domain, request): | ||
'''This is a monkey patch for the DefaultCookiePolicy class | ||
that allows arbitrary domains to be used in cookies.''' | ||
if domain == request.host: | ||
return True | ||
else: | ||
return old_method(self, domain, request) | ||
return inner | ||
|
||
|
||
cookiejar.DefaultCookiePolicy.return_ok_domain = better_return_ok_domain() | ||
cookiejar.DefaultCookiePolicy.domain_return_ok = better_domain_return_ok() | ||
|
||
|
||
class GoCookieJar(cookiejar.FileCookieJar): | ||
'''A CookieJar implementation that reads and writes cookies | ||
to the cookiejar format as understood by the Go package | ||
|
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.
For the casual observer - newer Juju controllers (2.9 onwards) use the "bakery-discharge-required" attribute.