store

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package store provides database access and persistence for DBBat.

Index

Constants

View Source
const (
	// APIKeyPrefix is the prefix for regular API keys
	APIKeyPrefix = "dbb_"
	// WebKeyPrefix is the prefix for web session keys
	WebKeyPrefix = "web_"
	// APIKeyRandomLength is the length of the random part of the key
	APIKeyRandomLength = 32
	// APIKeyPrefixLength is the length of the prefix stored for identification
	APIKeyPrefixLength = 8
	// WebSessionMaxDuration is the maximum duration for web sessions (1 hour)
	WebSessionMaxDuration = time.Hour
)

API key constants

View Source
const (
	GroupPublic        = "public"
	KeyPublicHost      = "host"
	KeyPublicPGHost    = "pg.host"
	KeyPublicOraHost   = "ora.host"
	KeyPublicMySQLHost = "mysql.host"
	KeyPublicPGPort    = "pg.port"
	KeyPublicOraPort   = "ora.port"
	KeyPublicMySQLPort = "mysql.port"
)

Public endpoint parameter group and key constants.

View Source
const (
	RoleAdmin     = "admin"
	RoleViewer    = "viewer"
	RoleConnector = "connector"
)

Role constants for user authorization

View Source
const (
	ControlReadOnly  = "read_only"
	ControlBlockCopy = "block_copy"
	ControlBlockDDL  = "block_ddl"
)

Control constants for grant restrictions

View Source
const (
	ProtocolPostgreSQL = "postgresql"
	ProtocolOracle     = "oracle"
	ProtocolMySQL      = "mysql"
	ProtocolMariaDB    = "mariadb"
)

Protocol constants for database connections

View Source
const (
	KeyTypeAPI = "api" // Regular API key (dbb_ prefix)
	KeyTypeWeb = "web" // Web session key (web_ prefix)
)

API key type constants

View Source
const (
	// MaxQueryRowsLimit is the maximum number of rows that can be returned per request
	MaxQueryRowsLimit = 1000
	// MaxQueryRowsDataSize is the maximum data size (1MB) that can be returned per request
	MaxQueryRowsDataSize = 1024 * 1024
	// DefaultQueryRowsLimit is the default number of rows returned if not specified
	DefaultQueryRowsLimit = 100
)
View Source
const (
	IdentityTypeSlack = "slack"
)

Identity provider constants

Variables

View Source
var (
	ErrAPIKeyNotFound = errors.New("API key not found")
	ErrAPIKeyRevoked  = errors.New("API key has been revoked")
	ErrAPIKeyExpired  = errors.New("API key has expired")
	ErrAPIKeyTooShort = errors.New("API key too short")
)

API key errors

View Source
var (
	ErrUserNotFound         = errors.New("user not found")
	ErrDatabaseNotFound     = errors.New("database not found")
	ErrGrantNotFound        = errors.New("grant not found")
	ErrNoActiveGrant        = errors.New("no active grant found")
	ErrGrantAlreadyRevoked  = errors.New("grant not found or already revoked")
	ErrConnectionNotFound   = errors.New("connection not found or already closed")
	ErrQueryNotFound        = errors.New("query not found")
	ErrInvalidCursor        = errors.New("invalid cursor")
	ErrTargetMatchesStorage = errors.New("target database cannot match DBBat storage database")
	ErrIdentityNotFound     = errors.New("identity not found")
	ErrOAuthStateNotFound   = errors.New("oauth state not found")
)

Store errors.

View Source
var ErrDefinitionInactive = errors.New("grant definition is no longer active")

ErrDefinitionInactive is returned by ApproveGrantRequest if the referenced definition has been deactivated between request and approval.

View Source
var ErrGrantDefinitionDuplicate = errors.New("grant definition with this name already exists")

ErrGrantDefinitionDuplicate is returned when an admin tries to create a definition whose name conflicts with an existing active definition.

View Source
var ErrGrantDefinitionNotFound = errors.New("grant definition not found")

ErrGrantDefinitionNotFound is returned when a grant definition lookup misses.

View Source
var ErrGrantRequestNotFound = errors.New("grant request not found")

ErrGrantRequestNotFound is returned when a request UID misses.

View Source
var ErrInvalidTransition = errors.New("grant request not pending")

ErrInvalidTransition is returned when a state transition is rejected because the request is not in `pending`.

View Source
var ErrParameterNotFound = errors.New("parameter not found")

ErrParameterNotFound is returned when no matching active parameter exists.

ValidControls lists all valid control values

Functions

func ExtractSourceIP

func ExtractSourceIP(addr net.Addr) string

ExtractSourceIP extracts the IP address from a net.Addr

func IsMySQLFamily added in v0.7.0

func IsMySQLFamily(protocol string) bool

IsMySQLFamily reports whether the given protocol speaks the MySQL wire protocol. The MySQL proxy serves both — they share the same listener, auth plugins, and wire-protocol handling. The distinction matters mostly for upstream connection setup (server version banner, auth plugin negotiation) and for UI labeling.

Types

type APIKey

type APIKey struct {
	bun.BaseModel `bun:"table:api_keys,alias:ak"`

	ID              uuid.UUID  `bun:"id,pk,type:uuid,default:gen_random_uuid()" json:"id"`
	UserID          uuid.UUID  `bun:"user_id,notnull,type:uuid" json:"user_id"`
	Name            string     `bun:"name,notnull" json:"name"`
	KeyHash         string     `bun:"key_hash,notnull" json:"-"`
	KeyPrefix       string     `bun:"key_prefix,notnull" json:"key_prefix"`
	KeyType         string     `bun:"key_type,notnull,default:'api'" json:"key_type"`
	ExpiresAt       *time.Time `bun:"expires_at" json:"expires_at"`
	LastUsedAt      *time.Time `bun:"last_used_at" json:"last_used_at"`
	RequestCount    int64      `bun:"request_count,notnull,default:0" json:"request_count"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
	RevokedAt       *time.Time `bun:"revoked_at" json:"revoked_at"`
	RevokedBy       *uuid.UUID `bun:"revoked_by,type:uuid" json:"revoked_by"`
	O5LogonSalt     []byte     `bun:"o5logon_salt" json:"-"`
	O5LogonVerifier []byte     `bun:"o5logon_verifier" json:"-"` // encrypted with dbbat master key
}

APIKey represents an API key for authentication

func (*APIKey) IsExpired

func (k *APIKey) IsExpired() bool

IsExpired returns true if the API key has expired

func (*APIKey) IsRevoked

func (k *APIKey) IsRevoked() bool

IsRevoked returns true if the API key has been revoked

func (*APIKey) IsValid

func (k *APIKey) IsValid() bool

IsValid returns true if the API key is not expired and not revoked

func (*APIKey) IsWebSession

func (k *APIKey) IsWebSession() bool

IsWebSession returns true if this is a web session key

type APIKeyFilter

type APIKeyFilter struct {
	UserID     *uuid.UUID
	KeyType    *string // Filter by key type (api, web)
	IncludeAll bool    // Include revoked/expired keys
	Limit      int
	Offset     int
}

APIKeyFilter represents filters for listing API keys

type AccessGrant

type AccessGrant struct {
	bun.BaseModel `bun:"table:access_grants,alias:ag"`

	UID                 uuid.UUID  `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	UserID              uuid.UUID  `bun:"user_id,notnull,type:uuid" json:"user_id"`
	DatabaseID          uuid.UUID  `bun:"database_id,notnull,type:uuid" json:"database_id"`
	Controls            []string   `bun:"controls,array" json:"controls"` // Array of controls: read_only, block_copy, block_ddl
	GrantedBy           uuid.UUID  `bun:"granted_by,notnull,type:uuid" json:"granted_by"`
	StartsAt            time.Time  `bun:"starts_at,notnull" json:"starts_at"`
	ExpiresAt           time.Time  `bun:"expires_at,notnull" json:"expires_at"`
	RevokedAt           *time.Time `bun:"revoked_at" json:"revoked_at"`
	RevokedBy           *uuid.UUID `bun:"revoked_by,type:uuid" json:"revoked_by"`
	MaxQueryCounts      *int64     `bun:"max_query_counts" json:"max_query_counts"`
	MaxBytesTransferred *int64     `bun:"max_bytes_transferred" json:"max_bytes_transferred"`
	CreatedAt           time.Time  `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`

	// Computed fields (not stored in DB)
	QueryCount       int64 `bun:"-" json:"query_count"`
	BytesTransferred int64 `bun:"-" json:"bytes_transferred"`
}

AccessGrant represents an access grant

func (*AccessGrant) HasControl

func (g *AccessGrant) HasControl(control string) bool

HasControl checks if the grant has a specific control enabled

func (*AccessGrant) IsReadOnly

func (g *AccessGrant) IsReadOnly() bool

IsReadOnly returns true if the grant has read_only control

func (*AccessGrant) ShouldBlockCopy

func (g *AccessGrant) ShouldBlockCopy() bool

ShouldBlockCopy returns true if COPY commands should be blocked

func (*AccessGrant) ShouldBlockDDL

func (g *AccessGrant) ShouldBlockDDL() bool

ShouldBlockDDL returns true if DDL commands should be blocked

type AuditEvent

type AuditEvent = AuditLog

AuditEvent is an alias for backward compatibility

type AuditFilter

type AuditFilter struct {
	EventType   *string
	UserID      *uuid.UUID
	PerformedBy *uuid.UUID
	StartTime   *time.Time
	EndTime     *time.Time
	BeforeUID   *uuid.UUID // Cursor: return events with UID < this value
	Limit       int
	Offset      int
}

AuditFilter represents filters for listing audit events

type AuditLog

type AuditLog struct {
	bun.BaseModel `bun:"table:audit_log,alias:al"`

	UID         uuid.UUID       `bun:"uid,pk,type:uuid" json:"uid"` // UUIDv7 set in Go
	EventType   string          `bun:"event_type,notnull" json:"event_type"`
	UserID      *uuid.UUID      `bun:"user_id,type:uuid" json:"user_id"`
	PerformedBy *uuid.UUID      `bun:"performed_by,type:uuid" json:"performed_by"`
	Details     json.RawMessage `bun:"details,type:jsonb" json:"details"`
	CreatedAt   time.Time       `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}

AuditLog represents an audit log entry

type Connection

type Connection struct {
	bun.BaseModel `bun:"table:connections,alias:c"`

	UID              uuid.UUID  `bun:"uid,pk,type:uuid" json:"uid"` // UUIDv7 set in Go
	UserID           uuid.UUID  `bun:"user_id,notnull,type:uuid" json:"user_id"`
	DatabaseID       uuid.UUID  `bun:"database_id,notnull,type:uuid" json:"database_id"`
	SourceIP         string     `bun:"source_ip,notnull,type:inet" json:"source_ip"`
	ConnectedAt      time.Time  `bun:"connected_at,notnull,default:current_timestamp" json:"connected_at"`
	LastActivityAt   time.Time  `bun:"last_activity_at,notnull,default:current_timestamp" json:"last_activity_at"`
	DisconnectedAt   *time.Time `bun:"disconnected_at" json:"disconnected_at"`
	Queries          int64      `bun:"queries,notnull,default:0" json:"queries"`
	BytesTransferred int64      `bun:"bytes_transferred,notnull,default:0" json:"bytes_transferred"`
}

Connection represents a connection through the proxy

type ConnectionFilter

type ConnectionFilter struct {
	UserID     *uuid.UUID
	DatabaseID *uuid.UUID
	BeforeUID  *uuid.UUID // Cursor: return connections with UID < this value
	Limit      int
	Offset     int
}

ConnectionFilter represents filters for listing connections

type DSNComponents

type DSNComponents struct {
	Host     string
	Port     string
	Database string
}

DSNComponents holds parsed PostgreSQL DSN components for comparison

type Database

type Database struct {
	bun.BaseModel `bun:"table:databases,alias:d"`

	UID               uuid.UUID  `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	Name              string     `bun:"name,notnull,unique" json:"name"`
	Description       string     `bun:"description" json:"description"`
	Host              string     `bun:"host,notnull" json:"host"`
	Port              int        `bun:"port,notnull" json:"port"`
	DatabaseName      string     `bun:"database_name,notnull" json:"database_name"`
	Username          string     `bun:"username,notnull" json:"username"`
	Password          string     `bun:"-" json:"-"`                          // Decrypted, not stored
	PasswordEncrypted []byte     `bun:"password_encrypted,notnull" json:"-"` // Encrypted form
	SSLMode           string     `bun:"ssl_mode,notnull,default:'prefer'" json:"ssl_mode"`
	Protocol          string     `bun:"protocol,notnull,default:'postgresql'" json:"protocol"`
	OracleServiceName *string    `bun:"oracle_service_name" json:"oracle_service_name,omitempty"`
	Listable          bool       `bun:"listable,notnull" json:"listable"`
	CreatedBy         *uuid.UUID `bun:"created_by,type:uuid" json:"created_by"`
	CreatedAt         time.Time  `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
	UpdatedAt         time.Time  `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
	DeletedAt         *time.Time `bun:"deleted_at,soft_delete" json:"-"`
}

Database represents a target database configuration

func (*Database) DecryptPassword

func (db *Database) DecryptPassword(encryptionKey []byte) error

DecryptPassword decrypts a database password using AAD bound to the database UID.

type DatabaseUpdate

type DatabaseUpdate struct {
	Description       *string
	Host              *string
	Port              *int
	DatabaseName      *string
	Username          *string
	Password          *string // Plaintext password to encrypt
	SSLMode           *string
	Protocol          *string
	OracleServiceName *string
	Listable          *bool
}

DatabaseUpdate represents fields that can be updated

type GlobalParameter added in v0.11.0

type GlobalParameter struct {
	bun.BaseModel `bun:"table:global_parameters,alias:gp"`

	UID       uuid.UUID  `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	GroupKey  string     `bun:"group_key,notnull" json:"group_key"`
	Key       string     `bun:"key,notnull" json:"key"`
	Value     string     `bun:"value,notnull" json:"value"`
	CreatedAt time.Time  `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
	UpdatedAt time.Time  `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
	DeletedAt *time.Time `bun:"deleted_at,soft_delete" json:"-"`
}

GlobalParameter stores runtime-editable key-value configuration.

type Grant

type Grant = AccessGrant

Grant is an alias for backward compatibility

func BuildGrantFromDefinition added in v0.10.0

func BuildGrantFromDefinition(def *GrantDefinition, userID, databaseID, grantedBy uuid.UUID, now time.Time) *Grant

BuildGrantFromDefinition assembles an AccessGrant from a GrantDefinition + the requesting user/database, anchoring the time window to `now`. Used by both the grant-request approval path and any future admin shortcut that wants to materialize a definition into a concrete grant.

type GrantDefinition added in v0.10.0

type GrantDefinition struct {
	bun.BaseModel `bun:"table:grant_definitions,alias:gd"`

	UID                 uuid.UUID `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	Name                string    `bun:"name,notnull" json:"name"`
	Description         string    `bun:"description,notnull,default:''" json:"description"`
	DurationSeconds     int64     `bun:"duration_seconds,notnull" json:"duration_seconds"`
	Controls            []string  `bun:"controls,array,notnull,default:'{}'" json:"controls"`
	MaxQueryCounts      *int64    `bun:"max_query_counts" json:"max_query_counts"`
	MaxBytesTransferred *int64    `bun:"max_bytes_transferred" json:"max_bytes_transferred"`
	IsActive            bool      `bun:"is_active,notnull,default:true" json:"is_active"`
	CreatedBy           uuid.UUID `bun:"created_by,notnull,type:uuid" json:"created_by"`
	CreatedAt           time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}

GrantDefinition is an admin-managed template describing the *shape* of a grant: name, duration, controls, optional quotas. Grant requests (separately implemented) reference a definition; on approval, a real AccessGrant is built from the definition + the request's user/database.

Direct admin grant creation bypasses definitions — they exist to bound what users can self-request, not to constrain admins.

type GrantDefinitionFilter added in v0.10.0

type GrantDefinitionFilter struct {
	ActiveOnly bool
}

GrantDefinitionFilter narrows ListGrantDefinitions queries.

type GrantFilter

type GrantFilter struct {
	UserID     *uuid.UUID
	DatabaseID *uuid.UUID
	ActiveOnly bool
}

GrantFilter represents filters for listing grants

type GrantRequest added in v0.10.0

type GrantRequest struct {
	bun.BaseModel `bun:"table:grant_requests,alias:gr"`

	UID               uuid.UUID          `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	UserID            uuid.UUID          `bun:"user_id,notnull,type:uuid" json:"user_id"`
	GrantDefinitionID uuid.UUID          `bun:"grant_definition_id,notnull,type:uuid" json:"grant_definition_id"`
	DatabaseID        uuid.UUID          `bun:"database_id,notnull,type:uuid" json:"database_id"`
	Justification     string             `bun:"justification,notnull,default:''" json:"justification"`
	Status            GrantRequestStatus `bun:"status,notnull" json:"status"`
	RequestedAt       time.Time          `bun:"requested_at,notnull,default:current_timestamp" json:"requested_at"`
	DecidedAt         *time.Time         `bun:"decided_at" json:"decided_at,omitempty"`
	DecidedBy         *uuid.UUID         `bun:"decided_by,type:uuid" json:"decided_by,omitempty"`
	DecisionReason    *string            `bun:"decision_reason" json:"decision_reason,omitempty"`
	ResultingGrantID  *uuid.UUID         `bun:"resulting_grant_id,type:uuid" json:"resulting_grant_id,omitempty"`

	// Slack bookkeeping — populated by the notifier (Spec 04). JSON-omitted
	// because the API has no need to expose Slack message coordinates.
	SlackChannel   *string `bun:"slack_channel" json:"-"`
	SlackMessageTS *string `bun:"slack_message_ts" json:"-"`
}

GrantRequest is a user-initiated request for a grant of a particular shape (definition) on a particular database. Admins approve or deny. On approval the system materializes a real AccessGrant from the definition + the request's user/database.

type GrantRequestFilter added in v0.10.0

type GrantRequestFilter struct {
	UserID     *uuid.UUID
	Status     *GrantRequestStatus
	DatabaseID *uuid.UUID
	Limit      int
	Offset     int
}

GrantRequestFilter narrows ListGrantRequests queries.

type GrantRequestStatus added in v0.10.0

type GrantRequestStatus string

GrantRequestStatus enumerates the lifecycle states a request can be in.

const (
	GrantRequestPending   GrantRequestStatus = "pending"
	GrantRequestApproved  GrantRequestStatus = "approved"
	GrantRequestDenied    GrantRequestStatus = "denied"
	GrantRequestCancelled GrantRequestStatus = "cancelled" //nolint:misspell // matches DB CHECK constraint
	GrantRequestExpired   GrantRequestStatus = "expired"
)

Lifecycle states for grant requests. Keep these constants matching the DB CHECK constraint values exactly.

type MigrationInfo

type MigrationInfo struct {
	Name       string
	MigratedAt time.Time
}

MigrationInfo contains information about a migration

type OAuthState added in v0.4.0

type OAuthState struct {
	bun.BaseModel `bun:"table:oauth_states,alias:os"`

	UID         uuid.UUID       `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	State       string          `bun:"state,notnull,unique" json:"state"`
	Provider    string          `bun:"provider,notnull" json:"provider"`
	RedirectURL string          `bun:"redirect_url" json:"redirect_url,omitempty"`
	Metadata    json.RawMessage `bun:"metadata,type:jsonb" json:"metadata,omitempty"`
	ExpiresAt   time.Time       `bun:"expires_at,notnull" json:"expires_at"`
	CreatedAt   time.Time       `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}

OAuthState represents a temporary OAuth state for CSRF protection

type Options

type Options struct {
	// DropTablesFirst drops all tables before running migrations (for test mode)
	DropTablesFirst bool
}

Options configures Store creation.

type PublicEndpoints added in v0.11.0

type PublicEndpoints struct {
	Host      string // default public hostname for all protocols
	PGHost    string // optional override; "" = fall back to Host
	OraHost   string
	MySQLHost string
	PGPort    *int // optional override; nil = fall back to local listen port
	OraPort   *int
	MySQLPort *int
}

PublicEndpoints holds the operator-configured public advertisement settings.

type Query

type Query struct {
	bun.BaseModel `bun:"table:queries,alias:q"`

	UID           uuid.UUID        `bun:"uid,pk,type:uuid" json:"uid"` // UUIDv7 set in Go
	ConnectionID  uuid.UUID        `bun:"connection_id,notnull,type:uuid" json:"connection_id"`
	SQLText       string           `bun:"sql_text,notnull" json:"sql_text"`
	Parameters    *QueryParameters `bun:"parameters,type:jsonb" json:"parameters,omitempty"`
	ExecutedAt    time.Time        `bun:"executed_at,notnull,default:current_timestamp" json:"executed_at"`
	DurationMs    *float64         `bun:"duration_ms,type:numeric(10,3)" json:"duration_ms"`
	RowsAffected  *int64           `bun:"rows_affected" json:"rows_affected"`
	Error         *string          `bun:"error" json:"error"`
	CopyFormat    *string          `bun:"copy_format" json:"copy_format,omitempty"`       // 'text', 'csv', 'binary', or nil for non-COPY
	CopyDirection *string          `bun:"copy_direction" json:"copy_direction,omitempty"` // 'in', 'out', or nil for non-COPY
}

Query represents a query execution record

func (*Query) BeforeAppendModel

func (q *Query) BeforeAppendModel(_ context.Context, _ bun.Query) error

BeforeAppendModel implements bun.BeforeAppendModelHook for Query

type QueryFilter

type QueryFilter struct {
	ConnectionID *uuid.UUID
	UserID       *uuid.UUID
	DatabaseID   *uuid.UUID
	StartTime    *time.Time
	EndTime      *time.Time
	BeforeUID    *uuid.UUID // Cursor: return queries with UID < this value (for stable pagination)
	Limit        int
	Offset       int
}

QueryFilter represents filters for listing queries

type QueryParameters

type QueryParameters struct {
	Values      []string `json:"values"`                 // Decoded string representation
	Raw         []string `json:"raw,omitempty"`          // Base64-encoded raw bytes
	FormatCodes []int16  `json:"format_codes,omitempty"` // 0=text, 1=binary
	TypeOIDs    []uint32 `json:"type_oids,omitempty"`    // PostgreSQL type OIDs
}

QueryParameters stores parameter values for prepared statements

type QueryRow

type QueryRow struct {
	RowNumber    int             `json:"row_number"`
	RowData      json.RawMessage `json:"row_data"`
	RowSizeBytes int64           `json:"row_size_bytes"`
}

QueryRow is an alias for API compatibility (without bun.BaseModel for simpler usage)

type QueryRowModel

type QueryRowModel struct {
	bun.BaseModel `bun:"table:query_rows,alias:qr"`

	UID          uuid.UUID       `bun:"uid,pk,type:uuid" json:"uid"` // UUIDv7 set in Go
	QueryID      uuid.UUID       `bun:"query_id,notnull,type:uuid" json:"query_id"`
	RowNumber    int             `bun:"row_number,notnull" json:"row_number"`
	RowData      json.RawMessage `bun:"row_data,notnull,type:jsonb" json:"row_data"`
	RowSizeBytes int64           `bun:"row_size_bytes,notnull" json:"row_size_bytes"`
}

QueryRowModel represents a single row from query results or COPY data

type QueryRowsCursor

type QueryRowsCursor struct {
	Offset int64 `json:"offset"`
}

QueryRowsCursor represents the pagination cursor state

type QueryRowsResult

type QueryRowsResult struct {
	Rows       []QueryRow `json:"rows"`
	NextCursor string     `json:"next_cursor,omitempty"`
	HasMore    bool       `json:"has_more"`
	TotalRows  int64      `json:"total_rows"`
}

QueryRowsResult contains paginated query rows

type QueryWithRows

type QueryWithRows struct {
	Query
	Rows []QueryRow `json:"rows"`
}

QueryWithRows combines a query with its result rows

type ResolvedEndpoints added in v0.11.0

type ResolvedEndpoints struct {
	PGHost    string
	OraHost   string
	MySQLHost string
	PGPort    int // 0 = protocol disabled
	OraPort   int
	MySQLPort int
}

ResolvedEndpoints holds the fully resolved connection advertisement values.

func ResolvePublicEndpoints added in v0.11.0

func ResolvePublicEndpoints(pe PublicEndpoints, cfg *config.Config) ResolvedEndpoints

ResolvePublicEndpoints applies fallback chains for host and port resolution.

type Store

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

Store provides access to the database

func New

func New(ctx context.Context, dsn string, opts ...Options) (*Store, error)

New creates a new Store instance and runs migrations

func (*Store) ApproveGrantRequest added in v0.10.0

func (s *Store) ApproveGrantRequest(ctx context.Context, uid, decidedBy uuid.UUID) (*Grant, *GrantRequest, error)

ApproveGrantRequest atomically transitions a pending request to approved and creates the resulting AccessGrant from the linked definition. The caller (admin) is captured in decided_by + grant.granted_by.

Returns:

  • the resulting grant, the updated request, nil on success
  • ErrGrantRequestNotFound if the request doesn't exist
  • ErrInvalidTransition if the request isn't pending
  • ErrDefinitionInactive if the linked definition was deactivated

Wrapped in a transaction so a partial failure (request flipped, grant not created) can't leak.

func (*Store) CancelGrantRequest added in v0.10.0

func (s *Store) CancelGrantRequest(ctx context.Context, uid, byUser uuid.UUID) (*GrantRequest, error)

CancelGrantRequest atomically transitions pending → cancelled. Used by the requester themselves; the caller layer enforces who can cancel whose request.

func (*Store) CleanupExpiredOAuthStates added in v0.4.0

func (s *Store) CleanupExpiredOAuthStates(ctx context.Context) (int64, error)

CleanupExpiredOAuthStates removes all expired OAuth states.

func (*Store) Close

func (s *Store) Close()

Close closes the database connection pool

func (*Store) CloseConnection

func (s *Store) CloseConnection(ctx context.Context, uid uuid.UUID) error

CloseConnection sets the disconnected_at timestamp

func (*Store) ConsumeOAuthState added in v0.4.0

func (s *Store) ConsumeOAuthState(ctx context.Context, stateToken string) (*OAuthState, error)

ConsumeOAuthState retrieves and deletes an OAuth state in one operation. It only matches states that have not yet expired.

func (*Store) CreateAPIKey

func (s *Store) CreateAPIKey(ctx context.Context, userID uuid.UUID, name string, expiresAt *time.Time, encryptionKey ...[]byte) (*APIKey, string, error)

CreateAPIKey creates a new API key for a user. Returns the created APIKey and the plain text key (only shown once). If encryptionKey is provided (non-nil), an O5LOGON verifier is computed and stored for Oracle proxy authentication.

func (*Store) CreateAPIKeyWithValue added in v0.5.0

func (s *Store) CreateAPIKeyWithValue(ctx context.Context, userID uuid.UUID, name string, plainKey string, expiresAt *time.Time, encryptionKey ...[]byte) (*APIKey, error)

CreateAPIKeyWithValue creates an API key with a specific plaintext value. Used for test mode provisioning where stable, predictable keys are needed. If encryptionKey is provided, an O5LOGON verifier is computed and stored.

func (*Store) CreateConnection

func (s *Store) CreateConnection(ctx context.Context, userID, databaseID uuid.UUID, sourceIP string) (*Connection, error)

CreateConnection creates a new connection record

func (*Store) CreateDatabase

func (s *Store) CreateDatabase(ctx context.Context, db *Database, encryptionKey []byte) (*Database, error)

CreateDatabase creates a new database configuration. It uses a transaction to ensure the password is encrypted with AAD bound to the database UID. Returns ErrTargetMatchesStorage if the target database matches the DBBat storage database.

func (*Store) CreateGrant

func (s *Store) CreateGrant(ctx context.Context, grant *Grant) (*Grant, error)

CreateGrant creates a new access grant

func (*Store) CreateGrantDefinition added in v0.10.0

func (s *Store) CreateGrantDefinition(ctx context.Context, def *GrantDefinition) (*GrantDefinition, error)

CreateGrantDefinition inserts a new GrantDefinition. The unique-active-name index enforces no two active definitions share a name; deactivated definitions don't block reuse.

func (*Store) CreateGrantRequest added in v0.10.0

func (s *Store) CreateGrantRequest(ctx context.Context, req *GrantRequest) (*GrantRequest, error)

CreateGrantRequest inserts a new pending request.

func (*Store) CreateOAuthState added in v0.4.0

func (s *Store) CreateOAuthState(ctx context.Context, state *OAuthState) (*OAuthState, error)

CreateOAuthState persists a new OAuth state for CSRF protection.

func (*Store) CreateQuery

func (s *Store) CreateQuery(ctx context.Context, query *Query) (*Query, error)

CreateQuery creates a new query record

func (*Store) CreateUser

func (s *Store) CreateUser(ctx context.Context, username, passwordHash string, roles []string) (*User, error)

CreateUser creates a new user with the specified roles

func (*Store) CreateUserIdentity added in v0.4.0

func (s *Store) CreateUserIdentity(ctx context.Context, identity *UserIdentity) (*UserIdentity, error)

CreateUserIdentity creates a new user identity link.

func (*Store) CreateWebSession

func (s *Store) CreateWebSession(ctx context.Context, userID uuid.UUID) (*APIKey, string, error)

CreateWebSession creates a new web session key for a user Web sessions have a fixed 1-hour expiration and use the web_ prefix Returns the created APIKey and the plain text key (only shown once)

func (*Store) DB

func (s *Store) DB() *bun.DB

DB returns the underlying bun.DB for advanced operations

func (*Store) DeactivateGrantDefinition added in v0.10.0

func (s *Store) DeactivateGrantDefinition(ctx context.Context, uid uuid.UUID) error

DeactivateGrantDefinition flips is_active to false. We never hard-delete because grant_requests reference definitions and we want the historical trail to stay intact.

func (*Store) DeleteDatabase

func (s *Store) DeleteDatabase(ctx context.Context, uid uuid.UUID) error

DeleteDatabase deletes a database

func (*Store) DeleteParameter added in v0.11.0

func (s *Store) DeleteParameter(ctx context.Context, groupKey, key string) error

DeleteParameter soft-deletes a parameter.

func (*Store) DeleteUser

func (s *Store) DeleteUser(ctx context.Context, uid uuid.UUID) error

DeleteUser deletes a user and all of their linked OAuth identities.

func (*Store) DeleteUserIdentity added in v0.4.0

func (s *Store) DeleteUserIdentity(ctx context.Context, uid uuid.UUID) error

DeleteUserIdentity soft-deletes a user identity.

func (*Store) DenyGrantRequest added in v0.10.0

func (s *Store) DenyGrantRequest(ctx context.Context, uid, decidedBy uuid.UUID, reason string) (*GrantRequest, error)

DenyGrantRequest atomically transitions pending → denied with an optional reason.

func (*Store) DropAllTables

func (s *Store) DropAllTables(ctx context.Context) error

DropAllTables drops all application tables and types (for test mode) This should be called BEFORE migrations to ensure a fresh start

func (*Store) EnsureDefaultAdmin

func (s *Store) EnsureDefaultAdmin(ctx context.Context, passwordHash string) error

EnsureDefaultAdmin creates a default admin user if no users exist

func (*Store) GetAPIKeyByID

func (s *Store) GetAPIKeyByID(ctx context.Context, id uuid.UUID) (*APIKey, error)

GetAPIKeyByID retrieves an API key by its ID

func (*Store) GetAPIKeyByPrefix

func (s *Store) GetAPIKeyByPrefix(ctx context.Context, prefix string) (*APIKey, error)

GetAPIKeyByPrefix retrieves all API keys with a given prefix Since prefix is unique, this returns at most one key

func (*Store) GetActiveGrant

func (s *Store) GetActiveGrant(ctx context.Context, userID, databaseID uuid.UUID) (*Grant, error)

GetActiveGrant retrieves an active grant for a user and database

func (*Store) GetAllParameters added in v0.11.0

func (s *Store) GetAllParameters(ctx context.Context, groupKey string) ([]GlobalParameter, error)

GetAllParameters retrieves all active parameters, optionally filtered by group.

func (*Store) GetDatabaseByName

func (s *Store) GetDatabaseByName(ctx context.Context, name string) (*Database, error)

GetDatabaseByName retrieves a database by name

func (*Store) GetDatabaseByOracleServiceName added in v0.4.0

func (s *Store) GetDatabaseByOracleServiceName(ctx context.Context, serviceName string) (*Database, error)

GetDatabaseByOracleServiceName retrieves an Oracle database by its service name.

func (*Store) GetDatabaseByUID

func (s *Store) GetDatabaseByUID(ctx context.Context, uid uuid.UUID) (*Database, error)

GetDatabaseByUID retrieves a database by UID

func (*Store) GetGrantByUID

func (s *Store) GetGrantByUID(ctx context.Context, uid uuid.UUID) (*Grant, error)

GetGrantByUID retrieves a grant by UID

func (*Store) GetGrantDefinition added in v0.10.0

func (s *Store) GetGrantDefinition(ctx context.Context, uid uuid.UUID) (*GrantDefinition, error)

GetGrantDefinition fetches a definition by UID. Returns ErrGrantDefinitionNotFound if the row doesn't exist.

func (*Store) GetGrantRequest added in v0.10.0

func (s *Store) GetGrantRequest(ctx context.Context, uid uuid.UUID) (*GrantRequest, error)

GetGrantRequest fetches a request by UID.

func (*Store) GetIdentityByProviderID added in v0.10.1

func (s *Store) GetIdentityByProviderID(ctx context.Context, provider, providerID string) (*UserIdentity, error)

GetIdentityByProviderID retrieves an identity row by (provider, provider_id) without joining the user. Use this when you need the identity uid itself rather than the associated User.

func (*Store) GetParameter added in v0.11.0

func (s *Store) GetParameter(ctx context.Context, groupKey, key string) (*GlobalParameter, error)

GetParameter retrieves a single active parameter by group and key.

func (*Store) GetParameters added in v0.11.0

func (s *Store) GetParameters(ctx context.Context, groupKey string) ([]GlobalParameter, error)

GetParameters retrieves all active parameters for a group.

func (*Store) GetPublicEndpoints added in v0.11.0

func (s *Store) GetPublicEndpoints(ctx context.Context) (PublicEndpoints, error)

GetPublicEndpoints reads all public.* parameters and returns the typed struct.

func (*Store) GetQuery

func (s *Store) GetQuery(ctx context.Context, uid uuid.UUID) (*Query, error)

GetQuery retrieves a query by UID without rows

func (*Store) GetQueryRows

func (s *Store) GetQueryRows(ctx context.Context, queryUID uuid.UUID, cursor string, limit int) (*QueryRowsResult, error)

GetQueryRows retrieves paginated rows for a query with cursor-based pagination

func (*Store) GetQueryWithRows

func (s *Store) GetQueryWithRows(ctx context.Context, uid uuid.UUID) (*QueryWithRows, error)

GetQueryWithRows retrieves a query with its result rows

func (*Store) GetUserByIdentity added in v0.4.0

func (s *Store) GetUserByIdentity(ctx context.Context, provider, providerID string) (*User, error)

GetUserByIdentity retrieves a user by their external identity (provider + provider_id).

func (*Store) GetUserByUID

func (s *Store) GetUserByUID(ctx context.Context, uid uuid.UUID) (*User, error)

GetUserByUID retrieves a user by UID

func (*Store) GetUserByUsername

func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error)

GetUserByUsername retrieves a user by username

func (*Store) GetUserIdentities added in v0.4.0

func (s *Store) GetUserIdentities(ctx context.Context, userID uuid.UUID) ([]UserIdentity, error)

GetUserIdentities retrieves all identities for a given user.

func (*Store) GetUserIdentity added in v0.4.0

func (s *Store) GetUserIdentity(ctx context.Context, uid uuid.UUID) (*UserIdentity, error)

GetUserIdentity retrieves a single user identity by UID.

func (*Store) HasPendingRequest added in v0.10.0

func (s *Store) HasPendingRequest(ctx context.Context, userID, definitionID, databaseID uuid.UUID) (bool, error)

HasPendingRequest checks whether a user already has an open request for the same database+definition. Used by the API to short-circuit duplicates before they get persisted.

func (*Store) Health

func (s *Store) Health(ctx context.Context) error

Health checks if the database is healthy

func (*Store) IncrementAPIKeyUsage

func (s *Store) IncrementAPIKeyUsage(ctx context.Context, id uuid.UUID) error

IncrementAPIKeyUsage updates the last_used_at and increments request_count

func (*Store) IncrementConnectionStats

func (s *Store) IncrementConnectionStats(ctx context.Context, uid uuid.UUID, bytes int64) error

IncrementConnectionStats increments the query count by 1 and adds bytes to bytes_transferred

func (*Store) ListAPIKeys

func (s *Store) ListAPIKeys(ctx context.Context, filter APIKeyFilter) ([]APIKey, error)

ListAPIKeys retrieves API keys with optional filters

func (*Store) ListAuditEvents

func (s *Store) ListAuditEvents(ctx context.Context, filter AuditFilter) ([]AuditEvent, error)

ListAuditEvents retrieves audit events with optional filters

func (*Store) ListConnections

func (s *Store) ListConnections(ctx context.Context, filter ConnectionFilter) ([]Connection, error)

ListConnections retrieves connections with optional filters

func (*Store) ListDatabases

func (s *Store) ListDatabases(ctx context.Context) ([]Database, error)

ListDatabases retrieves all databases

func (*Store) ListGrantDefinitions added in v0.10.0

func (s *Store) ListGrantDefinitions(ctx context.Context, filter GrantDefinitionFilter) ([]GrantDefinition, error)

ListGrantDefinitions returns definitions matching the filter. Admins want `ActiveOnly=false` to see soft-deleted entries; the request UI passes `ActiveOnly=true`.

func (*Store) ListGrantRequests added in v0.10.0

func (s *Store) ListGrantRequests(ctx context.Context, filter GrantRequestFilter) ([]GrantRequest, error)

ListGrantRequests returns requests matching the filter, newest first.

func (*Store) ListGrants

func (s *Store) ListGrants(ctx context.Context, filter GrantFilter) ([]Grant, error)

ListGrants retrieves grants with optional filters

func (*Store) ListListableDatabases added in v0.11.0

func (s *Store) ListListableDatabases(ctx context.Context) ([]Database, error)

ListListableDatabases retrieves databases that are marked as listable. Used by the non-admin listing path so any authenticated user can discover databases available to request access to.

func (*Store) ListQueries

func (s *Store) ListQueries(ctx context.Context, filter QueryFilter) ([]Query, error)

ListQueries retrieves queries with optional filters

func (*Store) ListUsers

func (s *Store) ListUsers(ctx context.Context) ([]User, error)

ListUsers retrieves all users

func (*Store) LogAuditEvent

func (s *Store) LogAuditEvent(ctx context.Context, event *AuditEvent) error

LogAuditEvent creates a new audit log entry

func (*Store) MatchesStorageDSN

func (s *Store) MatchesStorageDSN(host string, port int, databaseName string) bool

MatchesStorageDSN checks if a target database configuration matches the storage DSN. Returns true if the target appears to be the same database as DBBat storage.

func (*Store) Migrate

func (s *Store) Migrate(ctx context.Context) error

Migrate runs all pending migrations (for CLI command)

func (*Store) MigrationStatus

func (s *Store) MigrationStatus(ctx context.Context) ([]MigrationInfo, error)

MigrationStatus returns the status of all migrations

func (*Store) RevokeAPIKey

func (s *Store) RevokeAPIKey(ctx context.Context, id uuid.UUID, revokedBy uuid.UUID) error

RevokeAPIKey revokes an API key

func (*Store) RevokeGrant

func (s *Store) RevokeGrant(ctx context.Context, uid uuid.UUID, revokedBy uuid.UUID) error

RevokeGrant revokes a grant

func (*Store) Rollback

func (s *Store) Rollback(ctx context.Context) error

Rollback rolls back the last migration group

func (*Store) SetAuthCache added in v0.1.0

func (s *Store) SetAuthCache(authCache *cache.AuthCache)

SetAuthCache sets the authentication cache for API key verification.

func (*Store) SetGrantRequestSlackMessage added in v0.10.0

func (s *Store) SetGrantRequestSlackMessage(ctx context.Context, uid uuid.UUID, channel, ts string) error

SetGrantRequestSlackMessage records the channel/ts of the Slack post that announced this request, so the notifier can chat.update on status changes (Spec 04). NULL on either column means "no Slack post for this request" (notifier disabled, or first post failed).

func (*Store) SetParameter added in v0.11.0

func (s *Store) SetParameter(ctx context.Context, groupKey, key, value string) error

SetParameter creates or updates a parameter (upsert on group_key+key).

func (*Store) SetPublicEndpoints added in v0.11.0

func (s *Store) SetPublicEndpoints(ctx context.Context, pe PublicEndpoints) error

SetPublicEndpoints writes only the non-empty/non-nil fields.

func (*Store) StoreQueryRows

func (s *Store) StoreQueryRows(ctx context.Context, queryUID uuid.UUID, rows []QueryRow) error

StoreQueryRows stores result rows for a query

func (*Store) UpdateConnectionActivity

func (s *Store) UpdateConnectionActivity(ctx context.Context, uid uuid.UUID) error

UpdateConnectionActivity updates the last_activity_at timestamp

func (*Store) UpdateDatabase

func (s *Store) UpdateDatabase(ctx context.Context, uid uuid.UUID, updates DatabaseUpdate, encryptionKey []byte) error

UpdateDatabase updates a database. Returns ErrTargetMatchesStorage if the update would cause the target to match the DBBat storage database.

func (*Store) UpdateGrantDefinition added in v0.10.0

func (s *Store) UpdateGrantDefinition(ctx context.Context, def *GrantDefinition) error

UpdateGrantDefinition mutates an existing definition. Only the admin-editable fields are touched; uid / created_by / created_at / is_active stay put. Use DeactivateGrantDefinition for the lifecycle flip.

func (*Store) UpdateQueryCompletion added in v0.4.0

func (s *Store) UpdateQueryCompletion(ctx context.Context, uid uuid.UUID, durationMs *float64, rowsAffected *int64, queryError *string) error

UpdateQueryCompletion updates a query with duration, rows affected, and error.

func (*Store) UpdateUser

func (s *Store) UpdateUser(ctx context.Context, uid uuid.UUID, updates UserUpdate) error

UpdateUser updates a user

func (*Store) VerifyAPIKey

func (s *Store) VerifyAPIKey(ctx context.Context, plainKey string) (*APIKey, error)

VerifyAPIKey verifies a plain text API key and returns the associated key record It checks that the key exists, is not revoked, and is not expired

type User

type User struct {
	bun.BaseModel `bun:"table:users,alias:u"`

	UID               uuid.UUID  `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	Username          string     `bun:"username,notnull,unique" json:"username"`
	PasswordHash      string     `bun:"password_hash,notnull" json:"-"`
	Roles             []string   `bun:"roles,array" json:"roles"`
	RateLimitExempt   bool       `bun:"rate_limit_exempt,notnull,default:false" json:"rate_limit_exempt"`
	PasswordChangedAt *time.Time `bun:"password_changed_at" json:"-"`
	CreatedAt         time.Time  `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
	UpdatedAt         time.Time  `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
	DeletedAt         *time.Time `bun:"deleted_at,soft_delete" json:"-"`
}

User represents a DBBat user

func (*User) HasChangedPassword

func (u *User) HasChangedPassword() bool

HasChangedPassword returns true if the user has changed their initial password

func (*User) HasRole

func (u *User) HasRole(role string) bool

HasRole checks if the user has a specific role

func (*User) IsAdmin

func (u *User) IsAdmin() bool

IsAdmin returns true if the user has the admin role

func (*User) IsConnector

func (u *User) IsConnector() bool

IsConnector returns true if the user has the connector role

func (*User) IsViewer

func (u *User) IsViewer() bool

IsViewer returns true if the user has the viewer role

type UserIdentity added in v0.4.0

type UserIdentity struct {
	bun.BaseModel `bun:"table:user_identities,alias:ui"`

	UID         uuid.UUID       `bun:"uid,pk,type:uuid,default:gen_random_uuid()" json:"uid"`
	UserID      uuid.UUID       `bun:"user_id,notnull,type:uuid" json:"user_id"`
	Provider    string          `bun:"provider,notnull" json:"provider"`
	ProviderID  string          `bun:"provider_id,notnull" json:"provider_id"`
	Email       string          `bun:"email" json:"email,omitempty"`
	DisplayName string          `bun:"display_name" json:"display_name,omitempty"`
	Metadata    json.RawMessage `bun:"metadata,type:jsonb" json:"metadata,omitempty"`
	CreatedAt   time.Time       `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
	UpdatedAt   time.Time       `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
	DeletedAt   *time.Time      `bun:"deleted_at,soft_delete" json:"-"`
}

UserIdentity represents a link between a user and an external identity provider

type UserUpdate

type UserUpdate struct {
	PasswordHash *string
	Roles        []string
}

UserUpdate represents fields that can be updated

Jump to

Keyboard shortcuts

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