Documentation
¶
Overview ¶
Package offboard answers one question in one operation: "this person left, are they out?" (Task 20261).
Before this, the answer was no. An enterprise hub let an identity hold five independent credential surfaces, and disabling the account at the IdP severed none of them on its own:
- dashboard sessions, which outlive an IdP disablement until the absolute TTL because a refresh that returns no id_token cannot narrow claims;
- API tokens, which carry an owner binding that nothing consulted on departure, so a departed user's PAT worked until its ExpiresAt;
- glasses links, the same rows with a different Kind, whose own source conceded that an IdP dropping someone from a group "is not noticed until the link expires";
- secret leases held by their running tasks, which keep real credentials materialised inside a sandbox;
- the tasks themselves, still executing on the fleet in their name.
The documented remedy was a hand-written deny binding, which stops the account *acting* but ends nothing it already holds.
Why one package ¶
The CLI and the dashboard both need this, and they need it to mean exactly the same thing. Two implementations of "who counts as this person" is the failure mode that matters: the one that under-matches reports success while leaving a credential live. So resolution, planning and execution live here, and cmd/ and pkg/ui are thin.
Ordering ¶
Severing runs before stopping. Revoking credentials first means a task that notices its lease vanishing and retries cannot re-authenticate; stopping the task first would leave a window where its still-valid session could start another one. The durable credentials go in a single transaction (see statedb.OffboardIdentity) so there is no partially-offboarded state; leases and tasks cannot join that transaction — they are process memory and other databases — and so are applied after it, each reporting its own failures rather than rolling back a severing that already succeeded.
What it will not do ¶
Projects are reported, never deleted. A departing user's projects usually hold the team's work, and an offboarding command that silently removed them would be a data-loss tool wearing a security label. The operator gets the list and reassigns.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DenyRef ¶
type DenyRef struct {
ID string `json:"id"`
Claim string `json:"claim"`
Value string `json:"value"`
}
DenyRef is a deny binding the run will write.
type Failure ¶
Failure records a surface that could not be severed, so a partial run is legible instead of being reported as a success.
type LeaseRef ¶
type LeaseRef struct {
ID string `json:"id"`
ExecutorID string `json:"executor_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
Kinds []string `json:"kinds,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
LeaseRef is one live secret lease held on the departing user's behalf.
type Leases ¶
type Leases interface {
// LiveLeases reports every lease the broker would still honour.
LiveLeases() []LeaseRef
// Release ends one lease and destroys any credentials minted under it.
Release(id string)
}
Leases is the secret-broker surface offboarding needs. A hub with no broker passes nil, and leases are simply not part of the run.
type Options ¶
type Options struct {
// DB is the hub's control-plane database. Required.
DB *statedb.DB
// Identity is the email or subject the operator typed. Required.
Identity string
// Reason is recorded on every audit event. Required: an offboarding with
// no stated cause is not reviewable, and the reviewability is the point.
Reason string
// Actor is who is doing this — an operator's email, or an OS user for CLI
// runs.
Actor string
// Via distinguishes "cli" from "ui" in the trail.
Via string
// DryRun resolves and reports without writing anything.
DryRun bool
// Leases, Tasks and Projects are optional. A nil collaborator means that
// surface is skipped, and the report says so rather than implying it was
// clean.
Leases Leases
Tasks Tasks
Projects Projects
// Sessions is the live session authority. Strongly preferred where one
// exists — see the interface doc.
Sessions Sessions
// Now is the clock, swappable in tests.
Now func() time.Time
}
Options configures a run.
type Plan ¶
type Plan struct {
Target Target `json:"target"`
Sessions []SessionRef `json:"sessions"`
Tokens []TokenRef `json:"tokens"`
Glasses []TokenRef `json:"glasses"`
Denies []DenyRef `json:"denies"`
Leases []LeaseRef `json:"leases"`
Tasks []TaskRef `json:"tasks"`
Projects []ProjectRef `json:"projects"`
// Warnings name things the run cannot be sure about — most importantly
// tokens whose owner binding did not decode, which cannot be proven *not*
// to belong to this person.
Warnings []string `json:"warnings,omitempty"`
}
Plan is the blast radius: everything a run would touch, resolved before anything is written. It is what --dry-run prints.
type ProjectRef ¶
type ProjectRef struct {
Name string `json:"name"`
Path string `json:"path"`
Owner string `json:"owner,omitempty"`
}
ProjectRef is a project owned by the departing user. Reported, never deleted.
type Projects ¶
type Projects interface {
// Owned reports projects whose owner is any of the given owner keys.
Owned(ownerKeys []string) ([]ProjectRef, error)
}
Projects enumerates project ownership.
func RegistryProjects ¶
func RegistryProjects(entries []multiui.ProjectEntry) Projects
RegistryProjects reports ownership from an already-loaded project list.
It takes entries rather than calling multiui.Load itself because Load misses the hub's own WorkDir and anything passed via --projects; the dashboard assembles the full set separately, and an offboarding that swept only the registry file would under-report which projects need reassigning.
type Report ¶
type Report struct {
Plan
DryRun bool `json:"dry_run"`
Actor string `json:"actor,omitempty"`
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
SessionsRevoked []string `json:"sessions_revoked,omitempty"`
TokensRevoked []string `json:"tokens_revoked,omitempty"`
GlassesRevoked []string `json:"glasses_revoked,omitempty"`
DeniesWritten []string `json:"denies_written,omitempty"`
LeasesReleased []string `json:"leases_released,omitempty"`
TasksStopped []TaskRef `json:"tasks_stopped,omitempty"`
// Failures is non-empty when a surface could not be severed. The run does
// not abort on one: a lease that will not release is not a reason to leave
// the sessions live. The caller decides what a partial run means, and both
// front ends report it as an error with the detail attached.
Failures []Failure `json:"failures,omitempty"`
}
Report is the outcome of a run. A dry run returns the Plan with DryRun set and every "-ed" field empty.
type SessionRef ¶
type SessionRef struct {
ID string `json:"id"`
Subject string `json:"subject,omitempty"`
Email string `json:"email,omitempty"`
IP string `json:"ip,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
SessionRef is one dashboard session that will be, or was, ended.
type Sessions ¶
type Sessions interface {
// List reports every live session.
List() ([]SessionRef, error)
// Revoke ends one session and drops it from any in-process cache.
// It reports whether a session was actually ended.
Revoke(id, actor, reason string) (bool, error)
}
Sessions is the running hub's session authority — its authenticator.
Supplying it matters more than it looks. A hub whose session store is not durable (no sealing key, or no control-plane database) keeps sessions in process memory, where the control-plane table this package otherwise reads is simply empty. An offboarding driven off that table would report "0 sessions" and leave the person signed in — the worst possible answer to "are they out?". Going through the authenticator is also what drops the session from its 30-second read cache, which deleting the row underneath it does not.
A caller with no running hub (the CLI, on a host whose hub may be down) passes nil and the control-plane table is used instead, which is correct there: there is no process holding memory sessions to ask.
type Target ¶
type Target struct {
// Input is what the operator typed, preserved for the audit trail.
Input string `json:"input"`
// Key is the canonical owner key: lowercased email, or "sub:"+Subject.
// This is what project ownership rows are recorded under.
Key string `json:"key"`
// Subjects and Emails are every identifier found to belong to this person.
// Emails are lowercased; subjects are opaque and compared exactly.
//
// Always serialised, even when empty: a front end rendering "resolved to"
// needs to distinguish "no subject" from a field that was omitted, and
// omitempty would make an identity this hub has never seen indistinguishable
// from a decoding bug.
Subjects []string `json:"subjects"`
Emails []string `json:"emails"`
}
Target is the resolved identity: every identifier that turned out to address the same person.
Resolution matters because the operator types one thing — usually an email — and the surfaces are keyed inconsistently. Sessions carry a subject and an email; a token's owner may carry only a subject. Matching literally on what was typed would leave the subject-keyed token behind, which is precisely the credential an operator would never think to look for.
type TaskRef ¶
type TaskRef struct {
ProjectPath string `json:"project_path"`
ProjectName string `json:"project_name,omitempty"`
ID int `json:"id"`
Title string `json:"title,omitempty"`
Status string `json:"status,omitempty"`
// Attempt names the execution observed, so a kill filed here cannot land on
// a later attempt of the same task (Task 20203). Not serialised: it is an
// internal fencing token, not something an operator reads.
Attempt string `json:"-"`
}
TaskRef is one in-flight task running in a project this identity owns.
type Tasks ¶
type Tasks interface {
// Running reports in-flight tasks for one project path.
Running(projectPath string) ([]TaskRef, error)
// Stop requests that the named execution be killed. The ref carries the
// attempt token, so the request cannot land on a later attempt.
Stop(t TaskRef, actor, reason string) error
}
Tasks is the per-project execution surface. A hub that cannot reach project databases passes nil.
func LocalTasks ¶
func LocalTasks() Tasks
LocalTasks reads and aborts tasks through each project's own state database, which is where a project's plan lives — the hub's control plane does not hold other projects' tasks.
type TokenRef ¶
type TokenRef struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Prefix string `json:"prefix,omitempty"`
Kind string `json:"kind,omitempty"`
Owner string `json:"owner,omitempty"`
Roles []string `json:"roles,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
TokenRef is one API token or glasses link.