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
- Variables
- func CheckAddress(ip netip.Addr, allow []netip.Prefix) error
- func IsPermanent(err error) bool
- func Render(kind string, n Notice, link string, now time.Time) ([]byte, error)
- func Superseded(n Notice, snap Snapshot, now time.Time) bool
- func Test(ctx context.Context, w config.NotificationWebhook, link string, now time.Time) error
- type Client
- type Engine
- type Notice
- type Options
- type SendError
- type Settings
- type Snapshot
- type TokenInfo
Constants ¶
const ( OutcomeDelivered = hubmetrics.ApprovalNotifyDelivered OutcomeFailed = hubmetrics.ApprovalNotifyFailed OutcomeDropped = hubmetrics.ApprovalNotifyDropped OutcomeSuperseded = hubmetrics.ApprovalNotifySuperseded )
Delivery outcomes, the `outcome` label of cloop_approval_notifications_total.
const ( // DefaultPoll is how often a pass runs with nothing nudging it. DefaultPoll = 30 * time.Second )
const EventTest = "test"
EventTest is a message sent from Settings to check a webhook. It is never recorded as a notice.
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 ¶
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.
var ErrMetadataAddress = errors.New("approvalnotify: refusing to connect to a cloud metadata service")
ErrMetadataAddress refuses a connection to a cloud metadata service.
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 ¶
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 ¶
IsPermanent reports whether err is a failed send that retrying cannot fix.
func Render ¶
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 ¶
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.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client posts to one webhook.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the notifier.
func (*Engine) Pass ¶
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.
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 ¶
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.
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 ¶
SendError is a failed send. Permanent says retrying the same message cannot succeed: the URL, the address or the receiver's answer refuses it.
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.