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:
(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 }
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).
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
- Variables
- func AdvisoryLockKey(name string) int64
- func CheckRestrictedRole(ctx context.Context, q RowQueryer, field string) error
- func CheckSchemaVersion(ctx context.Context, q RowQueryer) error
- func ClassifyPgError(err error) error
- func EnsureTenant(ctx context.Context, conn Connection, tenantID, displayName string) error
- func IsPgErrorCode(err error, code string) bool
- func LatestMigrationVersion() (uint, error)
- func SCRAMVerifier(password string, salt []byte) (string, error)
- func TenantActive(ctx context.Context, conn Connection, tenantID string) (bool, error)
- func ValidateActor(actor string) error
- func ValidatePseudonym(p string) error
- func ValidateReason(reason string) error
- func ValidateReviewerID(id string) error
- func ValidateTenantSpec(s TenantSpec) error
- func ValidateTenantSpecs(specs []TenantSpec) error
- type AdvisoryLocker
- type Connection
- type ExtensionError
- type HealthStatus
- type IdentityEvent
- type IdentityRequest
- type IsolationLevel
- type Migrator
- type Option
- type Pool
- type PoolConfig
- type RetentionTenant
- type ReviewerRecord
- type Row
- type RowQueryer
- type Rows
- type SignedRequest
- type TenantAdmin
- func (a *TenantAdmin) AddReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (created bool, err error)
- func (a *TenantAdmin) Close() error
- func (a *TenantAdmin) DeleteReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)
- func (a *TenantAdmin) DeleteTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)
- func (a *TenantAdmin) ExportIdentityEvents(ctx context.Context, fn func(IdentityEvent) error) error
- func (a *TenantAdmin) Get(ctx context.Context, id string) (record TenantRecord, err error)
- func (a *TenantAdmin) HideReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (pseudonym string, err error)
- func (a *TenantAdmin) HideTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (pseudonym string, err error)
- func (a *TenantAdmin) Import(ctx context.Context, specs []TenantSpec) (created []bool, err error)
- func (a *TenantAdmin) List(ctx context.Context) (records []TenantRecord, err error)
- func (a *TenantAdmin) ListRetentionTenants(ctx context.Context) (tenants []RetentionTenant, err error)
- func (a *TenantAdmin) ListReviewers(ctx context.Context, tenantID string) (records []ReviewerRecord, err error)
- func (a *TenantAdmin) Provision(ctx context.Context, id, displayName string) (created bool, err error)
- func (a *TenantAdmin) RestoreReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)
- func (a *TenantAdmin) RestoreTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)
- func (a *TenantAdmin) SetStatus(ctx context.Context, id, status string) (previous string, err error)
- func (a *TenantAdmin) Trace(ctx context.Context, pseudonym, actor, reason string, s SignedRequest) (result TraceResult, err error)
- func (a *TenantAdmin) UnhideReviewer(ctx context.Context, tenantID, reviewer, actor, reason string, s SignedRequest) (err error)
- func (a *TenantAdmin) UnhideTenant(ctx context.Context, id, actor, reason string, s SignedRequest) (err error)
- type TenantConnection
- type TenantRecord
- type TenantSpec
- type TraceResult
- type Transaction
Constants ¶
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 )
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.
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.
const MaxIdentityExportPage = 10000
MaxIdentityExportPage is the largest page export_identity_events returns.
Variables ¶
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") )
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.
var ErrLockHeld = errors.New("backup/retention in progress, retry later")
ErrLockHeld means another session holds the advisory lock.
var ErrPseudonymNotFound = errors.New("no identity has this pseudonym")
ErrPseudonymNotFound reports a trace of a pseudonym no identity holds.
var ErrReviewerNotFound = errors.New("reviewer not registered")
ErrReviewerNotFound reports a reviewer with no reviewers row.
var ErrTenantNotFound = errors.New("tenant not found")
ErrTenantNotFound reports an operation on a tenant that has no tenants row.
Functions ¶
func AdvisoryLockKey ¶
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 ¶
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 ¶
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 ¶
LatestMigrationVersion returns the highest version among the embedded migrations: the schema version this binary needs.
func SCRAMVerifier ¶
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 ¶
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 ¶
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 ¶
ValidatePseudonym checks a pseudonym passed to Trace.
func ValidateReason ¶
ValidateReason checks --reason, which is stored in identity_events and shown in audits.
func ValidateReviewerID ¶
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 ¶
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 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 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 ¶
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 Pool ¶
type Pool interface {
Connection
Health(ctx context.Context) (*HealthStatus, error)
VerifyExtensions(ctx context.Context) error
}
Pool extends Connection with lifecycle management and observability.
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 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 SignedRequest ¶
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) 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 ¶
TenantSpec is a tenant to create, or to rename if it exists.
type TraceResult ¶
TraceResult is the identity behind a pseudonym.
Source Files
¶
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. |