Companies, People and Listings owned by a user or a group, and shared read/write or read-only #896
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#896
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?
Companies, People (contacts) and Listings can be owned by a user or by a group, and shared with other users or groups, either read/write or read-only. Everything a person writes about them stays their own: company notes, applications and the rest remain user-owned and are never shared by this.
This is the concrete form of #268 Part 4 (household or team instance), with Part 3's explicit grant used as the sharing primitive. It amends the README promise, so #268's wording question has to be settled first.
What exists today
OwnedModel(src/postulo/core/models.py:56) gives every record a singleowneruser, andfor_user()is the only authorisation model.OwnedObjectMixinmakes a foreign record a 404.Company's docstring states the opposite of this feature on purpose: "Companies are owner-scoped rather than shared. Two people … each keep their own notes on the same employer." (src/postulo/jobs/models.py:304)auth.Group, managed under Server settings → Groups (#886,core/views_access.py), and used today only by plugin access rules.Company.notesandContact.notes.What it needs
Ownership
docs/THREAT-MODEL.mdmust say so. Confirmed (2026-10-12): superusers have no access to personal notes or applications, nor to anything else that stays user-owned. Server settings → People keeps its promise for those.Sharing
What stays per user
OwnedModelas they are. An application of mine on a shared listing is still mine alone, and nobody the listing is shared with learns that it exists.JobPosting.stateand what goes with it (discard_reason,noted_at,decided_at) move off the shared row into a per-user record, with a migration that gives each current value to the listing's owner. One person discarding a shared listing does not discard it for anyone else. Facts about the posting itself stay on the record:closes_at,closing_announced_forandclosed_atsay whether the opening is still available, which is the same for everyone.ListingEvent, #270). The use is two people applying in parallel for the same position, each from their own side: each keeps their own messages, captures and notes about it, and their own application.Records hanging off a shared one
Everything #268 lists as broken by any sharing
for_user()becomesvisible_to(user, *, write=False)for these three models and their children, and every queryset, form choice, API endpoint, search group, map, calendar feed, export and plugin hook goes through it. One that does not is a leak.docs/PLUGINS.md.tests/security/isolation sweep (#232) extended to grants: unshared, read-only, read/write and group membership, through every surface.Out of scope
Acceptance
docs/THREAT-MODEL.mdand the wiki state the amended promise.Leaving a group: options (undecided)
Group-owned records stay with the group. The person who leaves loses access straight away. This holds whether they leave themselves or an administrator removes them on Server settings → Groups. The hard part is that their own data still points at records they can no longer see:
A 404 on their own application's listing is not acceptable. These are the ways to avoid it:
A. A private copy when they leave (recommended). Every group-owned record their own data points at is copied to them as a user-owned record, marked Copied from on . Their applications and notes are repointed to the copy. Nothing else of the group's goes with them. They keep a working record of their own search, and the group loses nothing. If they rejoin, the copies and the originals appear as duplicates on the conflicts page, where they can be merged.
B. Read-only access to what they used. They keep read-only access to exactly the records their own data points at, for as long as that data exists. Nothing is duplicated. But the group's later edits keep reaching someone who has left, and "who can see this" stops being answered by the grant list alone.
C. A frozen view. Their application shows the listing and company as they were when they left, stored on the application itself, with no live record behind it. Simplest to reason about. It is a third way of holding a company, though, and it cannot be edited.
D. Settle it before leaving. Leaving is refused until they have settled each such record on the conflicts page: take a copy, let it go, or ask the group to transfer it. This is complete, but an administrator removing someone cannot wait for them, so D still needs A or B as the default.
Whichever option is chosen, two further rules:
Suggested: A as the default, with D's page offered beforehand when the person leaves by themselves, so they can let go of what they do not want to keep instead of collecting copies.
Decided 2026-10-12: leaving a group follows option A (a private copy when they leave); the body now says so. Superusers have no access to personal notes or applications; also recorded in the body.
Decided 2026-10-12: listing status is per user. When a shared record points at a private one, a prompt asks whether to share that one too, and with which scope, unless the sharer cannot share it. Both are recorded in the body. What the recipient sees when the private record stays hidden (a placeholder) is assumed and marked to confirm.
Decided 2026-10-12: a share that would point at a record the recipient cannot see is refused altogether, with no placeholder. Sharing a listing shares only the listing, not its history, so two people can apply in parallel for the same position, each with their own history and application. Both are recorded in the body.
Decided 2026-10-13, before implementation starts:
896-sharingand landed as one commit.