-
Notifications
You must be signed in to change notification settings - Fork 827
Robustly clear sessions on user deletion to cause immediate logout #13757
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
Conversation
Build Artifacts
|
b6dfa77 to
59e4106
Compare
nucleogenesis
left a comment
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 left some questions / thoughts - nothing blocking or urgent to reply to.
Overall I think that I got a good sense of how the changes work. The Session* classes were a bit weird to grok at first but seems the crux is in customizing the SessionStore so you can override create_model_instance to handle the user_id business. The rest then seems like glue code w/ the new Session model and the DB.
I didn't spin it up yet but don't want to forget to submit this review so I'll come back for further review after some manual testing.
| def cleanup_legacy_file_sessions(): | ||
| """ | ||
| Clean up legacy file-based sessions when upgrading to database-backed sessions. | ||
| Removes the sessions directory from KOLIBRI_HOME if it exists. | ||
| """ |
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.
<3
| @@ -0,0 +1,69 @@ | |||
| from abc import ABC | |||
| from abc import abstractmethod | |||
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.
(non-blocking curiosity) The reason I'm seeing for using ABC/abstractmethod here is to ensure that classes inheriting this will be required to override specific methods or throw an error.
Are there any other benefits to using an abstract class or would it be functionally similar to:
def MODEL_CLASSES(self):
raise "Not implemented"I think this came to mind because that's how I've done abstract classes in Ruby
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.
Love how this whole module DRYs things up
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.
Yes, ABC and abstractmethods are the more explicit way to do this.
kolibri/core/auth/models.py
Outdated
| user=self.full_name or self.username, facility=self.facility | ||
| ) | ||
|
|
||
| def __init__(self, *args, **kwargs): |
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.
(non blocking opinion) - __init__ feels like it should always be at the top of a class to me but as I type this I'm feeling like this is largely based on vibes 😆 -- I think maybe I just like to see overrides at the top of a class so I can first answer the question "what is the class doing in relation to it's parent's implementation" then the second question of "now what does this class do uniquely".
In any case - __str__ is overridden down here too so the rest fit in well enough 😄
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.
Yeah - I think I have a similar intuition - the only advantage to it being here is that its closer to where the intialized value is used.
| # Remove NotificationsRouter if sqlite is not being used: | ||
| if settings.DATABASES["default"]["ENGINE"] != "django.db.backends.sqlite3": | ||
| ROUTER_ID = "kolibri.core.notifications.models.NotificationsRouter" | ||
| if ROUTER_ID in settings.DATABASE_ROUTERS: | ||
| settings.DATABASE_ROUTERS = tuple( | ||
| filter(lambda x: x != ROUTER_ID, settings.DATABASE_ROUTERS) | ||
| ) |
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.
Nice to see code like this in the red :)
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.
It has been redundant for quite some time, because of the router conditionality being in settings/base.py instead!
|
Hi @rtibbles, Logs: |
Update all implementations. Remove redundant unregistering of NotificationsRouter.
To allow sessions to be cleared via user id. Use separate sessions database to prevent bottlenecks.
59e4106 to
9c88a32
Compare
…eted users also get deleted.
9c88a32 to
a7848c7
Compare
|
Hi @rtibbles, LGTM in general, just noting the following:
error.mp4The error details:
server.error.mp4 |
nucleogenesis
left a comment
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.
The code overall looks great and made sense as I read through. I left a few questions, mostly for my own understanding of the changes.
I'll leave approval to @pcenov 's testing
| return obj | ||
|
|
||
| @classmethod | ||
| def delete_all_sessions(cls, user_ids): |
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.
(nitpick) - If this isn't overriding an existing method, would be clearer to call it delete_sessions as delete_all_sessions doesn't read like it should take any params but just delete all of the sessions
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.
The reason it's called delete_all_sessions is because each user can have multiple active sessions, if they have signed in from different devices, so this is to emphasize that this is what is happening.
| all_objects = AllObjectsFacilityUserModelManager() | ||
| objects = FacilityUserModelManager() | ||
|
|
||
| syncing_objects = BaseFacilityUserModelManager() |
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 only see this referenced here an in a migration - is this used internally to morango or somewhere else?
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.
Yes, this is the manager used specifically by Morango. Practically, I'm not sure this is actually needed, because of the additional morango sync hooks we have, but it means if things are getting deleted via regular queryset methods in morango we have this happening for sure.
| ) | ||
|
|
||
| def save(self, *args, **kwargs): | ||
| # Call the parent save method first |
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.
Just a question for my own understanding - this & the delete method below are very similar to the update and delete methods in the FacilityUserQuerySet up above. What are the primary differences in the use cases for the two classes' handling of clearing sessions of deleted users?
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'm not sure I understand the question - models can be updated and deleted either via the queryset, or by model methods - we have to do this in both places to ensure that the proper clearing of session data is not conditional on how it happens.
One possible weakness with the current implementation is that we are not catching bulk_update calls - but I am not aware of anywhere we use that in Kolibri currently.
| def DB_NAME(self): | ||
| pass | ||
|
|
||
| def db_for_read(self, model, **hints): |
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's **hints here and why isn't it used in the methods?
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.
It's a standard Django thing to provide additional context to the database router. We do not currently use it except in the allow_migrations method.
https://docs.djangoproject.com/en/3.2/topics/db/multi-db/#database-routers
|
@pcenov I think for both these cases, I'd rather file follow up issues - the use case of whole facility deletion is important, but I think significantly less common than individual user deletion, and I think we can push the minor 'error page' flash as a follow up in a patch release. |
pcenov
left a comment
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.
Thanks @rtibbles - I've filled the notes from my comment as follow-up issues. This one is good to go!
AlexVelezLl
left a comment
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'm not sure if an extra pair of eyes is helpful, but just a note that I read this entire PR critically (I learned quite a bit) and didn't find anything unusual.
Summary
References
FIxes #13542
Note that this reverts #3944 which was originally implemented for a very modest performance improvement. I think the combination of a wide range of other performance improvements, plus the use of a separate database in this implementation means that any performance degradation from changing back here should be negligible.
Reviewer guidance
I used Claude Code to do the initial model and queryset updates and write tests for it, and then heavily edited it to reduce code verbosity, fix errors and ensure the tests were asserting the right things. All other code was hand written.
Both the deletion flows in the issue itself, when deleting in an LoD and also when deleting using the new bulk user management flows should work here. Also, if a user is deleted due to a facility being deleted from a device that should also work. Additionally, it is important to test both the soft deletion that happens by default and the permanent deletion, to make sure both work.
The result here should be that the user is logged out identically if the user's session timed out.