Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

awnns
Copy link

@awnns awnns commented Aug 23, 2024

Description

At present, cookie based authentication does not work in pylibjuju. This is due to a number of bugs. This PR intends to fix many of these issues so that pylibjuju can use the juju client's cookies for authentication.

Fixes:

  • The library now detects the appropriate cookie domain from jujudata.
  • The library now adds correct user tags for external and local users.
  • The library now correctly detects when a discharge is required due to third party caveats.
  • The library now attempts to use cookies if no password has been provided and cookies are available.
  • A subtle implementation difference between juju's cookie jar and http.cookiejar.DefaultPolicy was fixed with monkey patching.

I have also cherry picked a commit from @Aflynn50 from a different ongoing PR.

Fixes: #1061 #1081

QA Steps

I have had some difficulty with running the tests locally. I am not sure what sort of setup they want to run in.

What I have tested manually is the following:

  • Creating connections and logging into controllers with username and password
  • Creating connections and logging into controllers with an external user and identity provider using cookies from the juju client.
  • Creating connections and logging into controllers with a local user using cookies from the juju client.
  • Various commands such as listing models and checking model statuses.

Things I have been unable to test that are affected by this PR:

  • Anything to do with proxied controllers.
  • Anything to do with controllers with proper certificates (all I have access to are self signed). There is potential for issues with the cookie domain for these since I have not tested whether it works. It might require trivial modifications.
  • Juju 2.X

Notes & Discussion

The monkey patches in gocookies.py are not a good look and they are somewhat brittle, but I believe it will take a lot more work to fix the CookiePolicy if we wish to retain all the other sane behaviors from http.cookiejar.DefaultCookiePolicy. An alternative is to use an empty CookiePolicy and subclass it to only check for domain equality and path. The problem the monkey patch fixes is caused by incompatible cookiejars. The juju client uses https://github.com/juju/persistent-cookiejar which permits domain fields without dots under the condition that the domain matches exactly with the request's host. Python's implementation instead rewrites the request's host by appending ".local", which makes the comparison fail.

There are still issues with CA certificates when requiring discharges from the juju controller. This is due to intricacies of the HTTP handling in pymacaroonbakery. It may require substantial changes in pymacaroonbakery, but due to a number of bugfixes in this PR it is not essential for most uses (self signed external identity providers are the exception). The lack of a fix does mean that if a user is not logged in on a local user, and tries to connect to the controller, then they will be met with the wrong exception. The intended outcome is to require an interaction to enter a password, but this is not implemented in pymacaroonbakery. In any case, allowing self signed certificates requires either a hack where pylibjuju sets an environment variable and writes a cacert to a temporary file, or making changes in pymacaroonbakery.

@jujubot
Copy link
Contributor

jujubot commented Aug 23, 2024

Thanks for opening a pull request! Please follow the instructions here to ensure your pull request is ready for review. Then, a maintainer will review your patch.

@hpidcock @anvial

1 similar comment
@jujubot
Copy link
Contributor

jujubot commented Aug 23, 2024

Thanks for opening a pull request! Please follow the instructions here to ensure your pull request is ready for review. Then, a maintainer will review your patch.

@hpidcock @anvial

Copy link
Contributor

@dimaqq dimaqq left a comment

Choose a reason for hiding this comment

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

Code looks sensible.

Unit tests would be great, unless the existing unit test actually cover this change.

I wonder how to validate this change against real Juju.

Copy link
Contributor

@Aflynn50 Aflynn50 left a comment

Choose a reason for hiding this comment

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

Thank you for the contribution! It looks good and it'll be great to get this working again.

As @dimaqq mentioned, it would be good to get some tests working for this. There are some disabled tests in /tests/integration/test_macaroon_auth.py that should work again with this fix. Would you be able to get the relevant ones from there passing and remove the skip decorators on them?

import pyrfc3339


# There are certain subtle differences between the Go and Python
Copy link
Contributor

Choose a reason for hiding this comment

The 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 cookiejar.FileCookieJar?

I believe this should achive the same effect without monkey patching the code.

Copy link
Author

Choose a reason for hiding this comment

The 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.

@@ -962,7 +967,7 @@ async def login(self):
params['credentials'] = self.password
else:
macaroons = _macaroons_for_domain(self.bakery_client.cookies,
self.endpoint)
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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".

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

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

Correct, the juju code is

cookieURL := api.CookieURLFromHost(api.PerferredHost(info))
info.Macaroons = httpbakery.MacaroonsForURL(cookieJar, cookieURL)

where PreferredHost is either the SNIHostName or controller UUID.

Copy link
Member

@SimonRichardson SimonRichardson left a comment

Choose a reason for hiding this comment

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

@wallyworld can you look over this PR, because you did a lot of work around macroons in the go side.

Copy link
Member

@wallyworld wallyworld left a comment

Choose a reason for hiding this comment

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

Code looks ok. Left a couple of comments to provide a little bit of extra context from the Juju side. I'll leave it to others to agree/disagree with the trade-offs around the cookie jar behaviour, suffice to say it's progress and if it works from the perspective of "don't let perfect be the enemy of good", then it seems ok as a step forward, especially since upstream Juju changes are out of scope here.

@@ -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')
Copy link
Member

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.

@@ -962,7 +967,7 @@ async def login(self):
params['credentials'] = self.password
else:
macaroons = _macaroons_for_domain(self.bakery_client.cookies,
self.endpoint)
Copy link
Member

Choose a reason for hiding this comment

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

Correct, the juju code is

cookieURL := api.CookieURLFromHost(api.PerferredHost(info))
info.Macaroons = httpbakery.MacaroonsForURL(cookieJar, cookieURL)

where PreferredHost is either the SNIHostName or controller UUID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cant' connect using macaroons or bakery_client
6 participants