Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/audit/audit.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/audit/audit.go |
| Original SHA-256 | 64a9735e6c64ecc799392498eb39769a1b4a037f0c1f8eecd8d3edee0bbe8345 |
| Git blob SHA | d5c9e91874c760cb26f60a6ab3bf3c686a07e96c |
| Reading-copy SHA-256 | 64a9735e6c64ecc799392498eb39769a1b4a037f0c1f8eecd8d3edee0bbe8345 |
Recovered source transcript
Download complete source text ↓102 lines · 3.7 KB
// Package audit writes the append only audit trail. It wraps dbgen.InsertAuditLog, which targets the
// audit_logs table (outside RLS, trigger enforced append only, app granted INSERT and SELECT only).
//
// Two write paths exist. Org scoped events run on the caller's tenant transaction so the audit row
// commits or rolls back with the work it records. Pre auth and platform security events (failed
// login, session rejected) run off the pool with a NULL org, because no tenant context exists yet.
// Both return an error, and security events MUST fail closed: a login the audit cannot record is
// rejected rather than allowed silently unaudited.
package audit
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrNoDatabase is returned when the writer has no pool to record a system event.
var ErrNoDatabase = errors.New("audit: no database configured")
// Event is one audit record. Action is required (for example login.success, auth.session.rejected,
// privacy.erase). The rest are optional context; empty strings are stored as NULL.
type Event struct {
Action string
ActorUserID *uuid.UUID
EntityType string
EntityID string
OldValues []byte
NewValues []byte
IPAddress string
UserAgent string
}
// Writer records audit events.
type Writer struct{ db *tenant.DB }
// New builds a Writer. db may be nil, in which case System returns ErrNoDatabase.
func New(db *tenant.DB) *Writer { return &Writer{db: db} }
// OnTx records an org scoped event on the caller's tenant transaction. The org is read from the
// validated context, never a caller argument, so it inherits the same authoritative org the session
// resolver set.
func (w *Writer) OnTx(ctx context.Context, tx pgx.Tx, e Event) error {
var org *uuid.UUID
if raw, ok := tenant.OrgFromContext(ctx); ok {
if id, err := uuid.Parse(raw); err == nil {
org = &id
}
}
return insert(ctx, tx, org, e)
}
// System records a pre auth or platform security event off the pool with a NULL org. The returned
// error is meaningful: the caller fails the request closed when a security event cannot be written.
func (w *Writer) System(ctx context.Context, e Event) error {
if w.db == nil || w.db.Pool == nil {
return ErrNoDatabase
}
return insert(ctx, w.db.Pool, nil, e)
}
// OnPool records an event off a raw pool for a KNOWN org. It exists for the ops commands that run outside a
// request and therefore have no session and no tenant context to read the org from. The org is a parameter
// here rather than read from the context, which is acceptable in exactly this case and nowhere else: a
// command iterating tenants has no session to inherit, and the alternative is a NULL org on a row that
// plainly belongs to one.
//
// It reuses insert rather than writing a second INSERT, so a row written by a command cannot drift in shape
// from a row written by a request.
func OnPool(ctx context.Context, db dbgen.DBTX, org *uuid.UUID, e Event) error {
if db == nil {
return ErrNoDatabase
}
return insert(ctx, db, org, e)
}
func insert(ctx context.Context, db dbgen.DBTX, org *uuid.UUID, e Event) error {
_, err := dbgen.New(db).InsertAuditLog(ctx, dbgen.InsertAuditLogParams{
OrganizationID: org,
ActorUserID: e.ActorUserID,
Action: e.Action,
EntityType: strPtr(e.EntityType),
EntityID: strPtr(e.EntityID),
OldValues: e.OldValues,
NewValues: e.NewValues,
IpAddress: strPtr(e.IPAddress),
UserAgent: strPtr(e.UserAgent),
})
return err
}
func strPtr(s string) *string {
if s == "" {
return nil
}
return &s
}