Server settings → People: an appointed administrator is a superuser, and a mounted Django admin shows them everyone's records #368
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#368
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the People actions and the admin registrations against the threat model, and checking with a test that mounts the admin and appoints a second administrator.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atcore/server_views.py:227-230(the grant inPersonAdminView.post) andcore/server_views.py:1304(PluginActionView), and the test that pins the grant attests/test_server_settings.py:153-155, with the assertion on line 155; the other locations are unchanged.Postulo promises that administrators see accounts and never anyone's applications or documents. Server settings keeps that promise, but the role is wider than Server settings: appointing an administrator from People sets
is_superuseras well asis_staff, and every user-owned model is registered in Django's admin with nothing that narrows it to one owner. Once an operator setsPOSTULO_ADMIN_URL, every administrator can therefore read and edit every member's job search there, and edits made there bypasschange_statusandrecord_event. Nothing tells the operator who mounts the admin, or the people on the instance. Separately, any administrator can install plugin code, which the same sentence does not allow for either.core/server_views.py:226-229(PersonAdminView.post):person.is_staff = True, thenperson.is_superuser = True;accounts/signals.py:58-60(first_account_is_the_administrator): the first account gets the same;core/mixins.py:81-82(StaffRequiredMixin.test_func) checksis_staffalone, and no check insrc/postuloreadsis_superuser, so nothing in Postulo's own pages needs it;tests/test_server_settings.py:151-153pins it:assert user.is_staff and user.is_superuser.owneras a list filter and none with aget_querysetor permission rule:applications/admin.py:13-53(ApplicationwithEventInline,ApplicationEvent,Reminder,Interview,Suggestion);documents/admin.py:12-46(CV,CoverLetter,UploadedDocument,RenderedDocument,DocumentCopy);jobs/admin.py:18-62(Company,Industry,Contact,JobPosting,Capture);resume/admin.py:15-70(the career record);plugins/admin.py:6-17(Connection,SyncLink);core/admin.py:6-11(Tag).accounts/admin.py:14-29:Useris registered on Django'sUserAdmin, which lets a superuser set any account's password and flags.core/admin_site.py:31-49(ThrottledAdminSite): overrides onlylogin; nohas_permissionand no queryset rule.config/urls.py:35-36: mountsadmin.site.urlswhenPOSTULO_ADMIN_URLis set.core/server_views.py:1303(PluginActionView, aStaffRequiredMixinview): any administrator uploads and installs a wheel, which is code running with the whole database.docs/THREAT-MODEL.md:14: "Administrators see accounts, never anyone's applications or documents.";templates/server/people.html:9, the same sentence, shown to administrators;Accounts-and-invitations.md:140-141.Accounts-and-invitations.md:240).is_staffandis_superuserboth set.applications/application,applications/applicationevent,documents/cv,documents/coverletter,documents/uploadeddocument,jobs/contact,plugins/connectionand the rest, each with every account's rows. The application list filtered by another member's id answers200and shows the posting title.statusfield, so an application's status can be changed with nochange_statusand no event in the timeline. TheUserchange form reaches other accounts' passwords and permission flags.POSTULO_ADMIN_URL. Mounting is opt-in and off by default (#116). No shell access is needed. The plugin point needs no mounted admin.config/settings/base.py:374-382), the wiki'sHardening.md:271-293andConfiguration.md:20describe the admin (Hardening: "a second and less careful way into the same data") and never say that it gives every administrator every person's data or that it voids the promise.is_staff, so nothing else changes". The code does more than that.tests/security/test_admin_exposure.py, cover mounting and throttling, not what the admin shows or to whom.PersonAdminView.post, stop settingis_superuseron appointment (keep clearing it on removal). Leaveis_superusertocreatesuperuserat the console and to the first account (accounts/signals.py:58-60), which is the operator's own. A staff account with no model permissions then sees nothing in a mounted admin unless the operator grants it deliberately, and the admin becomes the operator's alone. Whether a data migration also clearsis_superuseron administrators appointed earlier is the maintainer's choice; either way the release notes must say so, because an operator who relies on co-administrators using the admin has to grant it again at the console.User,Invite,ApiTokenmetadata, the instance's settings) by dropping the registration of everyOwnedModelsubclass and ofApplicationEvent(a plainmodels.Modelthat belongs to an application), or give those registrations a baseModelAdminwhosehas_view_permission,has_add_permission,has_change_permissionandhas_delete_permissionreturnFalse. That also removes the route by which a status is changed outside the event log. If full access for the operator's superuser is intended instead, keep the registrations and say so in the documents below.docs/THREAT-MODEL.md,templates/server/people.html, the wiki's Accounts and invitations) and wherever the admin is described (the settings comment, the wiki's Hardening and Configuration): Server settings shows administrators accounts and never anyone's records, but an administrator is trusted with the instance, since they install plugins; and a superuser in a mounted admin reaches every table, with edits that bypass the event log. Appoint only people you would trust with the database. Step 2 alone would not make the original sentence true while wheel installation is open to every administrator.Related: #268 discusses whether an administrator may ever see another person's records and says "An administrator who could grant themselves access is the whole promise gone." It does not cover the admin or the superuser flag. That the admin's own sign-in form takes a password alone is reported separately.
Done when
tests/test_server_settings.py: appointing a member throughserver:person_adminleavesis_superuserfalse, and removing an administrator still clears it. Today line 153 asserts the opposite.tests/security/test_admin_exposure.py: with the admin mounted, an account appointed throughserver:person_adminis refused at the admin'sapplications/application/changelist and is offered no owned model on the admin index. It lists every account's applications today.tests/security/test_admin_exposure.pywalksapps.get_models()and asserts that neither anOwnedModelsubclass norApplicationEventis viewable or editable throughadmin.site, and that a superuser gets no page listing another person's applications. Both fail today.docs/THREAT-MODEL.md,templates/server/people.html, the settings comment and the wiki's Accounts and invitations, Hardening and Configuration pages state the administrator's reach as in step 3.Landed on
mainasba5313a06(wiki56d1f04).Decisions: the owned-model admin registrations are dropped, not locked (less code, and no route that changes a status outside the event log).
ApiTokenis the one owned model kept, because revoking a leaked token is an operator's job; its admin is view/delete only, with no add or change. No data migration: the first account and acreatesuperuseraccount look the same as an appointed one, so the changelog entry and the wiki's Accounts and invitations give the one-line command that clears the flag from earlier appointees, naming your own account.The new walk in
tests/security/test_admin_exposure.pygoes overapps.get_models(), so a future registration of anOwnedModel(or one added by an installed plugin) fails it.Not touched: any administrator can still install plugin wheels. The threat model, the People page, the settings comment and the wiki now say so rather than leaving the sentence to imply otherwise.
The browser suite has four failures in
test_board_fold.py::test_the_focus_ring_of_a_heading_is_not_cut_by_the_boards_box(ring cut by 0.47px); they fail the same on the commit before this one and are unrelated.