Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/orgsettings/settings.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/orgsettings/settings.go |
| Original SHA-256 | 5a8e6b808f98c90b4e2f4bd5000384aaae5be257503e0c425445eec1b41da582 |
| Git blob SHA | 08ae1cb74aa9cdffe591028294cf30af4b2329ad |
| Reading-copy SHA-256 | 62b3d250af3aebd4b4e5fdfa193111a60a01fa90e4c948d74182d008343e18e8 |
Recovered source transcript
Download complete source text ↓523 lines · 23.4 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
// Package orgsettings holds the per organization configuration and the per organization sealed secrets.
//
// IT IS A PLATFORM PACKAGE, not a module, because three unrelated callers need it and none of them should
// depend on each other. The org module serves the settings screen. The auth module needs the mail
// configuration to deliver an invite. The recruiting module needs the visibility rules to decide whether one
// recruiter may see another's work. If this lived inside any one of those, the other two would import a
// sibling module, which is exactly the dependency the package layout rule exists to prevent.
//
// SECRETS NEVER LEAVE THIS PACKAGE AS PLAINTEXT BY ACCIDENT. Settings and secrets are deliberately separate
// types with separate loaders: Load returns configuration a handler may serialise straight to JSON and it
// CANNOT carry a credential, because no field of it holds one. Reaching a password takes a second, explicit
// call to OpenSecret, which no HTTP response path calls. The type system carries the rule so a future handler
// cannot leak a password by adding a field to a response struct.
package orgsettings
import (
"context"
"errors"
"fmt"
"strings"
"github.com/shopspring/decimal"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrNoSecret means the named secret is not stored for this organization. It is a normal outcome, not a
// fault: an organization that has never configured mail simply has no smtp_password.
var ErrNoSecret = errors.New("orgsettings: no such secret for this organization")
// ErrSealUnavailable means the server has no organization secret key configured, so a secret cannot be
// stored. It FAILS CLOSED and says so plainly rather than storing anything unprotected. A node without the
// key can still read every non secret setting; only writing a credential is refused.
var ErrSealUnavailable = errors.New("orgsettings: secret storage is not configured on this node, MM_ORG_SECRET_KEK is unset")
// Settings is one organization's configuration. Every field here is safe to return to a caller who holds
// org.settings.write. NONE of them is a credential.
type Settings struct {
// ---- outbound email ----
EmailDeliveryEnabled bool `json:"email_delivery_enabled"`
EmailFromAddress string `json:"email_from_address"`
EmailFromName string `json:"email_from_name"`
EmailReplyTo string `json:"email_reply_to"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPSecurity string `json:"smtp_security"`
// SMTPPasswordSet reports only WHETHER a password is [credential removed], never anything about it. This is what lets
// the settings screen render "password saved, leave blank to keep it" without the value ever being sent
// to a browser.
SMTPPasswordSet bool `json:"smtp_password_set"`
SMTPVerifiedAt *time.Time `json:"smtp_verified_at"`
SMTPLastAttemptAt *time.Time `json:"smtp_last_attempt_at"`
SMTPLastError string `json:"smtp_last_error"`
// ---- visibility ----
WorkVisibility security.WorkVisibility `json:"work_visibility"`
ShowActivityPresence bool `json:"show_activity_presence"`
ClaimRequired bool `json:"claim_required"`
ClaimExpiryHours int `json:"claim_expiry_hours"`
// ---- field sensitivity ----
RateVisibility security.Sensitivity `json:"rate_visibility"`
MarginVisibility security.Sensitivity `json:"margin_visibility"`
ContactVisibility security.Sensitivity `json:"contact_visibility"`
// ---- record scope, the third access axis ----
StaffRecordScope security.RecordScope `json:"staff_record_scope"`
ManagerRecordScope security.RecordScope `json:"manager_record_scope"`
UnownedRecordsVisible bool `json:"unowned_records_visible"`
// ---- operating defaults ----
InviteExpiryHours int `json:"invite_expiry_hours"`
DefaultInviteRole string `json:"default_invite_role"`
// DefaultBurdenPct is the organization's standard wrap rate, as a FRACTION, so 0.35 is a 35 percent wrap.
// It is the cost of employing somebody on top of their pay: payroll taxes, workers compensation,
// unemployment insurance, health cover, paid leave, retirement, general and administrative, overhead.
//
// NIL IS THE SHIPPED STATE AND IS NOT A ZERO. Nil means the firm has not told us its wrap, and per clause A
// of migration 0086 no margin may then be reported anywhere: the product shows spreads and says why. A wrap
// of zero is a different and legitimate answer, and it produces a margin equal to the spread. Collapsing
// the two would invent a cost structure for every organization that has not entered one.
//
// It is a STRING on the wire, in the decimal discipline the rest of the money follows, because a wrap that
// round tripped through a JSON number is a float and a float does not reconcile with an invoice.
DefaultBurdenPct *decimal.Decimal `json:"default_burden_pct,omitempty"`
// AEDTNoticeRequired refuses to score a candidate who has no automated decision notice on file.
//
// OFF BY DEFAULT, and the migration records why: switching it on for an existing customer would refuse
// their whole bench the moment they upgraded, because nobody has notices yet. What ships on for everybody
// is the ability to record a notice and to see whether one exists; the enforcement is the owner's call
// once their notices are in place.
AEDTNoticeRequired bool `json:"aedt_notice_required"`
UpdatedAt time.Time `json:"updated_at"`
}
// Defaults returns the same values the migration writes. It exists so a read against an organization whose
// row is somehow missing returns a SAFE configuration rather than a zero valued one. The distinction matters:
// the zero value of WorkVisibility is the empty string, which CanSeeOthersWork denies, but the zero value of
// EmailDeliveryEnabled is false and the zero value of ClaimExpiryHours is zero, which would expire every
// claim the instant it was made. Defaults are the correct answer, zeroes are not.
func Defaults() Settings {
return Settings{
SMTPSecurity: "starttls",
WorkVisibility: security.VisibilityOrganization,
ShowActivityPresence: true,
ClaimRequired: false,
ClaimExpiryHours: 72,
StaffRecordScope: security.ScopeOrganization,
ManagerRecordScope: security.ScopeOrganization,
UnownedRecordsVisible: true,
RateVisibility: security.SensitivityManagers,
MarginVisibility: security.SensitivityOwner,
ContactVisibility: security.SensitivityTeam,
InviteExpiryHours: 72,
DefaultInviteRole: string(security.RoleRecruiter),
}
}
// Store reads and writes settings and secrets inside the caller's tenant transaction, so RLS decides which
// organization is reachable and this package never names one. There is no organization_id parameter anywhere
// in this file, deliberately: a function that took one could be called with the wrong one.
type Store struct {
db *tenant.DB
kek []byte
// prev decrypts values sealed under the previous key so a rotation does not need a flag day. It is used
// only on the read path, never to seal anything new.
prev []byte
kekVersion int
}
func NewStore(db *tenant.DB, kek, prev []byte, kekVersion int) *Store {
return &Store{db: db, kek: kek, prev: prev, kekVersion: kekVersion}
}
// SealAvailable reports whether this node can store a secret. Callers use it to give an honest error before
// asking a user to type a password they cannot save.
func (s *Store) SealAvailable() bool { return len(s.kek) == security.AESKeySize }
const settingsColumns = `
email_delivery_enabled, coalesce(email_from_address,''), coalesce(email_from_name,''),
coalesce(email_reply_to,''), coalesce(smtp_host,''), coalesce(smtp_port,0),
coalesce(smtp_username,''), smtp_security, smtp_verified_at, smtp_last_attempt_at,
coalesce(smtp_last_error,''),
work_visibility, show_activity_presence, claim_required, claim_expiry_hours,
rate_visibility, margin_visibility, contact_visibility,
staff_record_scope, manager_record_scope, unowned_records_visible,
invite_expiry_hours, default_invite_role, default_burden_pct::text, aedt_notice_required, updated_at`
func scanSettings(row pgx.Row) (Settings, error) {
var st Settings
var burden *string
err := row.Scan(
&st.EmailDeliveryEnabled, &st.EmailFromAddress, &st.EmailFromName,
&st.EmailReplyTo, &st.SMTPHost, &st.SMTPPort,
&st.SMTPUsername, &st.SMTPSecurity, &st.SMTPVerifiedAt, &st.SMTPLastAttemptAt,
&st.SMTPLastError,
&st.WorkVisibility, &st.ShowActivityPresence, &st.ClaimRequired, &st.ClaimExpiryHours,
&st.RateVisibility, &st.MarginVisibility, &st.ContactVisibility,
&st.StaffRecordScope, &st.ManagerRecordScope, &st.UnownedRecordsVisible,
&st.InviteExpiryHours, &st.DefaultInviteRole, &burden, &st.AEDTNoticeRequired, &st.UpdatedAt,
)
if err == nil && burden != nil {
// Parsed rather than trusted. A value that reached the column can only have come through the CHECK, so
// a parse failure here would mean the column type changed under us, and a silently dropped wrap would
// turn every margin in the product into a spread again without saying so.
d, perr := decimal.NewFromString(*burden)
if perr != nil {
return Settings{}, fmt.Errorf("orgsettings: default_burden_pct %q is not a number: %w", *burden, perr)
}
st.DefaultBurdenPct = &d
}
return st, err
}
// LoadTx reads the settings for the tenant already established on tx. A missing row returns Defaults with no
// error, because an organization created before this migration, or one whose backfill has not run, should
// behave like a correctly configured new organization rather than fail.
func (s *Store) LoadTx(ctx context.Context, tx pgx.Tx) (Settings, error) {
st, err := scanSettings(tx.QueryRow(ctx, `SELECT `+settingsColumns+` FROM org_settings`))
if errors.Is(err, pgx.ErrNoRows) {
return Defaults(), nil
}
if err != nil {
return Settings{}, fmt.Errorf("orgsettings: load: %w", err)
}
var n int
if err := tx.QueryRow(ctx, `SELECT count(*)::int FROM org_secrets WHERE name = 'smtp_password'`).Scan(&n); err != nil {
return Settings{}, fmt.Errorf("orgsettings: probe smtp password: %w", err)
}
st.SMTPPasswordSet = n > 0
return st, nil
}
// Load opens its own tenant transaction for callers that are not already inside one.
func (s *Store) Load(ctx context.Context) (Settings, error) {
var out Settings
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = s.LoadTx(ctx, tx)
return e
})
return out, err
}
// Update is a sparse patch: only the fields a caller actually set are written. A pointer per field rather
// than a whole Settings value, because a settings screen that submits the entire object would silently
// overwrite a colleague's concurrent change to a field the submitter never touched.
type Update struct {
EmailDeliveryEnabled *bool
EmailFromAddress *string
EmailFromName *string
EmailReplyTo *string
SMTPHost *string
SMTPPort *int
SMTPUsername *string
SMTPSecurity *string
WorkVisibility *string
ShowActivityPresence *bool
ClaimRequired *bool
ClaimExpiryHours *int
RateVisibility *string
MarginVisibility *string
ContactVisibility *string
StaffRecordScope *string
ManagerRecordScope *string
UnownedRecordsVisible *bool
InviteExpiryHours *int
DefaultInviteRole *string
// DefaultBurdenPct is a POINTER TO A POINTER on purpose, which is the only shape that can express the three
// things an owner can mean. Absent, the outer nil, means leave the wrap alone. Present and pointing at nil
// means CLEAR it, which returns the organization to reporting spreads and no margins. Present and pointing
// at a number sets it. A plain pointer could not tell "do not touch" apart from "clear", and clearing a
// wrap by accident silently changes every margin in the product to a different number.
DefaultBurdenPct **decimal.Decimal
AEDTNoticeRequired *bool
}
// maxBurden is five, meaning a wrap of five hundred percent, matching the CHECK migration 0098 puts on every
// burden column. It is high enough to admit a genuinely heavy government wrap once general and administrative
// and fee are layered on, and low enough to catch the one error that would otherwise pass silently: typing 35
// to mean 35 percent.
var maxBurden = decimal.NewFromInt(5)
// Validate checks everything the database checks, plus the things it cannot: that a time zone actually
// exists, that a role is a real role, that an address looks like an address. Returning the offending field
// name lets the settings screen mark the input rather than show a banner.
func (u Update) Validate() (field string, err error) {
if u.DefaultBurdenPct != nil && *u.DefaultBurdenPct != nil {
b := **u.DefaultBurdenPct
if b.IsNegative() {
return "default_burden_pct", errors.New("a wrap rate cannot be negative")
}
if b.GreaterThan(maxBurden) {
// The message names the likely mistake rather than restating the bound, because somebody who typed
// 35 does not need to be told the limit is 5, they need to be told the field is a fraction.
return "default_burden_pct", errors.New(
"a wrap rate is a fraction, so a 35 percent wrap is 0.35 rather than 35")
}
}
if u.SMTPPort != nil && (*u.SMTPPort < 1 || *u.SMTPPort > 65535) {
return "smtp_port", errors.New("a port must be between 1 and 65535")
}
if u.SMTPSecurity != nil && *u.SMTPSecurity != "starttls" && *u.SMTPSecurity != "implicit_tls" {
return "smtp_security", errors.New("transport security must be starttls or implicit_tls, plaintext mail is not offered")
}
for f, v := range map[string]*string{
"email_from_address": u.EmailFromAddress,
"email_reply_to": u.EmailReplyTo,
} {
if v != nil && *v != "" && !plausibleAddress(*v) {
return f, errors.New("that does not look like an email address")
}
}
if u.WorkVisibility != nil && !security.ValidWorkVisibility(security.WorkVisibility(*u.WorkVisibility)) {
return "work_visibility", errors.New("visibility must be private, team or organization")
}
for f, v := range map[string]*string{
"rate_visibility": u.RateVisibility,
"margin_visibility": u.MarginVisibility,
"contact_visibility": u.ContactVisibility,
} {
if v != nil && !security.ValidSensitivity(security.Sensitivity(*v)) {
return f, errors.New("sensitivity must be owner, managers, team or organization")
}
}
if u.StaffRecordScope != nil && !security.ValidRecordScope(security.RecordScope(*u.StaffRecordScope)) {
return "staff_record_scope", errors.New("a record scope must be own, team or organization")
}
// own is deliberately not offered at the manager tier: a manager who cannot see their team's records
// cannot run a desk, and the setting exists to choose between the team and the floor.
if u.ManagerRecordScope != nil && !security.ValidManagerScope(security.RecordScope(*u.ManagerRecordScope)) {
return "manager_record_scope", errors.New("a manager may be scoped to team or organization, not to their own records alone")
}
if u.ClaimExpiryHours != nil && (*u.ClaimExpiryHours < 1 || *u.ClaimExpiryHours > 2160) {
return "claim_expiry_hours", errors.New("a claim may last between one hour and ninety days")
}
if u.InviteExpiryHours != nil && (*u.InviteExpiryHours < 1 || *u.InviteExpiryHours > 336) {
return "invite_expiry_hours", errors.New("an invite may last between one hour and fourteen days")
}
if u.DefaultInviteRole != nil {
r := security.Role(*u.DefaultInviteRole)
if !security.ValidRole(r) || r == security.RoleOwner {
return "default_invite_role", errors.New("the default invited role must be a real role below owner")
}
}
return "", nil
}
// invalidatesVerification reports whether a patch changes anything that a previous successful test send
// proved. Changing the host, port, username, security or sender means the old proof no longer describes the
// current configuration, so verification is cleared and mail stops until somebody tests again.
//
// THIS IS THE HONEST BEHAVIOUR AND IT IS DELIBERATELY INCONVENIENT. The alternative, leaving the old
// verified_at in place, means an admin edits a host name, saves, and the platform silently keeps claiming
// mail works until the first invite disappears into nothing.
func (u Update) invalidatesVerification() bool {
return u.SMTPHost != nil || u.SMTPPort != nil || u.SMTPUsername != nil ||
u.SMTPSecurity != nil || u.EmailFromAddress != nil
}
// Save applies a sparse patch inside tx. It returns the settings as they now stand.
func (s *Store) Save(ctx context.Context, tx pgx.Tx, u Update, actor uuid.UUID) (Settings, error) {
set := make([]string, 0, 20)
args := make([]any, 0, 20)
add := func(col string, v any) {
args = append(args, v)
set = append(set, fmt.Sprintf("%s = $%d", col, len(args)))
}
if u.EmailDeliveryEnabled != nil {
add("email_delivery_enabled", *u.EmailDeliveryEnabled)
}
if u.EmailFromAddress != nil {
add("email_from_address", nilIfEmpty(*u.EmailFromAddress))
}
if u.EmailFromName != nil {
add("email_from_name", nilIfEmpty(*u.EmailFromName))
}
if u.EmailReplyTo != nil {
add("email_reply_to", nilIfEmpty(*u.EmailReplyTo))
}
if u.SMTPHost != nil {
add("smtp_host", nilIfEmpty(strings.TrimSpace(*u.SMTPHost)))
}
if u.SMTPPort != nil {
add("smtp_port", *u.SMTPPort)
}
if u.SMTPUsername != nil {
add("smtp_username", nilIfEmpty(*u.SMTPUsername))
}
if u.SMTPSecurity != nil {
add("smtp_security", *u.SMTPSecurity)
}
if u.WorkVisibility != nil {
add("work_visibility", *u.WorkVisibility)
}
if u.ShowActivityPresence != nil {
add("show_activity_presence", *u.ShowActivityPresence)
}
if u.ClaimRequired != nil {
add("claim_required", *u.ClaimRequired)
}
if u.ClaimExpiryHours != nil {
add("claim_expiry_hours", *u.ClaimExpiryHours)
}
if u.RateVisibility != nil {
add("rate_visibility", *u.RateVisibility)
}
if u.MarginVisibility != nil {
add("margin_visibility", *u.MarginVisibility)
}
if u.ContactVisibility != nil {
add("contact_visibility", *u.ContactVisibility)
}
if u.StaffRecordScope != nil {
add("staff_record_scope", *u.StaffRecordScope)
}
if u.ManagerRecordScope != nil {
add("manager_record_scope", *u.ManagerRecordScope)
}
if u.UnownedRecordsVisible != nil {
add("unowned_records_visible", *u.UnownedRecordsVisible)
}
if u.InviteExpiryHours != nil {
add("invite_expiry_hours", *u.InviteExpiryHours)
}
if u.DefaultInviteRole != nil {
add("default_invite_role", *u.DefaultInviteRole)
}
if u.AEDTNoticeRequired != nil {
add("aedt_notice_required", *u.AEDTNoticeRequired)
}
if u.DefaultBurdenPct != nil {
// The inner nil is a deliberate CLEAR, written as a NULL, not skipped. An owner who removes their wrap
// is telling the product it no longer knows their cost structure, and the honest consequence is that
// every margin disappears and every spread stays.
if *u.DefaultBurdenPct == nil {
add("default_burden_pct", nil)
} else {
add("default_burden_pct", (*u.DefaultBurdenPct).String())
}
}
if u.invalidatesVerification() {
set = append(set, "smtp_verified_at = NULL")
// Delivery is switched off at the same moment, because the CHECK constraint refuses a row that has
// delivery enabled with no verification, and because an admin editing a host mid flight should not
// have invites silently queue against a configuration nobody has proved.
set = append(set, "email_delivery_enabled = false")
}
if len(set) == 0 {
return s.LoadTx(ctx, tx)
}
add("updated_by", actor)
set = append(set, "updated_at = now()")
// The row is guaranteed FIRST, then updated. This was written as a single INSERT ... ON CONFLICT DO
// UPDATE, which is wrong in a way that passed every review and failed the first walkthrough: an
// organization with no row yet took the INSERT branch, where the SET clause does not apply, so the very
// first save an owner performed reported 200 and changed nothing. provision_org does not create a
// settings row, so EVERY newly provisioned customer hit it. Two statements, no cleverness.
if err := s.ensureRow(ctx, tx); err != nil {
return Settings{}, err
}
if _, err := tx.Exec(ctx, `UPDATE org_settings SET `+strings.Join(set, ", "), args...); err != nil {
return Settings{}, fmt.Errorf("orgsettings: save: %w", err)
}
return s.LoadTx(ctx, tx)
}
// ensureRow creates this organization's settings row if it does not have one, so every writer below can be a
// plain UPDATE. An UPDATE against a missing row affects zero rows and reports success, which is the failure
// mode this exists to remove.
func (s *Store) ensureRow(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
INSERT INTO org_settings (organization_id)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid)
ON CONFLICT (organization_id) DO NOTHING`)
if err != nil {
return fmt.Errorf("orgsettings: ensure settings row: %w", err)
}
return nil
}
// MarkVerified records that a test send actually succeeded. It is the ONLY writer of smtp_verified_at, which
// is what makes that column mean what it says.
func (s *Store) MarkVerified(ctx context.Context, tx pgx.Tx) error {
if err := s.ensureRow(ctx, tx); err != nil {
return err
}
_, err := tx.Exec(ctx, `UPDATE org_settings
SET smtp_verified_at = now(), smtp_last_attempt_at = now(), smtp_last_error = NULL, updated_at = now()`)
return err
}
// MarkAttemptFailed records a failed send with the transport's own message, so an admin can fix a typo
// without reading a server log they have no access to.
//
// THE MESSAGE IS SCRUBBED FIRST. A transport error can echo the credentials it was rejecting, and this column
// is readable by anyone holding org.settings.write and is returned over the API. redactCredentials removes
// the username and password from the text before it is stored, so the worst case is a less useful error
// rather than a password sitting in a readable column.
func (s *Store) MarkAttemptFailed(ctx context.Context, tx pgx.Tx, cause string, username, password string) error {
msg := redactCredentials(cause, username, password)
if len(msg) > 500 {
msg = msg[:500]
}
if err := s.ensureRow(ctx, tx); err != nil {
return err
}
_, err := tx.Exec(ctx, `UPDATE org_settings
SET smtp_verified_at = NULL, email_delivery_enabled = false,
smtp_last_attempt_at = now(), smtp_last_error = $1, updated_at = now()`, msg)
return err
}
func redactCredentials(s, username, password string) string {
for _, secret := range []string{password, username} {
// Short values are skipped: redacting a two character username would replace half the alphabet in the
// message and tell the reader nothing.
if len(secret) >= 4 {
s = strings.ReplaceAll(s, secret, "[redacted]")
}
}
return s
}
func nilIfEmpty(s string) any {
if strings.TrimSpace(s) == "" {
return nil
}
return s
}
// plausibleAddress is a deliberately loose check. Address syntax is far wider than most validators admit, and
// rejecting a valid address is a worse failure than accepting an invalid one that simply bounces. It catches
// the actual mistakes: a name with no address, spaces, a missing at sign, a missing dot in the domain.
func plausibleAddress(v string) bool {
v = strings.TrimSpace(v)
if len(v) < 6 || len(v) > 320 || strings.ContainsAny(v, " \t\r\n<>,;") {
return false
}
at := strings.LastIndexByte(v, '@')
if at <= 0 || at == len(v)-1 {
return false
}
domain := v[at+1:]
dot := strings.IndexByte(domain, '.')
return dot > 0 && dot < len(domain)-1
}