Documentation
¶
Overview ¶
Package notify implements notification channels for grant request lifecycle events. Today this is Slack-only. Notifications are posted outbound to a configured channel and, when an inbound transport is configured, carry interactive Approve / Deny buttons whose clicks flow back into the decision pipeline. Two inbound transports are supported: a signing-secret-verified HTTPS endpoint (see internal/api/slack_interactions.go) and an outbound Socket Mode connection for deployments that can't accept inbound Slack traffic (see internal/api/slack_socketmode.go). With neither configured the feature degrades to link-through-UI: messages have no buttons and the admin decides in the dbbat UI.
Index ¶
Constants ¶
const ( // ActionApproveQuery releases a held statement. ActionApproveQuery = "query_approval_approve" // ActionDenyQuery rejects a held statement. ActionDenyQuery = "query_approval_deny" )
Slack interaction action IDs carried by the approval Approve / Deny buttons. Distinct from the grant-request ones so the inbound handler can tell a query hold from a grant request by action id alone.
const ( ActionApprove = "grant_request_approve" ActionDeny = "grant_request_deny" )
Slack interaction action IDs carried by the Approve / Deny buttons. The inbound interaction handler matches on these; kept here so the button rendering and the handler can't drift.
const MaxSlackSQLLength = 500
MaxSlackSQLLength bounds the query text copied into a Slack message.
Variables ¶
var ErrAppTokenWithoutBotToken = errors.New("DBB_SLACK_NOTIFY_APP_TOKEN set without DBB_SLACK_NOTIFY_BOT_TOKEN")
ErrAppTokenWithoutBotToken is returned by NewSlackNotifier when an app-level token (Socket Mode) is set but no bot token is: Socket Mode receives interactions that ride on notification messages and posts decisions with the bot token, so it is meaningless without notifications.
var ErrChannelMissing = errors.New("DBB_SLACK_NOTIFY_BOT_TOKEN set without DBB_SLACK_NOTIFY_CHANNEL")
ErrChannelMissing is returned by NewSlackNotifier when a bot token is set but no channel is configured.
var ErrPublicURLMissing = errors.New("DBB_SLACK_NOTIFY_BOT_TOKEN set without DBB_PUBLIC_URL")
ErrPublicURLMissing is returned by NewSlackNotifier when a bot token is set but no public URL is configured.
var ErrSigningSecretWithoutBotToken = errors.New("DBB_SLACK_SIGNING_SECRET set without DBB_SLACK_NOTIFY_BOT_TOKEN")
ErrSigningSecretWithoutBotToken is returned by NewSlackNotifier when a signing secret is set but no bot token is: interactivity lives on notification messages, so it is meaningless without notifications.
Functions ¶
This section is empty.
Types ¶
type ApprovalEscalator ¶ added in v0.20.0
type ApprovalEscalator struct {
// contains filtered or unexported fields
}
ApprovalEscalator posts a Slack message for a hold that is still pending after a fixed delay, and updates that message in place the moment the hold resolves — by any route.
The trigger is purely time-based. There is no live-subscriber detection and no cluster-wide presence bookkeeping: if an admin was watching, they had the delay to act, and resolving the hold cancels the pending notification. That behaves identically in the case that actually matters (nobody watching) and is dramatically simpler.
Exactly one instance posts per hold because the timer is owned by the replica running the parked session — the session is pinned to one pod anyway, so no election is needed.
func NewApprovalEscalator ¶ added in v0.20.0
func NewApprovalEscalator(notifier *SlackNotifier, delay time.Duration, includeSQL bool, log *slog.Logger) *ApprovalEscalator
NewApprovalEscalator returns an escalator, or nil when escalation is disabled (no notifier, or a non-positive delay). A nil escalator is a no-op on every method.
func (*ApprovalEscalator) Resolved ¶ added in v0.20.0
func (e *ApprovalEscalator) Resolved(ctx context.Context, queryUID uuid.UUID, status, byName, reason string)
Resolved cancels a not-yet-fired escalation, or rewrites the posted message in place if it already fired.
Because there is no approval timeout, a posted message would otherwise stay actionable indefinitely — and a stale Approve button that silently no-ops is worse than no button at all, especially when it can linger for hours.
func (*ApprovalEscalator) Schedule ¶ added in v0.20.0
func (e *ApprovalEscalator) Schedule(ctx context.Context, hold ApprovalHold)
Schedule arms the escalation timer for a hold.
type ApprovalHold ¶ added in v0.20.0
type ApprovalHold struct {
QueryUID uuid.UUID
ConnectionUID uuid.UUID
Username string
DatabaseName string
SQL string
Pattern string
StartedAt time.Time
}
ApprovalHold is what the escalator needs to render a message.
type GrantAction ¶
type GrantAction string
GrantAction names the lifecycle event a notification refers to.
const ( GrantActionCreated GrantAction = "created" GrantActionApproved GrantAction = "approved" GrantActionDenied GrantAction = "denied" GrantActionCancelled GrantAction = "cancelled" //nolint:misspell // matches DB lifecycle value )
Lifecycle actions a notification can describe. The cancel value matches the DB CHECK constraint spelling.
type GrantRequestEvent ¶
type GrantRequestEvent struct {
Action GrantAction
Request *store.GrantRequest
Definition *store.GrantDefinition
Server *store.Server
Requester *store.User
// Decider is set when Action is approved/denied/canceled.
Decider *store.User
// RequesterSlackID is the requester's linked Slack user ID, or "" if
// the requester has no linked Slack identity. Used to @-mention them.
RequesterSlackID string
// AdminSlackIDs are the Slack user IDs of admins with a linked Slack
// identity, used to @-mention them on the pending message.
AdminSlackIDs []string
// DeciderSlackID is the decider's linked Slack user ID (set on decision
// events), used to @-mention them in the thread reply.
DeciderSlackID string
// Interactive requests Approve / Deny buttons on the pending message.
// Only honored for GrantActionCreated; every other render (decision,
// cancel) rebuilds blocks without buttons so stale buttons disappear.
Interactive bool
}
GrantRequestEvent carries the data the notifier needs to render a message. Fields besides Request are looked up by the API handler before firing — denormalizing here keeps the notifier free of store lookups (so a slow DB doesn't block Slack and vice-versa).
type SlackNotifier ¶
type SlackNotifier struct {
// contains filtered or unexported fields
}
SlackNotifier posts Block Kit messages to a configured channel for grant request lifecycle events. A nil notifier is a graceful no-op (returned from NewSlackNotifier when the bot token is unset).
func NewSlackNotifier ¶
func NewSlackNotifier(cfg config.SlackNotifyConfig, publicURL string, persister SlackPersister, log *slog.Logger) (*SlackNotifier, error)
NewSlackNotifier returns a configured notifier or nil when the feature is disabled. A startup error fires only when notification is enabled but dependent fields are missing — silent disable is intentional for deployments that just don't want it.
func (*SlackNotifier) Interactive ¶ added in v0.14.0
func (n *SlackNotifier) Interactive() bool
Interactive reports whether Approve / Deny buttons are rendered and the inbound interaction endpoint should be served. A nil notifier is never interactive.
func (*SlackNotifier) NotifyGrantRequest ¶
func (n *SlackNotifier) NotifyGrantRequest(ctx context.Context, ev GrantRequestEvent)
NotifyGrantRequest posts a fresh message on creation and chat.update's the existing message on subsequent lifecycle events. All errors are logged and swallowed — notifications are best-effort and must never fail the API request that triggered them.
func (*SlackNotifier) PostThreadReply ¶ added in v0.14.0
func (n *SlackNotifier) PostThreadReply(ctx context.Context, channel, ts, text string)
PostThreadReply posts a plain-text reply in the thread of an existing message (channel + ts). It notifies watchers of a decision — unlike a chat.update, which edits silently. Best-effort: errors are logged and swallowed, and a nil notifier or missing coordinates is a no-op.
func (*SlackNotifier) SigningSecret ¶ added in v0.14.0
func (n *SlackNotifier) SigningSecret() string
SigningSecret returns the Slack app signing secret used to verify inbound interaction callbacks. Empty on a nil notifier or when interactivity is disabled.
type SlackPersister ¶
type SlackPersister interface {
SetGrantRequestSlackMessage(ctx context.Context, uid uuid.UUID, channel, ts string) error
}
SlackPersister is the slice of the store API the notifier uses to remember which Slack message corresponds to a request, so it can chat.update on follow-ups.