Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/orgsettings/scope.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/orgsettings/scope.go |
| Original SHA-256 | 4f53a42194a581b14b7bcda44f6678bbbf8caeea0543fcb6a6ba08fbdbcd24b4 |
| Git blob SHA | 525dd6657fb4336f0d261664accb264327db4209 |
| Reading-copy SHA-256 | 4f53a42194a581b14b7bcda44f6678bbbf8caeea0543fcb6a6ba08fbdbcd24b4 |
Recovered source transcript
Download complete source text ↓158 lines · 5.8 KB
package orgsettings
// RESOLVING A RECORD SCOPE INTO A PREDICATE.
//
// security.ResolveRecordScope decides WHICH scope a caller has, purely, with no database. This turns that
// answer into the set of owners the caller may see, and into the SQL fragment a list query appends.
//
// IT IS ONE FUNCTION ON PURPOSE. The alternative, letting each module work out its own owner predicate, is
// how a product ends up with six list endpoints that agree and a seventh that quietly does not, and the
// seventh is the one that shows a recruiter the whole floor.
//
// THE FRAGMENT IS BUILT HERE, NEVER BY THE CALLER. A caller passes the column name and receives finished SQL
// plus its argument. The column name is checked against a bare identifier rule before it is interpolated,
// because a column name is the one thing in this path that is not a bound parameter.
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Filter is a resolved record scope: who the caller may see, and whether records nobody owns count.
type Filter struct {
Scope security.RecordScope
// OwnerIDs is the set of owning users whose records are visible. It is nil when the scope is
// organization wide, which is the case that needs no predicate at all.
OwnerIDs []uuid.UUID
// IncludeUnowned reports whether a record with a NULL owner is visible. It defaults to true, because an
// unowned candidate that nobody can see is a candidate nobody will ever work.
IncludeUnowned bool
}
// Unrestricted reports that no owner predicate is needed.
func (f Filter) Unrestricted() bool { return f.Scope.Unrestricted() }
// ResolveScopeTx works out the caller's filter inside an existing tenant transaction.
func (s *Store) ResolveScopeTx(ctx context.Context, tx pgx.Tx) (Filter, error) {
p, ok := tenant.UserFromContext(ctx)
if !ok {
// NO PRINCIPAL MEANS NOTHING IS VISIBLE, matching the tenancy rule that no context yields zero rows.
// Returning an unrestricted filter here would be the single worst bug this file could contain.
return Filter{Scope: security.ScopeOwn, OwnerIDs: []uuid.UUID{}, IncludeUnowned: false}, nil
}
actorID, err := uuid.Parse(p.UserID)
if err != nil {
return Filter{Scope: security.ScopeOwn, OwnerIDs: []uuid.UUID{}, IncludeUnowned: false}, nil
}
st, err := s.LoadTx(ctx, tx)
if err != nil {
return Filter{}, err
}
scope := security.ResolveRecordScope(security.Role(p.Role), st.StaffRecordScope, st.ManagerRecordScope)
f := Filter{Scope: scope, IncludeUnowned: st.UnownedRecordsVisible}
switch scope {
case security.ScopeOrganization:
return f, nil
case security.ScopeOwn:
f.OwnerIDs = []uuid.UUID{actorID}
return f, nil
case security.ScopeTeam:
rows, err := tx.Query(ctx, `SELECT user_id FROM mm_team_peer_ids($1)`, actorID)
if err != nil {
return Filter{}, fmt.Errorf("orgsettings: resolve team peers: %w", err)
}
defer rows.Close()
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return Filter{}, err
}
f.OwnerIDs = append(f.OwnerIDs, id)
}
if rows.Err() != nil {
return Filter{}, rows.Err()
}
// The resolver already includes the caller, but a belt on the braces costs nothing and protects
// against somebody rewriting that function later: a person on no team must always see their own work.
if len(f.OwnerIDs) == 0 {
f.OwnerIDs = []uuid.UUID{actorID}
}
return f, nil
default:
// Unreachable, because ResolveRecordScope only returns the three above. Narrowest answer anyway.
f.Scope, f.OwnerIDs, f.IncludeUnowned = security.ScopeOwn, []uuid.UUID{actorID}, false
return f, nil
}
}
// ResolveScope opens its own transaction for callers not already inside one.
func (s *Store) ResolveScope(ctx context.Context) (Filter, error) {
var out Filter
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = s.ResolveScopeTx(ctx, tx)
return e
})
return out, err
}
var errBadColumn = errors.New("orgsettings: an owner column must be a bare lowercase identifier")
// SQL returns a predicate to AND into a list query, plus the argument it binds.
//
// nextArg is the position the caller's next placeholder would take, so this composes with a query that
// already has parameters. An unrestricted filter returns an empty string and no argument, so the caller
// appends nothing.
//
// The column is validated rather than trusted. It is the only part of this that is not a bound parameter.
func (f Filter) SQL(column string, nextArg int) (string, []any, error) {
if f.Unrestricted() {
return "", nil, nil
}
if column == "" {
return "", nil, errBadColumn
}
for _, r := range column {
if (r < 'a' || r > 'z') && r != '_' && (r < '0' || r > '9') {
return "", nil, errBadColumn
}
}
// An empty owner set with no unowned records visible means NOTHING is visible. Expressed as a literal
// false rather than an empty ANY, because "= ANY('{}')" is subtly not false for a NULL column and this
// must be unambiguous.
if len(f.OwnerIDs) == 0 && !f.IncludeUnowned {
return "false", nil, nil
}
if len(f.OwnerIDs) == 0 {
return fmt.Sprintf("%s IS NULL", column), nil, nil
}
if f.IncludeUnowned {
return fmt.Sprintf("(%s = ANY($%d) OR %s IS NULL)", column, nextArg, column), []any{f.OwnerIDs}, nil
}
return fmt.Sprintf("%s = ANY($%d)", column, nextArg), []any{f.OwnerIDs}, nil
}
// Allows reports whether one record's owner is visible under this filter. Used on single record reads, where
// building SQL would be heavier than answering directly.
func (f Filter) Allows(owner *uuid.UUID) bool {
if f.Unrestricted() {
return true
}
if owner == nil {
return f.IncludeUnowned
}
for _, id := range f.OwnerIDs {
if id == *owner {
return true
}
}
return false
}