notify

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const MaxSlackSQLLength = 500

MaxSlackSQLLength bounds the query text copied into a Slack message.

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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.

Jump to

Keyboard shortcuts

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