Skip to content

Admin user/group structure: Editors + Contributors (#1125)#1384

Merged
jonfroehlich merged 1 commit into
masterfrom
1125-admin-user-groups
Jun 23, 2026
Merged

Admin user/group structure: Editors + Contributors (#1125)#1384
jonfroehlich merged 1 commit into
masterfrom
1125-admin-user-groups

Conversation

@jonfroehlich

Copy link
Copy Markdown
Member

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:

Tier Who Access
Superuser maintainer + a break-glass backup is_superuser (bypasses all checks)
Editors PhD students / long-term staff Editors group — full add/change/delete/view on the 14 public content models
Contributors ugrads / interns Contributors group — Person add/change/view + add/view on publication/talk/poster/projectrole; no deletes

Deliberately admin-only (neither group): Grant (funding) and Award (external recognitions), plus all account-administration models — granting auth.user/auth.group to 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 /admin and persist across deploys.
  • Wired into 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 with python manage.py setup_admin_groups --dry-run.

Tests (16, all passing)

  • Spec correctness: exact permission codenames for both groups; no auth.*/LogEntry perms; Grant/Award withheld; Contributors have no delete perms; idempotent + self-healing. (This layer caught a since-removed ProjectHeader model that had been seeded from a stale DB dump.)
  • Anti-lockout invariants: a superuser keeps full access regardless of group config; the command never creates/deletes users, never changes is_staff/is_active/is_superuser or group membership; 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.

Run: python manage.py test website.tests.test_setup_admin_groups --settings=makeabilitylab.settings_test

Docs

  • New docs/ADMIN_USERS_AND_GROUPS.md — full reference + onboarding/offboarding runbook.
  • Pointer added in 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

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>
@jonfroehlich jonfroehlich added Admin Backend New Feature Security Security: XSS, auth, dependency CVEs, hardening labels Jun 23, 2026
@jonfroehlich jonfroehlich merged commit 4dd708e into master Jun 23, 2026
3 checks passed
@jonfroehlich jonfroehlich deleted the 1125-admin-user-groups branch June 23, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Admin Backend New Feature Security Security: XSS, auth, dependency CVEs, hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider using both Groups and Users for authentication

1 participant