Documentation
¶
Overview ¶
Package store provides database access and persistence for DBBat.
Index ¶
- Constants
- Variables
- func ExtractSourceIP(addr net.Addr) string
- func IsMySQLFamily(protocol string) bool
- func ValidateApprovalPatterns(patterns []string) error
- type APIKey
- type APIKeyFilter
- type AccessGrant
- type AuditEvent
- type AuditFilter
- type AuditLog
- type Connection
- type ConnectionFilter
- type DSNComponents
- type DeviceAuthRequest
- type EventNotification
- type GlobalParameter
- type Grant
- type GrantDefinition
- type GrantDefinitionFilter
- type GrantFilter
- type GrantRequest
- type GrantRequestFilter
- type GrantRequestStatus
- type Instance
- type MigrationInfo
- type MongoDatabaseData
- type MongoSCRAMCredentials
- type MongoUserData
- type OAuthState
- type Options
- type OracleAPIKeyData
- type OracleUserData
- type OrphanedConnections
- type PendingQueryRow
- type ProtocolData
- type PublicEndpoints
- type Query
- type QueryFilter
- type QueryParameters
- type QueryRow
- type QueryRowModel
- type QueryRowsCursor
- type QueryRowsResult
- type QueryWithRows
- type ResolvedEndpoints
- type RetentionSweepResult
- type SSHServerData
- type Server
- func (db *Server) DecryptPassword(encryptionKey []byte) error
- func (db *Server) DecryptSSHSecrets(encryptionKey []byte) error
- func (db *Server) IsSSH() bool
- func (db *Server) MongoAuthSourceOrDefault() string
- func (db *Server) MongoData() *MongoDatabaseData
- func (db *Server) SSHData() *SSHServerData
- type ServerProtocolData
- type ServerUpdate
- type Store
- func (s *Store) AddUserToGroup(ctx context.Context, groupUID, userUID uuid.UUID) error
- func (s *Store) ApproveGrantRequest(ctx context.Context, uid, decidedBy uuid.UUID) (*Grant, *GrantRequest, error)
- func (s *Store) AutoApproveGrantRequest(ctx context.Context, uid, requesterID uuid.UUID) (*Grant, *GrantRequest, error)
- func (s *Store) CancelGrantRequest(ctx context.Context, uid, byUser uuid.UUID) (*GrantRequest, error)
- func (s *Store) CleanupExpiredOAuthStates(ctx context.Context) (int64, error)
- func (s *Store) CleanupOldQueryRows(ctx context.Context, olderThan time.Duration) (RetentionSweepResult, error)
- func (s *Store) Close()
- func (s *Store) CloseConnection(ctx context.Context, uid uuid.UUID) error
- func (s *Store) CloseOrphanedConnections(ctx context.Context) (OrphanedConnections, error)
- func (s *Store) ConsumeOAuthState(ctx context.Context, stateToken string) (*OAuthState, error)
- func (s *Store) CountAdmins(ctx context.Context) (int, error)
- func (s *Store) CreateAPIKey(ctx context.Context, userID uuid.UUID, name string, expiresAt *time.Time, ...) (*APIKey, string, error)
- func (s *Store) CreateAPIKeyWithValue(ctx context.Context, userID uuid.UUID, name string, plainKey string, ...) (*APIKey, error)
- func (s *Store) CreateConnection(ctx context.Context, userID, databaseID uuid.UUID, sourceIP string) (*Connection, error)
- func (s *Store) CreateDeviceAuthRequest(ctx context.Context, clientName, deviceCode, userCode string) (*DeviceAuthRequest, error)
- func (s *Store) CreateGrant(ctx context.Context, grant *Grant) (*Grant, error)
- func (s *Store) CreateGrantDefinition(ctx context.Context, def *GrantDefinition) (*GrantDefinition, error)
- func (s *Store) CreateGrantRequest(ctx context.Context, req *GrantRequest) (*GrantRequest, error)
- func (s *Store) CreateOAuthState(ctx context.Context, state *OAuthState) (*OAuthState, error)
- func (s *Store) CreatePendingQuery(ctx context.Context, query *Query, pattern string) (*Query, error)
- func (s *Store) CreateQuery(ctx context.Context, query *Query) (*Query, error)
- func (s *Store) CreateServer(ctx context.Context, db *Server, encryptionKey []byte) (*Server, error)
- func (s *Store) CreateUser(ctx context.Context, username, passwordHash string, roles []string) (*User, error)
- func (s *Store) CreateUserGroup(ctx context.Context, group *UserGroup) (*UserGroup, error)
- func (s *Store) CreateUserIdentity(ctx context.Context, identity *UserIdentity) (*UserIdentity, error)
- func (s *Store) CreateWebSession(ctx context.Context, userID uuid.UUID) (*APIKey, string, error)
- func (s *Store) DB() *bun.DB
- func (s *Store) DeactivateGrantDefinition(ctx context.Context, uid uuid.UUID) error
- func (s *Store) DeleteParameter(ctx context.Context, groupKey, key string) error
- func (s *Store) DeleteServer(ctx context.Context, uid uuid.UUID) error
- func (s *Store) DeleteUser(ctx context.Context, uid uuid.UUID) error
- func (s *Store) DeleteUserGroup(ctx context.Context, uid uuid.UUID) error
- func (s *Store) DeleteUserIdentity(ctx context.Context, uid uuid.UUID) error
- func (s *Store) DenyGrantRequest(ctx context.Context, uid, decidedBy uuid.UUID, reason string) (*GrantRequest, error)
- func (s *Store) DeregisterInstance(ctx context.Context) error
- func (s *Store) DropAllTables(ctx context.Context) error
- func (s *Store) EnsureDefaultAdmin(ctx context.Context, passwordHash string) error
- func (s *Store) EnsureUserOracleSalts(ctx context.Context, userID uuid.UUID) (*OracleUserData, error)
- func (s *Store) GetAPIKeyByID(ctx context.Context, id uuid.UUID) (*APIKey, error)
- func (s *Store) GetAPIKeyByPrefix(ctx context.Context, prefix string) (*APIKey, error)
- func (s *Store) GetActiveGrant(ctx context.Context, userID, databaseID uuid.UUID) (*Grant, error)
- func (s *Store) GetAllParameters(ctx context.Context, groupKey string) ([]GlobalParameter, error)
- func (s *Store) GetConnectionByUID(ctx context.Context, uid uuid.UUID) (*Connection, error)
- func (s *Store) GetDeviceAuthByUserCode(ctx context.Context, userCode string) (*DeviceAuthRequest, error)
- func (s *Store) GetGrantByUID(ctx context.Context, uid uuid.UUID) (*Grant, error)
- func (s *Store) GetGrantDefinition(ctx context.Context, uid uuid.UUID) (*GrantDefinition, error)
- func (s *Store) GetGrantRequest(ctx context.Context, uid uuid.UUID) (*GrantRequest, error)
- func (s *Store) GetIdentityByProviderID(ctx context.Context, provider, providerID string) (*UserIdentity, error)
- func (s *Store) GetInstance(ctx context.Context, instanceID string) (*Instance, error)
- func (s *Store) GetParameter(ctx context.Context, groupKey, key string) (*GlobalParameter, error)
- func (s *Store) GetParameters(ctx context.Context, groupKey string) ([]GlobalParameter, error)
- func (s *Store) GetPublicEndpoints(ctx context.Context) (PublicEndpoints, error)
- func (s *Store) GetQuery(ctx context.Context, uid uuid.UUID) (*Query, error)
- func (s *Store) GetQueryRows(ctx context.Context, queryUID uuid.UUID, cursor string, limit int) (*QueryRowsResult, error)
- func (s *Store) GetQueryWithOwner(ctx context.Context, uid uuid.UUID) (*Query, error)
- func (s *Store) GetQueryWithRows(ctx context.Context, uid uuid.UUID) (*QueryWithRows, error)
- func (s *Store) GetServerByName(ctx context.Context, name string) (*Server, error)
- func (s *Store) GetServerByOracleServiceName(ctx context.Context, serviceName string) (*Server, error)
- func (s *Store) GetServerByUID(ctx context.Context, uid uuid.UUID) (*Server, error)
- func (s *Store) GetUserByIdentity(ctx context.Context, provider, providerID string) (*User, error)
- func (s *Store) GetUserByUID(ctx context.Context, uid uuid.UUID) (*User, error)
- func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error)
- func (s *Store) GetUserGroup(ctx context.Context, uid uuid.UUID) (*UserGroup, error)
- func (s *Store) GetUserIdentities(ctx context.Context, userID uuid.UUID) ([]UserIdentity, error)
- func (s *Store) GetUserIdentity(ctx context.Context, uid uuid.UUID) (*UserIdentity, error)
- func (s *Store) HasApproverGroups(ctx context.Context, groupUIDs []uuid.UUID) (bool, error)
- func (s *Store) HasPendingRequest(ctx context.Context, userID, definitionID, databaseID uuid.UUID) (bool, error)
- func (s *Store) Health(ctx context.Context) error
- func (s *Store) HeartbeatInstance(ctx context.Context) error
- func (s *Store) IncrementAPIKeyUsage(ctx context.Context, id uuid.UUID) error
- func (s *Store) IncrementConnectionBytes(ctx context.Context, uid uuid.UUID, bytes int64) error
- func (s *Store) IncrementConnectionStats(ctx context.Context, uid uuid.UUID, bytes int64) error
- func (s *Store) InstanceID() string
- func (s *Store) ListAPIKeys(ctx context.Context, filter APIKeyFilter) ([]APIKey, error)
- func (s *Store) ListAdminSlackUserIDs(ctx context.Context) ([]string, error)
- func (s *Store) ListAuditEvents(ctx context.Context, filter AuditFilter) ([]AuditEvent, error)
- func (s *Store) ListConnections(ctx context.Context, filter ConnectionFilter) ([]Connection, error)
- func (s *Store) ListGrantDefinitions(ctx context.Context, filter GrantDefinitionFilter) ([]GrantDefinition, error)
- func (s *Store) ListGrantRequests(ctx context.Context, filter GrantRequestFilter) ([]GrantRequest, error)
- func (s *Store) ListGrants(ctx context.Context, filter GrantFilter) ([]Grant, error)
- func (s *Store) ListGroupMemberUIDs(ctx context.Context, groupUID uuid.UUID) ([]uuid.UUID, error)
- func (s *Store) ListGroupMembers(ctx context.Context, groupUID uuid.UUID) ([]User, error)
- func (s *Store) ListGroupsForUser(ctx context.Context, userUID uuid.UUID) ([]UserGroup, error)
- func (s *Store) ListListableServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListPendingApprovalQueries(ctx context.Context) ([]Query, error)
- func (s *Store) ListQueries(ctx context.Context, filter QueryFilter) ([]Query, error)
- func (s *Store) ListSSHServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListServersByOracleServiceName(ctx context.Context, serviceName string) ([]Server, error)
- func (s *Store) ListUserGroupUIDs(ctx context.Context, userUID uuid.UUID) ([]uuid.UUID, error)
- func (s *Store) ListUserGroups(ctx context.Context) ([]UserGroup, error)
- func (s *Store) ListUsers(ctx context.Context) ([]User, error)
- func (s *Store) ListenEvents(ctx context.Context, logger *slog.Logger, handler func(EventNotification)) error
- func (s *Store) LogAuditEvent(ctx context.Context, event *AuditEvent) error
- func (s *Store) MatchesStorageDSN(host string, port int, databaseName string) bool
- func (s *Store) Migrate(ctx context.Context) error
- func (s *Store) MigrationStatus(ctx context.Context) ([]MigrationInfo, error)
- func (s *Store) NotifyEvent(ctx context.Context, channel string, payload EventNotification) error
- func (s *Store) PollDeviceAuthToken(ctx context.Context, deviceCode string) (*DeviceAuthRequest, []byte, error)
- func (s *Store) PruneStaleInstances(ctx context.Context) (int64, error)
- func (s *Store) RegisterInstance(ctx context.Context) error
- func (s *Store) RemoveUserFromGroup(ctx context.Context, groupUID, userUID uuid.UUID) error
- func (s *Store) ResolveQueryApproval(ctx context.Context, uid uuid.UUID, status string, resolvedBy *uuid.UUID, ...) error
- func (s *Store) ResolveWebUIURL(ctx context.Context, cfg *config.Config) string
- func (s *Store) RespondToDeviceAuthByUserCode(ctx context.Context, userCode string, userID uuid.UUID, approve bool, ...) error
- func (s *Store) Revocations() *cache.RevocationRegistry
- func (s *Store) RevokeAPIKey(ctx context.Context, id uuid.UUID, revokedBy uuid.UUID) error
- func (s *Store) RevokeGrant(ctx context.Context, uid uuid.UUID, revokedBy uuid.UUID) error
- func (s *Store) Rollback(ctx context.Context) error
- func (s *Store) SetAuthCache(authCache *cache.AuthCache)
- func (s *Store) SetGrantRequestSlackMessage(ctx context.Context, uid uuid.UUID, channel, ts string) error
- func (s *Store) SetGroupMembers(ctx context.Context, groupUID uuid.UUID, userUIDs []uuid.UUID) error
- func (s *Store) SetInstanceID(instanceID string)
- func (s *Store) SetKnownHostKey(ctx context.Context, uid uuid.UUID, hostKey string) error
- func (s *Store) SetParameter(ctx context.Context, groupKey, key, value string) error
- func (s *Store) SetPublicEndpoints(ctx context.Context, pe PublicEndpoints) error
- func (s *Store) SetUserGroups(ctx context.Context, userUID uuid.UUID, groupUIDs []uuid.UUID) error
- func (s *Store) SetUserMongoVerifier(ctx context.Context, userID uuid.UUID, password string, encryptionKey []byte) error
- func (s *Store) StoreQueryRows(ctx context.Context, rows []PendingQueryRow) error
- func (s *Store) UpdateConnectionActivity(ctx context.Context, uid uuid.UUID) error
- func (s *Store) UpdateGrantDefinition(ctx context.Context, def *GrantDefinition) error
- func (s *Store) UpdateQueryCompletion(ctx context.Context, uid uuid.UUID, durationMs *float64, rowsAffected *int64, ...) error
- func (s *Store) UpdateServer(ctx context.Context, uid uuid.UUID, updates ServerUpdate, encryptionKey []byte) error
- func (s *Store) UpdateUser(ctx context.Context, uid uuid.UUID, updates UserUpdate) error
- func (s *Store) UpdateUserGroup(ctx context.Context, group *UserGroup) error
- func (s *Store) UpgradeAPIKeyO5LogonVerifiers(ctx context.Context, keyID uuid.UUID, plainKey string, encryptionKey []byte) error
- func (s *Store) VerifyAPIKey(ctx context.Context, plainKey string) (*APIKey, error)
- type User
- func (u *User) HasChangedPassword() bool
- func (u *User) HasRole(role string) bool
- func (u *User) IsAdmin() bool
- func (u *User) IsConnector() bool
- func (u *User) IsViewer() bool
- func (u *User) MongoData() *MongoUserData
- func (u *User) MongoSCRAMCredentials() *MongoSCRAMCredentials
- func (u *User) OracleData() *OracleUserData
- type UserGroup
- type UserGroupMember
- type UserIdentity
- type UserProtocolData
- type UserUpdate
Constants ¶
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
const ( // ApprovalPending marks a query that is parked mid-flight waiting for a // human decision. Nothing has been forwarded upstream yet. ApprovalPending = "pending" // ApprovalApproved marks a hold a second human released; the statement // was then forwarded upstream. ApprovalApproved = "approved" // ApprovalDenied marks a hold a second human rejected; the statement was // never forwarded and the client got a protocol-native error. ApprovalDenied = "denied" // ApprovalAbandoned marks a hold whose client gave up (disconnect, cancel, // grant expiry, shutdown) before anyone decided. Nothing was forwarded. // Rendered distinctly from denied everywhere — the workflow looks broken // otherwise to whoever finally clicks Approve. ApprovalAbandoned = "abandoned" )
Approval hold states. These match the queries_approval_status_check CHECK constraint exactly. Note the absence of a "timeout" state: by design a hold has no clock of its own — it ends on approve, deny, or client disconnect.
const ( MaxApprovalPatterns = 32 MaxApprovalPatternLength = 512 )
MaxApprovalPatterns and MaxApprovalPatternLength bound what an admin may store on a definition. RE2 has no catastrophic backtracking, but an unbounded pattern list is still a per-statement cost on the proxy hot path.
const ( DeviceAuthStatusPending = "pending" DeviceAuthStatusApproved = "approved" DeviceAuthStatusDenied = "denied" )
Device authorization status values, stored inside OAuthState.Metadata.
const ( GroupPublic = "public" KeyPublicHost = "host" KeyPublicPGHost = "pg.host" KeyPublicOraHost = "ora.host" KeyPublicMySQLHost = "mysql.host" KeyPublicMongoHost = "mongo.host" KeyPublicPGPort = "pg.port" KeyPublicOraPort = "ora.port" KeyPublicMySQLPort = "mysql.port" KeyPublicMongoPort = "mongo.port" // KeyPublicWebUIURL is the operator-editable Web UI / public base URL // (e.g. "https://dbbat.company.com"), reached through an HTTP ingress / // reverse proxy. Distinct from Host/PGHost/etc, which advertise the // *connection* host reached via direct / TCP load-balancer access. KeyPublicWebUIURL = "web_ui_url" )
Public endpoint parameter group and key constants.
const ( RoleAdmin = "admin" RoleViewer = "viewer" RoleConnector = "connector" )
Role constants for user authorization
const ( ControlReadOnly = "read_only" ControlBlockCopy = "block_copy" ControlBlockDDL = "block_ddl" )
Control constants for grant restrictions
const ( ProtocolPostgreSQL = "postgresql" ProtocolOracle = "oracle" ProtocolMySQL = "mysql" ProtocolMariaDB = "mariadb" ProtocolMongoDB = "mongodb" // ProtocolSSH marks a row that is an SSH bastion (a dial path), not a // grantable/connectable database target. ProtocolSSH = "ssh" )
Protocol constants for database connections
const ( KeyTypeAPI = "api" // Regular API key (dbb_ prefix) KeyTypeWeb = "web" // Web session key (web_ prefix) )
API key type constants
const ( // NotifyChannelQueries carries per-connection query events. NotifyChannelQueries = "dbbat_query_events" // NotifyChannelApprovals carries approval pending/resolved events. NotifyChannelApprovals = "dbbat_approval_events" )
PostgreSQL LISTEN/NOTIFY channels used to fan events between dbbat replicas.
dbbat runs several pods: the proxy session holding a parked query lives on replica A while the admin's stream socket lives on replica B, so the in-process broker is not enough on its own. The store connection we already have is the cheapest correct bus available — no new infrastructure, no at-most-once message broker to operate.
The payload is deliberately just the topic plus the query uid: NOTIFY payloads are capped at 8000 bytes and, more importantly, SQL text has no business traveling through the database's notification queue where it would be visible to anything with LISTEN privileges. Receivers re-read the row.
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 )
const DeviceAuthProvider = "device"
DeviceAuthProvider namespaces oauth_states rows used for the OAuth 2.0 Device Authorization Grant (RFC 8628) handshake, so they can share the table with real OAuth CSRF states without ever colliding: /auth/:provider routes are only registered for configured providers, never "device".
const DeviceAuthTTL = 10 * time.Minute
DeviceAuthTTL bounds how long a device authorization request stays valid (RFC 8628 expires_in).
const (
IdentityTypeSlack = "slack"
)
Identity provider constants
const InstanceHeartbeatInterval = 30 * time.Second
InstanceHeartbeatInterval is how often a running process refreshes its instances row. Short — the whole point of the registry is to notice that a process is gone reasonably soon after it dies, and the write is a single primary-key upsert on a table with one row per replica.
const InstanceStaleAfter = 30 * InstanceHeartbeatInterval
InstanceStaleAfter is how long an instances row may go un-refreshed before its owner is treated as dead and its still-open connections are reclaimed by whoever starts next.
Deliberately 30 missed heartbeats, not two or three. This grace period is the only thing standing between the reclaim and the failure mode the instance scoping exists to prevent: closing a *live* replica's connections, which then satisfy the retention sweep's cutoff predicate and can be deleted while a session is still writing queries against them. The cost of being wrong in that direction is data loss and a foreign-key error in a live session; the cost of being wrong in the other direction is that a dead pod's rows linger until some instance restarts more than 15 minutes later. That asymmetry is why the multiplier is generous.
15 minutes also comfortably covers a rolling upgrade window: replicas still running a build that predates the instances table never heartbeat, and their seeded rows (see the 20260803030000_instances migration) must not go stale before the rollout has replaced them.
const MongoSCRAMIterations = 15000
MongoSCRAMIterations is the PBKDF2 iteration count used when deriving a MongoDB SCRAM-SHA-256 verifier from a user's password — MongoDB's own default for SCRAM-SHA-256.
const RetentionBatchSize = 1000
RetentionBatchSize bounds a single DELETE statement issued by the retention sweep. The sweep loops until nothing old is left, so this only caps how much one statement locks and how much WAL it writes at once — which matters on the very first run against a store that has been accumulating forever.
Variables ¶
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
var ( // ErrTooManyApprovalPatterns is returned when a definition carries more // than MaxApprovalPatterns patterns. ErrTooManyApprovalPatterns = errors.New("too many approval patterns") // ErrApprovalPatternTooLong is returned for a pattern over // MaxApprovalPatternLength characters. ErrApprovalPatternTooLong = errors.New("approval pattern too long") // ErrApprovalPatternEmpty is returned for a blank pattern, which would // match every statement — almost certainly a mistake, never a policy. ErrApprovalPatternEmpty = errors.New("approval pattern must not be empty") // ErrQueryNotPending is returned when resolving a query that is not (or // is no longer) in the pending state. ErrQueryNotPending = errors.New("query is not pending approval") )
Approval validation errors, surfaced as 400s by the API layer.
var ( ErrDeviceAuthNotFound = errors.New("device authorization request not found or expired") ErrDeviceAuthAlreadyResolved = errors.New("device authorization request already responded to") ErrDeviceAuthUserCodeTaken = errors.New("user code already in use") )
Device authorization errors.
var ( ErrUserNotFound = errors.New("user not found") ErrServerNotFound = 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") // ErrServerViaNotSSH is returned when via_uid points at a row whose protocol // is not 'ssh' — only SSH bastions can be tunneled through. ErrServerViaNotSSH = errors.New("via_uid must reference an ssh server") // ErrServerViaCycle is returned when a via_uid chain loops back on itself. ErrServerViaCycle = errors.New("via_uid chain forms a cycle") // ErrServerNameConflict is returned when creating or renaming a server to a // name that is already taken (violates the servers_name_key unique constraint). ErrServerNameConflict = errors.New("a server with this name already exists") // ErrUserNameConflict is returned when creating a user whose username is // already taken by an active (non-soft-deleted) user (violates the // users_username_active_uq unique index). ErrUserNameConflict = errors.New("a user with this username already exists") )
Store errors.
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.
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.
var ErrGrantDefinitionNotFound = errors.New("grant definition not found")
ErrGrantDefinitionNotFound is returned when a grant definition lookup misses.
var ErrGrantRequestNotFound = errors.New("grant request not found")
ErrGrantRequestNotFound is returned when a request UID misses.
var ErrInvalidTransition = errors.New("grant request not pending")
ErrInvalidTransition is returned when a state transition is rejected because the request is not in `pending`.
var ErrParameterNotFound = errors.New("parameter not found")
ErrParameterNotFound is returned when no matching active parameter exists.
var ErrUserGroupDuplicate = errors.New("user group with this name already exists")
ErrUserGroupDuplicate is returned when a group name collides (case insensitively) with an existing group.
var ErrUserGroupNotFound = errors.New("user group not found")
ErrUserGroupNotFound is returned when a group lookup misses.
var ValidControls = []string{ ControlReadOnly, ControlBlockCopy, ControlBlockDDL, }
ValidControls lists all valid control values
Functions ¶
func ExtractSourceIP ¶
ExtractSourceIP extracts the IP address from a net.Addr
func IsMySQLFamily ¶ added in v0.7.0
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.
func ValidateApprovalPatterns ¶ added in v0.20.0
ValidateApprovalPatterns compiles every pattern so a bad regexp is rejected at definition-save time rather than blowing up on the proxy hot path. The compiled forms are discarded — the proxy compiles its own copy once per session — this is purely a gate.
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"`
// ProtocolData holds protocol-specific material (Oracle O5LOGON verifiers,
// etc.) in a single jsonb column rather than dedicated per-protocol columns.
// nil when the key has no protocol-specific data.
ProtocolData *ProtocolData `bun:"protocol_data,type:jsonb,nullzero" json:"-"`
}
APIKey represents an API key for authentication
func (*APIKey) IsWebSession ¶
IsWebSession returns true if this is a web session key
func (*APIKey) OracleData ¶ added in v0.13.0
func (k *APIKey) OracleData() *OracleAPIKeyData
OracleData returns the key's Oracle protocol material, or nil if it has none.
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"`
// ApprovalPatterns are RE2 patterns that suspend a matching statement
// until an approver resolves it. Mirrored from the grant definition at
// materialization time — exactly like Controls / MaxQueryCounts — because
// the proxy session holds nothing but a *Grant, and admin-created grants
// bypass definitions entirely.
ApprovalPatterns []string `bun:"approval_patterns,array,notnull,default:'{}'" json:"approval_patterns"`
// ApproverGroupUIDs lists groups whose members may resolve holds on this
// grant, in addition to admins. Empty = admins only.
ApproverGroupUIDs []uuid.UUID `bun:"approver_group_uids,array,notnull,default:'{}'" json:"approver_group_uids"`
// 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) MayApprove ¶ added in v0.20.0
func (g *AccessGrant) MayApprove(userGroupUIDs []uuid.UUID) bool
MayApprove reports whether a user in the given groups may resolve holds on this grant. Admin-ness is checked by the caller (any admin may approve any hold); this covers the definition-scoped approver groups only.
func (*AccessGrant) RequiresApproval ¶ added in v0.20.0
func (g *AccessGrant) RequiresApproval() bool
RequiresApproval reports whether the grant carries any approval pattern at all. Cheap pre-check so the common case (no patterns) never compiles or matches anything.
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 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"`
// InstanceID is the dbbat process that opened this connection. It scopes
// the startup reconcile (Store.CloseOrphanedConnections) so a replica can
// never close another replica's live sessions. Internal bookkeeping, not
// part of the API surface — hence json:"-".
InstanceID string `bun:"instance_id,notnull,default:''" json:"-"`
}
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 ¶
DSNComponents holds parsed PostgreSQL DSN components for comparison
type DeviceAuthRequest ¶ added in v0.19.0
type DeviceAuthRequest struct {
UID uuid.UUID
ClientName string
UserCode string // canonical (dashless, uppercase)
Status string
ExpiresAt time.Time
}
DeviceAuthRequest is the store-level view of a pending or resolved device authorization request, without any secret material (device code, encrypted key) — safe to hand to callers that only need to display or check status.
type EventNotification ¶ added in v0.20.0
type EventNotification struct {
Topic string `json:"topic"`
Type string `json:"type"`
QueryUID uuid.UUID `json:"query_uid,omitempty"`
ConnUID uuid.UUID `json:"conn_uid,omitempty"`
}
EventNotification is the wire payload of a cross-replica notification.
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"`
// AutoApprove, when set, makes grant requests against this definition
// bypass the pending/admin-approval step: the request is approved and
// the grant materialized instantly at request time.
AutoApprove bool `bun:"auto_approve,notnull,default:false" json:"auto_approve"`
// GroupUIDs restricts which users may request this definition: a user
// must belong to at least one of the listed groups. Empty = every user
// (the pre-scoping behavior, which every existing definition keeps).
//
// Stored as an array on the definition rather than a join table on
// purpose: an empty scope means "everyone", so a cascade-on-delete join
// table would fail *open* when a group is deleted. A dangling uid here
// matches nobody, so the definition fails closed until an admin fixes it.
GroupUIDs []uuid.UUID `bun:"group_uids,array,notnull,default:'{}'" json:"group_uids"`
// DatabaseUIDs restricts which databases this definition can be
// requested against. Empty = every database.
DatabaseUIDs []uuid.UUID `bun:"database_uids,array,notnull,default:'{}'" json:"database_uids"`
// ApprovalPatterns are SQL patterns (RE2) that suspend a matching
// statement until an admin or an approver-group member approves it.
// Empty = no approval gating. Validated at save time so a bad pattern is
// a 400 rather than a runtime surprise on the proxy hot path.
ApprovalPatterns []string `bun:"approval_patterns,array,notnull,default:'{}'" json:"approval_patterns"`
// ApproverGroupUIDs lists groups whose members may resolve holds on
// grants built from this definition, *in addition to* admins.
// Empty = admins only.
ApproverGroupUIDs []uuid.UUID `bun:"approver_group_uids,array,notnull,default:'{}'" json:"approver_group_uids"`
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.
func (*GrantDefinition) AppliesTo ¶ added in v0.18.0
AppliesTo reports whether this definition can be requested by a user in the given groups against the given database. Both scopes must pass; an empty scope on either axis is unrestricted, which is what keeps every pre-existing (unscoped) definition behaving exactly as before.
func (*GrantDefinition) AppliesToDatabase ¶ added in v0.18.0
func (d *GrantDefinition) AppliesToDatabase(databaseUID uuid.UUID) bool
AppliesToDatabase reports whether the given database is within this definition's database scope. An empty scope applies to every database.
func (*GrantDefinition) AppliesToGroups ¶ added in v0.18.0
func (d *GrantDefinition) AppliesToGroups(userGroupUIDs []uuid.UUID) bool
AppliesToGroups reports whether a user belonging to the given groups is within this definition's group scope. An empty scope applies to everyone.
type GrantDefinitionFilter ¶ added in v0.10.0
type GrantDefinitionFilter struct {
ActiveOnly bool
}
GrantDefinitionFilter narrows ListGrantDefinitions queries.
type GrantFilter ¶
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 Instance ¶ added in v0.20.0
type Instance struct {
bun.BaseModel `bun:"table:instances,alias:i"`
InstanceID string `bun:"instance_id,pk" json:"instance_id"`
StartedAt time.Time `bun:"started_at,notnull,default:current_timestamp" json:"started_at"`
LastSeenAt time.Time `bun:"last_seen_at,notnull,default:current_timestamp" json:"last_seen_at"`
}
Instance is one dbbat process sharing this store. The row is upserted at startup, refreshed by a heartbeat, and deleted on a clean shutdown, so its presence and freshness answer "is this process still alive?" — which is what lets the startup reconcile reclaim the connections of an instance that is provably gone. See Store.CloseOrphanedConnections.
type MigrationInfo ¶
MigrationInfo contains information about a migration
type MongoDatabaseData ¶ added in v0.16.0
type MongoDatabaseData struct {
// AuthSource is the upstream SCRAM authSource; empty defers to
// MongoAuthSourceOrDefault's "admin" default.
AuthSource string `json:"auth_source,omitempty"`
}
MongoDatabaseData holds MongoDB-specific per-database settings.
type MongoSCRAMCredentials ¶ added in v0.16.0
type MongoSCRAMCredentials struct {
Salt []byte `json:"salt,omitempty"`
Iterations int `json:"iterations,omitempty"`
StoredKey []byte `json:"stored_key,omitempty"`
ServerKey []byte `json:"server_key,omitempty"`
}
MongoSCRAMCredentials are the SCRAM-SHA-256 stored credentials derived from the user's password (RFC 5802 / RFC 7677). Salt and Iterations are public challenge material; StoredKey and ServerKey are password-equivalent secrets and are encrypted at rest with the dbbat master key (AAD-bound to the user UID), mirroring the encrypted Oracle O5LOGON verifiers.
type MongoUserData ¶ added in v0.16.0
type MongoUserData struct {
SCRAMSHA256 *MongoSCRAMCredentials `json:"scram_sha256,omitempty"`
}
MongoUserData holds the per-user MongoDB SCRAM verifier material, letting a client authenticate to the proxy with the driver-default SCRAM-SHA-256 (which keeps the cleartext password off the wire) instead of being forced onto authMechanism=PLAIN. Populated lazily whenever the user's password is set after this feature shipped; absent otherwise (PLAIN stays the fallback).
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
// InstanceID identifies this dbbat process among the replicas sharing the
// store. It is stamped on every connection row this process opens, which is
// what lets CloseOrphanedConnections reconcile *its own* leftovers without
// touching another replica's live sessions. Empty disables that reconcile
// rather than widening it.
InstanceID string
}
Options configures Store creation.
type OracleAPIKeyData ¶ added in v0.13.0
type OracleAPIKeyData struct {
O5LogonSalt6949 []byte `json:"o5logon_salt_6949,omitempty"`
O5LogonVerifier6949 []byte `json:"o5logon_verifier_6949,omitempty"`
O5LogonSalt18453 []byte `json:"o5logon_salt_18453,omitempty"`
O5LogonVerifier18453 []byte `json:"o5logon_verifier_18453,omitempty"`
// UserSalt records which salt scheme derived the verifiers above:
// true = the USER's shared salts (users.protocol_data.oracle), so this key
// is a login candidate alongside the user's other user-salt keys; false /
// absent = legacy per-key random salts (only usable when it is the single
// key the challenge was built from). The salts are duplicated here either
// way so the challenge path never needs a user-row lookup.
UserSalt bool `json:"user_salt,omitempty"`
}
OracleAPIKeyData is the Oracle O5LOGON verifier material derived from the API key for the proxy's terminated authentication. Both verifier types are kept: 6949 (legacy SHA-1) and 18453 (12c PBKDF2/HMAC-SHA512). Verifier values are encrypted with the dbbat master key (AAD-bound to the key prefix); salts are public challenge material. Empty fields are omitted from the jsonb.
type OracleUserData ¶ added in v0.15.4
type OracleUserData struct {
O5LogonUserSalt6949 []byte `json:"o5logon_user_salt_6949,omitempty"`
O5LogonUserSalt18453 []byte `json:"o5logon_user_salt_18453,omitempty"`
}
OracleUserData holds the per-USER O5LOGON salts. Every API key created for the user derives its O5LOGON verifiers from these shared salts (instead of per-key random salts), so the Oracle proxy can commit to one salt in the AUTH challenge while keeping ALL of the user's keys as login candidates. Salts are public challenge material (sent to any connecting client), so they are stored unencrypted, like the per-key salts.
type OrphanedConnections ¶ added in v0.20.0
type OrphanedConnections struct {
// Own is the number of connections this instance id left open itself.
Own int64
// Reclaimed is the number of connections closed on behalf of instances that
// are provably gone — deregistered, or past InstanceStaleAfter.
Reclaimed int64
}
OrphanedConnections counts what one startup reconcile closed, split by whose rows they were. The two numbers mean very different things operationally: Own is this instance's own previous run not shutting down cleanly, Reclaimed is *another* process having died without shutting down at all.
func (OrphanedConnections) Total ¶ added in v0.20.0
func (o OrphanedConnections) Total() int64
Total is the number of connection rows the reconcile closed.
type PendingQueryRow ¶ added in v0.20.0
type PendingQueryRow struct {
QueryID uuid.UUID
RowNumber int
RowData json.RawMessage
RowSizeBytes int64
}
PendingQueryRow is a captured row on its way to storage. Unlike QueryRow it carries its own parent query id, so a single INSERT can cover rows belonging to different queries — which is what lets one process-wide writer batch across concurrent sessions instead of one batch per query.
type ProtocolData ¶ added in v0.13.0
type ProtocolData struct {
Oracle *OracleAPIKeyData `json:"oracle,omitempty"`
}
ProtocolData is the per-protocol material attached to an API key, stored as a single jsonb column so protocol-specific fields don't proliferate as table columns. Absent protocols are omitted.
type PublicEndpoints ¶ added in v0.11.0
type PublicEndpoints struct {
Host string // default public hostname for all protocols (connection host)
PGHost string // optional override; "" = fall back to Host
OraHost string
MySQLHost string
MongoHost string
PGPort *int // optional override; nil = fall back to local listen port
OraPort *int
MySQLPort *int
MongoPort *int
// WebUIURL is the operator-configured public base URL for the Web UI /
// REST API (e.g. "https://dbbat.company.com"), used for Slack deep-links
// and absolute-URL generation. Independent of Host: the UI is typically
// reached through an HTTP ingress while Host is reached via TCP
// load-balancer / direct access.
WebUIURL string
}
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
// ResultsTruncated is true when result capture stopped on a storage limit
// (max_result_rows / max_result_bytes). The rows that were captured before
// the limit are still stored, so this is what tells a short — or empty —
// row set apart from a query that genuinely returned that much.
ResultsTruncated bool `bun:"results_truncated,notnull,default:false" json:"results_truncated"`
// ResultsDropped is true when dbbat lost rows it meant to keep: the
// batched row writer's queue was full (the store fell behind the proxy) or
// a batch insert failed. It is deliberately distinct from
// ResultsTruncated — truncation is an expected, configured prefix, a drop
// is dbbat failing to keep up — and the two are never conflated.
ResultsDropped bool `bun:"results_dropped,notnull,default:false" json:"results_dropped"`
// Approval hold fields. ApprovalStatus is nil for the overwhelming
// majority of queries (no pattern matched); when set it is one of
// ApprovalPending / ApprovalApproved / ApprovalDenied / ApprovalAbandoned.
// There is deliberately no "timeout" state — see docs/approvals.md.
ApprovalStatus *string `bun:"approval_status" json:"approval_status,omitempty"`
ApprovalPattern *string `bun:"approval_pattern" json:"approval_pattern,omitempty"`
ResolvedBy *uuid.UUID `bun:"resolved_by,type:uuid" json:"resolved_by,omitempty"`
ResolvedAt *time.Time `bun:"resolved_at" json:"resolved_at,omitempty"`
ResolutionReason *string `bun:"resolution_reason" json:"resolution_reason,omitempty"`
// Joined fields populated only by ListQueries (via a JOIN on connections);
// not stored on the queries table itself.
UserID *uuid.UUID `bun:"user_id,scanonly" json:"user_id,omitempty"`
DatabaseID *uuid.UUID `bun:"database_id,scanonly" json:"database_id,omitempty"`
}
Query represents a query execution record
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 ¶
QueryWithRows combines a query with its result rows
type ResolvedEndpoints ¶ added in v0.11.0
type ResolvedEndpoints struct {
PGHost string
OraHost string
MySQLHost string
MongoHost string
PGPort int // 0 = protocol disabled
OraPort int
MySQLPort int
MongoPort int
// WebUIURL is the effective Web UI / public base URL: pe.WebUIURL when
// set, else cfg.PublicURL (the DBB_PUBLIC_URL env var).
WebUIURL string
}
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 RetentionSweepResult ¶ added in v0.20.0
type RetentionSweepResult struct {
// Connections is the number of closed connection records deleted.
Connections int64
// Queries is the number of query records deleted directly (i.e. queries on
// connections that survived the sweep).
Queries int64
}
RetentionSweepResult reports what a retention sweep removed.
Queries counts only queries deleted directly. Queries belonging to a reaped connection go away through the connection's ON DELETE CASCADE and are counted under Connections instead — as are all of their query_rows.
type SSHServerData ¶ added in v0.17.0
type SSHServerData struct {
PrivateKeyEncrypted []byte `json:"private_key_encrypted,omitempty"`
PassphraseEncrypted []byte `json:"passphrase_encrypted,omitempty"`
KnownHostKey string `json:"known_host_key,omitempty"`
// PrivateKey / Passphrase are the decrypted, in-memory-only forms (never
// serialized to jsonb — omitted via the encrypted round-trip helpers).
PrivateKey string `json:"-"`
Passphrase string `json:"-"`
}
SSHServerData holds the material for an SSH bastion row. The private key and passphrase are password-equivalent secrets, encrypted at rest with the dbbat master key (AAD-bound to the server UID), mirroring the encrypted database password. KnownHostKey is the bastion's public host key, learned on first connect (TOFU) and verified on every subsequent connect; it is public challenge material, stored in clear and surfaced read-only in the API/UI.
type Server ¶ added in v0.17.0
type Server struct {
bun.BaseModel `bun:"table:servers,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 is the target database name; nullable/empty for SSH bastions.
DatabaseName string `bun:"database_name" 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 is meaningful for database targets only; nullable for SSH bastions.
SSLMode string `bun:"ssl_mode" json:"ssl_mode"`
Protocol string `bun:"protocol,notnull,default:'postgresql'" json:"protocol"`
OracleServiceName *string `bun:"oracle_service_name" json:"oracle_service_name,omitempty"`
// ViaUID references an SSH server row to tunnel through; nil = direct dial.
ViaUID *uuid.UUID `bun:"via_uid,type:uuid" json:"via_uid,omitempty"`
// ProtocolData holds protocol-specific per-server settings (MongoDB upstream
// authSource, SSH key material, etc.) in a single generic jsonb column —
// mirroring User.ProtocolData — rather than a dedicated column per setting.
ProtocolData *ServerProtocolData `bun:"protocol_data,type:jsonb,nullzero" json:"-"`
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:"-"`
}
Server represents a target dbbat knows how to reach: a database target (protocol postgresql|oracle|mysql|mariadb|mongodb) or an SSH bastion (protocol ssh). Both share the same storage shape — host, port, username, encrypted secret — with the protocol column as discriminator. ViaUID, when set, points at an SSH server row: "dial this server through that bastion".
func (*Server) DecryptPassword ¶ added in v0.17.0
DecryptPassword decrypts a database password using AAD bound to the database UID.
func (*Server) DecryptSSHSecrets ¶ added in v0.17.0
DecryptSSHSecrets decrypts the SSH private key and passphrase into the in-memory PrivateKey/Passphrase fields (AAD-bound to the server UID). No-op when the server has no SSH material.
func (*Server) IsSSH ¶ added in v0.17.0
IsSSH reports whether this server row is an SSH bastion rather than a database target.
func (*Server) MongoAuthSourceOrDefault ¶ added in v0.17.0
MongoAuthSourceOrDefault returns the upstream MongoDB SCRAM authSource configured for this database, defaulting to "admin" (the MongoDB convention where service/root users are created) when unset.
func (*Server) MongoData ¶ added in v0.17.0
func (db *Server) MongoData() *MongoDatabaseData
MongoData returns the server's MongoDB protocol material, or nil if absent.
func (*Server) SSHData ¶ added in v0.17.0
func (db *Server) SSHData() *SSHServerData
SSHData returns the server's SSH protocol material, or nil if absent.
type ServerProtocolData ¶ added in v0.17.0
type ServerProtocolData struct {
MongoDB *MongoDatabaseData `json:"mongodb,omitempty"`
SSH *SSHServerData `json:"ssh,omitempty"`
}
ServerProtocolData is per-protocol material attached to a server, stored as a single jsonb column so protocol-specific settings don't proliferate as table columns — mirrors UserProtocolData. Absent protocols are omitted.
type ServerUpdate ¶ added in v0.17.0
type ServerUpdate struct {
Description *string
Host *string
Port *int
DatabaseName *string
Username *string
Password *string // Plaintext password to encrypt
SSLMode *string
Protocol *string
OracleServiceName *string
MongoAuthSource *string
Listable *bool
ViaUID *uuid.UUID // Set to tunnel through an SSH server
ClearViaUID bool // When true, clears via_uid (direct dial)
// SSH secrets (plaintext, to encrypt). Set on SSH server rows.
SSHPrivateKey *string
SSHPassphrase *string
}
ServerUpdate represents fields that can be updated
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store provides access to the database
func (*Store) AddUserToGroup ¶ added in v0.18.0
AddUserToGroup adds a membership. Idempotent: re-adding is a no-op.
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) AutoApproveGrantRequest ¶ added in v0.17.0
func (s *Store) AutoApproveGrantRequest(ctx context.Context, uid, requesterID uuid.UUID) (*Grant, *GrantRequest, error)
AutoApproveGrantRequest is like ApproveGrantRequest but for definitions flagged AutoApprove: there is no human decider, so decided_by is left NULL (nobody decided — the definition's policy did). The resulting grant still needs a non-nil granted_by column, so it's attributed to the requester themselves (a self-service grant, not an admin-approved one — the audit trail's `via: auto_approve` marker is what distinguishes it).
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
CleanupExpiredOAuthStates removes all expired OAuth states.
func (*Store) CleanupOldQueryRows ¶ added in v0.20.0
func (s *Store) CleanupOldQueryRows(ctx context.Context, olderThan time.Duration) (RetentionSweepResult, error)
CleanupOldQueryRows deletes query history older than olderThan, together with the result rows captured for it. A zero or negative duration is a no-op: retention is opt-in, and the default is to keep history forever.
The delete is driven from the parent rows, not from query_rows: both query_rows.query_id -> queries.uid and queries.connection_id -> connections.uid are ON DELETE CASCADE, so removing a query removes its rows and removing a connection removes its queries and their rows.
Two sweeps run, in this order:
- connections closed before the cutoff — cascading to their queries and rows. This keeps the UI consistent: a closed connection never survives as an empty shell whose queries have all been reaped.
- queries executed before the cutoff that are still attached to a connection the first sweep left alone (an open, long-lived session).
Connections that are still open (disconnected_at IS NULL) are never reaped, however old they are: the session may still be live, and deleting its record would break the foreign key for the next query it logs. Such a connection can therefore outlive all of its queries and show up in the UI with none left — its `queries` counter is a lifetime counter, not a count of retained rows.
func (*Store) CloseConnection ¶
CloseConnection sets the disconnected_at timestamp
func (*Store) CloseOrphanedConnections ¶ added in v0.20.0
func (s *Store) CloseOrphanedConnections(ctx context.Context) (OrphanedConnections, error)
CloseOrphanedConnections stamps disconnected_at on every connection left open by a process that is no longer running — this instance's own previous run, plus any other instance the registry proves is gone — and reports the two counts separately. Call it once at startup, before the proxies begin accepting, and after RegisterInstance.
Why it is needed: disconnected_at is otherwise only ever written by CloseConnection, on a clean session teardown. A crash, a kill or a pod reschedule skips that, so those rows keep disconnected_at NULL forever. The retention sweep (CleanupOldQueryRows) only reaps connections with disconnected_at IS NOT NULL — deleting a row a live session still logs against would break the foreign key — so an orphan survives every sweep, outlives all of its queries, and keeps counting as "currently connected".
Why it is not a blanket UPDATE ... WHERE disconnected_at IS NULL: dbbat is deployed with more than one replica against a shared store (see docs/approvals.md, "Multiple replicas", and charts/dbbat/values.yaml). A blanket update would let a starting replica mark another replica's *live* connections as disconnected. That is not cosmetic — those rows would immediately satisfy the retention sweep's cutoff predicate, so the sweep could delete a connection a live session is still writing queries against.
The test that replaces the blanket update is liveness, not identity. A running process upserts a row in `instances` at startup and refreshes it every InstanceHeartbeatInterval; a clean shutdown deletes it. So another instance's connections are only touched when that instance has no row at all (it shut down cleanly, or never registered) or has not been seen for InstanceStaleAfter — 30 missed heartbeats. A live replica is therefore never a candidate: it would have to fail every heartbeat for a quarter of an hour while still serving traffic.
Legacy rows carrying an empty instance id — created before the instance_id column existed — are folded into the "no instances row" case rather than being given a separate opt-in switch. That is a deliberate choice, and it is safe because the empty id can never be alive: config.resolveInstanceID guarantees a non-empty id for any serving process (hostname, else the FallbackInstanceID constant), a store with an empty instance id refuses to register or reconcile at all, and RegisterInstance refuses to write a row for it — so nothing can ever make it look fresh.
The one moment such rows could have belonged to a live session is the upgrade that introduces this liveness tracking, since no replica on the previous build can register itself. The 20260803030000_instances migration covers that by seeding the registry — the empty id included — from every instance id the connections table has recorded, which buys each of those owners a full grace period. The coverage is not total, and cannot be: an old-build replica that has never recorded a connection is not seeded, so if it accepts its first session between the migration and the next process start, that session is reclaimed through the no-registry-row branch, which by design has no grace period at all (a deleted row means a clean shutdown, and reclaiming it immediately is the point). Giving that branch a grace period would trade a window that lasts one upgrade, and only for a replica that has served nothing since the retention horizon, against permanently delaying the case this feature is built for. The window is left open knowingly.
A store whose own instance id is empty is refused outright (zero, nil). Reconciling would then treat the empty id as this process's identity, which is exactly the blanket update this design exists to prevent.
func (*Store) ConsumeOAuthState ¶ added in v0.4.0
ConsumeOAuthState retrieves and deletes an OAuth state in one operation. It only matches states that have not yet expired.
func (*Store) CountAdmins ¶ added in v0.15.0
CountAdmins returns the number of users holding the admin role
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) CreateDeviceAuthRequest ¶ added in v0.19.0
func (s *Store) CreateDeviceAuthRequest(ctx context.Context, clientName, deviceCode, userCode string) (*DeviceAuthRequest, error)
CreateDeviceAuthRequest persists a new device authorization request. deviceCode is the caller-generated secret that only the requesting client holds (it is never exposed to the browser); userCode is the short human-checkable code the approving user enters/verifies in the browser, passed in canonical form. Returns ErrDeviceAuthUserCodeTaken if a live request already holds that user code, so the caller can regenerate.
func (*Store) CreateGrant ¶
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) CreatePendingQuery ¶ added in v0.20.0
func (s *Store) CreatePendingQuery(ctx context.Context, query *Query, pattern string) (*Query, error)
CreatePendingQuery inserts a query row already marked pending, so the held statement is visible in /queries and addressable by UID *while* it hangs. The regular async-persist-on-completion path cannot provide that.
func (*Store) CreateQuery ¶
CreateQuery creates a new query record
func (*Store) CreateServer ¶ added in v0.17.0
func (s *Store) CreateServer(ctx context.Context, db *Server, encryptionKey []byte) (*Server, error)
CreateServer 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) 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) CreateUserGroup ¶ added in v0.18.0
CreateUserGroup inserts a new group. Names are unique case-insensitively.
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 ¶
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) DeactivateGrantDefinition ¶ added in v0.10.0
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) DeleteParameter ¶ added in v0.11.0
DeleteParameter soft-deletes a parameter.
func (*Store) DeleteServer ¶ added in v0.17.0
DeleteServer deletes a database
func (*Store) DeleteUser ¶
DeleteUser deletes a user and all of their linked OAuth identities.
func (*Store) DeleteUserGroup ¶ added in v0.18.0
DeleteUserGroup hard-deletes a group. Memberships cascade away; grant definition scopes do NOT — a definition scoped to the deleted group keeps the dangling uid and therefore matches nobody (fail closed) until an admin edits it.
func (*Store) DeleteUserIdentity ¶ added in v0.4.0
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) DeregisterInstance ¶ added in v0.20.0
DeregisterInstance removes this process from the registry on a clean shutdown. That is what makes the common case immediate: the next process to start sees no row for us and reclaims anything we left open straight away, instead of waiting out InstanceStaleAfter.
func (*Store) DropAllTables ¶
DropAllTables drops all application tables and types (for test mode) This should be called BEFORE migrations to ensure a fresh start
func (*Store) EnsureDefaultAdmin ¶
EnsureDefaultAdmin creates a default admin user if no users exist
func (*Store) EnsureUserOracleSalts ¶ added in v0.15.4
func (s *Store) EnsureUserOracleSalts(ctx context.Context, userID uuid.UUID) (*OracleUserData, error)
EnsureUserOracleSalts returns the user's shared O5LOGON salts, generating and persisting them lazily on first use (typically at API key creation). All of a user's API keys derive their O5LOGON verifiers from these salts so the Oracle proxy can commit to one salt in the AUTH challenge and still accept any of the user's keys as the password.
Concurrency-safe: the persist is a compare-and-set (only writes when the oracle material is still absent), and on a lost race the winner's salts are re-read so both callers converge on the same values.
func (*Store) GetAPIKeyByID ¶
GetAPIKeyByID retrieves an API key by its ID
func (*Store) GetAPIKeyByPrefix ¶
GetAPIKeyByPrefix retrieves all API keys with a given prefix Since prefix is unique, this returns at most one key
func (*Store) GetActiveGrant ¶
GetActiveGrant retrieves an active grant for a user and database
func (*Store) GetAllParameters ¶ added in v0.11.0
GetAllParameters retrieves all active parameters, optionally filtered by group.
func (*Store) GetConnectionByUID ¶ added in v0.17.0
GetConnectionByUID retrieves a single connection by UID
func (*Store) GetDeviceAuthByUserCode ¶ added in v0.19.0
func (s *Store) GetDeviceAuthByUserCode(ctx context.Context, userCode string) (*DeviceAuthRequest, error)
GetDeviceAuthByUserCode fetches a pending or resolved device authorization request by its canonical user code, for display on the consent page. Never exposes the device code or the encrypted key.
func (*Store) GetGrantByUID ¶
GetGrantByUID retrieves a grant by UID
func (*Store) GetGrantDefinition ¶ added in v0.10.0
GetGrantDefinition fetches a definition by UID. Returns ErrGrantDefinitionNotFound if the row doesn't exist.
func (*Store) GetGrantRequest ¶ added in v0.10.0
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) GetInstance ¶ added in v0.20.0
GetInstance returns one registry row, or nil when the instance is not registered. Mostly useful to tests and to operators eyeballing the registry.
func (*Store) GetParameter ¶ added in v0.11.0
GetParameter retrieves a single active parameter by group and key.
func (*Store) GetParameters ¶ added in v0.11.0
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) 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) GetQueryWithOwner ¶ added in v0.20.0
GetQueryWithOwner loads a query plus the user/database of its connection — what the approve/deny path needs to decide who may resolve it and to render the resolution event.
func (*Store) GetQueryWithRows ¶
GetQueryWithRows retrieves a query with its result rows
func (*Store) GetServerByName ¶ added in v0.17.0
GetServerByName retrieves a database by name
func (*Store) GetServerByOracleServiceName ¶ added in v0.17.0
func (s *Store) GetServerByOracleServiceName(ctx context.Context, serviceName string) (*Server, error)
GetServerByOracleServiceName retrieves an Oracle database by its service name.
CAUTION: several dbbat databases may share one upstream service name (a mutualized Oracle instance); this returns an arbitrary matching row in that case. Resolution paths that must be deterministic should use ListServersByOracleServiceName and disambiguate explicitly.
func (*Store) GetServerByUID ¶ added in v0.17.0
GetServerByUID retrieves a database by UID
func (*Store) GetUserByIdentity ¶ added in v0.4.0
GetUserByIdentity retrieves a user by their external identity (provider + provider_id).
func (*Store) GetUserByUID ¶
GetUserByUID retrieves a user by UID
func (*Store) GetUserByUsername ¶
GetUserByUsername retrieves a user by username
func (*Store) GetUserGroup ¶ added in v0.18.0
GetUserGroup fetches a group by UID.
func (*Store) GetUserIdentities ¶ added in v0.4.0
GetUserIdentities retrieves all identities for a given user.
func (*Store) GetUserIdentity ¶ added in v0.4.0
GetUserIdentity retrieves a single user identity by UID.
func (*Store) HasApproverGroups ¶ added in v0.20.0
HasApproverGroups reports whether any live grant names one of the given groups as an approver group. Used to gate subscription to the approvals/pending topic for non-admins: a user who approves nothing must not be able to watch every held statement in the fleet.
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) HeartbeatInstance ¶ added in v0.20.0
HeartbeatInstance refreshes this process's last_seen_at.
It is an upsert rather than an UPDATE on purpose: a missing row is what marks an instance as dead, so if ours ever disappears — pruned by another instance after a long enough series of failed heartbeats, or wiped by an operator — it must come back on the next tick instead of leaving our live connections reclaimable. started_at is preserved, so the row keeps describing this run.
func (*Store) IncrementAPIKeyUsage ¶
IncrementAPIKeyUsage updates the last_used_at and increments request_count
func (*Store) IncrementConnectionBytes ¶ added in v0.16.0
IncrementConnectionBytes adds bytes to bytes_transferred WITHOUT bumping the query count. Used to flush client-side bytes that are not attributable to a completed query log row — e.g. a query aborted mid-stream by a grant limit (whose response never reached the normal completion path) or the trailing response bytes of the last query, written after per-query bookkeeping ran. Persisting them keeps the grant's recomputed bytes_transferred honest across reconnects instead of undercounting.
func (*Store) IncrementConnectionStats ¶
IncrementConnectionStats increments the query count by 1 and adds bytes to bytes_transferred
func (*Store) InstanceID ¶ added in v0.20.0
InstanceID returns the identifier this process stamps on connection rows.
func (*Store) ListAPIKeys ¶
ListAPIKeys retrieves API keys with optional filters
func (*Store) ListAdminSlackUserIDs ¶ added in v0.14.0
ListAdminSlackUserIDs returns the Slack user IDs (provider_id) of every user holding the admin role who has a linked Slack identity. Used by the grant-request notifier to @-mention approvers on the pending message.
One query joins users carrying 'admin' in their roles array to their user_identities row for provider 'slack'. Soft-deleted users and identities are excluded (bun applies the soft-delete filter for the modeled UserIdentity; the users join is guarded explicitly).
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) 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 ¶
ListGrants retrieves grants with optional filters
func (*Store) ListGroupMemberUIDs ¶ added in v0.18.0
ListGroupMemberUIDs returns the user UIDs belonging to a group.
func (*Store) ListGroupMembers ¶ added in v0.18.0
ListGroupMembers returns the (non-deleted) users belonging to a group.
func (*Store) ListGroupsForUser ¶ added in v0.18.0
ListGroupsForUser returns the full group rows a user belongs to, for the user detail response and the admin UI.
func (*Store) ListListableServers ¶ added in v0.17.0
ListListableServers 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) ListPendingApprovalQueries ¶ added in v0.20.0
ListPendingApprovalQueries returns every query currently parked awaiting a decision, newest first. Backed by the partial index, so this stays cheap however large the queries table grows.
func (*Store) ListQueries ¶
ListQueries retrieves queries with optional filters
func (*Store) ListSSHServers ¶ added in v0.17.0
ListSSHServers returns every SSH bastion row (protocol = 'ssh'), for the admin SSH-server management view and the "via SSH server" selector. These rows are excluded from every grantable/connectable target listing.
func (*Store) ListServers ¶ added in v0.17.0
ListServers retrieves all database *targets* (every protocol except 'ssh'). SSH bastions are managed separately via ListSSHServers so they never leak into grantable/connectable target contexts (dropdowns, admin database list).
func (*Store) ListServersByOracleServiceName ¶ added in v0.17.0
func (s *Store) ListServersByOracleServiceName(ctx context.Context, serviceName string) ([]Server, error)
ListServersByOracleServiceName retrieves every Oracle database registered with the given upstream service name, ordered by name for determinism. Multiple dbbat logical databases can share one upstream SERVICE_NAME (e.g. several schemas of a mutualized Oracle instance behind MUTU01), so callers must handle 0, 1, or N results.
func (*Store) ListUserGroupUIDs ¶ added in v0.18.0
ListUserGroupUIDs returns the UIDs of the groups a user belongs to. This is the eligibility input for GrantDefinition.AppliesTo.
func (*Store) ListUserGroups ¶ added in v0.18.0
ListUserGroups returns every group, name-ordered.
func (*Store) ListenEvents ¶ added in v0.20.0
func (s *Store) ListenEvents(ctx context.Context, logger *slog.Logger, handler func(EventNotification)) error
ListenEvents subscribes to the cross-replica channels and invokes handler for every notification until ctx is canceled. It runs until the context ends; the underlying pgdriver listener reconnects on its own, so a database blip costs a gap in the live stream (which clients repair by refetching from REST) and nothing more.
func (*Store) LogAuditEvent ¶
func (s *Store) LogAuditEvent(ctx context.Context, event *AuditEvent) error
LogAuditEvent creates a new audit log entry
func (*Store) MatchesStorageDSN ¶
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) MigrationStatus ¶
func (s *Store) MigrationStatus(ctx context.Context) ([]MigrationInfo, error)
MigrationStatus returns the status of all migrations
func (*Store) NotifyEvent ¶ added in v0.20.0
NotifyEvent publishes a cross-replica notification. Best-effort by design: every caller sits on (or near) the proxy hot path, and a failed NOTIFY must degrade the stream on other replicas, never the database session.
func (*Store) PollDeviceAuthToken ¶ added in v0.19.0
func (s *Store) PollDeviceAuthToken(ctx context.Context, deviceCode string) (*DeviceAuthRequest, []byte, error)
PollDeviceAuthToken looks up a request by its device code (the client's secret). Terminal states (approved/denied) are consumed (the row is deleted) so the key material is delivered at most once; pending requests are left in place for the next poll. Returns ErrDeviceAuthNotFound if the device code is unknown or the request has expired.
func (*Store) PruneStaleInstances ¶ added in v0.20.0
PruneStaleInstances deletes registry rows whose owner is past the grace period. Purely housekeeping: a stale row and a missing row mean the same thing to the reconcile, so dropping it changes no decision — it just stops the table growing one row per pod name for the lifetime of the deployment.
Only call it after the reclaim has run, so the reclaim still sees the rows it is judging.
func (*Store) RegisterInstance ¶ added in v0.20.0
RegisterInstance records this process in the instance registry, resetting started_at: the row means "this process, this run".
Call it at startup, before the reconcile and before any proxy accepts. Until the row exists this process looks dead to every other replica, so the window between the first connection it opens and its registration must be zero.
An empty instance id is refused: it is not an identity, and registering it would make the legacy no-owner connection rows look alive forever.
func (*Store) RemoveUserFromGroup ¶ added in v0.18.0
RemoveUserFromGroup drops a membership. Removing a non-membership is a no-op rather than an error.
func (*Store) ResolveQueryApproval ¶ added in v0.20.0
func (s *Store) ResolveQueryApproval( ctx context.Context, uid uuid.UUID, status string, resolvedBy *uuid.UUID, reason string, ) error
ResolveQueryApproval transitions a pending query to a terminal approval state. It is a compare-and-set on approval_status = 'pending': the update affects zero rows if somebody (or some other replica) already resolved it, which is what makes double-approve and approve-after-abandon safe.
func (*Store) ResolveWebUIURL ¶ added in v0.16.0
ResolveWebUIURL returns the effective Web UI / public base URL: the operator-configured public.web_ui_url parameter when set, otherwise cfg.PublicURL. Best-effort — a store error falls back to cfg.PublicURL (or "" when cfg is nil too) rather than propagating, since callers use this for best-effort user-facing text (Slack messages, deep-links) rather than anything that should fail a request. Safe to call with a nil cfg.
func (*Store) RespondToDeviceAuthByUserCode ¶ added in v0.19.0
func (s *Store) RespondToDeviceAuthByUserCode(ctx context.Context, userCode string, userID uuid.UUID, approve bool, encryptedKey []byte, keyPrefix string) error
RespondToDeviceAuthByUserCode approves or denies a pending request, keyed by canonical user code. On approval, encryptedKey/keyPrefix are the minted dbb_ key's encrypted material, stashed until the client polls it exactly once. The update is conditioned (by internal uid) on the request still being pending and unexpired, so a request cannot be responded to twice — a losing concurrent responder gets ErrDeviceAuthAlreadyResolved.
func (*Store) Revocations ¶ added in v0.16.0
func (s *Store) Revocations() *cache.RevocationRegistry
Revocations returns the process-wide grant-revocation registry that live proxy sessions register with and the API's revoke handler signals. Always non-nil for a store built via New. Nil-safe on both a nil *Store receiver and a zero-value store (some tests build sessions without a real store); the returned registry's own methods are also nil-safe, so callers never have to nil-check.
func (*Store) RevokeAPIKey ¶
RevokeAPIKey revokes an API key
func (*Store) RevokeGrant ¶
RevokeGrant revokes a grant
func (*Store) SetAuthCache ¶ added in v0.1.0
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) SetGroupMembers ¶ added in v0.18.0
func (s *Store) SetGroupMembers(ctx context.Context, groupUID uuid.UUID, userUIDs []uuid.UUID) error
SetGroupMembers replaces a group's membership with exactly the given set of users, in one transaction so the group is never transiently empty (an empty group is a real access-control state, not a transient one).
func (*Store) SetInstanceID ¶ added in v0.20.0
SetInstanceID sets the identifier stamped on the connection rows this process opens. See Options.InstanceID.
func (*Store) SetKnownHostKey ¶ added in v0.17.0
SetKnownHostKey persists the TOFU-learned SSH host key for an SSH server row, merging into protocol_data.ssh.known_host_key without disturbing other keys.
func (*Store) SetParameter ¶ added in v0.11.0
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) SetUserGroups ¶ added in v0.18.0
SetUserGroups replaces a user's group memberships with exactly the given set, in one transaction so the user is never transiently ungrouped.
func (*Store) SetUserMongoVerifier ¶ added in v0.16.0
func (s *Store) SetUserMongoVerifier(ctx context.Context, userID uuid.UUID, password string, encryptionKey []byte) error
SetUserMongoVerifier derives and persists a MongoDB SCRAM-SHA-256 verifier for the user from their plaintext password, letting them authenticate to the MongoDB proxy with the driver-default SCRAM-SHA-256 instead of PLAIN. The StoredKey/ServerKey are encrypted at rest (AAD-bound to the user UID); salt and iteration count are public. Called on every password set; other protocol material in protocol_data is preserved. A row lock serializes concurrent protocol_data writers (e.g. Oracle salt generation).
func (*Store) StoreQueryRows ¶
func (s *Store) StoreQueryRows(ctx context.Context, rows []PendingQueryRow) error
StoreQueryRows stores captured result rows in a single bulk INSERT.
Each row carries its own QueryID rather than the whole slice sharing one, so a batch may span several queries — and therefore several concurrent sessions. That is what lets a single process-wide writer amortize the round-trip across a busy proxy instead of issuing one INSERT per query.
Every referenced query must already exist: query_rows.query_id is a foreign key, so the caller is responsible for creating the parent row first.
func (*Store) UpdateConnectionActivity ¶
UpdateConnectionActivity updates the last_activity_at timestamp
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, resultsTruncated bool, resultsDropped bool, ) error
UpdateQueryCompletion updates a query with duration, rows affected, and error.
resultsTruncated and resultsDropped are written unconditionally (unlike the pointer arguments, which are only written when set): protocols that persist the row before the result set is read only learn about a capture limit — or about rows the writer had to drop — here.
func (*Store) UpdateServer ¶ added in v0.17.0
func (s *Store) UpdateServer(ctx context.Context, uid uuid.UUID, updates ServerUpdate, encryptionKey []byte) error
UpdateServer updates a database. Returns ErrTargetMatchesStorage if the update would cause the target to match the DBBat storage database.
func (*Store) UpdateUser ¶
UpdateUser updates a user
func (*Store) UpdateUserGroup ¶ added in v0.18.0
UpdateUserGroup mutates the editable fields of a group.
func (*Store) UpgradeAPIKeyO5LogonVerifiers ¶ added in v0.16.0
func (s *Store) UpgradeAPIKeyO5LogonVerifiers(ctx context.Context, keyID uuid.UUID, plainKey string, encryptionKey []byte) error
UpgradeAPIKeyO5LogonVerifiers migrates a legacy per-key-salt O5LOGON key to the user's shared salts, so it joins the user's other keys as an interchangeable Oracle login candidate — without forcing the user to rotate the key. It re-derives both verifiers (6949 + 18453) from the plaintext key and the user's shared salts and persists the refreshed protocol_data.
Intended to be called best-effort (fire-and-forget, like IncrementAPIKeyUsage) on a successful Oracle login, while the proxy still holds the plaintext key. It is a no-op (returns nil) when no encryption key is available, when the key has been revoked/deleted, or when the key already uses the user's salts — so a repeat login on an already-upgraded key costs only a single read.
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:"-"`
// ProtocolData holds protocol-specific per-user material (Oracle O5LOGON
// user salts, etc.) in a single generic jsonb column — mirroring
// APIKey.ProtocolData — rather than protocol-specific user columns.
// nil until first needed (populated lazily at API key creation).
ProtocolData *UserProtocolData `bun:"protocol_data,type:jsonb,nullzero" json:"-"`
}
User represents a DBBat user
func (*User) HasChangedPassword ¶
HasChangedPassword returns true if the user has changed their initial password
func (*User) IsConnector ¶
IsConnector returns true if the user has the connector role
func (*User) MongoData ¶ added in v0.16.0
func (u *User) MongoData() *MongoUserData
MongoData returns the user's MongoDB protocol material, or nil if absent.
func (*User) MongoSCRAMCredentials ¶ added in v0.16.0
func (u *User) MongoSCRAMCredentials() *MongoSCRAMCredentials
MongoSCRAMCredentials returns the user's stored MongoDB SCRAM-SHA-256 credentials, or nil when the user has no stored verifier (so the MongoDB proxy falls back to PLAIN for them).
func (*User) OracleData ¶ added in v0.15.4
func (u *User) OracleData() *OracleUserData
OracleData returns the user's Oracle protocol material, or nil if absent.
type UserGroup ¶ added in v0.18.0
type UserGroup struct {
bun.BaseModel `bun:"table:user_groups,alias:ug"`
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"`
CreatedBy *uuid.UUID `bun:"created_by,type:uuid" json:"created_by,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
UserGroup is an organizational grouping of users (data-analysts, SRE, …), deliberately kept apart from User.Roles, which are functional (admin/viewer/connector). Groups exist to scope grant definitions.
type UserGroupMember ¶ added in v0.18.0
type UserGroupMember struct {
bun.BaseModel `bun:"table:user_group_members,alias:ugm"`
GroupUID uuid.UUID `bun:"group_uid,pk,type:uuid" json:"group_uid"`
UserUID uuid.UUID `bun:"user_uid,pk,type:uuid" json:"user_uid"`
}
UserGroupMember is the group ↔ user join row. Membership *is* a join table (queried in both directions), and cascading deletes are safe here precisely because definition scope does not live in it.
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 UserProtocolData ¶ added in v0.15.4
type UserProtocolData struct {
Oracle *OracleUserData `json:"oracle,omitempty"`
MongoDB *MongoUserData `json:"mongodb,omitempty"`
}
UserProtocolData is the per-protocol material attached to a user, stored as a single jsonb column so protocol-specific fields don't proliferate as table columns. Absent protocols are omitted.
type UserUpdate ¶
UserUpdate represents fields that can be updated