Help us shape the future of Go! Take the 2026 Go Developer Survey

db

package
v0.0.0-...-c2011ee Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package db provides PostgreSQL connection pooling, tenant-scoped Row-Level Security (RLS), schema migrations, and extension verification for the CrossCodex platform.

Startup Lifecycle

Schema changes and the default tenant are provisioned out of band by `crosscodexd migrate`, which connects as the schema owner (database.migration_dsn). A service then connects only as the restricted roles, in three stages:

  1. (Once, as the owner, in `crosscodexd migrate`) Run migrations:

    migrator, err := db.NewMigrator(ownerDSN) if err != nil { return err } defer migrator.Close() if err := migrator.Up(ctx); err != nil { return err }

  2. Open the application pool, check the role and schema version, and verify extensions:

    pool, err := db.NewPool(db.PoolConfig{ DSN: appUserDSN, MaxOpenConns: 20, Extensions: []string{"age", "vector"}, }) if err != nil { return err } defer pool.Close() if err := db.CheckRestrictedRole(ctx, appDB, "database.dsn"); err != nil { return err } if err := db.CheckSchemaVersion(ctx, appDB); err != nil { return err } if err := pool.VerifyExtensions(ctx); err != nil { return err }

    CheckRestrictedRole refuses a superuser, BYPASSRLS, table-owner or member-of-one connection. CheckSchemaVersion refuses a schema that is not at LatestMigrationVersion or is dirty. appDB is a *sql.DB on the same DSN (anything satisfying RowQueryer).

  3. Wrap the pool for tenant-scoped access:

    tenantConn := db.NewTenantPool(pool)

Tenant-Scoped Queries

Every tenant-scoped operation must go through a transaction. The TenantPool sets app.current_tenant via public.enter_tenant (which refuses deleted tenants) and optionally app.current_user via SET LOCAL. PostgreSQL reverts both on rollback or commit. Direct Query/Exec calls on TenantPool return ErrTenantRequired because SET LOCAL has no effect outside a transaction.

ctx, err := tenant.WithTenant(ctx, tenantID)
ctx  = tenant.WithUser(ctx, userID)  // optional: limits to user's own jobs

tx, err := tenantConn.Begin(ctx)
if err != nil { return err }
defer tx.Rollback()

rows, err := tx.Query(ctx, "SELECT job_id, status FROM jobs")
// ... process rows ...
return tx.Commit()

Security Model — Three Roles

Three PostgreSQL roles enforce defense-in-depth at the database level. Each role has the minimum privileges needed for its purpose, and none can escalate to another's capabilities.

  • postgres (superuser): Owns all tables and extensions. Used only by `crosscodexd migrate` (database.migration_dsn) for schema migrations, role passwords and default_tenant provisioning (EnsureTenant), and as the owner the tenant_admin functions run as. Never used by application code at runtime; the daemon refuses to start on this role (CheckRestrictedRole).

  • app_user: SELECT/INSERT/UPDATE/DELETE on public-schema tables, with Row-Level Security (RLS) policies enforcing tenant isolation per transaction. Has NO access to graph schemas or ag_catalog. Cannot perform DDL. This is the role used by the relational connection pool (configured via database.dsn).

  • graph_user: Owns per-tenant graph schemas (crosscodex_{tenant_id}) created by the tenant provisioning trigger. AGE cypher commands internally perform DDL (creating label tables, updating sequences), which requires ownership — GRANT INSERT/UPDATE/DELETE is not sufficient. graph_user has USAGE/SELECT/EXECUTE on ag_catalog for graph metadata queries, but has NO access to public-schema relational tables. This is the role used by the graph connection pool (configured via database.graph_dsn).

  • tenant_admin: no table, sequence or schema grants. It may only EXECUTE the SECURITY DEFINER functions provision_tenant, set_tenant_status and list_tenants, so operators can provision and suspend tenants (crosscodexd admin tenant, via TenantAdmin) without the owner credential, and cannot read tenant data or bulk-modify tenants. app_user may EXECUTE only tenant_is_active, which the gateway calls (TenantActive) to refuse suspended tenants despite the tenants RLS policy.

This separation means a bug in graph-handling code cannot read or modify relational data (jobs, classifications, vote summaries), and a bug in relational code cannot modify graph structure. Even if application code is compromised, PostgreSQL itself blocks cross-boundary access.

Privilege Matrix

The matrix below summarizes what each role can and cannot do. The integration tests in integration_test.go ("Role Isolation" section) verify every cell marked "no" and every cell marked "yes" — making this matrix an executable specification, not just documentation.

Capability                     postgres  app_user  graph_user
─────────────────────────────  ────────  ────────  ──────────
DDL (CREATE/ALTER/DROP)        yes       no        graph schemas only
Relational DML (public)        yes       yes+RLS   no
ag_catalog read                yes       no        yes
ag_catalog execute (cypher)    yes       no        yes
Graph schema DML               yes       no        yes (owner)
LOAD shared library            yes       no        no
Tenant provisioning (INSERT)   yes       via RLS   no
TRUNCATE relational tables     yes       no        no
Disable RLS / triggers         yes       no        no

The AGE shared library is loaded at server startup via shared_preload_libraries=age in postgresql.conf. Application code must NOT use the LOAD command — PostgreSQL restricts LOAD to superusers. shared_preload_libraries makes the library available to all sessions without per-session LOAD calls.

Index

Constants

View Source
const (
	// TenantStatusDeleted is the status of a soft-deleted tenant. Only
	// delete_tenant sets it; set_tenant_status refuses it as a target.
	TenantStatusDeleted = "deleted"

	IdentityKindTenant   = "tenant"
	IdentityKindReviewer = "reviewer"

	// MaxReasonBytes bounds --reason, as record_identity_event does.
	MaxReasonBytes = 500
	// MaxReviewerBytes bounds a reviewer ID and --by, as the reviewers CHECK
	// and record_identity_event do.
	MaxReviewerBytes = 256
)
View Source
const (
	TenantStatusActive    = "active"
	TenantStatusSuspended = "suspended"
)

Tenant lifecycle states. tenants_status_check allows exactly these and TenantStatusDeleted; the gateway refuses every tenant that is not active.

View Source
const BackupRetentionLockName = "crosscodex:backup-retention"

BackupRetentionLockName names the session advisory lock that keeps `crosscodexd admin backup run` and a non-dry-run retention scan apart. A purge during a backup could delete an object after Postgres was captured but before the objects step copied it, leaving a restored row that references a missing object.

View Source
const MaxIdentityExportPage = 10000

MaxIdentityExportPage is the largest page export_identity_events returns.

Variables

View Source
var (
	ErrNoRows               = errors.New("no rows in result set")
	ErrTxDone               = errors.New("transaction already completed")
	ErrConnClosed           = errors.New("connection closed")
	ErrTenantRequired       = errors.New("tenant ID required in context")
	ErrExtensionMissing     = errors.New("required PostgreSQL extension not available")
	ErrMigrationDirty       = errors.New("migration state is dirty")
	ErrPoolNotReady         = errors.New("connection pool not ready")
	ErrImmutableRecord      = errors.New("completed records cannot be modified")
	ErrTenantGraphViolation = errors.New("tenant graph context mismatch")
)
View Source
var ErrIdentityState = errors.New("identity is not in a state that allows this")

ErrIdentityState reports an action the identity's current state forbids (SQLSTATE 55000), such as hiding an active tenant. The wrapped message says what to do instead.

View Source
var ErrLockHeld = errors.New("backup/retention in progress, retry later")

ErrLockHeld means another session holds the advisory lock.

View Source
var ErrPseudonymNotFound = errors.New("no identity has this pseudonym")

ErrPseudonymNotFound reports a trace of a pseudonym no identity holds.

View Source
var ErrReviewerNotFound = errors.New("reviewer not registered")

ErrReviewerNotFound reports a reviewer with no reviewers row.

View Source
var ErrTenantNotFound = errors.New("tenant not found")

ErrTenantNotFound reports an operation on a tenant that has no tenants row.

Functions

func AdvisoryLockKey

func AdvisoryLockKey(name string) int64

AdvisoryLockKey returns the 64-bit FNV-1a hash of name, the key passed to pg_try_advisory_lock.

func CheckRestrictedRole

func CheckRestrictedRole(ctx context.Context, q RowQueryer, field string) error

CheckRestrictedRole refuses a connection whose role bypasses row-level security: a superuser, a BYPASSRLS role, or the owner of a public table (RLS is enabled but not forced, so owners skip it, and only the owner can disable the refusal triggers). Membership counts as identity: PostgreSQL applies an INHERIT member of such a role the same bypass, and a member of a superuser can SET ROLE to it. Tenant isolation and the immutability triggers hold only for the restricted app_user, graph_user and purge_user roles. field names the config field the connection came from; the refusal names the role that field must use.

Only the public schema is checked: graph_user owns the per-tenant AGE graph schemas by design (create_tenant_graph). Add any new application schema to the ownership test below.

func CheckSchemaVersion

func CheckSchemaVersion(ctx context.Context, q RowQueryer) error

CheckSchemaVersion refuses a database whose schema is not at LatestMigrationVersion, or whose last migration failed (dirty). The daemon never migrates; `crosscodexd migrate` does, as the schema owner. Call it on the app_user (database.dsn) connection; other restricted roles lack the SELECT grant on schema_migrations.

func ClassifyPgError

func ClassifyPgError(err error) error

ClassifyPgError maps PostgreSQL error codes from triggers/RLS to sentinel errors.

func EnsureTenant

func EnsureTenant(ctx context.Context, conn Connection, tenantID, displayName string) error

EnsureTenant idempotently provisions a tenant row. It MUST be called on a connection with privileges to bypass the tenants row-level security policy — i.e. the migration superuser/owner connection, not a TenantConnection.

The tenants tenant_isolation policy has no explicit WITH CHECK, so PostgreSQL applies its USING expression (tenant_id = current_setting('app.current_tenant')) as the INSERT check. Under the RLS-enforced app_user role, that blocks inserts outside a tenant-scoped transaction, which is why provisioning is deliberately a privileged operation (see pkg/db/doc.go).

Inserting a tenant fires the tenant_graph_create AFTER INSERT trigger, which provisions the per-tenant graph schema. ON CONFLICT (tenant_id) DO NOTHING makes repeat calls safe across restarts.

func IsPgErrorCode

func IsPgErrorCode(err error, code string) bool

IsPgErrorCode checks if err carries the given PostgreSQL SQLSTATE code. It returns true when err unwraps to a type with SQLState() that matches code.

func LatestMigrationVersion

func LatestMigrationVersion() (uint, error)

LatestMigrationVersion returns the highest version among the embedded migrations: the schema version this binary needs.

func SCRAMVerifier

func SCRAMVerifier(password string, salt []byte) (string, error)

SCRAMVerifier returns the SCRAM-SHA-256 verifier PostgreSQL stores for password (RFC 5802, RFC 7677), in the form ALTER ROLE ... PASSWORD accepts: SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey>. Sending the verifier instead of the password keeps the plaintext out of the server and its logs, as psql's \password does. password must be printable ASCII, for which SASLprep (applied by the server at login) is the identity. salt must be at least 16 bytes; callers pass crypto/rand output.

func TenantActive

func TenantActive(ctx context.Context, conn Connection, tenantID string) (bool, error)

TenantActive reports whether tenantID has a tenants row with status active. It calls tenant_is_active(), which app_user may execute: the tenants RLS policy hides every row outside a tenant transaction. An unknown tenant is not active.

func ValidateActor

func ValidateActor(actor string) error

ValidateActor checks --by, the operator recorded in identity_events. An operator ID follows the same rules as a reviewer ID, which are at least as strict as record_identity_event's.

func ValidatePseudonym

func ValidatePseudonym(p string) error

ValidatePseudonym checks a pseudonym passed to Trace.

func ValidateReason

func ValidateReason(reason string) error

ValidateReason checks --reason, which is stored in identity_events and shown in audits.

func ValidateReviewerID

func ValidateReviewerID(id string) error

ValidateReviewerID checks a human reviewer ID before it is registered or stored as determined_by in PostgreSQL and on graph edges. It is the only Go check of reviewer IDs; the reviewers CHECK constraints back it up.

func ValidateTenantSpec

func ValidateTenantSpec(s TenantSpec) error

ValidateTenantSpec applies provision_tenant's rules in Go: a valid tenant ID outside the reserved "hidden-" prefix, and a display name that is neither blank nor holds a control character (which would corrupt tabular output and terminals).

func ValidateTenantSpecs

func ValidateTenantSpecs(specs []TenantSpec) error

ValidateTenantSpecs checks an Import batch: at least one spec, each valid, and no tenant ID twice, which is a bug in the batch rather than an upsert. Positions in errors are 1-based.

Types

type AdvisoryLocker

type AdvisoryLocker struct {
	// contains filtered or unexported fields
}

AdvisoryLocker takes a session-level advisory lock on its own dedicated connection, so the lock survives transaction boundaries and lasts until release (or until the connection drops). Advisory locks need no GRANT, so restricted roles (app_user, backup_user) can use them. Locks are per-database: every holder must connect to the same database.

func NewAdvisoryLocker

func NewAdvisoryLocker(dsn, name string) *AdvisoryLocker

NewAdvisoryLocker returns a locker for name on the database at dsn.

func (*AdvisoryLocker) TryAcquire

func (l *AdvisoryLocker) TryAcquire(ctx context.Context) (func(context.Context) error, error)

TryAcquire takes the lock without waiting. When another session holds it, the error wraps ErrLockHeld. The returned release unlocks and closes the dedicated connection; call it exactly once.

type Connection

type Connection interface {
	Begin(ctx context.Context) (Transaction, error)
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) Row
	Exec(ctx context.Context, query string, args ...any) error
	Close() error
}

type ExtensionError

type ExtensionError struct {
	Missing []string
}

func (*ExtensionError) Error

func (e *ExtensionError) Error() string

func (*ExtensionError) Unwrap

func (e *ExtensionError) Unwrap() error

type HealthStatus

type HealthStatus struct {
	Connected    bool
	OpenConns    int
	InUse        int
	Idle         int
	MaxOpen      int
	WaitCount    int64
	WaitDuration time.Duration
}

type IdentityEvent

type IdentityEvent struct {
	EventID           int64
	OccurredAt        time.Time
	Action            string
	Kind              string
	TenantID          string
	IdentityID        string
	Actor             string
	DBUser            string
	Reason            string
	Request           []byte
	Attestation       []byte
	AttestationSHA256 string
}

IdentityEvent is one identity_events row as export_identity_events returns it. IDs are real, hidden tenants' and reviewers' included, so an export is sensitive. Request and Attestation are the stored bytes: a verifier hashes Request as stored, never a re-encoding.

type IdentityRequest

type IdentityRequest struct {
	Action string `json:"action"`
	// Kind and TenantID are empty for a trace: the operator does not know
	// them until the trace answers.
	Kind     string `json:"kind"`
	TenantID string `json:"tenant_id"`
	// IdentityID is the tenant or reviewer ID, or the pseudonym for a trace.
	IdentityID  string    `json:"identity_id"`
	Actor       string    `json:"actor"`
	Reason      string    `json:"reason"`
	RequestedAt time.Time `json:"-"`
}

IdentityRequest is what an operator asked for. Its canonical JSON is stored in identity_events.request and is the material an in-toto link signs, so a verifier needs only the row and the public key. record_identity_event refuses a request whose action, kind, tenant_id, identity_id, actor or reason differ from the call, so a signed request cannot be replayed against another identity.

func (IdentityRequest) Canonical

func (r IdentityRequest) Canonical() ([]byte, error)

Canonical returns the request as JSON with fixed key order and the time in UTC RFC 3339, so this encoder always produces the same bytes for the same request. That holds only for this Go encoder: a verifier must hash the request bytes stored in identity_events, never a re-encoding. A zero RequestedAt is refused, since the signed time would be meaningless.

type IsolationLevel

type IsolationLevel string
const (
	IsolationReadUncommitted IsolationLevel = "READ UNCOMMITTED"
	IsolationReadCommitted   IsolationLevel = "READ COMMITTED"
	IsolationRepeatableRead  IsolationLevel = "REPEATABLE READ"
	IsolationSerializable    IsolationLevel = "SERIALIZABLE"
)

type Migrator

type Migrator interface {
	Up(ctx context.Context) error
	Down(ctx context.Context) error
	Version(ctx context.Context) (version uint, dirty bool, err error)
	Close() error
}

Migrator manages database schema migrations.

func NewMigrator

func NewMigrator(dsn string) (Migrator, error)

NewMigrator creates a new migrator that runs embedded SQL migrations. It manages its own database connection, separate from the application pool, because golang-migrate handles connection lifecycle internally.

type Option

type Option func(*poolOptions)

func WithConnMaxLifetime

func WithConnMaxLifetime(d time.Duration) Option

func WithMaxIdleConns

func WithMaxIdleConns(n int) Option

func WithTelemetry

func WithTelemetry(tracer trace.Tracer, meter metric.Meter) Option

WithTelemetry configures OpenTelemetry tracing and metrics for the pool.

type Pool

type Pool interface {
	Connection
	Health(ctx context.Context) (*HealthStatus, error)
	VerifyExtensions(ctx context.Context) error
}

Pool extends Connection with lifecycle management and observability.

func NewPool

func NewPool(cfg PoolConfig, opts ...Option) (Pool, error)

type PoolConfig

type PoolConfig struct {
	DSN          string
	GraphDSN     string
	MaxOpenConns int
	MaxIdleConns int
	ConnMaxLife  time.Duration
	SSLMode      string
	Extensions   []string
}

func NewPoolConfigFrom

func NewPoolConfigFrom(dsn, graphDSN string, maxConns int, sslMode string, extensions []string) PoolConfig

type RetentionTenant

type RetentionTenant struct {
	// ID is the real tenant ID, for scoping a run. A hidden tenant's ID must
	// not be printed: show DisplayID instead.
	ID string
	// DisplayID is the pseudonym of a hidden tenant, else ID.
	DisplayID string
	Status    string
}

RetentionTenant is one tenant as list_retention_tenants() returns it.

type ReviewerRecord

type ReviewerRecord struct {
	ID        string // the pseudonym when Hidden
	Status    string
	CreatedAt time.Time
	CreatedBy string
	DeletedAt *time.Time
	Hidden    bool
}

ReviewerRecord is one reviewers row as list_reviewers() returns it.

type Row

type Row interface {
	Scan(dest ...any) error
}

type RowQueryer

type RowQueryer interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

RowQueryer is the query method CheckSchemaVersion and CheckRestrictedRole need; *sql.DB, *sql.Conn and *sql.Tx implement it.

type Rows

type Rows interface {
	Next() bool
	Scan(dest ...any) error
	Close() error
	Err() error
}

type SignedRequest

type SignedRequest struct {
	Request     []byte
	Attestation []byte
}

SignedRequest is a canonical request and the link envelope signing it.

type TenantAdmin

type TenantAdmin struct {
	// contains filtered or unexported fields
}

TenantAdmin provisions and suspends tenants as the tenant_admin role. That role holds no table privilege: it can only call the SECURITY DEFINER functions provision_tenant, set_tenant_status, list_tenants, list_retention_tenants and the identity lifecycle functions (see identity.go), so it cannot read tenant data or bulk-modify tenants. This is the only Go code that calls those functions.

func OpenTenantAdmin

func OpenTenantAdmin(dsn string, opts ...Option) (*TenantAdmin, error)

OpenTenantAdmin connects to dsn, which must authenticate as tenant_admin. opts are pool options; WithTelemetry also instruments the TenantAdmin methods, which otherwise use telemetry.Instrumentation("pkg/db").

func (*TenantAdmin) AddReviewer

func (a *TenantAdmin) AddReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (created bool, err error)

AddReviewer registers a reviewer for a tenant, or does nothing if they are already active; created reports whether it inserted.

func (*TenantAdmin) Close

func (a *TenantAdmin) Close() error

Close closes the connection.

func (*TenantAdmin) DeleteReviewer

func (a *TenantAdmin) DeleteReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)

DeleteReviewer soft-deletes a reviewer: their verdicts stay, and new verdicts by them are refused.

func (*TenantAdmin) DeleteTenant

func (a *TenantAdmin) DeleteTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)

DeleteTenant soft-deletes a tenant: its data and judgements stay, and every app path refuses it.

func (*TenantAdmin) ExportIdentityEvents

func (a *TenantAdmin) ExportIdentityEvents(ctx context.Context, fn func(IdentityEvent) error) error

ExportIdentityEvents calls fn with every identity_events row in event_id order, a page at a time, and stops at fn's first error. All pages come from one read-only snapshot, so events committed during the export are either wholly included or wholly absent, never skipped between pages. The span carries no tenant: the export spans every tenant.

func (*TenantAdmin) Get

func (a *TenantAdmin) Get(ctx context.Context, id string) (record TenantRecord, err error)

Get returns one tenant. An unknown tenant, or a hidden one (listed only by its pseudonym), returns an error wrapping ErrTenantNotFound.

func (*TenantAdmin) HideReviewer

func (a *TenantAdmin) HideReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (pseudonym string, err error)

HideReviewer hides a deleted reviewer behind a pseudonym, which it returns.

func (*TenantAdmin) HideTenant

func (a *TenantAdmin) HideTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (pseudonym string, err error)

HideTenant hides a deleted tenant behind a pseudonym, which it returns.

func (*TenantAdmin) Import

func (a *TenantAdmin) Import(ctx context.Context, specs []TenantSpec) (created []bool, err error)

Import provisions every spec in one transaction: either all are created or renamed, or none are. created[i] reports whether specs[i] was inserted.

func (*TenantAdmin) List

func (a *TenantAdmin) List(ctx context.Context) (records []TenantRecord, err error)

List returns every tenant ordered by ID.

func (*TenantAdmin) ListRetentionTenants

func (a *TenantAdmin) ListRetentionTenants(ctx context.Context) (tenants []RetentionTenant, err error)

ListRetentionTenants returns every tenant, deleted ones included, ordered by ID. A deleted tenant's job data still follows retention, so retention scans must reach it. Print DisplayID, never ID, of a hidden tenant.

func (*TenantAdmin) ListReviewers

func (a *TenantAdmin) ListReviewers(ctx context.Context, tenantID string) (records []ReviewerRecord, err error)

ListReviewers returns a tenant's reviewers ordered by ID; a hidden reviewer appears only by pseudonym. An unknown tenant has none.

func (*TenantAdmin) Provision

func (a *TenantAdmin) Provision(ctx context.Context, id, displayName string) (created bool, err error)

Provision creates the tenant, and through the tenant_graph_create trigger its graph, or renames it if it exists, leaving its status unchanged. created reports whether it inserted.

func (*TenantAdmin) RestoreReviewer

func (a *TenantAdmin) RestoreReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)

RestoreReviewer makes a deleted, unhidden reviewer active again.

func (*TenantAdmin) RestoreTenant

func (a *TenantAdmin) RestoreTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)

RestoreTenant brings a deleted, unhidden tenant back as suspended, so an operator re-activates it deliberately with SetStatus.

func (*TenantAdmin) SetStatus

func (a *TenantAdmin) SetStatus(ctx context.Context, id, status string) (previous string, err error)

SetStatus sets the tenant's status to TenantStatusActive or TenantStatusSuspended and returns the previous status. An unknown tenant returns an error wrapping ErrTenantNotFound.

func (*TenantAdmin) Trace

func (a *TenantAdmin) Trace(ctx context.Context, pseudonym, actor, reason string, s SignedRequest) (result TraceResult, err error)

Trace returns the identity behind a pseudonym and records the lookup, since revealing a hidden identity is itself audited. The span carries no tenant: the trace backend must not learn what the pseudonym hides.

func (*TenantAdmin) UnhideReviewer

func (a *TenantAdmin) UnhideReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)

UnhideReviewer shows a hidden reviewer by their ID again.

func (*TenantAdmin) UnhideTenant

func (a *TenantAdmin) UnhideTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)

UnhideTenant shows a hidden tenant by its ID again.

type TenantConnection

type TenantConnection interface {
	Connection
}

TenantConnection wraps a Connection with tenant-scoped RLS enforcement. Every transaction started via Begin() automatically scopes app.current_tenant to the tenant for the duration of that transaction (transaction-local, like SET LOCAL). NewTenantPool does so through enter_tenant, which refuses a deleted tenant; NewRetentionTenantPool does not. Query, QueryRow, and Exec are rejected with ErrTenantRequired because SET LOCAL has no effect outside a transaction.

func NewRetentionTenantPool

func NewRetentionTenantPool(pool Pool) TenantConnection

NewRetentionTenantPool is NewTenantPool without the deleted-tenant check. Use it for retention only: a deleted tenant's job data still follows retention, so scans and holds must reach it. Judgements (artifact pair verdicts) have no retention grant, so retention cannot touch them. Never use it on an app path; those must refuse deleted tenants.

func NewTenantPool

func NewTenantPool(pool Pool) TenantConnection

NewTenantPool creates a TenantConnection that enforces RLS on every transaction. Its Begin refuses a deleted tenant.

type TenantRecord

type TenantRecord struct {
	ID          string
	DisplayName string
	Status      string
	CreatedAt   time.Time
	DeletedAt   *time.Time
	Hidden      bool
}

TenantRecord is one tenants row as list_tenants() returns it. A hidden tenant has its pseudonym as ID and an empty DisplayName.

type TenantSpec

type TenantSpec struct {
	ID          string
	DisplayName string
}

TenantSpec is a tenant to create, or to rename if it exists.

type TraceResult

type TraceResult struct {
	Kind, TenantID, IdentityID, Status string
	Hidden                             bool
}

TraceResult is the identity behind a pseudonym.

type Transaction

type Transaction interface {
	Commit() error
	Rollback() error
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) Row
	Exec(ctx context.Context, query string, args ...any) error
}

Directories

Path Synopsis
Package dbtest provides test helpers for code that uses pkg/db.
Package dbtest provides test helpers for code that uses pkg/db.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL