Documentation
¶
Overview ¶
Package store provides database access and persistence for DBBat.
Index ¶
- Constants
- Variables
- func AutoPriority(controls []string) int16
- func DecryptO5LogonVerifier(encVerifier, encryptionKey []byte, keyPrefix string) ([]byte, error)
- func ExtractSourceIP(addr net.Addr) string
- func IsMySQLFamily(protocol string) bool
- func IsTunnelProtocol(protocol string) bool
- func IsValidServerName(name string) bool
- func ResolvePriority(explicit int16, controls []string) int16
- func ValidateApprovalPatterns(patterns []string) error
- func ValidateSampleQueries(queries []string) error
- type APIKey
- func (k *APIKey) DecryptedO5LogonVerifier6949(encryptionKey []byte) ([]byte, error)
- func (k *APIKey) IsExpired() bool
- func (k *APIKey) IsRevoked() bool
- func (k *APIKey) IsValid() bool
- func (k *APIKey) IsWebSession() bool
- func (k *APIKey) OracleData() *OracleAPIKeyData
- func (k *APIKey) OracleLoginCapable(encryptionKey []byte) bool
- type APIKeyFilter
- type AccessGrant
- func (g *AccessGrant) ApprovalPatterns() []string
- func (g *AccessGrant) ApproverUserGroupUIDs() []uuid.UUID
- func (g *AccessGrant) Controls() []string
- func (g *AccessGrant) HasControl(control string) bool
- func (g *AccessGrant) IsReadOnly() bool
- func (g *AccessGrant) MaxBytesTransferred() *int64
- func (g *AccessGrant) MaxQueryCounts() *int64
- func (g *AccessGrant) MayApprove(userGroupUIDs []uuid.UUID) bool
- func (g *AccessGrant) RequiresApproval() bool
- func (g *AccessGrant) ShouldBlockCopy() bool
- func (g *AccessGrant) ShouldBlockDDL() bool
- type ApproverKind
- type AuditChainResult
- type AuditEvent
- type AuditFilter
- type AuditLog
- type ChainBreak
- type Connection
- type ConnectionFilter
- type ConnectionOption
- type DSNComponents
- type DeviceAuthRequest
- type EventNotification
- type GlobalParameter
- type Grant
- type GrantDefinition
- func (d *GrantDefinition) AppliesTo(userGroupUIDs, serverGroupUIDs []uuid.UUID) bool
- func (d *GrantDefinition) AppliesToServerGroups(serverGroupUIDs []uuid.UUID) bool
- func (d *GrantDefinition) AppliesToUserGroups(userGroupUIDs []uuid.UUID) bool
- func (d *GrantDefinition) IsLive() bool
- func (d *GrantDefinition) MatchingServerGroup(serverGroupUIDs []uuid.UUID) *uuid.UUID
- type GrantDefinitionFilter
- type GrantDefinitionInUseError
- type GrantFilter
- type GrantRequest
- type GrantRequestFilter
- type GrantRequestStatus
- type Instance
- type KubernetesServerData
- type MigrationInfo
- type MongoDatabaseData
- type MongoSCRAMCredentials
- type MongoUserData
- type OAuthState
- type Options
- type OracleAPIKeyData
- type OracleServiceNameConflict
- type OracleServiceNameConflictServer
- type OracleUserData
- type OrphanedConnections
- type PendingQueryRow
- type ProtocolData
- type PublicEndpoints
- type Query
- type QueryChainResult
- type QueryChainsResult
- type QueryFilter
- type QueryParameters
- type QueryRow
- type QueryRowModel
- type QueryRowsCursor
- type QueryRowsResult
- type QueryWithRows
- type ResolvedEndpoints
- type RetentionSweepResult
- type RowChainResult
- type RowChainsResult
- type SSHServerData
- type Server
- func (db *Server) ApproverUserGroupUIDs(kind ApproverKind) []uuid.UUID
- func (db *Server) DecryptPassword(encryptionKey []byte) error
- func (db *Server) DecryptSSHSecrets(encryptionKey []byte) error
- func (db *Server) IsKubernetes() bool
- func (db *Server) IsSSH() bool
- func (db *Server) IsTunnel() bool
- func (db *Server) KubernetesData() *KubernetesServerData
- func (db *Server) KubernetesNamespaceOrDefault() string
- func (db *Server) MongoAuthSourceOrDefault() string
- func (db *Server) MongoData() *MongoDatabaseData
- func (db *Server) SSHData() *SSHServerData
- type ServerApprovers
- type ServerGroup
- type ServerGroupMember
- type ServerProtocolData
- type ServerUpdate
- type Store
- func (s *Store) AddServerToGroup(ctx context.Context, groupUID, serverUID uuid.UUID) error
- func (s *Store) AddUserToUserGroup(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) ChainEnabled() bool
- func (s *Store) CleanupExpiredOAuthStates(ctx context.Context) (int64, error)
- func (s *Store) CleanupOldQueryRows(ctx context.Context, olderThan time.Duration) (RetentionSweepResult, error)
- func (s *Store) ClearConnectionDumpKey(ctx context.Context, uid uuid.UUID) error
- func (s *Store) Close()
- func (s *Store) CloseConnection(ctx context.Context, uid uuid.UUID) error
- func (s *Store) CloseConnectionAt(ctx context.Context, uid uuid.UUID, closedAt time.Time) error
- func (s *Store) CloseOrphanedConnections(ctx context.Context) (OrphanedConnections, error)
- func (s *Store) ConsumeLoginExchange(ctx context.Context, code string) (uuid.UUID, uuid.UUID, []byte, error)
- func (s *Store) ConsumeOAuthState(ctx context.Context, stateToken string) (*OAuthState, error)
- func (s *Store) CountActiveGrantsForServerGroup(ctx context.Context, groupUID uuid.UUID) (int64, error)
- func (s *Store) CountAdmins(ctx context.Context) (int, error)
- func (s *Store) CountGrantsForLineage(ctx context.Context, uid uuid.UUID) (int64, int64, error)
- func (s *Store) CountUnchainedCapturedRows(ctx context.Context, queryUID uuid.UUID) (int64, 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) CreateConnectionAt(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) CreateLoginExchange(ctx context.Context, exchangeUID uuid.UUID, code string, userID uuid.UUID, ...) error
- func (s *Store) CreateOAuthState(ctx context.Context, state *OAuthState, ttl time.Duration) (*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) CreateServerGroup(ctx context.Context, group *ServerGroup) (*ServerGroup, 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) DeleteGrantDefinition(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) DeleteServerGroup(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) GetConnectionsByUIDs(ctx context.Context, uids []uuid.UUID) (map[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) GetGrantDefinitionBySlug(ctx context.Context, slug string) (*GrantDefinition, error)
- func (s *Store) GetGrantRequest(ctx context.Context, uid uuid.UUID) (*GrantRequest, error)
- func (s *Store) GetGrantsByUIDs(ctx context.Context, uids []uuid.UUID) (map[uuid.UUID]*Grant, 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) GetLiveGrantDefinition(ctx context.Context, uid uuid.UUID) (*GrantDefinition, error)
- func (s *Store) GetOracleServiceNameConflict(ctx context.Context, serviceName string) (*OracleServiceNameConflict, bool, 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) GetServerGroup(ctx context.Context, uid uuid.UUID) (*ServerGroup, 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) HasServerApproverGroups(ctx context.Context, groupUIDs []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) ListLatestEventPerUser(ctx context.Context, eventType string) ([]UserRoleSync, error)
- func (s *Store) ListListableServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListOracleServiceNameConflicts(ctx context.Context) ([]OracleServiceNameConflict, 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) ListServerGroupMemberUIDs(ctx context.Context, groupUID uuid.UUID) ([]uuid.UUID, error)
- func (s *Store) ListServerGroupMemberUIDsByGroups(ctx context.Context, groupUIDs []uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
- func (s *Store) ListServerGroupMembers(ctx context.Context, groupUID uuid.UUID) ([]Server, error)
- func (s *Store) ListServerGroupUIDsForServer(ctx context.Context, serverUID uuid.UUID) ([]uuid.UUID, error)
- func (s *Store) ListServerGroups(ctx context.Context) ([]ServerGroup, error)
- func (s *Store) ListServerGroupsForServer(ctx context.Context, serverUID uuid.UUID) ([]ServerGroup, error)
- func (s *Store) ListServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListServersByOracleServiceName(ctx context.Context, serviceName string) ([]Server, error)
- func (s *Store) ListTunnelServers(ctx context.Context) ([]Server, error)
- func (s *Store) ListUserGroupMemberUIDs(ctx context.Context, groupUID uuid.UUID) ([]uuid.UUID, error)
- func (s *Store) ListUserGroupMembers(ctx context.Context, groupUID uuid.UUID) ([]User, 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) ListUserGroupsForUser(ctx context.Context, userUID uuid.UUID) ([]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) LiveRunsSharingInstanceID(ctx context.Context) ([]Instance, error)
- func (s *Store) LogAuditEvent(ctx context.Context, event *AuditEvent) error
- func (s *Store) LogAuditEvents(ctx context.Context, events ...*AuditEvent) error
- func (s *Store) MatchesStorageDSN(host string, port int, databaseName string) bool
- func (s *Store) MayApproveForServer(ctx context.Context, serverUID uuid.UUID, kind ApproverKind, ...) (bool, error)
- 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) Now(ctx context.Context) (time.Time, 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) ReactivateGrantDefinition(ctx context.Context, uid uuid.UUID) error
- func (s *Store) ReclaimDeadInstanceConnections(ctx context.Context) (int64, error)
- func (s *Store) RefreshOpenChainStamps(ctx context.Context) (int64, error)
- func (s *Store) RegisterInstance(ctx context.Context) error
- func (s *Store) RemoveServerFromGroup(ctx context.Context, groupUID, serverUID uuid.UUID) error
- func (s *Store) RemoveUserFromUserGroup(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) ResolveServerApproverGroups(ctx context.Context, serverUID uuid.UUID, kind ApproverKind) ([]uuid.UUID, error)
- func (s *Store) ResolveServerApproverGroupsByServers(ctx context.Context, serverUIDs []uuid.UUID, kind ApproverKind) (ServerApprovers, 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) RunID() string
- func (s *Store) SealQueryRowChain(ctx context.Context, queryUID uuid.UUID) error
- func (s *Store) SetAuthCache(authCache *cache.AuthCache)
- func (s *Store) SetChainKey(key []byte)
- func (s *Store) SetConnectionDumpKey(ctx context.Context, uid uuid.UUID, key string) error
- func (s *Store) SetGrantRequestSlackMessage(ctx context.Context, uid uuid.UUID, channel, ts string) error
- func (s *Store) SetInstanceID(instanceID string)
- func (s *Store) SetKnownHostKey(ctx context.Context, uid uuid.UUID, hostKey string) error
- func (s *Store) SetKubernetesCACert(ctx context.Context, uid uuid.UUID, caCert 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) SetRunID(runID string)
- func (s *Store) SetServerGroupMembers(ctx context.Context, groupUID uuid.UUID, serverUIDs []uuid.UUID) error
- func (s *Store) SetUserGroupMembers(ctx context.Context, groupUID uuid.UUID, userUIDs []uuid.UUID) 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) (*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) UpdateServerGroup(ctx context.Context, group *ServerGroup) 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) UserHasGrantForDefinition(ctx context.Context, userID, definitionID uuid.UUID) (bool, error)
- func (s *Store) VerifyAPIKey(ctx context.Context, plainKey string) (*APIKey, error)
- func (s *Store) VerifyAuditChain(ctx context.Context) (AuditChainResult, error)
- func (s *Store) VerifyQueryChain(ctx context.Context, connectionUID uuid.UUID) (QueryChainResult, error)
- func (s *Store) VerifyQueryChains(ctx context.Context, connectionUID *uuid.UUID) (QueryChainsResult, error)
- func (s *Store) VerifyRowChain(ctx context.Context, queryUID uuid.UUID) (RowChainResult, error)
- func (s *Store) VerifyRowChains(ctx context.Context, connectionUID *uuid.UUID) (RowChainsResult, error)
- type StringArray
- 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 UserRoleSync
- 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 ( MaxSampleQueries = 32 MaxSampleQueryLength = 4096 )
MaxSampleQueries and MaxSampleQueryLength bound the pattern-authoring test bench (sample_queries) an admin may save on a definition. Purely a sanity cap — samples never touch the proxy hot path — but an unbounded list is still an unbounded column and an unbounded validate-patterns request body.
const ( // AuditChainAnchorUID is the fixed uid of the marker row the migration // inserts to record where chaining begins. It is chain_seq 0 and carries no // MAC: it is a signpost, not a link. AuditChainAnchorUID = "00000000-0000-7000-8000-0000dbba7000" // AuditChainAnchorEventType is that row's event_type. AuditChainAnchorEventType = "audit.chain_anchor" )
const ( // AuditEventConnectionOpened is written once per session, after the // connection row exists. AuditEventConnectionOpened = "connection.opened" // AuditEventConnectionClosed is written once per session, after // disconnected_at is committed — by whichever writer got there, which the // entry's closed_by records. AuditEventConnectionClosed = "connection.closed" )
The session audit entries.
`connections` is not chained, and deliberately so: a whole-session delete
DELETE FROM connections WHERE uid = '…';
cascades through `queries` and `query_rows`, which is the cheapest attack on the query history. Chaining that table would have to be reconciled against the retention sweep that deletes from it — the very problem the query chain is split per connection to avoid — so instead every session open and every session close writes an entry into `audit_log`, which is already HMAC-chained and is never reaped by retention.
The evidence therefore lives in a table the delete does not touch: the session vanishes, its two entries do not, and they carry enough of the row's immutable identity — who, from where, against which database, when, under which run — to say what was removed. The close entry additionally carries the session's query_chain_mac, so the sealed record points at the query chain it owned.
What this does *not* buy is a sealed `connections` row. `connected_at` on the row itself is still a plain column anyone with write access can backdate; see docs/audit-chain.md, "What it proves, and what it does not".
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 ( // PriorityFullWrite ranks a writable grant carrying no controls at all. PriorityFullWrite int16 = 100 // PriorityRestrictedWrite ranks a still-writable grant that carries // controls (block_copy / block_ddl). PriorityRestrictedWrite int16 = 50 // PriorityReadOnly ranks a read_only grant, whatever else it carries — // read_only is the most restrictive control, so it loses to anything // writable. PriorityReadOnly int16 = 10 )
Auto-calculated grant priority tiers. A user holding several active grants on one database gets the highest-priority one at auth time, so the default ranking has to encode "the grant that lets me do the most wins" — otherwise creation order decides, which nobody controls deliberately.
The gaps between tiers are the point: an operator can slot a manual override between two tiers (say 75) without renumbering anything.
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" // ProtocolMSSQL is Microsoft SQL Server (TDS). The protocol column is a // plain TEXT column with no CHECK constraint, so adding a value needs no // migration. ProtocolMSSQL = "mssql" // ProtocolSSH marks a row that is an SSH bastion (a dial path), not a // grantable/connectable database target. ProtocolSSH = "ssh" // ProtocolKubernetes marks a row that is a Kubernetes cluster (a dial path // via `pods/portforward`), not a grantable/connectable database target. // Like ProtocolSSH it is a tunnel discriminator, not a wire protocol. ProtocolKubernetes = "kubernetes" )
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 InstanceReclaimInterval = InstanceStaleAfter / 2
InstanceReclaimInterval is how often a running process re-runs Store.ReclaimDeadInstanceConnections and PruneStaleInstances.
Half the grace period, so a dead instance's rows are picked up within roughly one grace period of going stale rather than waiting for some unrelated process to start. That matters most in the crash case: a SIGKILLed pod leaves a registry row seconds old, so its immediate replacement reclaims nothing at startup, and 15 minutes later — when the row finally goes stale — nothing is starting any more. On a stable deployment the rows would then sit open until the next restart, which may be days away.
It is not shorter because there is nothing to gain: the rows only become reclaimable after InstanceStaleAfter anyway, so polling faster would only add UPDATEs that match nothing. See the jitter in the heartbeat loop for why several replicas do not run it in lockstep.
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 LoginExchangeProvider = "login-exchange"
LoginExchangeProvider namespaces oauth_states rows that hold a freshly created web session waiting to be picked up by the browser, so they share the table with real OAuth CSRF states (and device authorization requests) without ever colliding: /auth/:provider routes are only registered for configured providers, never "login-exchange".
const LoginExchangeTTL = 2 * time.Minute
LoginExchangeTTL bounds how long an unclaimed exchange code stays usable. The browser redeems it on the very next page load, so this is deliberately far shorter than the OAuth state TTL — it only has to survive one redirect.
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") // ErrTooManySampleQueries is returned when a definition carries more than // MaxSampleQueries sample queries. ErrTooManySampleQueries = errors.New("too many sample queries") // ErrSampleQueryTooLong is returned for a sample query over // MaxSampleQueryLength characters. ErrSampleQueryTooLong = errors.New("sample query too long") // 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 that is not a // dial path — only tunnel rows ('ssh' bastions and 'kubernetes' clusters) // can be tunneled through. // // The name predates the Kubernetes tunnel and is kept so existing callers // and tests keep matching; ErrServerViaNotTunnel is the accurate alias. ErrServerViaNotSSH = errors.New("via_uid must reference an ssh or kubernetes server") // ErrServerViaNotTunnel is the protocol-neutral name for ErrServerViaNotSSH. ErrServerViaNotTunnel = ErrServerViaNotSSH // 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") // ErrServerNameInvalid is returned when a server name does not match the // required slug format (see IsValidServerName) — wrong charset or too long. ErrServerNameInvalid = errors.New("server name must be a slug matching ^[a-z0-9_]{1,63}$") // 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") // ErrGrantDefinitionRequired is returned when a grant is created without // naming the definition it instantiates. A grant carries no shape of its // own, so one without a definition would authorize nothing meaningful — // it is rejected rather than stored. ErrGrantDefinitionRequired = errors.New("a grant must reference a grant definition") )
Store errors.
var ErrAPIKeyNoOracleVerifier = errors.New("API key carries no O5LOGON verifier material")
ErrAPIKeyNoOracleVerifier is what an API key minted before Oracle support (or by a build that had no encryption key to hand) reports when asked for its O5LOGON material: there is none, and none can ever be recovered — the key itself is Argon2id-hashed, so the verifier only exists if it was derived at mint time.
ErrChainKeyUnavailable is returned by the verifiers when the store has no chain key. Verification without the key is impossible by design.
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 ErrGrantDefinitionArchived = errors.New("grant definition version has been superseded")
ErrGrantDefinitionArchived is returned when an operation that only makes sense on the current version of a definition (editing it) is attempted on a version some later edit already superseded.
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 ErrGrantDefinitionSlugDuplicate = errors.New("grant definition with this slug already exists")
ErrGrantDefinitionSlugDuplicate is returned when a definition's slug conflicts with another *live* definition's slug. Archived versions keep their slug and never conflict — one slug has exactly one live owner and any number of superseded ones.
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 ErrLoginExchangeNotFound = errors.New("login exchange code not found or expired")
ErrLoginExchangeNotFound means the code is unknown, already redeemed, or expired. The three are deliberately indistinguishable to the caller.
var ErrParameterNotFound = errors.New("parameter not found")
ErrParameterNotFound is returned when no matching active parameter exists.
var ErrServerGroupDuplicate = errors.New("server group with this name already exists")
ErrServerGroupDuplicate is returned when a server group name collides (case insensitively) with an existing group.
var ErrServerGroupNotFound = errors.New("server group not found")
ErrServerGroupNotFound is returned when a server group lookup misses.
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 SessionAuditEventTypes = []string{AuditEventConnectionOpened, AuditEventConnectionClosed}
SessionAuditEventTypes are the audit events one *session* produces, as opposed to the control-plane changes the rest of `audit_log` records.
They are excluded from an unfiltered ListAuditEvents, and that is a deliberate call rather than an oversight. A proxy that serves ten thousand sessions a day writes twenty thousand of these against a handful of grant, user and key changes; folded into the same listing they would push every control-plane event off the first page within seconds, and the audit page is where an operator goes to see who changed access. The same information already has a purpose-built surface — the connections list, which shows live counters these entries deliberately omit — so nothing is hidden, only unmixed. Asking for one by name (`?event_type=connection.closed`) returns it, and neither the chain nor `dbbat audit verify` knows the difference: these are ordinary chained rows.
var ValidControls = []string{ ControlReadOnly, ControlBlockCopy, ControlBlockDDL, }
ValidControls lists all valid control values
Functions ¶
func AutoPriority ¶ added in v0.23.0
AutoPriority computes the default selection priority for a grant with the given controls. It is the single source of truth for the tiering: the API, the definition-materialization path and the SQL backfill in 20260806000000_grants_priority.up.sql all mirror this exact formula.
func DecryptO5LogonVerifier ¶ added in v0.25.0
DecryptO5LogonVerifier decrypts one stored O5LOGON verifier blob with the dbbat master key, under the AAD storeO5LogonVerifiers bound it to (the key prefix). It lives here, next to the code that writes those blobs, so the Oracle proxy and the REST API read them exactly the same way.
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 IsTunnelProtocol ¶ added in v0.24.0
IsTunnelProtocol reports whether a protocol names a *dial path* rather than a database target. Tunnel rows are never grantable, never listable, and never appear in target listings; they exist only to be pointed at by a via_uid.
func IsValidServerName ¶ added in v0.25.0
IsValidServerName reports whether name is a valid server slug (^[a-z0-9_]{1,63}$). Exported so any future rename path can reuse the exact same check CreateServer enforces, rather than re-deriving it.
func ResolvePriority ¶ added in v0.23.0
ResolvePriority returns the priority a grant carrying these controls should be stored with. Zero — the column default, the Go zero value and the value a caller that never heard of priorities leaves behind — reads as "unset" and falls back to AutoPriority. Every creation path funnels through here, so no path can silently insert a grant ranked below every tier. An operator who genuinely wants a grant that always loses sets 1 (or a negative value; smallint goes down to -32768).
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.
func ValidateSampleQueries ¶ added in v0.23.0
ValidateSampleQueries bounds the count and length of a definition's sample queries. Unlike approval patterns, samples are plain strings — nothing to compile — so this is a sanity cap, not a correctness check. A sample that fails to match a pattern is not an error at all: see POST /grant-definitions/validate-patterns, which reports match/no-match without failing the save.
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:"-"`
// OracleCapable answers "can this key be used as the Oracle password?" —
// computed, never stored (`bun:"-"`), and only by the handlers that mean to
// say it. A key minted before Oracle support carries no O5LOGON verifier and
// can never authenticate against an Oracle database, while working perfectly
// against the REST API and every other protocol; without this field the two
// are indistinguishable and the failure reads as a wrong password.
//
// A pointer, so "not computed" and "computed as false" stay distinguishable:
// omitempty drops it from responses that did not evaluate it rather than
// asserting `false` about a key nobody asked about.
OracleCapable *bool `bun:"-" json:"oracle_capable,omitempty"`
}
APIKey represents an API key for authentication
func (*APIKey) DecryptedO5LogonVerifier6949 ¶ added in v0.25.0
DecryptedO5LogonVerifier6949 returns the key's legacy verifier-6949 material, decrypted with the given master key. It is the *one* implementation of "this key can serve an Oracle login": the proxy calls it to build the O5LOGON challenge and OracleLoginCapable below calls it to answer the same question for `GET /api/v1/keys`. A second, cheaper-looking predicate ("the column is non-empty") would drift from it the moment DBB_KEY changes — material that does not decrypt is exactly as unusable as material that was never written.
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.
func (*APIKey) OracleLoginCapable ¶ added in v0.25.0
OracleLoginCapable reports whether this key can be used as the Oracle password for an O5LOGON login: it carries verifier-6949 material AND that material decrypts under the master key this process runs with.
Deliberately NOT a backfill point. A key presented over REST arrives in plaintext, so its verifier *could* be derived and written then — and that was weighed while implementing this (see docs/oracle.md, "Keys that cannot do Oracle") and rejected: verifier material is encrypted rather than hashed, so minting it for every legacy key on first REST use widens what a stolen store yields, to save the user one `dbbat key create`. Reporting the fact is the honest fix; the key stays unusable for Oracle until the user rotates it.
Cost: one AES-GCM open (a few microseconds) per key that has material, and no crypto at all for one that has none. A listing is bounded by the number of keys a user holds, so calling this per row is cheaper than the query that fetched them.
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 is the grant's anchor: the database it was issued for. It is
// what an unbound grant covers, and what a group-bound one falls back to
// if its group is deleted outright.
DatabaseID uuid.UUID `bun:"database_id,notnull,type:uuid" json:"database_id"`
// ServerGroupUID, when set, binds this grant to a server group: the grant
// then covers every server the group contains **right now**, and only
// those — the binding replaces the single-database scope rather than
// adding to it, so a server removed from the group stops being covered
// even when it is the anchor. "Adding widens, removing narrows", with no
// exceptions to remember.
//
// Membership is live and never snapshotted, so an edit takes effect the
// instant it is saved — the one deliberate exception to "a live grant's
// behavior never changes under it" (see ServerGroup). nil = anchor
// database only, which is what every grant issued from an unscoped
// definition gets.
//
// Quotas and Priority follow the grant, not the database: one
// max_query_counts and one max_bytes_transferred budget are consumed
// across the whole group, and Priority ranks group-bound grants against
// each other on the databases where their groups overlap.
ServerGroupUID *uuid.UUID `bun:"server_group_uid,type:uuid" json:"server_group_uid,omitempty"`
// GrantDefinitionID pins the exact definition *version* this grant was
// issued from. Definitions are immutably versioned (an edit archives the
// row and inserts a successor), so this reference can never make a live
// grant's behavior change underneath it.
GrantDefinitionID uuid.UUID `bun:"grant_definition_id,notnull,type:uuid" json:"grant_definition_id"`
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"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
// Priority ranks this grant against the other active grants the same user
// holds on the same database: the highest priority wins at auth time (ties
// broken by latest expiry, then newest). Auto-calculated from the
// definition's controls by AutoPriority unless an admin set it explicitly,
// which is why it is a plain int16 rather than a pointer — by the time a
// grant row exists the value is always resolved.
Priority int16 `bun:"priority,notnull,default:0" json:"priority"`
// Definition is the grant's shape, attached by the store on every read
// path (see Store.attachDefinitions). It is not a bun relation: the
// attachment is one batched query, which keeps the auth path free of ORM
// join semantics.
Definition *GrantDefinition `bun:"-" json:"definition,omitempty"`
// Computed fields (not stored in DB)
QueryCount int64 `bun:"-" json:"query_count"`
BytesTransferred int64 `bun:"-" json:"bytes_transferred"`
}
AccessGrant is one *instance* of a GrantDefinition: a named user's time-boxed access to one database, issued from a definition that describes what that access may do.
It deliberately carries no behavioral shape of its own. Controls, quotas, approval patterns and approver groups all live on GrantDefinitionID's row and are read through the accessors below. The grant row holds only instance-lifecycle data — who, where, the window, revocation, usage — plus Priority, which ranks this instance against the user's other instances and is not policy.
func (*AccessGrant) ApprovalPatterns ¶ added in v0.20.0
func (g *AccessGrant) ApprovalPatterns() []string
ApprovalPatterns are the RE2 patterns that suspend a matching statement until an approver resolves it, read from the grant's definition.
func (*AccessGrant) ApproverUserGroupUIDs ¶ added in v0.24.0
func (g *AccessGrant) ApproverUserGroupUIDs() []uuid.UUID
ApproverUserGroupUIDs lists the groups whose members may resolve holds on this grant, in addition to admins, read from the grant's definition. Empty = admins only, which is also what a shapeless grant reports: the narrowest possible approver set.
func (*AccessGrant) Controls ¶
func (g *AccessGrant) Controls() []string
Controls returns the controls this grant enforces, read from its definition.
A grant with no definition attached is treated as carrying *every* control: the shape is unknown, so the answer that cannot widen access is "everything is restricted". Combined with MaxQueryCounts/MaxBytesTransferred returning a zero quota in the same situation, a shapeless grant authorizes nothing at all. That is a backstop, not a mode: GetActiveGrant refuses to hand out a grant whose definition it could not attach.
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) MaxBytesTransferred ¶
func (g *AccessGrant) MaxBytesTransferred() *int64
MaxBytesTransferred is the grant's transfer quota, read from its definition. Same nil/zero semantics as MaxQueryCounts.
func (*AccessGrant) MaxQueryCounts ¶
func (g *AccessGrant) MaxQueryCounts() *int64
MaxQueryCounts is the grant's query quota, read from its definition. nil means unlimited; a shapeless grant reports a zero quota (nothing allowed) rather than nil, so it fails closed. See Controls.
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 ApproverKind ¶ added in v0.24.0
type ApproverKind string
ApproverKind names one of the two, deliberately non-hierarchical approver roles a server (or a server group) can carry.
They do not imply one another: a query approver gains no say over grant requests, and an access approver gains no say over a held statement. An organization wanting overlap lists the same user group in both.
const ( // ApproverKindAccess names the group allowed to approve or deny grant // *requests* targeting a server. A grant request is an asynchronous policy // decision; its audience is ops. ApproverKindAccess ApproverKind = "access" // ApproverKindQuery names the group allowed to release approval *holds* on // statements running against a server. A hold blocks a live wire-protocol // connection, so its audience is whoever can answer fastest — typically // lead ops. ApproverKindQuery ApproverKind = "query" )
type AuditChainResult ¶ added in v0.24.0
type AuditChainResult struct {
// Verified is how many chained rows were checked.
Verified int64
// Unchained is how many audit rows predate the anchor. Those rows cannot
// be verified — nothing sealed them — and are reported so the number is
// visible rather than implied.
Unchained int64
// HeadSeq and HeadMAC are the head the chain ends on. An operator notes
// the head down outside the database: comparing it against the next run is
// what detects a chain that was truncated and re-sealed wholesale by
// someone who did have the key.
HeadSeq int64
HeadMAC []byte
// Break is the first failure, or nil when the chain is intact.
Break *ChainBreak
}
AuditChainResult is the outcome of verifying the store-wide audit chain.
func (AuditChainResult) HeadMACHex ¶ added in v0.24.0
func (r AuditChainResult) HeadMACHex() string
HeadMACHex renders the head MAC for an operator to record.
func (AuditChainResult) OK ¶ added in v0.24.0
func (r AuditChainResult) OK() bool
OK reports whether the chain verified.
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
// IncludeSessionEvents folds the per-session entries
// (SessionAuditEventTypes) back into an otherwise unfiltered listing. They
// are excluded by default because they outnumber control-plane events by
// orders of magnitude on a busy proxy; naming one in EventType returns it
// whatever this says.
IncludeSessionEvents bool
}
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"`
// Tamper-evidence: ChainSeq is this row's position in the audit chain, MAC
// is the HMAC over its canonical serialization plus PrevMAC. All three are
// nil/zero on rows written before the chain anchor (unverifiable by
// construction) and on a store built without an encryption key. They are
// internal integrity state, not API surface — hence json:"-"; `dbbat audit
// verify` is how they are consumed. See internal/store/chain.go.
ChainSeq *int64 `bun:"chain_seq" json:"-"`
PrevMAC []byte `bun:"prev_mac" json:"-"`
MAC []byte `bun:"mac" json:"-"`
}
AuditLog represents an audit log entry
type ChainBreak ¶ added in v0.24.0
type ChainBreak struct {
// UID of the offending row, or of the connection when the break is a
// stamped-head mismatch.
UID uuid.UUID
// ChainSeq is its position, or 0 when the break is about the chain as a
// whole rather than one row.
ChainSeq int64
// ConnectionUID is set for query-chain breaks.
ConnectionUID *uuid.UUID
// QueryUID is set for result-row-chain breaks. ChainSeq then carries the
// offending row's row_number.
QueryUID *uuid.UUID
// Reason is a human-readable description of what did not add up.
Reason string
}
ChainBreak names the first row that failed to verify.
func (ChainBreak) String ¶ added in v0.24.0
func (b ChainBreak) String() string
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"`
// UpstreamTLS reports whether the proxy→upstream leg of this session was
// encrypted. The server row's ssl_mode states a policy, not an outcome:
// the opportunistic modes ("prefer", and the empty default) fall back to
// plaintext when the target refuses TLS, so only the session knows which
// way it went. Recording it is what makes that fallback auditable rather
// than silent.
UpstreamTLS bool `bun:"upstream_tls,notnull,default:false" json:"upstream_tls"`
// 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:"-"`
// RunID is the *run* that opened this connection: a UUID minted in memory
// at startup, so it is unique per live process even when several replicas
// share an instance id. It is what lets the reconcile ask whether a live
// run still owns a row, rather than trusting that an id means a process.
//
// A nil pointer means the row was written before run tracking existed —
// deliberately distinct from a run whose id is empty. See noLiveOwner for
// how those rows are judged.
RunID *string `bun:"run_id" json:"-"`
// DumpKey is the blob-storage object key of this session's capture, once
// it has been uploaded (DBB_DUMP_UPLOAD_URL). Empty means the capture — if
// there is one — is still in the local spool, which is also the permanent
// state when uploads are disabled.
//
// It is stored rather than derived because the API addresses captures by
// connection UID alone and cannot otherwise tell which instance wrote the
// object; deriving it would turn every download into a bucket LIST.
// Internal bookkeeping, not API surface — hence json:"-": the key exposes
// the bucket layout and callers already have the download endpoint.
DumpKey string `bun:"dump_key,notnull,default:''" json:"-"`
// GrantUID is the access grant this session authenticated under — the
// auth-time GetActiveGrant (or equivalent per-protocol) pick, stamped at
// CreateConnection and never updated afterwards. A connection is bound to
// exactly one grant for its whole life: the LimitGuard watchdog already
// terminates the session when that grant expires or is revoked
// (internal/proxy/shared/limits.go), so there is never a live row that
// should be re-pointed at a different grant.
//
// nil covers two cases that are indistinguishable from here: the row
// predates this column, or the owning grant has since been deleted.
// Either way, consumers (mayApproveQuery, populateGrantCounters) fall back
// to their pre-stamp heuristics.
GrantUID *uuid.UUID `bun:"grant_uid,type:uuid" json:"grant_uid"`
// QueryChainMAC is the head of this connection's query chain, and
// QueryChainLen is the position that head sits at. Without them, deleting
// the *last* queries of a connection would leave a chain that still
// verified; with them the stored head no longer matches what the surviving
// rows compute.
//
// Three writers, all sealing with the key: CloseConnection at a clean
// teardown, the reconcile for a session whose process died, and
// RefreshOpenChainStamps on the reclaim timer for a session that is still
// open. The last is why the stamp is not necessarily *final*: a live
// session's stamp is a prefix of its chain, re-sealed one sweep at a time,
// and checkStampedHead judges an open session accordingly.
//
// nil/0 on a connection that logged nothing, or one no sweep has reached
// yet. Those are the only two legitimate NULLs: a *closed* connection whose
// chained statements survive always carries a stamp, so a NULL one there is
// a break (checkMissingStamp), as is a surviving QueryChainLen beside a nil
// MAC — the three columns are only ever written together. Internal
// integrity state, not API surface.
QueryChainMAC []byte `bun:"query_chain_mac" json:"-"`
// QueryChainLen is the head's chain_seq — which, chain_seq being dense from
// 1, is the number of statements the session logged. Deliberately not a
// count of *surviving* statements: DBB_QUERY_STORAGE_RETENTION reaps the
// oldest statements of a long-lived session, and a stamp that moved with
// them would report a break on every truncated session.
QueryChainLen int64 `bun:"query_chain_len,notnull,default:0" json:"-"`
// QueryChainStampVersion says which format QueryChainMAC is in:
// queryChainStampLegacy (a verbatim copy of the head MAC, forgeable without
// the key) or queryChainStampKeyed. Every writer produces the keyed format,
// and only a store written by a pre-0.24 development build can hold an
// unkeyed one — which no migration can re-seal without the chain key, so it
// stays a break forever.
QueryChainStampVersion int16 `bun:"query_chain_stamp_version,notnull,default:0" 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 ConnectionOption ¶ added in v0.21.0
type ConnectionOption func(*Connection)
ConnectionOption sets a field on a connection row before it is inserted. Variadic so the many call sites that only need the four required columns stay as they are.
func WithGrantUID ¶ added in v0.23.0
func WithGrantUID(grantUID uuid.UUID) ConnectionOption
WithGrantUID stamps the connection with the grant it authenticated under. Every proxy holds the grant returned by its auth-time GetActiveGrant (or equivalent) lookup right where it creates the connection row — this is simply that pick, pinned for the row's whole life. See Connection.GrantUID.
func WithUpstreamTLS ¶ added in v0.21.0
func WithUpstreamTLS(encrypted bool) ConnectionOption
WithUpstreamTLS records whether the proxy→upstream leg of the session is encrypted. Only the session knows: under an opportunistic ssl_mode the row's policy does not determine the outcome.
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 target user/database, anchoring the time window to `now`. It is the only way a grant comes into existence: the grant-request approval path and the admin direct-assign path both go through here.
Nothing about the definition's shape is copied onto the grant — the grant pins the definition's uid and reads its shape back through the accessors on AccessGrant. The only value derived from the shape is Priority, which is a selection rank rather than policy and has to be stored so the ordering can happen in SQL.
ServerGroupUID is deliberately *not* set here: binding needs a database round-trip, so it is resolved by the creation paths that own a handle — CreateGrant and the grant-request approval transaction — through resolveServerGroupBinding. This function stays pure.
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"`
// LineageUID is stable across every version of a definition (the root
// version's own uid). It is what deactivation, hard deletion and
// "which versions are this definition?" act on. The slug cannot serve
// that purpose: it is unique only among live rows, so a new definition
// may legally reuse a slug whose archived rows belong to another lineage.
LineageUID uuid.UUID `bun:"lineage_uid,notnull,type:uuid" json:"lineage_uid"`
// ArchivedAt marks a superseded version. NULL = this is the live version
// of its lineage — exactly one row per lineage has it NULL, enforced by a
// partial unique index on the slug.
ArchivedAt *time.Time `bun:"archived_at" json:"archived_at,omitempty"`
Name string `bun:"name,notnull" json:"name"`
// Slug is a stable, human-typeable, machine-friendly identifier — the
// thing a CLI invocation, an agent prompt, or a runbook references
// instead of copying a UUID out of the UI. Mandatory and unique at the
// database level; the API never auto-generates it (the frontend does,
// from the name, until the operator edits it manually).
Slug string `bun:"slug,notnull" json:"slug"`
Description string `bun:"description,notnull,default:''" json:"description"`
DurationSeconds int64 `bun:"duration_seconds,notnull" json:"duration_seconds"`
Controls StringArray `bun:"controls,notnull,default:'{}'" json:"controls"`
MaxQueryCounts *int64 `bun:"max_query_counts" json:"max_query_counts"`
MaxBytesTransferred *int64 `bun:"max_bytes_transferred" json:"max_bytes_transferred"`
// Priority, when non-nil, is copied verbatim onto every grant
// materialized from this definition, pinning it above or below the tier
// its controls would otherwise earn. nil — the default — means "compute
// it from the controls at materialization time" (see AutoPriority).
Priority *int16 `bun:"priority" json:"priority"`
// 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"`
// UserGroupUIDs restricts which users may request this definition: a user
// must belong to at least one of the listed user 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.
//
// Named `user_group_uids`, not `group_uids`: server groups exist too, and
// a bare "group" is ambiguous. The old JSON name is still accepted on
// input for one release; responses only ever emit the new one.
UserGroupUIDs []uuid.UUID `bun:"user_group_uids,array,notnull,default:'{}'" json:"user_group_uids"`
// ServerGroupUIDs restricts which databases this definition can be
// requested against, by naming server groups rather than enumerating
// servers: a database is in scope when it currently belongs to at least
// one of the listed groups. Empty = every database — the same semantics
// the old per-database list had, so every pre-existing definition keeps
// behaving as before.
//
// Membership is resolved **live**, exactly like the user-group scope
// above: adding a server to a scoped group makes the definition
// requestable against it immediately, with no edit and therefore no new
// version. That is the point of groups, and the reason fleet growth is no
// longer O(definitions) of toil.
//
// Stored as an array on the definition rather than a join table for the
// same reason UserGroupUIDs is: an empty scope means "every database", so
// a cascade-on-delete join table would fail *open* when a group is
// deleted. A dangling uid here matches no database — fail closed.
ServerGroupUIDs []uuid.UUID `bun:"server_group_uids,array,notnull,default:'{}'" json:"server_group_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 StringArray `bun:"approval_patterns,notnull,default:'{}'" json:"approval_patterns"`
// SampleQueries are representative SQL statements an author saves
// alongside the patterns to validate them against — a test bench for
// pattern authoring, not a first-class matcher: the RE2 patterns above
// remain what the proxy actually evaluates. Because it is just another
// column on this row, it versions for free with every edit (see the
// type doc): a definition's saved samples always describe the patterns
// of that exact version. A sample that stops matching after an edit does
// not block the save — see POST /grant-definitions/validate-patterns,
// which reports match/no-match without failing the request.
SampleQueries StringArray `bun:"sample_queries,notnull,default:'{}'" json:"sample_queries"`
// ApproverUserGroupUIDs lists the user groups whose members may resolve
// holds on grants built from this definition, *in addition to* admins.
// Empty = admins only.
ApproverUserGroupUIDs []uuid.UUID `bun:"approver_user_group_uids,array,notnull,default:'{}'" json:"approver_user_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"`
// ActiveGrantCount is how many non-revoked, non-expired grants this
// definition's *lineage* is currently authorizing. Computed on listing
// (one grouped query, never per row) so the UI can tell an operator what
// deactivating it would cut off before they confirm.
ActiveGrantCount int64 `bun:"-" json:"active_grant_count"`
// ScopedDatabaseUIDs is ServerGroupUIDs resolved to the concrete
// databases currently in scope — a read-only convenience so a non-admin
// requester can narrow their database picker without needing the
// admin-gated /server-groups endpoint (membership is access-relevant,
// so that gate stays). It leaks no group names or membership shape,
// only "these databases are in scope," which a requester could already
// discover by trying. It is never the access-control gate: that stays
// enforceRequestScope in internal/api/grant_requests.go, which
// re-resolves scope itself and does not read this field.
//
// The two "in scope" states an empty ServerGroupUIDs and a scoped-but-
// currently-empty group set would otherwise both stringify as `[]` are
// told apart with a pointer:
// - nil (omitted from the JSON response): ServerGroupUIDs is empty,
// meaning every database is in scope.
// - non-nil, possibly empty ([]): ServerGroupUIDs is non-empty; this
// is the resolved union of every scoped group's current members. An
// empty slice means the definition is scoped but currently covers
// zero databases (e.g. every referenced group is empty).
//
// Only handleListGrantDefinitions and handleGetGrantDefinition populate
// this (one batched membership query per response, never one per
// definition); every other read or write path leaves it nil.
ScopedDatabaseUIDs *[]uuid.UUID `bun:"-" json:"scoped_database_uids,omitempty"`
}
GrantDefinition is the *shape* of a grant: name, duration, controls, optional quotas, approval gating. It is the single source of truth for what a grant may do — every AccessGrant is an instance of one, whether it came from a user's grant request or from an admin assigning it directly. Nothing creates a grant without a definition.
Definitions are immutably versioned. Editing one archives the current row (ArchivedAt) and inserts a successor sharing its LineageUID, so grants keep pointing at the exact row they were issued from and an edit never retroactively changes live access. Two lifecycle states must not be confused:
- **Archived** (ArchivedAt != nil): superseded by a newer version. Still authorizes the grants pinned to it — it was replaced, not withdrawn.
- **Deactivated** (IsActive == false): explicitly withdrawn by an operator, across the whole lineage. Fails closed at auth time: grants pinned to any version of it stop authorizing new connections.
func (*GrantDefinition) AppliesTo ¶ added in v0.18.0
func (d *GrantDefinition) AppliesTo(userGroupUIDs, serverGroupUIDs []uuid.UUID) bool
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) AppliesToServerGroups ¶ added in v0.24.0
func (d *GrantDefinition) AppliesToServerGroups(serverGroupUIDs []uuid.UUID) bool
AppliesToServerGroups reports whether a database belonging to the given server groups is within this definition's scope. An empty scope applies to every database.
The caller passes the target database's *current* group membership (see Store.ListServerGroupUIDsForServer) rather than the database uid, which is what makes the scope follow group membership live.
func (*GrantDefinition) AppliesToUserGroups ¶ added in v0.24.0
func (d *GrantDefinition) AppliesToUserGroups(userGroupUIDs []uuid.UUID) bool
AppliesToUserGroups reports whether a user belonging to the given groups is within this definition's group scope. An empty scope applies to everyone.
func (*GrantDefinition) IsLive ¶ added in v0.23.0
func (d *GrantDefinition) IsLive() bool
IsLive reports whether this is the current version of its lineage, as opposed to a version archived by a later edit.
func (*GrantDefinition) MatchingServerGroup ¶ added in v0.24.0
func (d *GrantDefinition) MatchingServerGroup(serverGroupUIDs []uuid.UUID) *uuid.UUID
MatchingServerGroup returns the definition's server group that the given database currently belongs to — the group a grant materialized from this definition binds to. The first of the definition's groups that contains the database wins, so the choice is stable for a given definition version.
nil means "no group binds": either the definition is unscoped (it applies to every database, which is not the same thing as covering a named set) or the database is not in any of its groups. Either way the resulting grant covers its anchor database only.
type GrantDefinitionFilter ¶ added in v0.10.0
type GrantDefinitionFilter struct {
ActiveOnly bool
}
GrantDefinitionFilter narrows ListGrantDefinitions queries.
type GrantDefinitionInUseError ¶ added in v0.23.0
GrantDefinitionInUseError is returned when a definition cannot be hard deleted because rows still reference it. Deleting anyway would orphan grants, whose grant_definition_id is NOT NULL — a grant with no shape is exactly what this model exists to make impossible.
func (*GrantDefinitionInUseError) Error ¶ added in v0.23.0
func (e *GrantDefinitionInUseError) Error() string
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:"-"`
// Definition is the **live** version of the lineage GrantDefinitionID
// points into, attached by the store on every read path (see
// Store.attachRequestDefinitions).
//
// Live, not pinned, and deliberately so: a request is a claim on a policy,
// not a grant of one. Approving it materializes today's version of that
// policy (see approveGrantRequestTx), so the version a reader must be
// shown — name, auto-approve, controls — is today's too. Resolving
// GrantDefinitionID directly would render the version the request was
// filed under, which an edit has since superseded and which no listing
// returns anymore: that is how an edited definition used to leave its
// requests displaying a bare uid.
//
// A grant is the opposite case and keeps the opposite rule: it pins the
// exact version it was issued from (see AccessGrant.Definition).
Definition *GrantDefinition `bun:"-" json:"definition,omitempty"`
}
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"`
RunID string `bun:"run_id,pk,notnull,default:''" json:"run_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 *run* of 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 run still alive?" — which is what lets the startup reconcile reclaim the connections of a run that is provably gone. See Store.CloseOrphanedConnections.
The key is the pair, not the instance id: two live replicas that share an instance id must both be able to prove they are alive, or the second one to register would silently make the first look dead. A row whose run id is ” predates run tracking (seeded by a migration, or written by an older build).
type KubernetesServerData ¶ added in v0.24.0
type KubernetesServerData struct {
CACert string `json:"ca_cert,omitempty"`
// LearnedCACert is the bundle dbbat pinned itself, on the first connect of
// a row that supplied none (TOFU) — deliberately a *separate* field from
// CACert so "I pasted this" and "we learned this" never blur into each
// other, and the UI can say which of the two is in force.
//
// It is only ever consulted when CACert is empty: an operator-supplied
// bundle always wins, and pasting one is what retires a stale learned pin.
LearnedCACert string `json:"learned_ca_cert,omitempty"`
Namespace string `json:"namespace,omitempty"`
// InsecureSkipTLSVerify disables API server certificate verification. It
// exists for the throwaway-cluster case (a kind cluster with a rotating CA)
// and is deliberately not the default: with it set, anything that can
// intercept the API server connection can read the ServiceAccount token.
InsecureSkipTLSVerify bool `json:"insecure_skip_tls_verify,omitempty"`
}
KubernetesServerData holds the material for a Kubernetes cluster row. The ServiceAccount bearer token is *not* here: it reuses the row's password_encrypted column, so it travels the same AAD-bound encryption path as a database password or an SSH private key.
CACert is the API server's PEM CA bundle — public challenge material, stored in clear and surfaced read-only in the API/UI, exactly like SSH's KnownHostKey. Namespace scopes every lookup and every port-forward: it is the namespace the Role/RoleBinding in docs/kubernetes.md grants access to.
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
// EncryptionKey is the master AES-256 key (DBB_KEY / DBB_KEYFILE). The
// store keeps only an HKDF subkey of it, used to seal the tamper-evident
// audit and query chains. Empty leaves those rows unchained — which is
// what every test store that does not care about the chain gets, and what
// a serving process never gets, because config always resolves a key.
EncryptionKey []byte
// QueryRetention is the configured query-history retention window
// (DBB_QUERY_STORAGE_RETENTION), zero when retention is disabled — the
// default. Chain verification needs it to judge a session whose statements
// are *all* gone: retention can only account for that when the session ran
// entirely before the retention cutoff. Zero therefore means "nothing
// legitimately deletes statements here", which is the strictest reading.
QueryRetention time.Duration
}
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 OracleServiceNameConflict ¶ added in v0.25.0
type OracleServiceNameConflict struct {
// ServiceName is the shared upstream `oracle_service_name`.
ServiceName string `json:"service_name"`
// Upstreams are the distinct `host:port` spellings, sorted. Two entries or
// more is what makes this a conflict.
Upstreams []string `json:"upstreams"`
// Servers are the rows claiming the service name, ordered by name.
Servers []OracleServiceNameConflictServer `json:"servers"`
}
OracleServiceNameConflict describes one upstream Oracle service name that several dbbat server rows claim while disagreeing on the upstream address.
It exists because the Oracle proxy resolves a shared service name by comparing candidate rows' `host:port` **as text** (session.go, resolveDatabase): two spellings of one machine — a CNAME in one row, the A-record in another — read as two different upstreams, and the connect is refused ORA-12514 even though every candidate points at the same database. That compare stays textual on purpose (it never surprises, and resolving DNS on the connect path could answer differently between two connects of one DSN), so the misconfiguration is surfaced instead: on the server row in the admin UI, and in the connectivity check.
func OracleServiceNameConflictFor ¶ added in v0.25.0
func OracleServiceNameConflictFor(serviceName string, servers []Server) *OracleServiceNameConflict
OracleServiceNameConflictFor builds the conflict for a set of candidate rows already in hand, or nil when they all agree on one `host:port`.
It is the single definition of "these rows disagree", used both by the proxy — which has already loaded the candidates via ListServersByOracleServiceName and must decide whether to refuse — and by the store queries below, which scan the fleet. A second, SQL-shaped implementation of the same rule is exactly the drift that would let the UI call a configuration healthy while the proxy refuses it.
func (*OracleServiceNameConflict) Describe ¶ added in v0.25.0
func (c *OracleServiceNameConflict) Describe() string
Describe renders the conflict as one operator-facing sentence, shared by every surface that reports it (the proxy's refusal log, the connectivity check, the API response the admin UI renders) so the wording cannot drift between them.
type OracleServiceNameConflictServer ¶ added in v0.25.0
type OracleServiceNameConflictServer struct {
UID uuid.UUID `json:"uid"`
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
}
OracleServiceNameConflictServer identifies one row taking part in a conflict. It carries no credential material: the name and the address are exactly what the admin needs to reconcile the spellings.
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 a previous run carrying this instance id
// left open — never this run's own, and never a live peer's, however the
// id came to be shared.
Own int64
// Reclaimed is the number of connections closed on behalf of runs 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 a previous run of this instance id 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"`
// Tamper-evidence: this statement's position in its *connection's* chain,
// and the HMAC sealing its immutable identity (uid, connection, position,
// SQL text, parameters, executed_at) plus PrevMAC. The outcome columns
// above are written after the insert and are deliberately not covered —
// see queryChainPayload. Internal integrity state, not API surface.
ChainSeq *int64 `bun:"chain_seq" json:"-"`
PrevMAC []byte `bun:"prev_mac" json:"-"`
MAC []byte `bun:"mac" json:"-"`
// RowChainMAC seals the final head of this query's *result row* chain when
// the capture finishes, and RowChainLen is how many captured rows that head
// covers. They are to query_rows what QueryChainMAC / QueryChainLen are to
// a connection's statements: the only thing that catches rows deleted from
// the *end* of a capture.
//
// It is a keyed MAC over (query, length, head MAC) — deliberately *not* a
// copy of the head MAC, which is readable from query_rows and could
// therefore be rewritten to match a truncated capture without the chain
// key. See rowChainStampMAC.
//
// RowChainLen is a count, not the head's row_number — a capture with gaps
// (rows the writer dropped, rows that failed to encode) has fewer stored
// rows than its last row_number.
//
// nil/0 for a query that captured nothing, for captures written before the
// row chain migration, and for a capture whose process died before the
// flush barrier. Internal integrity state, not API surface.
RowChainMAC []byte `bun:"row_chain_mac" json:"-"`
RowChainLen int64 `bun:"row_chain_len,notnull,default:0" json:"-"`
// 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 QueryChainResult ¶ added in v0.24.0
type QueryChainResult struct {
ConnectionUID uuid.UUID
// Verified is how many statements were checked.
Verified int64
// TruncatedPrefix is true when the chain does not start at 1 — the
// expected shape for a connection whose oldest statements were reaped by
// DBB_QUERY_STORAGE_RETENTION.
TruncatedPrefix bool
// FirstSeq is the oldest surviving statement's chain_seq, or 0 when nothing
// survives. It is 1 unless retention reaped the chain's prefix, and it is
// what tells a stamp whose sealed position was *reaped* from one that names
// a position which never existed. See checkStampedHead.
FirstSeq int64
HeadSeq int64
HeadMAC []byte
Break *ChainBreak
}
QueryChainResult is the outcome of verifying one connection's query chain.
type QueryChainsResult ¶ added in v0.24.0
type QueryChainsResult struct {
// Connections is how many connections carried a chain and were walked.
Connections int64
// Verified is how many statements were checked across them.
Verified int64
// Truncated is how many of those chains were missing a prefix.
Truncated int64
// Break is the first failure found, or nil.
Break *ChainBreak
}
QueryChainsResult aggregates a sweep over many connections.
func (QueryChainsResult) OK ¶ added in v0.24.0
func (r QueryChainsResult) OK() bool
OK reports whether every chain walked verified.
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"`
// Tamper-evidence: the HMAC sealing this captured row (its query, its
// row_number, its uid, its data and its size) plus the previous stored
// row's MAC. The chain is per query and its position is row_number, which
// is an ordering and not a dense sequence — see rowChainPayload. Internal
// integrity state, not API surface.
PrevMAC []byte `bun:"prev_mac" json:"-"`
MAC []byte `bun:"mac" json:"-"`
}
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 RowChainResult ¶ added in v0.24.0
type RowChainResult struct {
QueryUID uuid.UUID
// Verified is how many captured rows were checked.
Verified int64
// HeadRowNumber and HeadMAC are where the surviving rows end.
HeadRowNumber int64
HeadMAC []byte
Break *ChainBreak
}
RowChainResult is the outcome of verifying one capture's result-row chain.
type RowChainsResult ¶ added in v0.24.0
type RowChainsResult struct {
// Captures is how many queries carried a row chain and were walked.
Captures int64
// Verified is how many captured rows were checked across them.
Verified int64
// Unchained is how many query_rows predate the row chain migration.
// Nothing sealed them, so they are reported rather than counted as
// verified — the same treatment pre-anchor audit rows get.
Unchained int64
Break *ChainBreak
}
RowChainsResult aggregates a sweep over many captures.
func (RowChainsResult) OK ¶ added in v0.24.0
func (r RowChainsResult) OK() bool
OK reports whether every capture walked verified.
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"`
// AccessApproverUserGroupUIDs / QueryApproverUserGroupUIDs are the two
// approver kinds attached to this server: who may decide grant *requests*
// targeting it, and who may release approval *holds* on statements against
// it. Empty (the default) falls back to the server groups this server
// belongs to, and then to admins — see ResolveServerApproverGroups.
//
// Read live at decision time, never snapshotted onto a grant: this is the
// second deliberate exception to the immutable-versioning rule, alongside
// live server-group membership.
AccessApproverUserGroupUIDs []uuid.UUID `bun:"access_approver_user_group_uids,array,notnull,default:'{}'" json:"access_approver_user_group_uids"`
QueryApproverUserGroupUIDs []uuid.UUID `bun:"query_approver_user_group_uids,array,notnull,default:'{}'" json:"query_approver_user_group_uids"`
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|mssql) 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) ApproverUserGroupUIDs ¶ added in v0.24.0
func (db *Server) ApproverUserGroupUIDs(kind ApproverKind) []uuid.UUID
ApproverUserGroupUIDs returns the server's own list for one approver kind. Empty means "this server names nobody" — the caller falls back to the server's groups, and then to admins.
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) IsKubernetes ¶ added in v0.24.0
IsKubernetes reports whether this server row is a Kubernetes cluster tunnel rather than a database target.
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) IsTunnel ¶ added in v0.24.0
IsTunnel reports whether this row is a dial path (SSH bastion or Kubernetes cluster) rather than a grantable database target.
func (*Server) KubernetesData ¶ added in v0.24.0
func (db *Server) KubernetesData() *KubernetesServerData
KubernetesData returns the server's Kubernetes material, or nil if absent.
func (*Server) KubernetesNamespaceOrDefault ¶ added in v0.24.0
KubernetesNamespaceOrDefault returns the namespace every lookup and port-forward for this cluster row is scoped to, defaulting to "default" when unset — the same convention kubectl applies to a context with no namespace.
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 ServerApprovers ¶ added in v0.24.0
ServerApprovers is a resolved approver answer for a set of servers, keyed by server uid: exactly what ResolveServerApproverGroups would have returned for each of them, computed in one shot.
A server present in the store has an entry, possibly an empty one ("nobody but admins"). A server that does not exist — deleted since the row naming it was written — is **absent**, and every read of an absent key yields nil, which is the same fail-closed answer the per-server path gives by erroring.
func (ServerApprovers) MayApprove ¶ added in v0.24.0
MayApprove is MayApproveForServer's decision half applied to an already-resolved answer. Same rule, same fail-closed direction: no user groups, an unknown server, or a server naming nobody all mean false.
type ServerGroup ¶ added in v0.24.0
type ServerGroup struct {
bun.BaseModel `bun:"table:server_groups,alias:sg"`
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"`
// AccessApproverUserGroupUIDs / QueryApproverUserGroupUIDs are the
// group-level fallback for the two approver kinds: they apply to every
// server in this group that names none of its own. Several groups holding
// the same server union, matching how a definition's approver groups union.
//
// Read live at decision time, like membership itself.
AccessApproverUserGroupUIDs []uuid.UUID `bun:"access_approver_user_group_uids,array,notnull,default:'{}'" json:"access_approver_user_group_uids"`
QueryApproverUserGroupUIDs []uuid.UUID `bun:"query_approver_user_group_uids,array,notnull,default:'{}'" json:"query_approver_user_group_uids"`
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"`
}
ServerGroup is a named set of database servers ("the analytics replicas", "all staging databases") — the unit rights are scoped on, so a policy names a stable set instead of enumerating servers one by one.
It is the server-side mirror of UserGroup, and its membership is deliberately **live**: a grant bound to a group covers whatever the group contains *right now*. Adding a server to a group therefore immediately widens every live grant bound to it — the one place where dbbat's "a live grant's behavior never changes under it" rule is knowingly broken, because group membership is operational data, exactly as user-group membership already is. See docs/grants.md.
func (*ServerGroup) ApproverUserGroupUIDs ¶ added in v0.24.0
func (g *ServerGroup) ApproverUserGroupUIDs(kind ApproverKind) []uuid.UUID
ApproverUserGroupUIDs returns the group's list for one approver kind — the fallback for every member server that names none of its own.
type ServerGroupMember ¶ added in v0.24.0
type ServerGroupMember struct {
bun.BaseModel `bun:"table:server_group_members,alias:sgm"`
GroupUID uuid.UUID `bun:"group_uid,pk,type:uuid" json:"group_uid"`
ServerUID uuid.UUID `bun:"server_uid,pk,type:uuid" json:"server_uid"`
}
ServerGroupMember is the group ↔ server join row, the exact counterpart of UserGroupMember.
type ServerProtocolData ¶ added in v0.17.0
type ServerProtocolData struct {
MongoDB *MongoDatabaseData `json:"mongodb,omitempty"`
SSH *SSHServerData `json:"ssh,omitempty"`
Kubernetes *KubernetesServerData `json:"kubernetes,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 {
// Name renames the server. It is the client-facing selector on all five
// protocols (the "database name" a client types in its connection string),
// so a rename is a breaking change for every saved connection string —
// which is exactly why it has to be doable through the API rather than by
// hand against the storage database. Validated against IsValidServerName,
// the same check CreateServer applies, and a collision with an existing
// name — soft-deleted rows included, since servers_name_key is global —
// comes back as ErrServerNameConflict.
Name *string
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
// Kubernetes cluster material. Public (the CA bundle and the namespace),
// so unlike the ServiceAccount token — which travels as Password — these
// are stored in clear in protocol_data.kubernetes.
K8sCACert *string
K8sNamespace *string
K8sInsecureSkipTLSVerify *bool
// K8sClearLearnedCACert, when true, forgets the TOFU-learned bundle so the
// next connect learns afresh. It is the exit for a cluster whose CA
// rotated — the case that made TOFU worth having at all.
K8sClearLearnedCACert bool
// AccessApproverUserGroupUIDs / QueryApproverUserGroupUIDs replace the
// server's approver lists wholesale. nil leaves them untouched; an explicit
// empty slice clears them, which is what hands the decision back to the
// server groups (and then to admins).
AccessApproverUserGroupUIDs *[]uuid.UUID
QueryApproverUserGroupUIDs *[]uuid.UUID
}
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) AddServerToGroup ¶ added in v0.24.0
AddServerToGroup adds a membership. Idempotent: re-adding is a no-op.
func (*Store) AddUserToUserGroup ¶ added in v0.24.0
AddUserToUserGroup 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) ChainEnabled ¶ added in v0.24.0
ChainEnabled reports whether this store seals what it writes. It is false only when no encryption key was handed to the store, which in a served process cannot happen: config always resolves a key, creating ~/.dbbat/key if it has to.
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) ClearConnectionDumpKey ¶ added in v0.23.0
ClearConnectionDumpKey forgets the uploaded capture of a connection, after the object itself has been deleted.
func (*Store) CloseConnection ¶
CloseConnection sets the disconnected_at timestamp and, when chaining is on, stamps the final head of this connection's query chain onto the row.
The stamped head is what makes a *trailing* deletion detectable: without it, removing the last statements of a session would leave a shorter chain that still verified end to end. With it, the surviving rows no longer compute the head the connection claims.
The stamp is keyed (queryChainStampMAC), not a copy of the head MAC. A verbatim head is readable out of `queries`, so an attacker who deleted the tail of a session could simply recopy it; sealing means correcting the stamp after a deletion needs the chain key, exactly like forging a statement.
func (*Store) CloseConnectionAt ¶ added in v0.25.1
CloseConnectionAt is CloseConnection with the close instant supplied by the caller instead of taken from time.Now(). It exists for seeding a back-dated demo/fixture session: the row's disconnected_at and the connection.closed audit entry built from it must both carry the staged historical instant, not wall-clock time, and this is the same seal-from-stored-statements routine CloseConnection uses — never a second MAC implementation.
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.
Why an instance id is not enough to scope it: an id is unique per live process only by convention — an operator can pin DBB_INSTANCE_ID to the same value on every replica, and config.FallbackInstanceID is where every replica that cannot read its hostname lands. Both halves therefore key on the run id, minted in memory at startup and unshareable, and both then ask the same question of the registry: does a live run still own this row? Identity only decides which of the two counts a row lands in.
The two halves are separate methods because they are reported separately and because only one of them belongs at startup. The own half is the previous runs of this instance id; the reclaim half — ReclaimDeadInstanceConnections — is everyone else, and is also run periodically (see InstanceReclaimInterval), which is what eventually picks up a run that was still inside its grace period when we started.
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) ConsumeLoginExchange ¶ added in v0.23.0
func (s *Store) ConsumeLoginExchange(ctx context.Context, code string) (uuid.UUID, uuid.UUID, []byte, error)
ConsumeLoginExchange redeems a code exactly once: the row is deleted as part of the read, so a replay (or a second tab racing the first) gets ErrLoginExchangeNotFound. Returns the row UID so the caller can rebuild the decryption AAD, the owning user, and the encrypted session token.
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) CountActiveGrantsForServerGroup ¶ added in v0.24.0
func (s *Store) CountActiveGrantsForServerGroup(ctx context.Context, groupUID uuid.UUID) (int64, error)
CountActiveGrantsForServerGroup reports how many currently-authorizing grants are bound to this server group. It is what tells an operator, before they add or remove a server, how much live access the edit moves — the blast radius of live membership, surfaced rather than implied.
func (*Store) CountAdmins ¶ added in v0.15.0
CountAdmins returns the number of users holding the admin role
func (*Store) CountGrantsForLineage ¶ added in v0.23.0
CountGrantsForLineage reports how many grants the lineage of the given definition uid is responsible for: every grant ever issued from any of its versions, and the subset still authorizing access. Both numbers matter — the first is what blocks a hard delete, the second is what an operator needs to see before deactivating.
func (*Store) CountUnchainedCapturedRows ¶ added in v0.24.0
CountUnchainedCapturedRows counts one capture's rows written before the row chain migration — the per-query counterpart of what VerifyRowChains reports as Unchained. VerifyRowChain walks a single capture but only sees the rows that carry a MAC, so a caller scoping to one query needs this to report the same "nothing sealed these" number the sweep does.
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, opts ...ConnectionOption, ) (*Connection, error)
CreateConnection creates a new connection record, stamping connected_at from time.Now().
func (*Store) CreateConnectionAt ¶ added in v0.25.1
func (s *Store) CreateConnectionAt( ctx context.Context, userID, databaseID uuid.UUID, sourceIP string, connectedAt time.Time, opts ...ConnectionOption, ) (*Connection, error)
CreateConnectionAt is CreateConnection with the open instant supplied by the caller instead of taken from time.Now(). It exists for seeding a back-dated demo/fixture session: the row's connected_at and the connection.opened audit entry built from it must both carry the staged historical instant, not wall-clock time, and this is the same write-then-evidence routine CreateConnection uses — never a second evidence-writing path.
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. The grant must name the definition it is an instance of: a grant with no definition would carry no shape at all, and there is deliberately no code path that produces one.
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) CreateLoginExchange ¶ added in v0.23.0
func (s *Store) CreateLoginExchange(ctx context.Context, exchangeUID uuid.UUID, code string, userID uuid.UUID, encryptedKey []byte) error
CreateLoginExchange parks an encrypted web session token behind a one-time code. The row UID is generated here (rather than by the database default) so the caller can bind the ciphertext's AAD to it before the insert.
func (*Store) CreateOAuthState ¶ added in v0.4.0
func (s *Store) CreateOAuthState(ctx context.Context, state *OAuthState, ttl time.Duration) (*OAuthState, error)
CreateOAuthState persists a new OAuth state (or one of the rows that share its table: a device authorization request, a login exchange) with a lifetime of ttl.
The expiry is stamped by the *database* clock, in the very statement that inserts the row, because that is the clock every reader tests it against (`expires_at > NOW()`). Stamping it from time.Now() instead made the real TTL `ttl ± skew`: a process running ahead of its store lengthened it — a login exchange or device authorization redeemable past its intended life — and one running behind shortened it. See Store.Now.
A negative ttl yields a row that is already expired; tests use that to build historical rows without reintroducing the process clock.
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) CreateServerGroup ¶ added in v0.24.0
func (s *Store) CreateServerGroup(ctx context.Context, group *ServerGroup) (*ServerGroup, error)
CreateServerGroup inserts a new server group. Names are unique case-insensitively.
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 withdraws a definition across its **whole lineage** — every version, archived ones included.
That reach is the point. An operator deactivates a policy, not one row of its version history; leaving older versions active would keep authorizing the grants pinned to them and make deactivation a kill switch that doesn't kill. Auth checks is_active, so this fails those grants closed on their next connection.
func (*Store) DeleteGrantDefinition ¶ added in v0.23.0
DeleteGrantDefinition hard-deletes a definition and every version of its lineage.
It refuses — GrantDefinitionInUseError, carrying the blocking counts — as soon as anything references any version. Grants carry no shape of their own and their grant_definition_id is NOT NULL, so deleting a referenced definition would either be impossible (the FK says so) or would strand a grant whose behavior nothing can describe. Deactivation is the operation for retiring a definition that has been used; deletion is only for one that never was.
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) DeleteServerGroup ¶ added in v0.24.0
DeleteServerGroup hard-deletes a server group. Memberships cascade away, and so does the `server_group_uid` of any grant bound to it (ON DELETE SET NULL), which narrows those grants back to their anchor database — never widens them. Grant definition scopes do NOT cascade: a definition scoped to the deleted group keeps the dangling uid and therefore matches no database (fail closed) until an admin edits it.
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 run 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.
Scoped to our own run, not to our instance id: deleting every row carrying the id would deregister a live replica that shares it, and a replica with no registry row has all of its open sessions reclaimed by the next reconcile.
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.
This is the auth path. All five protocol proxies resolve their session's grant through here, so it is the one place group-bound coverage has to be understood: a grant matches the target database when it is that grant's anchor **or** when the grant's server group currently contains it. Adding a server to a group therefore extends every live grant bound to that group, with no re-issuance — membership is read live and never snapshotted.
"Active" also includes the definition's own state: a grant whose definition was **deactivated** is not returned, so deactivation fails closed for every grant issued from that definition — including grants pinned to an older version, since deactivation applies to the whole lineage. Being **archived** (superseded by an edit) is explicitly not deactivation and is never consulted here: a grant keeps authorizing under the exact version it was issued from.
The window is compared against the *database's* clock (NOW() below), which is why every issuance path stamps starts_at from that same clock rather than from time.Now() — see Store.Now. A grant stamped from a process running ahead of its store would be approved and refused here until the skew elapsed.
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) GetConnectionsByUIDs ¶ added in v0.24.0
func (s *Store) GetConnectionsByUIDs(ctx context.Context, uids []uuid.UUID) (map[uuid.UUID]*Connection, error)
GetConnectionsByUIDs is the batched form of GetConnectionByUID: it reads every connection named in uids with exactly one query, however many uids are asked about, and returns them keyed by uid.
Same projection as GetConnectionByUID, deliberately — a batched read must not see a different row shape than the per-row one, since callers switch between them (see the pending-approvals listing, which resolves the grant governing a whole page from this map and then runs the *same* decision function over it).
A uid that names no row is simply absent from the map; the caller reads that as the fail-closed equivalent of GetConnectionByUID's ErrConnectionNotFound.
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) GetGrantDefinitionBySlug ¶ added in v0.22.0
func (s *Store) GetGrantDefinitionBySlug(ctx context.Context, slug string) (*GrantDefinition, error)
GetGrantDefinitionBySlug fetches the **live** definition with that slug. Archived versions keep their slug, so resolving by slug has to be pinned to `archived_at IS NULL` — addressing a historical version is by uid, which is the only unambiguous handle for one. Returns ErrGrantDefinitionNotFound if no live definition has that slug — same sentinel as GetGrantDefinition, so callers can try one after the other without distinguishing the miss.
func (*Store) GetGrantRequest ¶ added in v0.10.0
GetGrantRequest fetches a request by UID.
func (*Store) GetGrantsByUIDs ¶ added in v0.24.0
func (s *Store) GetGrantsByUIDs(ctx context.Context, uids []uuid.UUID) (map[uuid.UUID]*Grant, error)
GetGrantsByUIDs is the batched form of GetGrantByUID: two queries whatever the number of uids — one for the grant rows, one for the definitions they point at (attachDefinitions) — returning the grants keyed by uid.
Unfiltered on purpose, exactly like GetGrantByUID: a lookup by uid must still resolve a grant whose definition has since been deactivated or superseded.
**It does not populate the live usage counters** (QueryCount and BytesTransferred stay zero), which is the one way it differs from GetGrantByUID. Those are two aggregate queries *per grant* — the very per-row cost batching exists to remove — and they say nothing about who may approve anything. The caller this exists for is the approval chain, which reads only the definition's approver list. Anything that needs the counters must keep using GetGrantByUID.
A uid naming no row is absent from the map, which the caller reads as the fail-closed equivalent of GetGrantByUID's ErrGrantNotFound.
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 the most recently seen run registered under an instance id, or nil when the instance is not registered at all. Mostly useful to tests and to operators eyeballing the registry.
One row per id was the schema until run ids arrived; now an id can carry several runs — a restart whose predecessor has not gone stale yet, or replicas sharing a pinned DBB_INSTANCE_ID — so this answers "is anything running under this id, and how fresh is it?". Nothing in the reconcile uses it: liveness is decided in SQL, on the pair. See LiveRunsSharingInstanceID for the full picture of one id.
func (*Store) GetLiveGrantDefinition ¶ added in v0.23.0
func (s *Store) GetLiveGrantDefinition(ctx context.Context, uid uuid.UUID) (*GrantDefinition, error)
GetLiveGrantDefinition returns the current version of the lineage the given definition uid belongs to. Passing the live version's own uid returns it unchanged; passing an archived version walks forward to whatever superseded it. Used wherever a decision must apply *today's* policy — approving a request filed before an edit, for instance — rather than the version that happened to be current when the row was first referenced.
func (*Store) GetOracleServiceNameConflict ¶ added in v0.25.0
func (s *Store) GetOracleServiceNameConflict( ctx context.Context, serviceName string, ) (*OracleServiceNameConflict, bool, error)
GetOracleServiceNameConflict returns the conflict for one service name. The bool is what says whether there is one: the rows claiming the name may well agree, or nothing may claim it, and neither is an error.
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) GetServerGroup ¶ added in v0.24.0
GetServerGroup fetches a server group 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) HasServerApproverGroups ¶ added in v0.24.0
HasServerApproverGroups reports whether any server or server group names one of the given user groups as an approver, of either kind. It is the server-level half of the "is this user an approver *somewhere*" question the approvals/pending subscribe gate asks, next to HasApproverGroups' grant-level half.
Deliberately coarse — like its sibling, it gates subscribing, not receiving; each event is still filtered individually.
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.
The session events (connection.opened / connection.closed) are left out of an unfiltered listing — see SessionAuditEventTypes for why, and ask for one by name to get it back.
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 live definitions matching the filter — archived versions never appear in a listing or a picker, since issuing from a superseded version is exactly what versioning exists to prevent. Admins want `ActiveOnly=false` to see deactivated entries; the request UI passes `ActiveOnly=true`.
Each returned definition carries ActiveGrantCount for its whole lineage, so the UI can tell an operator how much access deactivating it would cut off. It is one grouped query, not one per row.
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) ListLatestEventPerUser ¶ added in v0.24.0
func (s *Store) ListLatestEventPerUser(ctx context.Context, eventType string) ([]UserRoleSync, error)
ListLatestEventPerUser returns, for each user that has one, the newest audit entry of eventType — one row per user, whatever the volume of the audit log.
This exists because the alternative the UI used first — fetch the newest N entries of a type and keep the first one seen per user — degrades silently: a user whose last sync fell outside that window becomes indistinguishable from a user who never had one. Raising N moves the cliff instead of removing it. DISTINCT ON does the picking in the database, so the answer is exact.
Ordering is `user_id, uid DESC`: `uid` is a UUIDv7 assigned at insert, so descending is newest-first, and DISTINCT ON keeps the first row of each group. The join to `users` is what supplies the username and, incidentally, drops entries whose user has since been deleted — including soft-deleted ones, filtered here explicitly because bun only applies soft-delete rules to the model's own table.
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) ListOracleServiceNameConflicts ¶ added in v0.25.0
func (s *Store) ListOracleServiceNameConflicts(ctx context.Context) ([]OracleServiceNameConflict, error)
ListOracleServiceNameConflicts returns every upstream Oracle service name the fleet claims from more than one address, ordered by service name.
Only the dedicated `oracle_service_name` column is considered — not the database-name fallback the probe uses — because that column is exactly what the proxy's candidate lookup keys off, and therefore the only value that can produce the ambiguous-service refusal. Soft-deleted rows are excluded (bun's soft-delete filter), so a row an admin has already removed never keeps a warning alive.
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) ListServerGroupMemberUIDs ¶ added in v0.24.0
func (s *Store) ListServerGroupMemberUIDs(ctx context.Context, groupUID uuid.UUID) ([]uuid.UUID, error)
ListServerGroupMemberUIDs returns the server UIDs belonging to a group.
func (*Store) ListServerGroupMemberUIDsByGroups ¶ added in v0.24.0
func (s *Store) ListServerGroupMemberUIDsByGroups(ctx context.Context, groupUIDs []uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
ListServerGroupMemberUIDsByGroups is the batched form of ListServerGroupMemberUIDs: it resolves the current membership of every group in groupUIDs with exactly one query, however many groups are asked about. This is what lets a grant-definition listing resolve every definition's scoped_database_uids without firing one membership query per definition (see GrantDefinition.ScopedDatabaseUIDs).
A group with no members (or one not present in groupUIDs at all) is simply absent from the result map; callers read it with a plain map index, which yields a nil slice either way.
func (*Store) ListServerGroupMembers ¶ added in v0.24.0
ListServerGroupMembers returns the server rows belonging to a group.
func (*Store) ListServerGroupUIDsForServer ¶ added in v0.24.0
func (s *Store) ListServerGroupUIDsForServer(ctx context.Context, serverUID uuid.UUID) ([]uuid.UUID, error)
ListServerGroupUIDsForServer returns the UIDs of the server groups a given server currently belongs to.
This is the auth path's membership lookup: every protocol resolves its grant through GetActiveGrant, and every scoping decision about a definition asks this question about the target database. Membership is read live and never snapshotted — see ServerGroup's doc comment for the consequence.
func (*Store) ListServerGroups ¶ added in v0.24.0
func (s *Store) ListServerGroups(ctx context.Context) ([]ServerGroup, error)
ListServerGroups returns every server group, name-ordered.
func (*Store) ListServerGroupsForServer ¶ added in v0.24.0
func (s *Store) ListServerGroupsForServer(ctx context.Context, serverUID uuid.UUID) ([]ServerGroup, error)
ListServerGroupsForServer returns the full group rows a server belongs to, for the server detail response and the admin UI.
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) ListTunnelServers ¶ added in v0.24.0
ListTunnelServers returns every *dial path* row — SSH bastions and Kubernetes clusters — for the "via" selector and the tunnels admin view. Like ListSSHServers these rows are excluded from every grantable/connectable target listing; unlike it, it does not pretend SSH is the only way through.
func (*Store) ListUserGroupMemberUIDs ¶ added in v0.24.0
func (s *Store) ListUserGroupMemberUIDs(ctx context.Context, groupUID uuid.UUID) ([]uuid.UUID, error)
ListUserGroupMemberUIDs returns the user UIDs belonging to a user group.
func (*Store) ListUserGroupMembers ¶ added in v0.24.0
ListUserGroupMembers returns the (non-deleted) users belonging to a user group.
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) ListUserGroupsForUser ¶ added in v0.24.0
ListUserGroupsForUser returns the full user-group rows a user belongs to, for the user detail response and the admin UI.
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) LiveRunsSharingInstanceID ¶ added in v0.21.0
LiveRunsSharingInstanceID returns the registry rows of *other* runs that carry this process's instance id and are still heartbeating.
Non-empty means several live processes share one DBB_INSTANCE_ID. The reconcile is safe when that happens — it keys on the run id — but it is still worth saying out loud at startup: the operator almost certainly meant the ids to be unique, the "own previous run" count stops meaning what its name says, and every session in the UI attributes to the same instance. Call it after RegisterInstance, which excludes our own freshly written row.
func (*Store) LogAuditEvent ¶
func (s *Store) LogAuditEvent(ctx context.Context, event *AuditEvent) error
LogAuditEvent creates a new audit log entry.
When the store has a chain key, the row is appended to the tamper-evident audit chain: it is inserted inside a transaction holding a PostgreSQL advisory lock, carries the previous row's MAC, and carries its own MAC over (chain_seq, uid, event_type, user_id, performed_by, details, created_at, prev_mac). The lock plus the in-process mutex make the head a real serialization point — two concurrent appends must not read the same head, or one of them would be silently overwritten. Volume here is admin actions, so the contention that buys is negligible.
func (*Store) LogAuditEvents ¶ added in v0.24.0
func (s *Store) LogAuditEvents(ctx context.Context, events ...*AuditEvent) error
LogAuditEvents appends several audit entries as one chained batch: one transaction, one advisory lock, one bulk INSERT, and the entries linked to each other in the order they were given.
It exists for the session reconcile, which closes an arbitrary number of crash-orphaned connections in one pass and owes each of them a `connection.closed` entry. Appending those one at a time would be one transaction and one round trip to the store-wide chain lock *per connection*, which is exactly the shape the row-chain batching exists to avoid.
The batch is all-or-nothing. A chain is a sequence, so a partially applied batch would either leave a gap or force the survivors to be re-linked; the transaction is what makes "some of these landed" not a state.
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) MayApproveForServer ¶ added in v0.24.0
func (s *Store) MayApproveForServer( ctx context.Context, serverUID uuid.UUID, kind ApproverKind, userGroupUIDs []uuid.UUID, ) (bool, error)
MayApproveForServer reports whether a user holding userGroupUIDs is named, directly or through the server's groups, as an approver of this kind for this server. Admin-ness is the caller's business — any admin may decide anything — and so is the self-approval refusal, which no membership can override.
A server that cannot be loaded (deleted since) resolves to false: the fail-closed direction.
This is the one-server convenience. A caller judging many rows at once resolves with ResolveServerApproverGroupsByServers and asks the resulting ServerApprovers.MayApprove instead — the same intersection over the same chain, minus the per-row round trips.
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) Now ¶ added in v0.24.0
Now returns the *database server's* clock.
Anything whose window is later evaluated in SQL against NOW() has to be stamped from here rather than from time.Now(): dbbat and its store are two machines, their clocks are only ever approximately equal, and a grant stamped a few milliseconds in the store's future is refused by the auth path (`starts_at <= NOW()` in GetActiveGrant) for exactly as long as the skew lasts. See dbNow.
Which writers must use it, decided site by site when the short TTLs were moved over:
- Production code writing a bound that SQL later judges — grant windows (IssueGrant, ApproveGrantRequest) and the oauth_states TTLs (device authorizations, login exchanges, OAuth CSRF states) — must. The last three do it SQL-side, inside the insert, so there is no extra round trip: see CreateOAuthState.
- Integration fixtures that insert a window into a real store and then drive the auth path over it must too, because the container's clock is nobody's guarantee — internal/proxy/testsupport does.
- Unit tests holding a store.Grant in memory (internal/mcp, internal/proxy/*) need not: no database ever judges those bounds, so there is only one clock. Same for a Go value compared against time.Now() on both sides, like an APIKey's expiry.
- This package's own tests keep time.Now() for the same reason, margins notwithstanding: several of them stamp `starts_at: now` with no margin at all, and it is harmless because nothing filters those rows on it. They assert on the value CreateGrant returns, or fetch it back with GetGrantByUID — a primary-key lookup with no window predicate. The moment a test reads a row through GetActiveGrant instead, the SQL window filter is in play and it has to stamp from here, whatever the margin: a zero-margin window issued and immediately read back through that filter is precisely the parent flake.
So the criterion is "is this window ever judged by SQL?", not "is the margin wide enough". Margin width only buys comfort where the answer is already yes.
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, and now one row per crashed run of a stable instance id as well.
Only call it after the reclaim has run, so the reclaim still sees the rows it is judging.
func (*Store) ReactivateGrantDefinition ¶ added in v0.23.0
ReactivateGrantDefinition is the inverse of DeactivateGrantDefinition, again across the whole lineage.
func (*Store) ReclaimDeadInstanceConnections ¶ added in v0.21.0
ReclaimDeadInstanceConnections closes the connections of every run other than this one that the registry proves is gone, and returns how many it closed.
Unlike the own half of CloseOrphanedConnections this is not tied to startup: everything except this run is in scope, so it holds at any point in the process's life. It runs both from the startup reconcile and on a timer (InstanceReclaimInterval), because the crash case is otherwise only ever noticed by an unrelated restart: a SIGKILLed pod leaves a registry row whose last_seen_at is seconds old, so its replacement — starting immediately — reclaims nothing, and by the time the row does go stale nothing is starting any more. Since a restart mints a fresh run id, that now covers our own predecessor as well as other instances: a stable instance id no longer means its crashed run's rows wait for a restart that may be days away.
The test is liveness, not identity. A running process upserts a row in `instances` for its (instance id, run id) at startup and refreshes it every InstanceHeartbeatInterval; a clean shutdown deletes it. So another run's connections are only touched when that run 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, even one sharing our instance id: 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 same reasoning, and the same remedy, apply to the run id one migration later: rows written before it existed carry NULL, are judged by their instance id alone (see noLiveOwner), and 20260804120000 refreshes the registry rows of every pre-run-tracking owner so the upgrade window is a full grace period rather than an instant.
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 reclaims nothing (zero, nil), matching CloseOrphanedConnections: a process with no identity of its own has no business judging anyone else's.
func (*Store) RefreshOpenChainStamps ¶ added in v0.24.0
RefreshOpenChainStamps re-seals the query chain head of every session this run still has open, and returns how many rows it stamped.
Without it the stamp is only ever written by a *close* — CloseConnection or the reconcile — so a session that never ends has no stamp at all, and the stamp is the only thing that detects statements deleted from the *end* of a chain. A psql window left open all day, a pooled application connection or an approval hold waiting on a human is therefore unprotected at its tail for its whole life, and a crashed session is unprotected from the crash until the reclaim notices it (up to InstanceStaleAfter plus InstanceReclaimInterval). Running this on the reclaim timer bounds both windows by the sweep interval instead.
Scoped to rows this run owns (run_id = s.runID). Two replicas sharing a store would otherwise both read and rewrite the same stamps every pass, for no gain: a row's head is best known to the process serving it, and a run id is minted in memory and cannot be shared. Rows predating run tracking carry NULL and are nobody's to refresh — they belong to a build that never wrote one.
What the stamp then attests to is a *prefix*: the chain up to the position the last sweep sealed. Statements appended since are not covered, which is why checkStampedHead judges an open session by the prefix rule and only a closed one exactly. Deleting statements newer than the last sweep stays undetectable; this shrinks that window to the sweep interval rather than closing it.
func (*Store) RegisterInstance ¶ added in v0.20.0
RegisterInstance records this run 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.
The row is keyed by the pair, so registering never disturbs another live run that happens to carry the same instance id — before run ids that upsert overwrote the peer's heartbeat, which made a process that was serving traffic look like it had stopped reporting. In exchange, a restart leaves the previous run's row behind until it goes stale and is pruned; that is what makes the reconcile wait out the grace period rather than trusting an id.
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) RemoveServerFromGroup ¶ added in v0.24.0
RemoveServerFromGroup drops a membership. Removing a non-membership is a no-op rather than an error.
func (*Store) RemoveUserFromUserGroup ¶ added in v0.24.0
RemoveUserFromUserGroup 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) ResolveServerApproverGroups ¶ added in v0.24.0
func (s *Store) ResolveServerApproverGroups( ctx context.Context, serverUID uuid.UUID, kind ApproverKind, ) ([]uuid.UUID, error)
ResolveServerApproverGroups is **the** approver resolution: the single function every decision path (grant requests, query holds, both Slack transports) asks who may decide for a given database. Implementing the chain twice would guarantee the two drift, and a drift here is an authorization bug.
The chain, most specific first:
- the server's own list for this kind, when non-empty;
- otherwise the **union** of the lists on every server group the server currently belongs to — the same union rule a definition's multiple approver groups already follow;
- otherwise nothing, which every caller reads as "admins only".
Levels do not union with each other: naming a group on the server is how an operator overrides the group-level default for that one database.
Every read is live. Editing a list — or moving a server between groups — changes who may decide immediately, including for grant requests already filed and statements already parked. That is the deliberate second exception to dbbat's "a live grant's behavior never changes under it" rule (the first being server-group membership itself): approver lists are operational data, and a departed lead's replacement has to be effective now.
func (*Store) ResolveServerApproverGroupsByServers ¶ added in v0.24.0
func (s *Store) ResolveServerApproverGroupsByServers( ctx context.Context, serverUIDs []uuid.UUID, kind ApproverKind, ) (ServerApprovers, error)
ResolveServerApproverGroupsByServers is the batched form of ResolveServerApproverGroups, and — since that function is now a thin wrapper over this one — the actual home of the chain documented there. There is still one implementation; a listing simply asks it about every row at once.
Cost is two queries whatever the number of servers: one for the server rows and their own lists, one joining server_group_members to server_groups for the group-level fallback. That is what keeps a delegated approver's pending page from firing two round trips per row.
Liveness is unchanged: both reads happen now, so the answer reflects the approver lists and the group membership as they stand at this instant, not as they stood when the request was filed or the statement parked.
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) RunID ¶ added in v0.21.0
RunID returns the identifier of this run: unique per live process, unlike the instance id, which an operator can pin to the same value on every replica.
func (*Store) SealQueryRowChain ¶ added in v0.24.0
SealQueryRowChain stamps the final head of a capture's row chain onto its query, and forgets the cached head.
It is called at the flush barrier — the point where every captured row is durable (or definitively lost) and the query is about to be marked complete — so it is the row-chain counterpart of what CloseConnection does for a session's statements. Without the stamp, deleting the last captured rows would leave a shorter chain that still verified end to end.
A query this process chained nothing for costs nothing: no cached head means nothing was captured (or the capture was already sealed), and the answer is given without touching the database.
The gap it shares with connections.query_chain_mac: a process that dies mid-capture never reaches the barrier, so that capture keeps a NULL stamp and only its prefix and interior are protected. A missing stamp is never a break.
func (*Store) SetAuthCache ¶ added in v0.1.0
SetAuthCache sets the authentication cache for API key verification.
func (*Store) SetChainKey ¶ added in v0.24.0
SetChainKey installs the HMAC key that seals the audit and query chains. It is the derived subkey, never the master key — see crypto.DeriveAuditChainKey. Passing nil disables chaining, which is what a store built without an encryption key does.
func (*Store) SetConnectionDumpKey ¶ added in v0.23.0
SetConnectionDumpKey records where this connection's session capture was uploaded. Called by the capture uploader after the object is in place and before the local spool copy is removed, so a capture is always addressable in exactly one place.
A connection row that has since been reaped by the retention sweep is not an error: the capture outliving its row is a retention-ordering artifact, not a failed upload, and failing here would make the uploader retry forever.
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) 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) SetKubernetesCACert ¶ added in v0.24.0
SetKubernetesCACert persists the TOFU-learned API server CA bundle for a kubernetes cluster row, merging into protocol_data.kubernetes.learned_ca_cert without disturbing other keys — the analog of SetKnownHostKey.
It writes learned_ca_cert, never ca_cert: an operator-supplied bundle is a statement of intent that dbbat must never overwrite with something it merely observed on the wire.
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) SetRunID ¶ added in v0.21.0
SetRunID overrides the run id minted by New. Tests use it to impersonate another run — a previous one of this instance id, or a live peer sharing the id; a serving process never calls it, because a run id that is not unique to one live process defeats the whole point of having one.
func (*Store) SetServerGroupMembers ¶ added in v0.24.0
func (s *Store) SetServerGroupMembers(ctx context.Context, groupUID uuid.UUID, serverUIDs []uuid.UUID) error
SetServerGroupMembers replaces a group's membership with exactly the given set of servers, 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) SetUserGroupMembers ¶ added in v0.24.0
func (s *Store) SetUserGroupMembers(ctx context.Context, groupUID uuid.UUID, userUIDs []uuid.UUID) error
SetUserGroupMembers replaces a user 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) 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.
When chaining is on the batch is sealed as it lands: it is split per query, each slice extends that query's own row chain, and the whole thing still goes in as one INSERT — see appendRowChains for why that split does not cost a round trip per query.
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) (*GrantDefinition, error)
UpdateGrantDefinition edits a definition by **versioning** it: the current row is archived (`archived_at = now()`) and a successor carrying the change is inserted with the same lineage. It returns the new version.
Nothing is mutated in place, and that is the whole point: grants pin the exact row they were issued from, so tightening — or loosening — a definition can never retroactively change access that is already live. The new version applies to everything issued from here on.
A no-op edit (every editable field already equal) is not versioned; the existing row is returned untouched, so a targeted PATCH that happens to change nothing doesn't litter the history.
Editing an already-archived version is refused with ErrGrantDefinitionArchived: history is immutable, and the caller means the live version.
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, ErrServerNameInvalid if a rename does not match the slug format, and ErrServerNameConflict if the new name is already taken (by a live *or* soft-deleted row — servers_name_key is global).
func (*Store) UpdateServerGroup ¶ added in v0.24.0
func (s *Store) UpdateServerGroup(ctx context.Context, group *ServerGroup) error
UpdateServerGroup mutates the editable fields of a server group.
The approver lists are written unconditionally, like name and description: the caller passes the group it read and mutated, so an unchanged list writes itself back. They take effect immediately for every decision — see ResolveServerApproverGroups.
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.
func (*Store) UserHasGrantForDefinition ¶ added in v0.23.0
func (s *Store) UserHasGrantForDefinition(ctx context.Context, userID, definitionID uuid.UUID) (bool, error)
UserHasGrantForDefinition reports whether userID holds any grant (active, expired, or revoked) issued from definitionID. Used to let a non-admin read a grant definition by uid even when it is archived or otherwise out of their normal scope: GET /grants already embeds that same definition unfiltered (see attachDefinitions), so a direct GET of the definition a caller's own grant points at must not be more restrictive than what the grant response already shows them.
func (*Store) VerifyAPIKey ¶
VerifyAPIKey verifies a plain text API key and returns the associated key record It checks that the key exists, is not revoked, and is not expired
func (*Store) VerifyAuditChain ¶ added in v0.24.0
func (s *Store) VerifyAuditChain(ctx context.Context) (AuditChainResult, error)
VerifyAuditChain walks the store-wide audit chain oldest to newest.
func (*Store) VerifyQueryChain ¶ added in v0.24.0
func (s *Store) VerifyQueryChain( ctx context.Context, connectionUID uuid.UUID, ) (QueryChainResult, error)
VerifyQueryChain walks one connection's query chain.
func (*Store) VerifyQueryChains ¶ added in v0.24.0
func (s *Store) VerifyQueryChains( ctx context.Context, connectionUID *uuid.UUID, ) (QueryChainsResult, error)
VerifyQueryChains walks the query chain of every connection that has one, or of one connection when connectionUID is set.
func (*Store) VerifyRowChain ¶ added in v0.24.0
VerifyRowChain walks one capture's result-row chain.
The position is row_number, and it is deliberately not required to be dense: a row the batched writer had to drop, or one that failed to encode, leaves a gap that is not evidence of anything. Density is not what makes a deletion detectable — the prev_mac linkage is. Removing a row from the middle leaves its successor pointing at a MAC no surviving row has.
func (*Store) VerifyRowChains ¶ added in v0.24.0
func (s *Store) VerifyRowChains(ctx context.Context, connectionUID *uuid.UUID) (RowChainsResult, error)
VerifyRowChains walks the result-row chain of every capture that has one, or of one connection's captures when connectionUID is set.
The row chains differ from the query chains in one way that matters to a walk: retention never deletes an individual captured row. It deletes whole queries (and whole connections), and query_rows.query_id cascades — so unlike a session's statements, a capture has no legitimate reason to be missing its prefix. A first row whose prev_mac is not the capture's genesis MAC is a break, not housekeeping.
type StringArray ¶ added in v0.23.0
type StringArray []string
StringArray is a []string that round-trips through a PostgreSQL `text[]` column without going through bun's own array codec.
Why dbbat carries its own: bun's PostgreSQL array *parser* (`dialect/pgdialect`, `arrayParser.readNext`) treats an element whose first byte is `(` or `[` as a range/composite literal and terminates it at the matching bracket, so the rest of the element becomes a *second* element. PostgreSQL emits array elements unquoted whenever they need no quoting, so a perfectly-stored `{(?i)^DELETE}` reads back as `["(?i)", "^DELETE"]`.
That is not cosmetic for `grant_definitions.approval_patterns`: `(?i)` on its own is a regexp that matches *every* statement, so a definition whose only pattern is the documented `(?i)^DELETE` form would put an approval hold on every query the grant runs — and the surviving `^DELETE` half is case-sensitive, exactly what the `(?i)` was there to prevent.
Fields using this type must NOT carry bun's `,array` tag option: the option is what selects bun's codec, and the whole point here is to use driver.Valuer / sql.Scanner instead. Encoding quotes every element, so the literal dbbat writes is never ambiguous; decoding implements the PostgreSQL `array_out` grammar (quoted or bare elements, `\` escapes inside quotes) and nothing else.
Round-tripping is lossy in exactly one place: a NULL *element* (which PostgreSQL renders as a bare `NULL`) decodes to the empty string, because []string cannot represent it. No dbbat column stores NULL elements.
func (*StringArray) Scan ¶ added in v0.23.0
func (a *StringArray) Scan(src any) error
Scan parses a PostgreSQL array literal as emitted by `array_out`.
func (StringArray) Value ¶ added in v0.23.0
func (a StringArray) Value() (driver.Value, error)
Value renders the slice as a PostgreSQL array literal with every element double-quoted. A nil slice becomes the empty array rather than NULL: every column dbbat uses this type for is `notnull default '{}'`, and an empty array is the honest representation of "no entries" anyway.
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 StringArray `bun:"roles" 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 UserRoleSync ¶ added in v0.24.0
type UserRoleSync struct {
bun.BaseModel `bun:"table:audit_log,alias:al"`
UID uuid.UUID `bun:"uid,type:uuid" json:"uid"`
EventType string `bun:"event_type" json:"event_type"`
UserID uuid.UUID `bun:"user_id,type:uuid" json:"user_id"`
Username string `bun:"username,scanonly" json:"username"`
Details json.RawMessage `bun:"details,type:jsonb" json:"details"`
CreatedAt time.Time `bun:"created_at" json:"created_at"`
}
UserRoleSync is the newest directory role-sync audit entry of one user.
It is a projection of `audit_log`, not a table: the audit entry stays the only record of what happened, and this carries it verbatim — `Details` included, because the directory groups it names are the answer to "why did this change?". The joined `Username` saves the caller a second lookup to render a row.
type UserUpdate ¶
type UserUpdate struct {
PasswordHash *string
Roles StringArray
}
UserUpdate represents fields that can be updated
Source Files
¶
- api_keys.go
- api_keys_oracle.go
- approvals.go
- array.go
- audit.go
- chain.go
- chain_canonical.go
- chain_verify.go
- connection_audit.go
- connections.go
- device_auth.go
- errors.go
- global_parameters.go
- grant_definitions.go
- grant_requests.go
- grants.go
- instances.go
- models.go
- mongo_scram.go
- notify.go
- oauth_exchange.go
- oauth_states.go
- queries.go
- server_approvers.go
- server_groups.go
- servers.go
- servers_oracle_conflicts.go
- store.go
- uid.go
- user_groups.go
- user_identities.go
- users.go