Admin user/group structure: Editors + Contributors (#1125)#1384
Merged
Conversation
Stop using shared, role-named user accounts (gradmin/ugradmin/collabmin) as de-facto groups. Introduce two declarative Django groups so individuals get personal accounts with per-person attribution and clean offboarding: - Editors (PhD students + long-term staff): full add/change/delete/view on the public content models. Excludes Grant and Award (admin-only) and all account-administration models (no user/group mgmt outside the superuser, which would be a privilege-escalation path). - Contributors (ugrads/interns): Person add/change/view + add/view on publication/talk/poster/projectrole (submit their own work and review it, via the changelist). No deletes anywhere. setup_admin_groups is idempotent (.set() makes each group match the spec exactly), so it is the source of truth for these two groups' permissions; group membership and all other auth objects are left to /admin. Wired into docker-entrypoint.sh (step 4.10) because the test/prod servers have no shell access -- the entrypoint one-shot is the only push-deploy-compatible path. Tests (16) cover two layers: (1) exact permission codenames + the security boundaries (no auth.* perms; no grant/award; no contributor deletes) -- this caught a since-removed ProjectHeader model seeded from a stale DB dump; and (2) anti-lockout invariants -- the command never creates/deletes users, never changes is_staff/is_active/is_superuser or group membership, a superuser keeps full access regardless of group config, effective has_perm matches the spec through the stack, an unknown model in the spec is skipped (not raised), and the entrypoint has no `set -e` so a command failure can't block the site boot. Docs in docs/ADMIN_USERS_AND_GROUPS.md plus a pointer in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1125.
Problem
We've been using shared, role-named user accounts (
gradmin,ugradmin,collabmin) as if they were groups — everyone in a role shares one login. That costs us per-person attribution in the admin history, clean offboarding (revoking one person means rotating a shared password), and least-privilege scoping.Change
Map it the way Django intends — Users = people, Groups = roles. Two declarative groups, plus superuser:
is_superuser(bypasses all checks)Editorsgroup — full add/change/delete/view on the 14 public content modelsContributorsgroup — Person add/change/view + add/view on publication/talk/poster/projectrole; no deletesDeliberately admin-only (neither group):
Grant(funding) andAward(external recognitions), plus all account-administration models — grantingauth.user/auth.groupto a group is a privilege-escalation path, so account creation stays superuser-only.How it's enforced
website/management/commands/setup_admin_groups.py— idempotent (.set()), so it is the source of truth for these two groups' permissions. Group membership and all other auth objects remain managed in/adminand persist across deploys.docker-entrypoint.sh(step 4.10), after migrations. This is the only push-deploy-compatible path since test/prod have no shell access. Preview locally withpython manage.py setup_admin_groups --dry-run.Tests (16, all passing)
auth.*/LogEntry perms; Grant/Award withheld; Contributors have no delete perms; idempotent + self-healing. (This layer caught a since-removedProjectHeadermodel that had been seeded from a stale DB dump.)is_staff/is_active/is_superuseror group membership; effectivehas_perm()matches the spec through the stack; an unknown model in the spec is skipped (not raised); and the entrypoint has noset -e, so a command failure can't block the site boot.Run:
python manage.py test website.tests.test_setup_admin_groups --settings=makeabilitylab.settings_testDocs
docs/ADMIN_USERS_AND_GROUPS.md— full reference + onboarding/offboarding runbook.CLAUDE.md.Not in this PR (manual, post-merge, per the runbook)
Creating the personal accounts, the break-glass superuser, and deactivating (never deleting) the legacy shared accounts — all done in
/admin. The groups themselves appear automatically on the next deploy.Screenshots
N/A — admin permission configuration, no visual UI change.
🤖 Generated with Claude Code