Help us shape the future of Go! Take the 2026 Go Developer Survey

console

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package console is the human side of a lockkit product: it holds the credentials and the only backend connection, enforces the approved policy, shows every request that needs a human decision on an approval screen, and writes the audit trail. Clients (a product's CLI and MCP server) reach it over the per-profile socket and never see a secret.

A product supplies a Backend (its methods, its connection, its checks and its policy) and calls Run. The pieces Run is made of — the terminal, the approvers, the isolation checks, the request loop — are exported for products that assemble their own console.

Index

Constants

View Source
const (
	// ApprovalTimeout bounds every human answer.
	ApprovalTimeout = 5 * time.Minute
	// PolicyPoll is how often the config files are checked for changes.
	PolicyPoll = 2 * time.Second
	// ResponseQuantum levels the response time of a request whose answer
	// could otherwise tell the agent how long a human or a backend took.
	ResponseQuantum = 250 * time.Millisecond
)

Timeouts of a console session. The idle timeout and the maximum session length are the profile's.

View Source
const CredentialsNone = "none"

CredentialsNone is the credentials mode of a backend that needs no secret (a local file, a socket the account owns).

View Source
const FingerDeadline = 30 * time.Second

FingerDeadline bounds the wait for a finger after a typed yes.

View Source
const HubApproval = "hub"

HubApproval is the audit approval of a request handed to an AsyncApprover.

View Source
const StandaloneReason = "the lock was restarted without its hub (standalone)"

StandaloneReason is the reason of the plans a standalone start withdraws, as the hub and the plans store record it.

Variables

View Source
var ErrConfig = errors.New("configuration error")

ErrConfig marks usage and configuration errors (exit code 3).

View Source
var ErrSecretTimeout = errors.New("console: no password entered")

ErrSecretTimeout is AskSecret's error when no answer comes within ApprovalTimeout.

View Source
var ErrTooManyNoMatch = errors.New("fingerprint: three consecutive non-matching fingers")

ErrTooManyNoMatch is set on the third consecutive no-match: the caller ends the console.

View Source
var Paint = ui.Painter{On: true}

Paint styles console lines.

Functions

func Bell

func Bell(io IO)

Bell rings io's bell when it has one.

func CloseVerifier

func CloseVerifier(v fingerprint.Verifier)

CloseVerifier releases a verifier that holds a connection (fprintd's).

func Decode

func Decode(req ipc.Request, v any) *ipc.Response

Decode decodes the params of req into v, refusing unknown fields; nil when they decode (or are absent), else the error response.

func DenyReason

func DenyReason(ans string) string

DenyReason is the reason of an answer "n <text>" or "no <text>" (n and no in any case), trimmed, cut at 200 bytes and escaped; "" for any other answer.

func ErrResp

func ErrResp(id int64, code int, msg string) ipc.Response

ErrResp is an error response.

func FormatChanges

func FormatChanges(changes []config.Change) []string

FormatChanges renders a policy diff, loosenings in red. Every value is escaped: a pattern or a host could otherwise carry terminal escapes that hide a loosening from the human.

func Harden

func Harden() error

Harden keeps the secret out of core dumps and away from same-user ptrace and /proc/<pid>/mem readers: RLIMIT_CORE=0 and PR_SET_DUMPABLE=0.

func Level

func Level(ctx context.Context, quantum time.Duration, start time.Time)

Level waits until the next multiple of quantum since start, so that the response time tells nothing finer than quantum (0 or less: no wait).

func Loosens

func Loosens(changes []config.Change) bool

Loosens reports whether any change loosens the policy.

func OKResp

func OKResp(id int64, v any) ipc.Response

OKResp is a result response; a value that does not encode gives an internal error.

func PeerFrom

func PeerFrom(ctx context.Context) (ipc.Cred, bool)

PeerFrom returns the client behind the request of ctx.

func PeerText

func PeerText(ctx context.Context) string

PeerText describes the client behind a request for the approval screen.

func PendingResp added in v0.3.0

func PendingResp(id int64, lock string, p Pending) ipc.Response

PendingResp is the answer to a request waiting for a decision taken elsewhere: a result, not an error, so that the agent stops and waits rather than retries. It names p.Instance when set (hub mode).

func PolicyPendingOf added in v0.7.0

func PolicyPendingOf(env Env) bool

PolicyPendingOf reports a loosening awaiting :review in the console behind env.

func ProcessText

func ProcessText(pid int) string

ProcessText names the process behind pid on Linux: its executable path when the kernel lets the console read it (a process of its own account), else the name the process gave itself, labelled as such; "" elsewhere. The uid and pid are the kernel's; the name is the process's choice.

func Run

func Run(ctx context.Context, p kit.Product, b Backend, o Options) error

Run is a product's `console` command: the isolation checks, the policy review, the production confirmation, the credentials and the connection, the backend's checks, the socket, then the request loop until Ctrl-C, logout, :quit or a timeout.

func SameUserRefusal

func SameUserRefusal(p kit.Product) string

SameUserRefusal is the one text of the same-user refusal of p.

func SameUserRefused

func SameUserRefused(p kit.Product, sys *sysconf.Config, allowSameUser bool) error

SameUserRefused is the refusal of a console of p without a separated setup (sys nil, as sysconf.Load returns it), or nil.

func StampPeer

func StampPeer(ctx context.Context, b *audit.Base)

StampPeer sets the peer fields of b from the client behind ctx: its uid, pid and account name. The process name, chosen by the agent, stays on the screen (PeerText) and out of the audit log.

func StatusBoundaryRow added in v0.7.0

func StatusBoundaryRow(v StatusView) string

StatusBoundaryRow is the value of the boundary row; "" in the session shape (no row).

func StatusModeRow added in v0.7.0

func StatusModeRow(v StatusView, w Words) string

StatusModeRow is the value of the mode row.

func StatusPendingRow added in v0.7.0

func StatusPendingRow(v StatusView) string

StatusPendingRow is the value of the policy change row.

func StatusResultsRow added in v0.7.0

func StatusResultsRow(v StatusView, w Words) string

StatusResultsRow is the value of the results row.

func StatusUnmaskRow added in v0.7.0

func StatusUnmaskRow(v StatusView) string

StatusUnmaskRow is the value of the unmask row.

func TicketParam added in v0.4.0

func TicketParam(params json.RawMessage) string

TicketParam is the hub ticket a request names in its params ("ticket"), "" without one.

func WithPeer

func WithPeer(ctx context.Context, c ipc.Cred) context.Context

WithPeer returns ctx carrying the credentials of the client behind a request.

func WithdrawnResp added in v0.4.0

func WithdrawnResp(id int64, tk string) ipc.Response

WithdrawnResp is the explicit answer of a standalone console to a run of hub ticket tk: the ticket was withdrawn, plan again.

Types

type Approval

type Approval struct {
	Approved bool
	// Auto: approved by auto mode, without a prompt.
	Auto bool
	// Retype: the human typed Screen.Retype.
	Retype bool
	// Refusal is the answer to send when neither Approved nor Retype: the
	// human denied (with the reason they typed after "n"), the approval
	// timed out, the client left, or no finger matched. For a Pending
	// request it is the pending answer (PendingResp).
	Refusal ipc.Response
	// Pending: an AsyncApprover took the request; it is decided elsewhere.
	Pending *Pending
}

Approval is the outcome of Env.Approve.

type Approver

type Approver interface {
	Approve(ctx context.Context, d Decision) Result
	// Mode is the audit value: "session" or "desktop+fingerprint".
	Mode() string
}

Approver asks the human for one decision. Each call is one decision: a fingerprint match never carries over.

func NewDesktopTypedApprover

func NewDesktopTypedApprover(io IO) Approver

NewDesktopTypedApprover is the level-1 approver: typed answers in a window the agent can type into, audited apart from the session shape.

func NewFingerprintApprover

func NewFingerprintApprover(io IO, v fingerprint.Verifier, finger string, deadline time.Duration) Approver

NewFingerprintApprover wraps the typed approval with a fingerprint of the dedicated finger (system.toml `finger`), named in the prompt (level 2).

func NewTypedApprover

func NewTypedApprover(io IO) Approver

NewTypedApprover is the session approver: typed answers in a login session of the console account, audited as "session".

type Ask added in v0.3.0

type Ask struct {
	// Frame is the colour of the screen's frame.
	Frame string
	// Production asks for the profile name typed instead of "y".
	Production bool
	// Retype, when set, is an answer that asks to retype values; Ask then
	// returns Approval.Retype. RetypeNote follows it in the prompt:
	// "(<Retype> to retype<RetypeNote>) ". An answer equal to the approval
	// answer is not offered.
	Retype, RetypeNote string
	// For names the decision in the fingerprint prompt (Decision.For); ""
	// is "to approve the request above".
	For string
	// Screen is the approval screen (Decision.Screen).
	Screen *Screen
}

Ask is the prompt that closes an approval screen.

type Asker added in v0.3.0

type Asker struct {
	IO       IO
	Approver Approver
	// Profile is the profile name, typed to approve on production.
	Profile string
	// Lock names the product in a pending answer (kit.Product.Name).
	Lock string
	// End ends the session (too many non-matching fingers); nil: ignored.
	End func(reason string)
	// Waiting, when set, is told when a decision starts (true) and ends
	// (false) waiting for the human.
	Waiting func(on bool)
}

Asker takes the human decisions of a console: it closes an approval screen with the prompt and turns the answer into an Approval. Server uses one; a product that draws its own approval screen (and prompts inside it, for values to type) uses one too, so that every approval is decided in one place.

func AskerOf added in v0.7.0

func AskerOf(env Env) (Asker, bool)

AskerOf returns the Asker of the console behind env: a Handler that draws its own approval screen (prompts inside the frame, values to type) decides with it, so that a switch pressed meanwhile is refused and a missing finger ends the session as with Env.Approve.

func (Asker) Ask added in v0.3.0

func (a Asker) Ask(ctx context.Context, id int64, q Ask, record func(audit.Base)) Approval

Ask rings the bell, prompts and decides. Every outcome but a retype is passed to record, filled with the event, the decision, the reason and the approval mode; the Refusal of a decision other than approved answers request id.

func (Asker) Decide added in v0.3.0

func (a Asker) Decide(ctx context.Context, d Decision) Result

Decide asks the approver and explains a missing finger. Too many non-matching fingers end the console.

type AsyncApprover added in v0.3.0

type AsyncApprover interface {
	Approver
	// Submit hands d over. Pending.Ticket is set even when err is not nil
	// (the hub refused it or is unreachable): the request stays pending,
	// is never asked on the terminal instead, and the approver pushes the
	// ticket again.
	Submit(ctx context.Context, d Decision) (Pending, error)
}

AsyncApprover is an Approver whose decisions are taken elsewhere (by lockhub): Submit hands the request over and returns at once, and the request is answered "pending" instead of waiting for a human here. The decision comes back later, through the hub, never through this console. Approve still takes the console's own decisions (a policy change, unmask) and the console's auto mode is off: the hub has its own.

type Backend

type Backend interface {
	// Methods are the product's IPC methods, beyond the console's own
	// (hello, status, change.request, logout). A name the console serves
	// is refused at start.
	Methods() map[string]Handler
	// Connect opens a session with the credentials the console obtained
	// (asked, from the OS keychain, or from Vault) to the policy in force
	// (c.Policy). It is called at start and again after a lost connection
	// or an expired login. The console wipes c.Secret once Connect returns.
	// The session handed back to the backend (Env.Session, Checks) may be
	// wrapped: Unwrap returns the backend's own.
	Connect(ctx context.Context, c Credentials) (Session, error)
	// Checks audits a new session: what the backend account can do beyond
	// the profile (a red warning, typed "continue" to go on), or a reason
	// to refuse it. On a production profile every finding refuses.
	Checks(ctx context.Context, s Session) []Finding
	// LoadPolicy reads the current policy from the config files. The
	// console calls it at start and every PolicyPoll.
	LoadPolicy() (Policy, error)
	// Status is the product's part of the status method's answer.
	Status(s Session) any
}

Backend is what a product plugs into the console: its methods, its connection to the backend it guards, the checks of that connection and its policy. Everything else — the terminal, the isolation, the approval screen, the modes, the policy review, the session limits, the audit log, the socket — is the console's.

type Beller

type Beller interface {
	Bell()
}

Beller is implemented by an IO that can ring the terminal bell, so that a human away from the console notices a prompt waiting for them.

type ChangeFormatter added in v0.7.0

type ChangeFormatter interface {
	FormatChanges(changes []config.Change) []string
}

ChangeFormatter is implemented by a backend that renders a policy diff itself (at start, on a tightening and at :review). Every value must be escaped, as FormatChanges does.

type ConnectionComparer added in v0.7.0

type ConnectionComparer interface {
	ConnectionChanged(old, next Policy) bool
}

ConnectionComparer is implemented by a backend whose connection depends on more than Profile's Where, User, Host and Port (a TLS setting, a tunnel): a policy change for which ConnectionChanged reports true ends the session, as the open connection no longer matches the policy. It adds to the console's own check, which a change of those four fields always trips.

type Credentials

type Credentials struct {
	User string
	// Secret is nil for a profile with credentials "none".
	Secret []byte
	// Policy is the approved policy in force, the one to connect to: a
	// backend reads its endpoint from it, never from the config files (an
	// unconfirmed edit). Reconnections pass the policy in force then. Nil
	// when the console that calls Connect has none (a product's own tests).
	Policy Policy
}

Credentials are what Connect needs: the user and its secret.

type Decision

type Decision struct {
	// Prompt is the typed prompt, already framed by the caller.
	Prompt string
	// Expected is the typed answer: "y" (any case) or the production
	// profile name (exact).
	Expected string
	// Production selects the exact match.
	Production bool
	// Retype is the typed answer that asks to retype placeholders ("" when
	// not offered).
	Retype string
	// For names the decision in the fingerprint prompt ("to approve the
	// request"): a finger is never offered without saying what it decides.
	For string
	// Screen is the approval screen the decision is about (nil for the
	// console's own decisions). A terminal approver ignores it: the screen
	// is already on the terminal; an AsyncApprover sends it on.
	Screen *Screen
}

Decision names what a human is approving.

type EndError added in v0.7.0

type EndError struct {
	Reason string
	Err    error
}

EndError, returned by ServerConfig.Reconnect (wrapped or not), ends the session with Reason.

func (*EndError) Error added in v0.7.0

func (e *EndError) Error() string

func (*EndError) Unwrap added in v0.7.0

func (e *EndError) Unwrap() error

type Env

type Env interface {
	// Approve shows s and asks the human (or approves it in auto mode, see
	// Screen.NeverAuto). The decision is audited before Approve returns.
	Approve(ctx context.Context, id int64, s Screen) Approval
	// Auto reports auto mode in effect (never on a production profile).
	Auto() bool
	// ShowResults reports the clear results switch (Ctrl-O).
	ShowResults() bool
	// UnmaskAllowed reports whether clients may ask for unmasked output
	// (:unmask), each such request approved by the human.
	UnmaskAllowed() bool
	// Session returns the live session, reconnecting first when the login
	// expired (credentials_ttl, a Vault lease) or the connection was lost
	// (Lost).
	Session(ctx context.Context) (Session, error)
	// Lost drops the session after a lost connection: the next Session
	// reconnects.
	Lost()
	// Policy is the approved policy in force.
	Policy() Policy
	// Audit writes rec as is; AuditBase stamps b with the profile and the
	// client behind ctx first.
	Audit(rec audit.Record)
	AuditBase(ctx context.Context, b audit.Base)
	// Println writes one console line; Safe escapes client text for it.
	Println(line string)
	Safe(s string) string
	// Sanitizer scrubs error text for a client or the audit log.
	Sanitizer() *sanitize.Sanitizer
	// Level levels a response time (Options.Quantum) from start.
	Level(ctx context.Context, start time.Time)
	// End ends the session after the current request.
	End(reason string)
}

Env is what a Handler may use of the console.

type Finding

type Finding struct {
	// Refuse stops the console (or the reconnection) whatever the profile.
	Refuse bool
	Text   string
}

Finding is one result of Checks.

type Handler

type Handler func(ctx context.Context, req ipc.Request, env Env) ipc.Response

Handler serves one product method. env is the console: the approval screen, the modes, the session, the audit log.

type Hub added in v0.3.0

type Hub struct {
	Client   *hubclient.Client
	Store    *plans.Store
	Instance string
	// contains filtered or unexported fields
}

Hub is a console's hub mode: the plans awaiting a decision and the hub client. A product runs a decided ticket with Client.Begin, then Client.Finish.

func HubOf added in v0.3.0

func HubOf(env Env) *Hub

HubOf returns env's hub, nil when the console is standalone.

func OpenHub added in v0.3.0

func OpenHub(p kit.Product, o HubOptions, stateDir string, now func() time.Time) (*Hub, error)

OpenHub opens the hub mode of product p: it checks o, reads the pinned hub key and the instance key, and opens the plans and the hub state under the product's state directory (stateDir as Options.StateDir; now nil is time.Now). Run calls it; a product with its own console lifecycle calls it too, then Approver and Watch.

func (*Hub) Approver added in v0.3.0

func (h *Hub) Approver(base Approver) AsyncApprover

Approver is the hub approver: requests (a Decision with a Screen) go to the hub, the console's own decisions to base.

func (*Hub) Close added in v0.3.0

func (h *Hub) Close() error

Close releases the plans store (its directory lock): Run closes its hub when it returns.

func (*Hub) RenewResp added in v0.5.0

func (h *Hub) RenewResp(ctx context.Context, id int64, tk string, caller plans.Caller) ipc.Response

RenewResp answers the run of expired ticket tk (Client.Begin returned ticket.ErrExpired): the ticket is renewed (Client.Renew, decision H1) and the agent hears pending with the new ticket, which it runs once decided. A ticket that cannot be renewed answers CodeTimeout.

func (*Hub) Requests added in v0.5.0

func (h *Hub) Requests(io IO, rec func(audit.Base))

Requests audits and prints the hub requests the lock acts on (reveals and renewals) through rec and io. Run sets it; a product with its own console lifecycle calls it with its own audit.

func (*Hub) Watch added in v0.3.0

func (h *Hub) Watch(ctx context.Context, io IO)

Watch delivers the hub's decisions until ctx ends, printing its errors on io (hubclient.Client.Watch).

type HubOptions added in v0.3.0

type HubOptions struct {
	// URL is the hub's base URL ("http://lockhub:8443").
	URL string
	// KeyFile holds the pinned hub key (hubclient.ReadHubKey).
	KeyFile string
	// Instance is the lock instance's name (hubclient.ValidInstance).
	Instance string
	// PlainHTTP lets URL be http:// to a hub on a network of its own
	// (--hub-plain-http, hubclient.Config.PlainHTTP).
	PlainHTTP bool
	// DevicesFile holds the approval devices this lock pins
	// (--devices-file, hubclient.ReadDevices): decisions taken on a device
	// must carry its signature, and the clear screen is sealed to each.
	DevicesFile string
	// RequireDevice lists the risk classes whose approvals must be signed
	// by a pinned device (--require-device, comma-separated).
	RequireDevice string
	// SealClear seals the clear screen to the pinned devices
	// (--seal-clear, hubclient.Config.SealClear).
	SealClear bool
	// FacadeKeysFile holds the facade keys this lock pins
	// (--facade-keys-file, hubclient.ReadFacades): a reveal (Ctrl-O on a
	// local facade) is sealed only to a key one of them signed.
	FacadeKeysFile string
	// Home is the lock's home volume, where the instance key lives
	// (hubclient.InstanceKey, made by `hub-key`); "" is the user's home.
	Home string
}

HubOptions put a console in hub mode: requests are decided on lockhub, never on this terminal. They are the console flags `lock up` writes: --hub URL --hub-key-file PATH --instance NAME, all three or none. There is no discovery: without them the console is standalone.

func (HubOptions) Args added in v0.4.0

func (h HubOptions) Args() []string

Args are the flags of h, as Register reads them back: what a product that starts its console again (a handoff) passes on.

func (HubOptions) Enabled added in v0.3.0

func (h HubOptions) Enabled() bool

Enabled reports whether any hub flag is set.

func (*HubOptions) Register added in v0.3.0

func (h *HubOptions) Register(fs *flag.FlagSet)

Register adds the hub flags to fs.

type HubTicket added in v0.3.0

type HubTicket struct {
	// PlanID is the plan in Hub.Store, made by Caller.
	PlanID string
	Caller plans.Caller
	Risk   ticket.Risk
	// Masked is the screen as it leaves the lock (render.MaskScreen).
	Masked render.MaskedScreen
	// Requester and Workspace are shown by the hub (display only).
	Requester ticket.Requester
	Workspace string
}

HubTicket is what a product adds to a Screen in hub mode: the plan the decision is about and the masked screen the hub shows. A Screen without one is refused in hub mode, never asked on the terminal.

type IO

type IO interface {
	Println(s string)
	// Ask prints prompt and reads one line. It returns false when no answer
	// came within timeout, when ctx ended (Ctrl-C, client gone) or when the
	// terminal is closed.
	Ask(ctx context.Context, prompt string, timeout time.Duration) (string, bool)
	// AskSecret reads a line with echo off.
	AskSecret(ctx context.Context, prompt string) ([]byte, error)
}

IO is the console's terminal. Ask and AskSecret discard pending input before prompting, so type-ahead never answers a prompt.

type Isolation

type Isolation struct {
	Product kit.Product
	Sys     *sysconf.Config
	Display sysconf.Display
	Self    int
	GID     int
	// Desktop is the desktop shape; Verifier is then its fingerprint
	// reader (nil at level 1 and in the session shape).
	Desktop  bool
	Verifier fingerprint.Verifier
	// contains filtered or unexported fields
}

Isolation is how the console is kept apart from the agent: in a separated setup (sysconf) the console runs as the service account and serves the client group over a shared socket, either in its own login session (the session shape: the kernel keeps the agent out of its terminal) or in a terminal of the console account opened on the agent's desktop (the desktop shape: every approval also takes a fingerprint). Without one it refuses to start (except when IsolationEnv.AllowSameUser is set by a product's test build).

func CheckIsolation

func CheckIsolation(p kit.Product, io IO, env IsolationEnv) (*Isolation, error)

CheckIsolation applies the separation rules before any prompt and before the console writes any state. Warnings go to the terminal; a violation is an error. The production rule on X11 needs the profile and is applied later, by RefuseX11IfProduction.

func (*Isolation) Boundary

func (iso *Isolation) Boundary() string

Boundary names where the approval screen stands, for the status screen.

func (*Isolation) Level

func (iso *Isolation) Level() int

Level is the security level of the isolation: 2 for the desktop shape with a fingerprint reader, 1 for the desktop shape without, 0 for the session shape (the kernel is the boundary).

func (*Isolation) Listen

func (iso *Isolation) Listen(path string) (net.Listener, error)

Listen opens the socket.

func (*Isolation) PeerCheck

func (iso *Isolation) PeerCheck() func(ipc.Cred) bool

PeerCheck decides which peers are served: the console's own account and, in a separated setup, allowed_uids and the client group. The group is looked up at every connection: a uid removed from it loses access at its next connection, not at the next console start. Nil means the console's own uid only.

func (*Isolation) RefuseX11IfProduction

func (iso *Isolation) RefuseX11IfProduction(production bool) error

RefuseX11IfProduction applies the production rule once the profile is known: a production profile never runs in an X11 session.

func (*Isolation) SocketPath

func (iso *Isolation) SocketPath(projectKey, profile string) (string, error)

SocketPath is the console socket of a project and profile: in the shared socket directory of a separated setup, else in the user's runtime directory.

type IsolationEnv

type IsolationEnv struct {
	LoadSys       func() (*sysconf.Config, error)
	Display       func() sysconf.Display
	UID           func() int
	UserName      func() (string, error)
	LoginUID      func() (int, bool)
	TerminalOwner func() (int, bool)
	ClientAllowed func(sys *sysconf.Config, uid int) bool
	ClientGID     func(sys *sysconf.Config) (int, error)
	// AccountExists reports whether uid names an account.
	AccountExists func(uid int) bool
	// Relay reports a session opened through a relay (sysconf.SessionRelay).
	Relay func() string
	// MachinectlUIDs lists the callers of running `machinectl shell`
	// processes (sysconf.MachinectlShellUIDs).
	MachinectlUIDs func() ([]int, bool)
	// DesktopEnv reports <PREFIX>DESKTOP=1: the desktop shape is asked for.
	DesktopEnv func() bool
	// DisplaySet reports a non-empty DISPLAY.
	DisplaySet func() bool
	// ParentChain lists the ancestors of the process, from the parent up
	// (sysconf.ParentChain).
	ParentChain func() ([]sysconf.Proc, error)
	// NewVerifier opens the fingerprint reader of user.
	NewVerifier func(user string) (fingerprint.Verifier, error)
	// EnrolledTimeout bounds the start-up question to fprintd; past it, the
	// reader counts as unavailable. 0 is 10s.
	EnrolledTimeout time.Duration
	// AllowSameUser lets the console run without a separated setup, in the
	// agent's account. Only a product's test build sets it (a build tag):
	// no config file, flag, environment variable or client may.
	AllowSameUser bool
}

IsolationEnv holds what CheckIsolation reads from the system; tests replace its functions (RealIsolationEnv is the real one).

func RealIsolationEnv

func RealIsolationEnv(p kit.Product, tty *os.File) IsolationEnv

RealIsolationEnv is the environment of a console of p on terminal tty (nil when unknown).

type Key

type Key int

Key is a control key the human pressed, outside a line. Keys reach the console only from its own terminal: no client can send one.

const (
	// KeyShiftTab is back-tab (ESC [ Z, the same on Linux and macOS): it
	// cycles the approval mode.
	KeyShiftTab Key = iota + 1
	// KeyCtrlO toggles the clear results.
	KeyCtrlO
)

type KeySource

type KeySource interface {
	Keys() <-chan Key
	SetKeyHandler(func(Key))
}

KeySource is implemented by an IO that reports the control keys pressed in the console: on Keys between prompts, to the handler during one.

type LineSource

type LineSource interface {
	Lines() <-chan string
}

LineSource is implemented by an IO that also delivers the lines the human types between requests (console commands such as :review).

type Loop

type Loop struct {
	IO IO
	// Handle serves one request (required).
	Handle func(ctx context.Context, req ipc.Request) ipc.Response
	// Command runs a line typed by the human between requests.
	Command func(ctx context.Context, line string)
	// Key handles a control key pressed between prompts; with a KeySource
	// IO it also handles the keys pressed during a prompt.
	Key func(Key)
	// Tick runs every second (session timeouts).
	Tick func()
	// Poll runs every PolicyPoll (config file changes).
	Poll func()
	// Ended reports the end of the session (required); End ends it.
	Ended func() bool
	End   func(reason string)
	// Refresh runs after every event (the status row); Clear on return.
	Refresh func()
	Clear   func()
	// PeerAllowed decides which clients are served, from the kernel's
	// credentials of the peer. Nil serves the console's own account only.
	PeerAllowed func(ipc.Cred) bool
}

Loop is the console's request loop: it accepts clients, checks their peer credentials and serves their requests one at a time on one goroutine, between the lines and control keys the human types and the periodic ticks. A request whose client goes away has its context cancelled, which cancels a pending approval prompt.

func (*Loop) Serve

func (l *Loop) Serve(ctx context.Context, ln net.Listener, lines <-chan string) error

Serve accepts clients on ln and serves their requests one at a time until ctx ends or the session ends (logout, timeouts, :quit). lines are the console commands typed by the human (nil for none); the control keys come from the IO when it is a KeySource. ln is closed on return, which removes the socket.

type Options

type Options struct {
	// Profile is the profile to serve; ProjectRoot the project whose
	// config holds it ("" for the user config).
	Profile     string
	ProjectRoot string
	IO          IO
	// Auto, AllowUnmask and ShowResults are the start-up switches (all
	// three refused at level 2; ShowResults refused at level 1 too).
	Auto, AllowUnmask, ShowResults bool
	// StateDir is the state base (kit.StateBase() when empty).
	StateDir string
	// TTY is the console's terminal device (nil when unknown).
	TTY *os.File
	// Now defaults to time.Now.
	Now func() time.Time
	// Quantum levels response times (Env.Level): 0 is ResponseQuantum, a
	// negative value none.
	Quantum time.Duration
	// Sanitizer scrubs error text; nil is sanitize.New().
	Sanitizer *sanitize.Sanitizer
	// Isolation replaces the system the isolation checks read (tests and
	// a product's test build); nil is RealIsolationEnv.
	Isolation *IsolationEnv
	// Harden replaces Harden (tests).
	Harden func() error
	// VaultRead replaces secrets.VaultRead (tests).
	VaultRead func(ctx context.Context, path string) (*secrets.VaultCredentials, error)
	// Keychain replaces the product's OS keychain (tests).
	Keychain *secrets.Keychain
	// Ready, when set, is told the socket path once the console listens.
	Ready func(socket string)
	// Hub, when enabled, decides requests on lockhub (HubOptions); --auto
	// is refused with it.
	Hub HubOptions
	// OnHub, when set, is called with the console's hub once it is open,
	// before any request is served: where a product registers its
	// hubclient.Recompute (hub.Client.SetRecompute). It runs before the
	// policy review, so Run may still fail and close the hub; Run sets the
	// client's OnRequest itself (Hub.Requests), replacing any set here.
	OnHub func(*Hub)
}

Options configure Run.

type Outcome

type Outcome int

Outcome of an approval.

const (
	Approved Outcome = iota
	// Denied is a typed answer that is not the expected one; Answer holds it.
	Denied
	// Unanswered: no answer within the timeout, or ctx ended.
	Unanswered
	RetypeRequested
	// NoFinger: desktop mode, typed yes, then no match, timeout or reader
	// error; Err holds it.
	NoFinger
)

type PIIBackend

type PIIBackend interface {
	// PIIRules returns the mask and allow rules of p.
	PIIRules(p Policy) (mask, allow []string)
	// CheckMask validates a pattern sent by a client and returns its
	// canonical form; an error is shown to the client (invalid params).
	CheckMask(pattern string) (string, error)
	// SaveMask adds the canonical pattern to the config file as it is on
	// disk, so the human's unconfirmed edits are neither lost nor applied.
	SaveMask(pattern string) error
	// WithMask returns p with the canonical pattern added (canonical form,
	// sorted lists); p itself is not changed.
	WithMask(p Policy, pattern string) Policy
}

PIIBackend is implemented by a backend whose policy has mask rules: the console then serves pii.list and pii.add for it. A pattern's syntax (db.table.column, a JSON path, a header name) is the product's. Adding a mask rule only tightens the policy, so it applies at once.

type Pending added in v0.3.0

type Pending struct {
	// Ticket is the id the agent presents to run the request once decided.
	Ticket string
	// Instance is the lock instance the ticket is on (hub mode).
	Instance string
}

Pending is a request handed to an AsyncApprover.

type Policy

type Policy interface {
	config.Policy
	Profile() Profile
}

Policy is a product's policy: the diff and description of config.Policy, plus the profile the console needs. Its JSON encoding is what the console fingerprints and stores as the approved policy, so it must be canonical (sorted lists, defaults applied) and decode back into the same type.

type PolicyApplier added in v0.7.0

type PolicyApplier interface {
	ApplyPolicy(p Policy) error
}

PolicyApplier is implemented by a backend that prepares itself for a policy before it is in force (compiles its rules). While the console runs, an error refuses the policy, which then is neither saved nor applied: a loosening stays out, and a tightening the backend cannot apply ends the session (it would otherwise serve under the looser one). On an error the backend must keep the policy it had (the console does not apply that one again). NewServer calls it with the start policy and fails on an error (Run has then saved the start policy already, and stops before serving).

type Profile

type Profile struct {
	Name       string
	Production bool
	// Where names the backend for the human (host:port, a path): it is
	// shown escaped.
	Where string
	// Host, Port and DefaultPort name the OS keychain item of the secret
	// ("<profile>@<host>:<port>").
	Host              string
	Port, DefaultPort int
	// User is the backend account; "" asks the human (or comes from Vault).
	User string
	// Credentials is config.CredentialsAsk, CredentialsKeychain,
	// CredentialsVault or CredentialsNone; VaultPath is read for "vault".
	Credentials string
	VaultPath   string
	// CredentialsTTL asks for the secret again (and reconnects) once the
	// connection is that old; 0 keeps it for the session.
	CredentialsTTL time.Duration
	// IdleTimeout ends the session after that long without activity,
	// MaxSession after that long in all. 0 takes 20m and 4h.
	IdleTimeout, MaxSession time.Duration
}

Profile is what the console needs to know of a profile.

type Result

type Result struct {
	Outcome Outcome
	Answer  string
	Err     error
}

Result of an approval.

type Screen

type Screen struct {
	// Header follows the profile name on the top line ("db / shop · tier read").
	Header string
	// Requester names the client; "" takes the peer behind the request.
	Requester string
	// Note is the agent's one-line reason (untrusted, escaped, audited).
	Note string
	// Body are the lines of the request; Warnings are shown in red.
	Body     []string
	Warnings []string
	// Production forces the production prompt; the profile's own flag
	// always does.
	Production bool
	// NeverAuto is never approved by auto mode (an unmasked read, a
	// destructive-looking operation): the human answers every time.
	NeverAuto bool
	// Red frames the screen in red outside production too.
	Red bool
	// Retype, when set, is an answer that asks to retype values; Approve
	// then returns Approval.Retype.
	Retype string
	// Record, when set, turns the decision record into the product's own
	// (with its fields); nil writes the audit.Base.
	Record func(audit.Base) audit.Record
	// Hub is the plan and masked screen for the hub, in hub mode
	// (HubOf(env) not nil); a request without one is refused there.
	Hub *HubTicket
}

Screen is the content of an approval screen, entirely supplied by the product. The console frames it (red on production), closes it with the prompt and keeps the rules: typed profile name on production, timeout, fingerprint at level 2, audit of the decision.

type Server

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

Server serves client requests one at a time against one session. It is not safe for concurrent use: Serve runs every request on one goroutine.

func NewServer

func NewServer(cfg ServerConfig) (*Server, error)

NewServer checks cfg and returns a server enforcing cfg.Policy.

func (*Server) Approve

func (s *Server) Approve(ctx context.Context, id int64, sc Screen) Approval

Approve implements Env: it frames sc, approves it in auto mode or asks the human, and audits the decision.

func (*Server) Asker added in v0.7.0

func (s *Server) Asker() Asker

Asker returns the console's Asker (AskerOf).

func (*Server) Audit

func (s *Server) Audit(rec audit.Record)

Audit implements Env.

func (*Server) AuditBase

func (s *Server) AuditBase(ctx context.Context, b audit.Base)

AuditBase implements Env.

func (*Server) Auto

func (s *Server) Auto() bool

Auto implements Env.

func (*Server) CheckPolicy

func (s *Server) CheckPolicy()

CheckPolicy re-reads the policy from the config files. Changes that only tighten are applied at once; a loosening waits for :review.

func (*Server) Close

func (s *Server) Close()

Close closes the session.

func (*Server) Command

func (s *Server) Command(ctx context.Context, line string)

Command runs a console command typed by the human between requests.

func (*Server) End

func (s *Server) End(reason string)

End stops the session after the current request.

func (*Server) Ended

func (s *Server) Ended() (string, bool)

Ended reports whether the session is over, and why.

func (*Server) Handle

func (s *Server) Handle(ctx context.Context, req ipc.Request) ipc.Response

Handle serves one request. None of the console's methods loosens the policy.

func (*Server) Hub added in v0.3.0

func (s *Server) Hub() *Hub

Hub is the console's hub mode, nil standalone (HubOf).

func (*Server) Key

func (s *Server) Key(k Key)

Key handles a control key pressed in the console.

func (*Server) Level

func (s *Server) Level(ctx context.Context, start time.Time)

Level implements Env.

func (*Server) Loop

func (s *Server) Loop() *Loop

Loop is the request loop of s, for Serve.

func (*Server) Lost

func (s *Server) Lost()

Lost implements Env.

func (*Server) Policy

func (s *Server) Policy() Policy

Policy implements Env.

func (*Server) PolicyPending

func (s *Server) PolicyPending() bool

PolicyPending reports a loosening awaiting :review: a product refuses requests meanwhile (ipc.CodePolicyPending) when its policy matters to them.

func (*Server) PolicyRefused added in v0.7.0

func (s *Server) PolicyRefused() bool

PolicyRefused reports a loosening in the config files that the human refused (at start or at :review) and that is still there: the last approved policy applies, which a product may tell its clients.

func (*Server) Println

func (s *Server) Println(line string)

Println implements Env.

func (*Server) Safe

func (s *Server) Safe(text string) string

Safe implements Env.

func (*Server) Sanitizer

func (s *Server) Sanitizer() *sanitize.Sanitizer

Sanitizer implements Env.

func (*Server) Session

func (s *Server) Session(ctx context.Context) (Session, error)

Session implements Env.

func (*Server) ShowResults

func (s *Server) ShowResults() bool

ShowResults implements Env.

func (*Server) Status

func (s *Server) Status() StatusResult

Status is the status method's answer (without StatusAnswerer).

func (*Server) Tick

func (s *Server) Tick()

Tick ends the session on idle or maximum session timeout, as set by the policy in force.

func (*Server) UnmaskAllowed

func (s *Server) UnmaskAllowed() bool

UnmaskAllowed implements Env.

func (*Server) WriteAudit added in v0.7.0

func (s *Server) WriteAudit(rec audit.Record) error

WriteAudit writes rec as is, like Env.Audit (no profile, peer or AuditRecord stamping: rec is the product's own record), and returns the failure: a product that records an approval itself refuses the request when it cannot be written (fail closed). Failures count towards ending the session as for the console's own records.

type ServerConfig

type ServerConfig struct {
	Product kit.Product
	Backend Backend
	// Policy is the approved policy the server enforces.
	Policy Policy
	// StateDir and ApprovedKey locate the approved policy file ("" keeps
	// adoptions in memory).
	StateDir    string
	ApprovedKey string
	Session     Session
	Audit       *audit.Log
	IO          IO
	// Approver takes every human decision; nil means typed approvals.
	Approver Approver
	// Level is the isolation level (2: every decision takes a finger, and
	// a typed answer alone never widens anything).
	Level int
	// Boundary names the desktop boundary for the status screen.
	Boundary string
	Now      func() time.Time
	// Auto, AllowUnmask and ShowResults are the switches at start.
	Auto, AllowUnmask, ShowResults bool
	// Reconnect opens a new session (credentials, Connect, Checks) under
	// the profile in force; nil (and ReconnectPolicy nil) means a lost
	// connection ends the console. An EndError it returns ends the session
	// with its reason.
	Reconnect func(ctx context.Context, p Profile) (Session, error)
	// ReconnectPolicy, when set, is used instead of Reconnect and gets the
	// whole policy in force.
	ReconnectPolicy func(ctx context.Context, p Policy) (Session, error)
	// Quantum levels response times (Env.Level).
	Quantum time.Duration
	// PeerAllowed decides which clients are served; nil: the console's own
	// account.
	PeerAllowed func(ipc.Cred) bool
	// Sanitizer scrubs error text; nil is sanitize.New().
	Sanitizer *sanitize.Sanitizer
	// Refused is the fingerprint of a loosening the human refused at start,
	// not announced again until the files change.
	Refused string
	// RefuseProduction re-applies the start-up refusals of a production
	// profile (an X11 session, core dumps that could not be disabled) when
	// the policy in force becomes production; nil refuses nothing.
	RefuseProduction func() error
	// Hub is the hub mode (Run with HubOptions); nil standalone.
	Hub *Hub
}

ServerConfig holds what a Server needs once the start-up sequence is done. Run builds it; tests build it directly.

type Session

type Session interface {
	Close() error
}

Session is one connection of a backend.

func Unwrap added in v0.7.0

func Unwrap(s Session) Session

Unwrap returns the backend's own session behind s: the console wraps a session whose login expires (a Vault lease), and a backend type-asserts what Unwrap returns to its own session type.

type Sized

type Sized interface {
	Width() int
}

Sized is implemented by an IO that knows its width in columns.

type StatusAnswerer added in v0.7.0

type StatusAnswerer interface {
	StatusAnswer(c StatusResult, s Session) any
}

StatusAnswerer is implemented by a backend that answers the status method itself: its StatusAnswer value is sent instead of StatusResult with Backend.Status under "product" (a product whose clients read a flat answer). c is the console's part; s is nil while disconnected.

type StatusLine

type StatusLine interface {
	SetStatus(string)
}

StatusLine is implemented by an IO that keeps a status row on screen.

type StatusLiner added in v0.7.0

type StatusLiner interface {
	StatusLineParts(v StatusView) (suffix string, parts []string)
}

StatusLiner is implemented by a backend that adds to the status row: Suffix follows the profile name, Parts come after the results switch.

type StatusResult

type StatusResult struct {
	Profile        string `json:"profile"`
	Production     bool   `json:"production"`
	AutoApprove    bool   `json:"auto_approve"`
	AllowUnmask    bool   `json:"allow_unmask"`
	ShowResults    bool   `json:"show_results"`
	IdleTimeoutInS int    `json:"idle_timeout_in_s"`
	SessionEndsInS int    `json:"session_ends_in_s"`
	// Level is the security level: 0 session shape, 1 or 2 desktop shape.
	Level   int `json:"level"`
	Product any `json:"product,omitempty"`
}

StatusResult answers the status method: the console's part, and the product's (Backend.Status) under "product".

type StatusRower added in v0.7.0

type StatusRower interface {
	StatusRows(v StatusView) [][2]string
}

StatusRower is implemented by a backend that lists its own :status rows (label, value; values escaped by the backend). The console aligns them.

type StatusView added in v0.7.0

type StatusView struct {
	// Status is the console's part of the status answer (Product unset:
	// the status row is drawn every second, Backend.Status is not called
	// for it).
	Status StatusResult
	// Profile and Policy are the approved policy in force.
	Profile Profile
	Policy  Policy
	// Auto is auto mode in effect; ShowResults and AllowUnmask the other
	// switches.
	Auto, ShowResults, AllowUnmask bool
	// Pending: a loosening awaits :review. Requests counts the change
	// requests queued.
	Pending  bool
	Requests int
	// Boundary names the desktop boundary ("" in the session shape); Level
	// is the security level (2: every decision takes a finger).
	Boundary string
	Level    int
	// Session is the live session, nil while disconnected.
	Session Session
}

StatusView is what the console knows when it draws :status or the status row, for the product hooks that draw them.

type Terminal

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

Terminal is the IO of a real console: prompts and messages go to out, answers and console commands are read from in, which must be the controlling terminal.

Interactive puts the terminal in cbreak mode: the Terminal does the line editing itself, so that it also sees the control keys (Shift+Tab, Ctrl-O) and can keep a status line on the bottom row, above which the output scrolls. One goroutine reads in from the first prompt on, so that the console can wait for a request and for a typed command at the same time. Before each prompt the kernel's pending input is flushed and the lines already read are dropped: type-ahead never answers a prompt.

func NewTerminal

func NewTerminal(in *os.File, out io.Writer) *Terminal

NewTerminal returns the IO over in and out (normally os.Stdin and os.Stdout).

func (*Terminal) Ask

func (t *Terminal) Ask(ctx context.Context, prompt string, timeout time.Duration) (string, bool)

Ask flushes pending input, prints prompt and waits for one line.

func (*Terminal) AskSecret

func (t *Terminal) AskSecret(ctx context.Context, prompt string) ([]byte, error)

AskSecret reads a line with echo off. Before the reader goroutine runs it uses secrets.PromptPassword; in cbreak mode it stops echoing itself; otherwise it turns the kernel echo off on in.

func (*Terminal) Bell

func (t *Terminal) Bell()

Bell rings the terminal bell.

func (*Terminal) Interactive

func (t *Terminal) Interactive() error

Interactive puts the terminal in cbreak mode (no line discipline, no kernel echo; signals and output post-processing kept) and starts reading it. Restore undoes it. An input that is not a terminal (a test pipe) stays as it is: lines are read, keys and the status line are off.

func (*Terminal) Keys

func (t *Terminal) Keys() <-chan Key

Keys delivers the control keys pressed while no prompt is active.

func (*Terminal) Lines

func (t *Terminal) Lines() <-chan string

Lines delivers the lines typed while no prompt is active.

func (*Terminal) Println

func (t *Terminal) Println(s string)

Println writes one line.

func (*Terminal) Restore

func (t *Terminal) Restore()

Restore ends the status line and puts the terminal back in the mode it had before Interactive.

func (*Terminal) SetKeyHandler

func (t *Terminal) SetKeyHandler(f func(Key))

SetKeyHandler sets the function that handles a control key pressed while a prompt waits. It runs on the goroutine that asked.

func (*Terminal) SetStatus

func (t *Terminal) SetStatus(s string)

SetStatus shows s on the bottom row of the terminal, above which the output scrolls; "" removes the row. Nothing happens before Interactive.

func (*Terminal) Width

func (t *Terminal) Width() int

Width is the terminal width in columns (80 when unknown).

type Withdrawer added in v0.3.0

type Withdrawer interface {
	Withdraw(ctx context.Context, d Decision, ticket, reason string) error
}

Withdrawer is an AsyncApprover that can take back a ticket it submitted: a pending request the console could not record is withdrawn.

type Withdrawn added in v0.4.0

type Withdrawn struct {
	Instance string
	// Tickets are the tickets withdrawn now.
	Tickets []string
}

Withdrawn is what a standalone start withdrew from the hub mode of one instance.

func WithdrawStandalone added in v0.4.0

func WithdrawStandalone(p kit.Product, stateDir string) ([]Withdrawn, error)

WithdrawStandalone is the rollback of hub mode (a console started without --hub after running with it): every pending plan of every hub instance under stateDir (as Options.StateDir) ends Withdrawn, marked for the hub to hear of at the next start in hub mode (hubclient.Client.Sync). An instance whose plans another console holds open (plans.ErrLocked: it runs in hub mode now) is left alone. Run calls it; a product with its own console lifecycle calls it at its standalone start.

type Worder added in v0.7.0

type Worder interface {
	Words() Words
}

Worder is implemented by a backend that words the console's lines in its product's terms (statements rather than requests). An empty field keeps the console's own text.

type Words added in v0.7.0

type Words struct {
	// AutoStart is printed when the console starts in auto mode.
	AutoStart string
	// AutoOn and Manual are printed when the mode switches.
	AutoOn, Manual string
	// NoAutoProduction refuses auto mode on a production profile.
	NoAutoProduction string
	// NoAutoHub, when set, refuses auto mode in hub mode with this line
	// (the console otherwise accepts the switch, which has no effect there).
	NoAutoHub string
	// StatusAuto and StatusManual are the mode row of :status.
	StatusAuto, StatusManual string
	// ResultsOn is printed when the clear results are switched on;
	// StatusResults is their row of :status when on.
	ResultsOn, StatusResults string
	// UnmaskAsk is the question that allows unmasked output; UnmaskOff is
	// printed when it is turned off.
	UnmaskAsk, UnmaskOff string
	// ReviewAuto warns at :review that auto mode is on and the loosening
	// turns a production profile into another.
	ReviewAuto string
	// ChangeQueued words a change request from a client (already escaped).
	ChangeQueued func(change string) string
	// Reconnecting is printed before a reconnection.
	Reconnecting string
	// TTLReached is printed when credentials_ttl ends a connection.
	TTLReached func(ttl time.Duration, credentials string) string
	// ProductionChecks words the end of a session whose checks found
	// something once the profile became production: the console line and
	// the end reason.
	ProductionChecks func(p Profile, findings []Finding) (line, reason string)
	// ConnLost is the error a Handler gets from Env.Session when the
	// connection is lost and cannot be opened again.
	ConnLost string
}

Words are the console lines a product may word itself. Each is the whole line, colours excepted; an empty one keeps the console's text. The console prints them as they are: a line built from config or server text (a finding, a credentials mode, a change) must be escaped by the product (sanitize.Terminal), as the console's own lines are.

Jump to

Keyboard shortcuts

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