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

Refactor(oauth): resolve RuntimeWarning #2152

Merged
merged 5 commits into from
Jun 12, 2024

Conversation

angela-tran
Copy link
Member

@angela-tran angela-tran commented Jun 10, 2024

Closes #2003

The problem with calling register_providers in ready() is that it reads from the database (to get all the AuthProviders), but ready is called during management commands, so it is not entirely safe to interact with the database at that time.

This PR refactors the function to take one provider model object and moves it from ready() to the view where the OAuth client object is needed. The register method on BaseOAuth looks safe to call multiple times and returns a client object.

When reviewing

You should see that the RuntimeWarning no longer occurs when starting up the app (or running any management command - e.g. makemigrations, makemessages).

@angela-tran angela-tran self-assigned this Jun 10, 2024
@github-actions github-actions bot added back-end Django views, sessions, middleware, models, migrations etc. deployment-dev [auto] Changes that will trigger a deploy if merged to dev labels Jun 10, 2024
@angela-tran angela-tran added chore Chores and tasks for code cleanup, dev experience, admin/configuration settings, etc. and removed deployment-dev [auto] Changes that will trigger a deploy if merged to dev labels Jun 10, 2024
Copy link

github-actions bot commented Jun 10, 2024

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  benefits/oauth
  apps.py
  client.py
  views.py
Project Total  

This report was generated by python-coverage-comment-action

Copy link

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  benefits/oauth
  apps.py
  client.py
  views.py
Project Total  

This report was generated by python-coverage-comment-action

@angela-tran angela-tran marked this pull request as ready for review June 10, 2024 20:22
@angela-tran angela-tran requested a review from a team as a code owner June 10, 2024 20:22
benefits/oauth/views.py Outdated Show resolved Hide resolved
Copy link
Member

@lalver1 lalver1 left a comment

Choose a reason for hiding this comment

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

This looks like it fixed the issue! 👍 I ran the app locally and saw that the RuntimeWarning no longer shows up.

I was wondering about the difference between
oauth_client = register_provider(oauth, verifier.auth_provider) and
oauth_client = oauth.create_client(verifier.auth_provider.client_name)

and when to use one over the other 🤔. It looks like both of them return the same object and calling register_provider() multiple times is safe, so they seem to be equivalent (as long as a provider has been registered). I guess this observation may relate to Kegan's comment.


for provider in providers:
logger.debug(f"Registering OAuth client: {provider.client_name}")
client = oauth_registry.register(
Copy link
Member

Choose a reason for hiding this comment

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

At first I was wondering about this line since the Authlib documentation does not mention a return type for .register(), but after running the app in debug mode and looking at the Authlib code, it turns out that .register() does return a client (<authlib.integrations.django_client.apps.DjangoOAuth2App object), which is the same client shown in oauth._clients after calling register() on oauth.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was wondering about the difference between oauth_client = register_provider(oauth, verifier.auth_provider) and oauth_client = oauth.create_client(verifier.auth_provider.client_name)

and when to use one over the other 🤔. It looks like both of them return the same object and calling register_provider() multiple times is safe, so they seem to be equivalent (as long as a provider has been registered). I guess this observation may relate to Kegan's comment.

Yeah, so you'd want to use register_provider(oauth, verifier.auth_provider) instead of oauth.create_client(verifier.auth_provider.client_name) when it's possible that the AuthProvider's config hasn't been registered with Authlib yet.

I used register_provider in the login view but not in authorize because we know authorize will always be preceded by login and therefore the client will already be registered. But as @thekaveman is suggesting, it wouldn't hurt to do that in authorize too, so I'm good with making that change.

Ideally I would've liked to somehow check if the client_name has already been registered, call register for it if it hasn't, and then call create_client. But Authlib doesn't provide a way for us to check the registry, so all we can do is call register every time.

Copy link
Member

Choose a reason for hiding this comment

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

But Authlib doesn't provide a way for us to check the registry, so all we can do is call register every time.

What happens when we try to get a client_name that hasn't been registered yet? Exception, or does it just return None? Either way, we could handle that case and use it to register. Wrap both cases up in a helper function and call it a day 😎

Copy link
Member Author

Choose a reason for hiding this comment

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

I think an exception could mean a misconfiguration, so I'll leave that case out. But I went ahead and made the helper function that registers the client if we got None back - 4d2e1ec. Hopefully this code reads as more intuitive.

this way we avoid reading from the database during any AppConfig.ready
implementations, which Django warns us about because they are executed
as a part of management commands as well.
the register method seems to be idempotent, so it's fine to call it like
this. the approach of registering during view import made importing the
module complicated (e.g. when importing from the module for our tests)
currently the only way to get to the authorize view is from the login
view, so the client should already be registered. But it doesn't hurt
to call it here either.
@angela-tran angela-tran force-pushed the refactor/auth-provider-registration branch from 47a1ae6 to 770f8e1 Compare June 12, 2024 21:20
Copy link

Copy link
Member

@thekaveman thekaveman left a comment

Choose a reason for hiding this comment

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

Looks good 👍


Adapted from https://stackoverflow.com/a/64174413.
"""
logger.info("Registering OAuth clients")
logger.debug(f"Registering OAuth client: {provider.client_name}")
Copy link
Member

Choose a reason for hiding this comment

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

👍

@angela-tran angela-tran merged commit 9556fe5 into dev Jun 12, 2024
17 checks passed
@angela-tran angela-tran deleted the refactor/auth-provider-registration branch June 12, 2024 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
back-end Django views, sessions, middleware, models, migrations etc. chore Chores and tasks for code cleanup, dev experience, admin/configuration settings, etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Resolve RuntimeWarning about interacting with database during AppConfig.ready()
3 participants