approvalnotify

package
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package approvalnotify tells the people who can decide a grant request or approve an API token that one is waiting on them (Task 20427).

Two decisions wait on an administrator — a self-service grant request (Task 20271) and an API token a role policy suspends until it is approved (Task 20422) — and before this package nothing reached the people who could make them: a request lapsed after three days unless an approver happened to open the Secrets tab. The hub's leader now records a notice for every moment worth telling — a request filed, twelve hours from lapsing, decided, withdrawn or lapsed; a token that started waiting — and posts each to the webhooks in notifications.webhooks.

Three properties shape it:

  • Once per cluster. Notices are found by a scan of the control plane's own rows, so every path that changes a request — the dashboard, `cloop hub grant`, an offboarding — is seen, and each is recorded under a unique key, so however many passes or members notice a moment it is sent once. Only the leader scans and sends.
  • Nothing secret leaves. A notice carries identities, the secret's name and kind, the subject, deadlines and a link to the dashboard. Not the requester's justification (free text, kept for the dashboard), not a decision note, and nothing credential-shaped: every string is run through the credential-pattern scrubber (pkg/redact) before it is rendered.
  • Nowhere it was not sent. The client posts https only, follows no redirect, uses no proxy, and checks the address it actually dials: loopback, private and link-local receivers only when the webhook's allow_addresses names them, cloud metadata services never.

Index

Constants

Delivery outcomes, the `outcome` label of cloop_approval_notifications_total.

View Source
const (
	// DefaultPoll is how often a pass runs with nothing nudging it.
	DefaultPoll = 30 * time.Second
)
View Source
const EventTest = "test"

EventTest is a message sent from Settings to check a webhook. It is never recorded as a notice.

View Source
const MaxBodyBytes = 32 << 10

MaxBodyBytes bounds a rendered message. Every channel's own limit is well under it; it exists so that no input can make the hub POST megabytes.

Variables

View Source
var ErrBlockedAddress = errors.New("approvalnotify: refusing to connect to a non-public address")

ErrBlockedAddress refuses a connection to a non-public address the webhook does not allow.

View Source
var ErrMetadataAddress = errors.New("approvalnotify: refusing to connect to a cloud metadata service")

ErrMetadataAddress refuses a connection to a cloud metadata service.

View Source
var ErrRedirect = errors.New("approvalnotify: the webhook answered with a redirect, which is never followed")

ErrRedirect is a webhook that answered with a redirect.

Functions

func CheckAddress

func CheckAddress(ip netip.Addr, allow []netip.Prefix) error

CheckAddress is the address rule, on an address about to be connected to: never a metadata service, and a non-public address only when allowed.

func IsPermanent

func IsPermanent(err error) bool

IsPermanent reports whether err is a failed send that retrying cannot fix.

func Render

func Render(kind string, n Notice, link string, now time.Time) ([]byte, error)

Render builds the body a webhook of type kind is sent for n. link is the dashboard address a message points at, or empty for none.

func Superseded

func Superseded(n Notice, snap Snapshot, now time.Time) bool

Superseded reports whether a notice that something is waiting no longer describes the world: the request was decided, withdrawn or lapsed, or the token approved or revoked, before the notice could be sent. Telling an approver to act on something already settled is worse than saying nothing.

func Test

Test sends one test message to a webhook, outside the outbox: the Settings panel's check that a URL works before anything depends on it.

Types

type Client

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

Client posts to one webhook.

func NewClient

func NewClient(allow []netip.Prefix, caFile string, timeout time.Duration) (*Client, error)

NewClient builds the client for a webhook: allow are the non-public prefixes it may reach, caFile an extra PEM bundle to trust, timeout the bound on one POST.

func (*Client) Post

func (c *Client) Post(ctx context.Context, rawURL string, body []byte) error

Post sends body to rawURL as JSON.

type Engine

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

Engine is the notifier.

func New

func New(opts Options) *Engine

New returns an engine.

func (*Engine) Pass

func (e *Engine) Pass(ctx context.Context) error

Pass runs one scan-record-deliver round.

The scan runs whether or not a webhook is configured: what it records is also what the hub announces to dashboards (OnRecorded) — a request lapsing, a token starting to wait — which nothing else observes as it happens. A webhook configured later starts at the newest notice, so the outbox it inherits is not replayed into its channel.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context)

Run passes until ctx ends.

type Notice

type Notice struct {
	Event string    `json:"event"`
	At    time.Time `json:"at"`

	// The grant request, for request.* events.
	RequestID   string `json:"request_id,omitempty"`
	RequestedBy string `json:"requested_by,omitempty"`
	SecretName  string `json:"secret_name,omitempty"`
	SecretKind  string `json:"secret_kind,omitempty"`
	Subject     string `json:"subject,omitempty"`
	DecidedBy   string `json:"decided_by,omitempty"`

	// The API token, for token.waiting.
	TokenID   string   `json:"token_id,omitempty"`
	TokenName string   `json:"token_name,omitempty"`
	Roles     []string `json:"roles,omitempty"`
	CreatedBy string   `json:"created_by,omitempty"`

	// Deadline is when the request lapses undecided, or the token expires;
	// zero for a token that never expires.
	Deadline time.Time `json:"deadline,omitempty"`
}

Notice is one thing worth telling approvers, as recorded in the outbox.

It holds what a message may say and nothing else. There is no field for a justification, a note, constraints or anything a credential could ride in, so a renderer cannot leak one by mistake.

func Scan

func Scan(snap Snapshot, since, now time.Time) []Notice

Scan returns every notice the snapshot supports at now. Decisions and lapses are reported from since on, so a webhook configured today is not told about last month's decisions; what is still waiting is reported whenever it is seen, because it is still actionable. The caller records each by Key, so a notice found again is not sent again.

func (Notice) Key

func (n Notice) Key() string

Key is the notice's outbox key: the event and what it is about. Unique, so a moment noticed twice is recorded once.

type Options

type Options struct {
	// OpenDB opens the control plane's database; the engine closes it.
	OpenDB func() (*statedb.DB, error)
	// Settings reads the configuration in force, per pass.
	Settings func() (Settings, error)
	// Snapshot reads the requests and the waiting tokens.
	Snapshot func(ctx context.Context) (Snapshot, error)
	// LookupEnv reads a webhook's url_env; nil means os.LookupEnv.
	LookupEnv func(name string) (string, bool)
	// Record counts one delivery outcome (hubmetrics).
	Record func(sink, event, outcome string)
	// Now is the clock; nil means time.Now.
	Now func() time.Time
	// Poll is how often a pass runs; zero means DefaultPoll.
	Poll time.Duration
	// Nudge wakes the loop for a pass now.
	Nudge <-chan struct{}
	// OnRecorded is told, once per pass, of the notices that pass recorded
	// for the first time: how the hub announces a lapse, a waiting token or
	// a change made outside its handlers to its dashboards.
	OnRecorded func([]Notice)
	// Logf reports what the operator should know.
	Logf func(format string, args ...any)
}

Options wires the engine to the hub.

type SendError

type SendError struct {
	Status    int
	Permanent bool
	Err       error
}

SendError is a failed send. Permanent says retrying the same message cannot succeed: the URL, the address or the receiver's answer refuses it.

func (*SendError) Error

func (e *SendError) Error() string

func (*SendError) Unwrap

func (e *SendError) Unwrap() error

type Settings

type Settings struct {
	// Webhooks are the destinations in force, already validated.
	Webhooks []config.NotificationWebhook
	// Link is the dashboard address a message points at; empty for none.
	Link string
}

Settings is what one pass runs under.

type Snapshot

type Snapshot struct {
	// Requests is every access request; RequestsKnown is false when they
	// could not be read (no secret broker on this hub), so the scan and the
	// staleness check know an absence means nothing.
	Requests      []secretbroker.AccessRequest
	RequestsKnown bool

	// Waiting is the tokens a role policy this hub enforces suspends until
	// approved — empty, and TokensKnown true, on a hub enforcing none.
	Waiting     []TokenInfo
	TokensKnown bool
}

Snapshot is what the scan looks at: the control plane's requests and the tokens waiting on an approval.

type TokenInfo

type TokenInfo struct {
	ID        string
	Name      string
	Roles     []string
	CreatedBy string
	CreatedAt time.Time
	ExpiresAt time.Time
}

TokenInfo is an API token waiting for an administrator's approval, as the hub describes it: never its hash, never its secret.

Jump to

Keyboard shortcuts

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