feat: grant staff access to superusers automatically#38848
feat: grant staff access to superusers automatically#38848Abdul-Muqadim-Arbisoft wants to merge 1 commit into
Conversation
Django's is_superuser and is_staff are independent flags, so a superuser does not get the is_staff access that gates the Django admin and various staff-only Studio/LMS views. Since a superuser can grant itself is_staff at any time, that split offers no protection and is only surprising. Add a pre_save signal on the User model that marks any superuser as staff, and a data migration to backfill existing superusers. Discussion: https://discuss.openedx.org/t/shouldnt-superuser-automatically-inherit-staff-access-in-open-edx/18657
f5defd9 to
8eccf6e
Compare
| user.refresh_from_db() | ||
| assert user.is_staff is True | ||
|
|
||
| def test_non_superuser_is_not_forced_to_staff(self): |
There was a problem hiding this comment.
You never call .save() or trigger the signal here
There was a problem hiding this comment.
Pull request overview
This PR makes Django superusers automatically gain staff access (is_staff=True) to avoid the confusing (and security-meaningless) state where a superuser cannot access Django admin or other staff-gated views until is_staff is manually set.
Changes:
- Add a
pre_savesignal receiver that forcesis_staff=Truewheneveris_superuser=True(skipping raw fixture loads). - Add a one-time data migration that bulk-updates existing
is_superuser=True, is_staff=Falseusers tois_staff=True. - Add unit tests covering the receiver’s behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| common/djangoapps/student/tests/test_receivers.py | Adds tests for the new pre_save receiver that grants staff to superusers. |
| common/djangoapps/student/signals/receivers.py | Adds the pre_save receiver that grants is_staff to superusers on save. |
| common/djangoapps/student/migrations/0050_grant_staff_to_existing_superusers.py | Backfills is_staff=True for existing superusers via a bulk update. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """ | ||
| Keep Django's ``is_staff`` flag in sync with ``is_superuser``. | ||
|
|
| A new ``pre_save`` signal keeps ``is_staff`` in sync with ``is_superuser`` for | ||
| any user saved from now on, but that does not touch superusers that already | ||
| exist in the database. This one-time data migration grants staff access to | ||
| those accounts so the behaviour is consistent for old and new superusers alike. |
| """ | ||
| Tests for the ``grant_staff_access_to_superusers`` pre_save receiver, which | ||
| keeps ``is_staff`` in sync with ``is_superuser``. | ||
| """ |
| def test_existing_staff_superuser_unchanged(self): | ||
| """ A superuser that is already staff stays staff. """ | ||
| user = UserFactory(is_superuser=True, is_staff=True) | ||
| user.save() | ||
| user.refresh_from_db() | ||
| assert user.is_staff is True |
| if kwargs.get('raw'): | ||
| return | ||
|
|
||
| if instance.is_superuser and not instance.is_staff: |
There was a problem hiding this comment.
This logic will run on every save() call, so with a high volume of user updates it can introduce unnecessary overhead. It’s not a major issue, but worth noting as a potential optimization point.
Description
Django's
is_superuserandis_staffare independent flags. A superuser bypasses Django's permission checks, but it isis_staffthat actually gates the Django admin and a number of staff-only Studio/LMS views. As a result, a superuser can end up unable to reach the admin or those views until someone also setsis_staff, which is confusing and offers no real protection, since a superuser can grant itselfis_staffat any time.This change makes every superuser a staff user automatically:
pre_savesignal (common/djangoapps/student/signals/receivers.py),grant_staff_access_to_superuserssetsis_staff = Trueon any user being saved withis_superuser = True. It runs before the DB write, so there is noextra query and no signal recursion. Fixture (
raw) loads are skipped.student/migrations/0050_grant_staff_to_existing_superusers.py), backfills existing superusers (is_superuser=True, is_staff=False → is_staff=True) in a single bulk update, since the signal only affects future saves. Thereverse operation is intentionally a no-op.
Behavior is one-directional by design: it grants staff to superusers but does not remove staff when a user stops being a superuser.
Supporting information
Forum discussion: https://discuss.openedx.org/t/shouldnt-superuser-automatically-inherit-staff-access-in-open-edx/18657
Feanil suggested "a small signal handler or something that would just grant all superusers the
staffrole as well", this implements exactly that, plus a one-time backfill for existing installs.Testing instructions
is_superuser=Trueandis_staff=False, save it, and confirmis_staffis nowTrue.is_staffvalue is left untouched.