Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/recruiting/ownership.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/recruiting/ownership.go |
| Original SHA-256 | 610fe6a6c7941a24f9536144cd0f598f484cf41c09bb8a05fd1491b28903080d |
| Git blob SHA | 07c063052d3461a2303de4e92967e39d4b935439 |
| Reading-copy SHA-256 | 610fe6a6c7941a24f9536144cd0f598f484cf41c09bb8a05fd1491b28903080d |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/recruiting/ownership.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/recruiting/ownership.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓228 lines · 7.8 KB
package recruiting
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrNotOwner is returned when a recruiter tries to edit an account someone else holds. It carries the
// holder so the caller can be told WHO to talk to, which is the entire point of showing ownership: the
// answer to a collision is a conversation, not a retry.
type ErrNotOwner struct {
OwnerUserID string
OwnerName string
// Entity is the noun shown to the user. Clients and requisitions share this lock, and telling a
// recruiter their job order is "assigned to" someone as a CLIENT would send them looking in the wrong
// place. Empty falls back to a neutral word rather than guessing.
Entity string
}
func (e *ErrNotOwner) noun() string {
if e.Entity == "" {
return "record"
}
return e.Entity
}
func (e *ErrNotOwner) Error() string {
if e.OwnerName != "" {
return "recruiting: this " + e.noun() + " is assigned to " + e.OwnerName
}
return "recruiting: this " + e.noun() + " is assigned to another recruiter"
}
// owner is the ownership state of one client, read inside the caller's transaction.
type owner struct {
userID *uuid.UUID
name string
entity string
}
// loadOwner reads current ownership FOR UPDATE.
//
// The row lock matters. Two recruiters claiming the same unassigned client at the same moment would
// both read NULL and both write themselves in, and the last writer would silently take it while the
// first believed they had it. FOR UPDATE serializes the pair so the second sees the first's claim and
// is refused. This is the one place a pessimistic lock is correct: it is held for microseconds inside a
// single transaction, not across a user's editing session.
func loadOwner(ctx context.Context, tx pgx.Tx, clientID uuid.UUID) (owner, error) {
o := owner{entity: "client"}
var name *string
err := tx.QueryRow(ctx, `
SELECT c.owner_user_id, u.full_name
FROM clients c
LEFT JOIN users u ON u.organization_id = c.organization_id AND u.id = c.owner_user_id
WHERE c.id = $1
FOR UPDATE OF c`, clientID).Scan(&o.userID, &name)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return owner{}, ErrNotFound
}
return owner{}, translatePG(err)
}
if name != nil {
o.name = *name
}
return o, nil
}
// canOverride reports whether the caller may act on an account they do not hold.
//
// Gated on desk.reassign rather than on a role. That capability is held by manager and above, which is
// the point of the manager tier: reassigning a desk is the most common thing a team lead does, and
// requiring admin for it meant handing them PII deletion and account management as well.
//
// A recruiter still cannot take an account from a peer. That is a management decision, and making it one
// is the difference between a shared system and a land grab.
func canOverride(ctx context.Context) bool {
p, ok := tenant.UserFromContext(ctx)
if !ok {
return false
}
return security.Has(security.Role(p.Role), security.CapDeskReassign)
}
// assertCanEdit is the gate every client write passes through.
//
// Unclaimed accounts are editable by anyone with write capability, and the edit claims them. That is
// deliberate: requiring a separate claim step before a first edit adds a click to the most common path,
// and an unowned client sitting in the pool benefits nobody.
func assertCanEdit(ctx context.Context, tx pgx.Tx, clientID uuid.UUID) (claim bool, err error) {
o, err := loadOwner(ctx, tx, clientID)
if err != nil {
return false, err
}
actor := actorPtr(ctx)
if actor == nil {
return false, tenant.ErrNoTenant
}
return ownershipDecision(o, actor, canOverride(ctx))
}
// ownershipDecision is the whole access rule, pure and free of a database, so every combination of
// holder, actor and role is provable in a unit test rather than only reachable by logging in as three
// different people.
//
// Returns claim=true when the caller takes an unowned account by editing it.
func ownershipDecision(o owner, actor *uuid.UUID, isAdmin bool) (bool, error) {
switch {
case o.userID == nil:
// Unclaimed. This edit takes it.
return true, nil
case *o.userID == *actor:
return false, nil
case isAdmin:
// An admin edits without silently seizing the account. Taking it is a separate, explicit action
// so the audit trail distinguishes a manager fixing a typo from the account changing hands.
return false, nil
default:
return false, &ErrNotOwner{OwnerUserID: o.userID.String(), OwnerName: o.name, Entity: o.entity}
}
}
// Claim assigns an unclaimed client to the calling recruiter.
func (s *Service) Claim(ctx context.Context, clientID uuid.UUID) (Client, error) {
if s.db == nil {
return Client{}, ErrNoDatabase
}
actor := actorPtr(ctx)
if actor == nil {
return Client{}, tenant.ErrNoTenant
}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
o, err := loadOwner(ctx, tx, clientID)
if err != nil {
return err
}
if o.userID != nil {
if *o.userID == *actor {
return nil // already held by the caller, claiming again is a no op rather than an error
}
if !canOverride(ctx) {
return &ErrNotOwner{OwnerUserID: o.userID.String(), OwnerName: o.name, Entity: o.entity}
}
}
return s.setOwner(ctx, tx, clientID, actor, "client.claimed")
})
if err != nil {
return Client{}, err
}
return s.clientRow(ctx, clientID)
}
// Release returns a client to the unassigned pool.
func (s *Service) Release(ctx context.Context, clientID uuid.UUID) (Client, error) {
if s.db == nil {
return Client{}, ErrNoDatabase
}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
if _, err := assertCanEdit(ctx, tx, clientID); err != nil {
return err
}
return s.setOwner(ctx, tx, clientID, nil, "client.released")
})
if err != nil {
return Client{}, err
}
return s.clientRow(ctx, clientID)
}
// Reassign moves a client to another recruiter. Admin or owner only: moving an account between people
// is a management decision.
func (s *Service) Reassign(ctx context.Context, clientID, toUser uuid.UUID) (Client, error) {
if s.db == nil {
return Client{}, ErrNoDatabase
}
if !canOverride(ctx) {
return Client{}, &ValidationError{Field: "owner_user_id", Message: "only an admin or owner can reassign an account"}
}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
if _, err := loadOwner(ctx, tx, clientID); err != nil {
return err
}
// The target must be a real, active member of this tenant. RLS scopes the lookup, so a user id
// from another organization simply does not exist here.
var active bool
if err := tx.QueryRow(ctx,
`SELECT status = 'active' FROM users WHERE id = $1`, toUser).Scan(&active); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &ValidationError{Field: "owner_user_id", Message: "is not a user in this organization"}
}
return translatePG(err)
}
if !active {
return &ValidationError{Field: "owner_user_id", Message: "is not an active user"}
}
return s.setOwner(ctx, tx, clientID, &toUser, "client.reassigned")
})
if err != nil {
return Client{}, err
}
return s.clientRow(ctx, clientID)
}
// setOwner writes ownership and its timestamp together and records the change. owner_since is set from
// the database clock rather than the application so two nodes cannot disagree about tenure.
func (s *Service) setOwner(ctx context.Context, tx pgx.Tx, clientID uuid.UUID, to *uuid.UUID, action string) error {
var since any
if to != nil {
since = "now"
}
_, err := tx.Exec(ctx, `
UPDATE clients
SET owner_user_id = $2,
owner_since = CASE WHEN $3::text IS NULL THEN NULL ELSE now() END,
updated_at = now()
WHERE id = $1`, clientID, to, since)
if err != nil {
return translatePG(err)
}
return s.auditOwnership(ctx, tx, "client", clientID, to, action)
}