opensafely-core / job-server

A server for mediating jobs that can be run in an OpenSAFELY secure environment. q.v. job-runner
https://jobs.opensafely.org
Other
5 stars 11 forks source link

Simplify `jobserver.models.User` #4627

Open mikerkelly opened 1 month ago

mikerkelly commented 1 month ago

4625 removed User.is_staff and User.is_superuser as no longer required. They were present as we once used django.contrib.admin but no longer do (instead having a completely custom staff app).

While working on #4625 I noticed some possible further simplifications to the models.User module. There are several more bits of code or peculiarities that may exist for historical reasons but are no longer required. Simplifying the module and using less custom-behaviour where appropriate would aid understanding and maintenance.

This might need breaking up into multiple issues. Possible changes to investigate:

iaindillingham commented 3 weeks ago

@mikerkelly to break this into smaller issues.

mikerkelly commented 2 weeks ago

This comment investigates the first two bullets from the issue description. How much of UserManager, UserManager.create_user, get_or_create_user and UserQuerySet do we need?

Do we need django.contrib.auth.models.UserManager?

The UserManager that our User model's manager extends provides the following functionality:

It requires a bit of digging to understand this. None of these behaviours were documented as important in our code or tested for (except for the _create_user behaviours in one #4625 unit test test_create_user where this behaviour was recently identified). If they're needed, we should clarify and test them. If not, we can simplify by slightly extending django.db.models.Manager or removing the manager entirely.

Social-auth users (GitHub) don't need these steps, as fields are validated through GitHub OAuth. Interactive users are created via interactive.commands.create_user, which calls User.objects.create and bypasses UserManager.create_user altogether.

Our UserManager create_user relies on the private parent _create_user function because the public methods automatically set is_staff and is_superuser, which we don’t want. Depending on an internal method is both difficult to follow and risks breaking when we update Django.

Our get_or_create_user function mimics standard Python Django get_or_create. There is a comment that this is needed because of the above special behaviour, as the standard manager calls create instead of _create_user. However, if we don't need _create_user, we don't need this function. It's used in two places:

We don’t seem to rely on any of the special behaviors of Django's UserManager. The create_user management command only marginally benefits from normalization. We don't allow password-based logins, and currently, no users are created with an actual password. However, as a precaution, we should still hash any password passed to User creation in case it’s used in the future or if a real password is provided.

Conclusion: we can simplify by extending django.db.models.Manager with a custom create method that handles normalization and password hashing. This will make it clearer the behaviours User creation exhibits. This also lets us remove the custom get_or_create_user. Both of these reduce the amount of code to understand and maintain.

Do we need UserQuerySet?

UserQuerySet exists to order User instances so that those with a blank fullname appear last.

We can replicate this behaviour using Case and When clauses in Meta.ordering, making it the default ordering for the User model. This way, client code can call objects.all() without needing to explicitly use order_by_name. It’s also more in line with standard Django practices to define it this way.

mikerkelly commented 2 weeks ago

A quick prototype showed that most of the troublesome code in the User model could just be removed and everything still worked fine with not-too-many-changes. So instead of splitting this I've created PR #4698 with that and some other small improvements from this ticket or noted during that work.

The only outstanding thing from the description is this:

" TODO: rename name and remove the name property once all users have filledin their names"

Raised #4699 for this for triage.