connalert

package
v1.131.2 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package connalert tells somebody when a connection's stored OAuth credential is discarded, and tells somebody else when nobody has come back to it (#1694).

#395 made the revocation observable: the platform refreshes proactively, records a durable auth-event history, and a status card distinguishes revoked from never-connected. Every one of those is pull — each answers an operator who already knows to ask. This package is the push.

It matters here more than on the platform's other auth surfaces because this is the one where the person holding the upstream credential is deliberately not the person using the connection day to day. An agent's call comes back needing reauthorization and the agent reports that to whoever is watching; on a schedule nobody is, and the run's empty answer reads as data rather than as an outage.

Three pieces:

Settings   the operator's escalation window and recipient list, stored in
           the platform_settings section the admin API writes
AlertStore the open revocations: one row per connection, inserted when the
           credential is discarded, deleted when it is authorized again,
           and stamped once when the escalation goes out
Alerter    the authevents.RevocationSink that opens the row and mails the
           person whose authorization lapsed
Escalator  the timer that mails the operator's recipients about a
           revocation nobody has acted on

It must not import pkg/platform: the HTTP composition root supplies the enqueuer and the portal base URL it already holds.

Index

Constants

View Source
const DefaultEscalateAfterHours = 24

DefaultEscalateAfterHours is the delay before a revocation nobody has acted on is raised with the operator's recipients.

A day, because that is how long the person who authorized the connection plausibly takes to see their mail, and because the premise of the whole feature is that they may be unreachable rather than merely slow. A window shorter than that mails a second group of people about something the first is about to handle.

View Source
const DefaultSweepInterval = time.Minute

DefaultSweepInterval is how often the open revocations are swept.

Finer than the review-queue check's hour because the window it watches can be set as low as an hour, and the tick is what bounds how late an escalation lands inside it. The cost is one statement against a partial index that holds only unescalated revocations — on a deployment where nothing is broken, no rows at all.

View Source
const (
	// MaxRecipients caps the escalation's distribution list. It is the list of
	// people accountable for the deployment's connections, not an announcement
	// channel.
	MaxRecipients = 20
)

Input bounds. They keep a typo from turning the escalation into a mail loop or an unreachable window, not to express policy.

View Source
const NoRecipientsWarning = "no escalation recipients are configured, so only the person who authorized a connection is told when it is revoked"

NoRecipientsWarning is reported for a configuration that saves cleanly and escalates nowhere. It is a warning rather than a validation error because that configuration is the default and a reasonable choice: the person who authorized the connection is still told, and nobody else is.

View Source
const Route = "/admin/connections"

Route is the path both alerts link to, relative to the portal base that notification.PortalLink prefixes: the Connections page, where the connection is reauthorized.

View Source
const SettingsSection = "connection_auth_alert"

SettingsSection is the platform_settings section holding the operator's configuration. It is a stored key: renaming it would strand every deployment's configured recipients behind a section nothing reads.

Variables

View Source
var ErrNotFound = errors.New("connalert: settings not found")

ErrNotFound is returned by the settings store when the alert has never been configured. Callers apply DefaultSettings instead.

Functions

This section is empty.

Types

type Alert

type Alert struct {
	// Kind is the connection kind (mcp, api, graphql).
	Kind string
	// Name is the connection name within the kind.
	Name string
	// AuthorizedBy is the identity that authorized the connection, and the
	// address the first alert goes to.
	AuthorizedBy string
	// IDPHost is the host of the upstream that rejected the refresh. Empty
	// when the platform reached the verdict without calling it.
	IDPHost string
	// Reason is the stable short form the auth-event history records.
	Reason string
	// RevokedAt is when the credential was discarded, and the instant the
	// escalation window is measured from.
	RevokedAt time.Time
}

Alert is one connection's open revocation: the row this package keeps between the credential being discarded and somebody reauthorizing.

type AlertStore

type AlertStore interface {
	// Open records a revocation and reports whether this call is the one that
	// recorded it. It loses when the connection already has an open
	// revocation, which is the whole de-duplication mechanism: a connection
	// that keeps being called is announced once rather than once per rejected
	// call.
	Open(ctx context.Context, a Alert) (bool, error)
	// Clear forgets a connection's open revocation. Called when the connection
	// is authorized again, which is what makes a later revocation news.
	Clear(ctx context.Context, kind, name string) error
	// ClaimEscalations stamps and returns every revocation older than window
	// that has not been escalated and whose connection is still unauthorized.
	// Stamping and selecting are one statement, so two replicas sweeping at
	// once cannot both claim a row.
	ClaimEscalations(ctx context.Context, window time.Duration, now time.Time) ([]Alert, error)
}

AlertStore is the open-revocation half: which connections have lost their credential and which of those have been escalated.

type Alerter

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

Alerter is the authevents.RevocationSink that records a revocation and tells the person who authorized the connection.

func NewAlerter

func NewAlerter(cfg Config) *Alerter

NewAlerter builds the sink, or nil when a dependency is absent. A nil Alerter is still a usable sink (its method is nil-safe), so the caller can wire it unconditionally.

func (*Alerter) Revoked

func (a *Alerter) Revoked(ctx context.Context, rev authevents.Revocation)

Revoked records the revocation and mails the identity that authorized the connection.

It runs on the refresh path, so everything it does is bounded and nothing it does can fail the refresh: the credential is already gone either way, and a caller that returned an error here would turn "we could not send an email" into a second, unrelated failure for the agent that made the call.

type Config

type Config struct {
	// Settings holds the operator's escalation window and recipients.
	Settings SettingsStore
	// Alerts holds the open revocations.
	Alerts AlertStore
	// Enqueuer is the notification substrate's trigger-side entry point.
	Enqueuer *notification.Enqueuer
	// BaseURL is the portal's public base URL, for the alert's deep link.
	BaseURL string
	// Interval overrides DefaultSweepInterval. Testing hook.
	Interval time.Duration
	// Now overrides time.Now. Testing hook.
	Now func() time.Time
}

Config carries what both the Alerter and the Escalator need. Every field is required; the constructors return nil when one is missing, which is how the composition root says "this deployment has no database" without a flag.

type Escalator

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

Escalator raises a revocation nobody has acted on with the addresses the operator named.

It exists because the premise of the first alert is that its recipient may be unreachable. A connection is a tenant asset: the person who authorized it holds the credential, but everyone using it is stopped while it is revoked, and on a scheduled run nobody is there to notice.

func NewEscalator

func NewEscalator(cfg Config) *Escalator

NewEscalator builds the sweep, or nil when a dependency is absent. A nil Escalator's methods are no-ops, so the caller brackets Start/Stop unconditionally.

func (*Escalator) Start

func (e *Escalator) Start(ctx context.Context)

Start runs the sweep until ctx is canceled or Stop is called. The first sweep runs one interval in rather than at startup, keeping it clear of boot: what bounds a repeat escalation is the stamp in the database, which survives a restart. Nil-safe.

func (*Escalator) Stop

func (e *Escalator) Stop()

Stop ends the sweep and waits for one in flight. Nil-safe and idempotent.

func (*Escalator) Sweep

func (e *Escalator) Sweep(ctx context.Context) error

Sweep escalates every revocation past the operator's window that nobody has acted on. It is the whole behavior of the type; Start only supplies the clock.

type PostgresStore

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

PostgresStore is this feature's PostgreSQL persistence: the operator's configuration (SettingsStore) and the open revocations (AlertStore). One store because they are one feature's state, always built over the same pool; two interfaces because the admin API and the sweep each need only their half.

func NewPostgresStore

func NewPostgresStore(db *sql.DB) *PostgresStore

NewPostgresStore creates the PostgreSQL-backed store.

func (*PostgresStore) ClaimEscalations

func (s *PostgresStore) ClaimEscalations(ctx context.Context, window time.Duration, now time.Time) ([]Alert, error)

ClaimEscalations stamps and returns the revocations due for escalation.

func (*PostgresStore) Clear

func (s *PostgresStore) Clear(ctx context.Context, kind, name string) error

Clear forgets a connection's open revocation.

func (*PostgresStore) Get

func (s *PostgresStore) Get(ctx context.Context) (*Settings, error)

Get returns the stored configuration.

func (*PostgresStore) Open

func (s *PostgresStore) Open(ctx context.Context, a Alert) (bool, error)

Open records a revocation, reporting whether it was this call that recorded it.

func (*PostgresStore) Set

func (s *PostgresStore) Set(ctx context.Context, in Settings, author string) error

Set upserts the configuration. The audit columns carry the author and the time; Settings marks its copies of them json:"-", so the section value holds the configuration alone.

type Settings

type Settings struct {
	// Enabled turns both alerts on. Off, a revocation is still recorded in the
	// auth-event history and on the status card exactly as before — what is
	// turned off is telling anyone.
	Enabled bool `json:"enabled"`
	// EscalateAfterHours is how long a revoked connection goes unauthorized
	// before Recipients are told. Zero or less applies
	// DefaultEscalateAfterHours.
	EscalateAfterHours int `json:"escalate_after_hours"`
	// Recipients are the addresses the escalation is delivered to, in
	// notification.NormalizeAddress form. Empty is the default and means no
	// escalation: the person who authorized the connection is still told, and
	// nobody else is.
	//
	// They are addresses rather than a role because the platform stores no
	// directory of administrators — admin is a claim on a token at request
	// time, so there is no set of people to resolve. Naming them is also what
	// makes the escalation a deliberate choice about who is accountable for a
	// connection rather than a broadcast.
	Recipients []string `json:"recipients"`
	// UpdatedBy and UpdatedAt describe the last admin write. They live in the
	// platform_settings audit columns, which are authoritative, so they are
	// excluded from the section value rather than written into it twice.
	UpdatedBy string    `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

Settings is the operator's configuration for connection-revocation alerts. It is that feature's section of the platform_settings table.

func DefaultSettings

func DefaultSettings() Settings

DefaultSettings returns the configuration applied before an operator has written one: alerts on, escalating after a day, to nobody yet.

func SettingsOf

func SettingsOf(ctx context.Context, store SettingsStore) (Settings, error)

SettingsOf returns the stored configuration, or the defaults when none has been written. Every caller wants this rather than the raw ErrNotFound: an operator who has never opened the settings page still gets the alert, and the recipient list is what gates the escalation.

func (Settings) EscalateAfter

func (s Settings) EscalateAfter() time.Duration

EscalateAfter returns the configured escalation window, or the default when unset or non-positive.

func (Settings) EscalatesTo

func (s Settings) EscalatesTo() []string

EscalatesTo returns the recipients the escalation is addressed to, with blank entries dropped. An empty result means this deployment escalates nowhere.

func (Settings) View

func (s Settings) View() SettingsView

View maps stored settings to the read shape.

type SettingsInput

type SettingsInput struct {
	Enabled            bool     `json:"enabled" example:"true"`
	EscalateAfterHours int      `json:"escalate_after_hours" example:"24"`
	Recipients         []string `json:"recipients" example:"platform-admin@example.com"`
}

SettingsInput is the write shape for the admin alert configuration.

func (*SettingsInput) Settings

func (in *SettingsInput) Settings() Settings

Settings maps the validated input to stored settings.

func (*SettingsInput) Validate

func (in *SettingsInput) Validate() string

Validate normalizes the input in place and returns a non-empty message when it is invalid. Recipients are reduced to their storage form and de-duplicated, so the sweep never mails one person twice because their address was listed in two shapes.

type SettingsStore

type SettingsStore interface {
	// Get returns the stored configuration, or ErrNotFound when the alert has
	// never been configured.
	Get(ctx context.Context) (*Settings, error)
	// Set upserts the configuration.
	Set(ctx context.Context, s Settings, author string) error
}

SettingsStore is the configuration half of this feature's persistence: the escalation window and its recipients, held as a section of the platform_settings table.

It is a contract of its own because the admin settings surface needs nothing else — it writes the configuration without being handed the open revocations.

type SettingsView

type SettingsView struct {
	Enabled            bool      `json:"enabled" example:"true"`
	EscalateAfterHours int       `json:"escalate_after_hours" example:"24"`
	Recipients         []string  `json:"recipients"`
	UpdatedBy          string    `json:"updated_by,omitempty" example:"admin@example.com"`
	UpdatedAt          time.Time `json:"updated_at"`
	// Warnings describes a configuration that saves cleanly but escalates
	// nowhere. They never block a save; they exist so the operator sees what
	// the setting does at the surface where it was chosen.
	Warnings []string `json:"warnings,omitempty"`
}

SettingsView is the read shape for the admin alert configuration.

Jump to

Keyboard shortcuts

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