Documentation
¶
Overview ¶
Package postgres provides the separately releasable PostgreSQL adapter for audit without adding pgx or database dependencies to the core module. This independently versioned module consumes github.com/faustbrian/go-audit v1 records, not root-v2 records. Applications own pools, authorization, migrations, deployment-specific roles and archive/retention policy.
Store commits atomic batches; TxWriter only stages in a caller transaction. RetentionAdmin is a separately privileged boundary, not an archive verifier. Calls start no background workers. Cancellation is cooperative; rollback attempts deliberately retain context values while detaching cancellation under a five-second timeout. Driver diagnostics are hidden behind safe text and errors.Is classifications, rather than exposed through unwrapping.
Index ¶
- Constants
- Variables
- func FreshInstallPreflightSQL() string
- func Migrations() fs.FS
- func PrivilegeSQL(roles RoleNames) (string, error)
- type Config
- type IdentityContributor
- func (*IdentityContributor) Descriptor() identitypostgres.ContributorDescriptor
- func (c *IdentityContributor) PersistAudits(ctx context.Context, w identitypostgres.Work) error
- func (c *IdentityContributor) RecoverParticipant(ctx context.Context, w identitypostgres.RecoveryWork) (identitypostgres.RecoveryStatus, error)
- type RetentionAdmin
- func (admin *RetentionAdmin) AppendRetentionEvent(ctx context.Context, event audit.RetentionEvent) (audit.AppendResult, error)
- func (admin *RetentionAdmin) ApplyRetention(ctx context.Context, plan audit.RetentionPlan) (audit.RetentionApplyResult, error)
- func (admin *RetentionAdmin) PlanRetention(ctx context.Context, request audit.RetentionRequest) (audit.RetentionPlan, error)
- type RoleNames
- type Store
- func (store *Store) Append(ctx context.Context, record audit.Record) (audit.AppendResult, error)
- func (store *Store) AppendBatch(ctx context.Context, records []audit.Record) (audit.BatchResult, error)
- func (store *Store) Export(ctx context.Context, query audit.Query, consume func(audit.Record) error) error
- func (store *Store) Query(ctx context.Context, query audit.Query) (audit.Page, error)
- type TxWriter
Constants ¶
const ( // DefaultMaxBatchBytes is the 16 MiB canonical payload budget selected by a zero // override. DefaultMaxBatchBytes = 16 << 20 // MaxBatchBytes is the absolute 64 MiB canonical payload ceiling for one append or // stage. MaxBatchBytes = 64 << 20 )
Variables ¶
var ( // ErrPoolRequired reports construction without a PostgreSQL pool. ErrPoolRequired = errors.New("audit/postgres: pool is required") // ErrTransactionRequired reports transaction-writer construction without a // caller-owned transaction. ErrTransactionRequired = errors.New("audit/postgres: transaction is required") // ErrRetryableTransaction reports a PostgreSQL deadlock or serialization // failure. The complete transaction must be retried with identical IDs. ErrRetryableTransaction = errors.New("audit/postgres: transaction must be retried") )
Functions ¶
func FreshInstallPreflightSQL ¶
func FreshInstallPreflightSQL() string
FreshInstallPreflightSQL atomically reserves the fixed NOLOGIN roles before an empty database applies migration 1. Existing role-name collisions fail with PostgreSQL duplicate-object error 42710; concurrent collisions wait for the reservation and then fail under PostgreSQL role-name uniqueness. Execute it in the same outer transaction as every migration returned by Migrations so a failed installation cannot retain the reserved roles. Existing installations must apply the forward migrations instead.
func Migrations ¶
Migrations returns the embedded PostgreSQL SQL files rooted at the migrations directory. It neither applies them nor owns a migration runner. Preserve published file checksums and apply forward migrations with deployment-owned credentials; the files are not portable SQL for arbitrary database engines.
func PrivilegeSQL ¶
PrivilegeSQL returns least-privilege grants for deployment-owned roles. Apply it with the migration owner after creating and auditing the roles. Names must be distinct valid UTF-8 without NUL, at most 63 bytes, and not the reserved fixed names. Identifiers are quoted, but SQL is only returned, never executed; existing grants, ownership and role memberships are not audited or revoked.
Types ¶
type Config ¶
type Config struct {
// Limits bounds canonical decoding; an entirely zero value selects root-v1 defaults.
// Root-v1 callers must also retain the pre-construction work bounds in SECURITY.md.
Limits audit.Limits
// MaxBatchRecords selects a positive count ceiling up to audit.MaxAppendBatchRecords;
// zero uses that ceiling.
MaxBatchRecords int
// MaxBatchBytes caps total canonical payload bytes; zero uses DefaultMaxBatchBytes.
// It does not measure driver, SQL protocol or heap overhead.
MaxBatchBytes int
}
Config bounds decoded records and append batches. Zero values select core defaults and the absolute core batch ceiling.
type IdentityContributor ¶ added in v1.1.0
type IdentityContributor struct {
// contains filtered or unexported fields
}
IdentityContributor supplies the identity coordinator's audit phase without owning a pool, transaction, Work, registry or goroutine. Configuration is immutable after construction, so one instance may serve independent Work values concurrently. Each Work itself remains serial-use and generation-bound. Existing standalone Store and TxWriter contracts remain separate and unchanged.
func NewIdentityContributor ¶ added in v1.1.0
func NewIdentityContributor(config Config) (*IdentityContributor, error)
NewIdentityContributor validates and copies record/batch limits without I/O or transaction ownership. Zero Config selects the existing audit defaults. Apply all Migrations, including the command-binding migration, before use; missing schema or incompatible storage fails closed at persistence.
func (*IdentityContributor) Descriptor ¶ added in v1.1.0
func (*IdentityContributor) Descriptor() identitypostgres.ContributorDescriptor
Descriptor is the exact stable open contract admitted at composition time. It is pure and independent of configuration or database state.
func (*IdentityContributor) PersistAudits ¶ added in v1.1.0
func (c *IdentityContributor) PersistAudits(ctx context.Context, w identitypostgres.Work) error
PersistAudits writes every complete staged event and its immutable command/ index binding through this same Work. Exact replay is idempotent; changed canonical bytes, event-ID/index reuse or binding/policy mismatch fails closed. Event IDs are retained, never derived. A successful return is not a commit receipt: only the Coordinator closes the transaction. On any error it must abort/reconcile, never ignore a partial prefix. No sink, network, savepoint, independent transaction or caller callback is used, and Work is not retained. Errors are redacted. Nil receiver/context, cancelled context and invalid, incomplete, expired or closed Work fail without staging persistence.
func (*IdentityContributor) RecoverParticipant ¶ added in v1.1.0
func (c *IdentityContributor) RecoverParticipant(ctx context.Context, w identitypostgres.RecoveryWork) (identitypostgres.RecoveryStatus, error)
RecoverParticipant inspects retained command bindings through the sole coordinator's frozen, invocation-bound primary evidence capability. Absent bindings prove no audit effect; one exact binding with retained record identity proves only that participant's effect, never a command receipt. Conflicting, additional or incomplete bindings remain unknown. Legal payload pruning does not erase evidence. No writes, callbacks, network, private transaction or retained handles are used. Errors are redacted; invalid or cancelled authority returns RecoveryUnknown. Independent invocations may run concurrently.
type RetentionAdmin ¶
type RetentionAdmin struct {
// contains filtered or unexported fields
}
RetentionAdmin is a separately constructed privileged boundary. Applications can give Store only deployment-specific writer credentials and reserve this value for an archive-and-retention process with separately generated retention privileges.
func NewRetentionAdmin ¶
func NewRetentionAdmin(pool *pgxpool.Pool, config Config) (*RetentionAdmin, error)
NewRetentionAdmin constructs the separately privileged retention adapter. The supplied pool remains caller-owned. Construction does not inspect or grant privileges and does not prove that a deployment has separated credentials.
func (*RetentionAdmin) AppendRetentionEvent ¶
func (admin *RetentionAdmin) AppendRetentionEvent(ctx context.Context, event audit.RetentionEvent) (audit.AppendResult, error)
AppendRetentionEvent atomically appends one idempotent hold or release event. A commit failure is classified as unknown. The result RecordID identifies the retention event. Hold precedence uses database acceptance order, not the event occurrence time; event ID reuse with different content is rejected.
func (*RetentionAdmin) ApplyRetention ¶
func (admin *RetentionAdmin) ApplyRetention(ctx context.Context, plan audit.RetentionPlan) (audit.RetentionApplyResult, error)
ApplyRetention reconciles and prunes an unchanged plan under per-record advisory locking in one transaction. It does not check archive custody, the original cutoff or tenant authorization: that handoff is caller-owned. Pruning removes content but preserves identity tombstones and retention history. Deleted/Held/Changed counts are confirmed only after commit; commit failure returns an unknown outcome and requires reconciliation. Empty plans do no I/O.
func (*RetentionAdmin) PlanRetention ¶
func (admin *RetentionAdmin) PlanRetention(ctx context.Context, request audit.RetentionRequest) (audit.RetentionPlan, error)
PlanRetention returns bounded, stable, digest-bound candidates that are not held at query time, selected by recording-time/ID order and returned sorted by record ID for later lock ordering. Callers must archive and independently verify the complete plan before application; this method never writes an archive.
type RoleNames ¶
type RoleNames struct {
// Writer names the role granted append-function execution but no table reads.
Writer string
// Reader names the role granted audit-record and retention-history reads.
Reader string
// Retention names the separately privileged planning, hold-event and prune role.
Retention string
}
RoleNames identifies deployment-owned PostgreSQL roles. Names must be unique to the deployment or database; the adapter never creates cluster-wide roles or chooses memberships. The historical fresh-install preflight and embedded migration 1 separately reserve fixed roles that must remain inert.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns no pool and starts no goroutines. Append batches are atomic; query and export ordering is always recorded_at then record_id.
func New ¶
New constructs a durable adapter over an existing pool. The pool remains caller-owned; Store starts no goroutines and Close is unnecessary.
func (*Store) Append ¶
Append inserts one record atomically. An identical existing ID is a successful duplicate; a commit error is classified as unknown.
func (*Store) AppendBatch ¶
func (store *Store) AppendBatch(ctx context.Context, records []audit.Record) (audit.BatchResult, error)
AppendBatch atomically inserts a bounded input-order batch. A commit error is unknown; every earlier validation, begin, or statement error is a confirmed rejection. Retrying identical IDs and canonical bytes is idempotent, including after pruning because identity tombstones remain. Every input must have crossed an explicit redaction boundary. Preparation revalidates canonical bytes against configured decode limits; it does not verify a chain or authorize the supplied actor/tenant. The input slice must remain unchanged during use.
func (*Store) Export ¶
func (store *Store) Export(ctx context.Context, query audit.Query, consume func(audit.Record) error) error
Export streams at most Query.Limit records without collecting the full result set. The callback runs in recording-time/ID order while rows and their database connection remain open, but no adapter transaction lock is held. Persisted digests/canonical bytes are checked. Consumer side effects before a failure are not undone; ordinary callback errors/panics become audit.ErrExportConsumerFailed, while cancellation/deadline classifications remain public. Callbacks are synchronous.
func (*Store) Query ¶
Query returns one bounded page ordered by recording time then record ID. It conveys no read authorization and closes database rows on every path. Fresh queries capture an acceptance watermark; continuations exclude later accepts. Persisted canonical SHA-256 and schema-v1 bytes are checked before exposure, but this unkeyed digest is not chain authentication. Page slices are caller-owned.
type TxWriter ¶
type TxWriter struct {
// contains filtered or unexported fields
}
TxWriter stages records in one caller-owned pgx transaction. It deliberately does not implement audit.Sink because a successful Stage is not durable until the caller commits. A retryable failure or failed savepoint cleanup attempts rollback of the complete caller transaction. The caller must finish or discard an unusable transaction if cleanup fails; rollback success is not guaranteed.
func NewTx ¶
NewTx constructs a staging writer over a caller-owned transaction. The caller commits the business transaction and owns its concurrency/lifecycle. Stage may roll back the whole transaction after retryable errors or failed savepoint cleanup; it never commits the outer transaction.
func (*TxWriter) Stage ¶
func (writer *TxWriter) Stage(ctx context.Context, records []audit.Record) (audit.BatchResult, error)
Stage inserts an atomic bounded batch without committing the caller-owned transaction. Ordinary stage failures roll back only the batch savepoint; a deadlock or serialization failure rolls back the complete transaction, which must be retried with identical record IDs. Failed savepoint rollback also triggers an outer rollback attempt. All rollback attempts use a fresh detached five-second timeout; errors still require the caller to finish/discard an unusable transaction. A successful result acknowledges staging, not durability.