Documentation
¶
Overview ¶
Package approval implements AgentSSH's optional async approval backend.
It is deliberately out-of-band: run requests that need approval return immediately, operators adjudicate later, and execution happens only when the agent reruns the command through the normal policy path.
Index ¶
- Constants
- Variables
- func CheckHostGrantRule(rule policy.Rule) error
- func NewID() (string, error)
- func NewPlanID() (string, error)
- func RequestDigest(req PendingRequest, scope Scope) string
- type ApplyOptions
- type ApplyResult
- type Authorization
- type AuthorizationStatus
- type Grant
- type HostGrantMode
- type Matcher
- type MatcherKind
- type PendingRequest
- type PendingStore
- func (s PendingStore) Create(req PendingRequest) (PendingRequest, error)
- func (s PendingStore) CreatePlan(manifest PlanManifest) (PlanManifest, error)
- func (s PendingStore) Get(id string) (PendingRequest, error)
- func (s PendingStore) GetPlan(id string) (PlanManifest, error)
- func (s PendingStore) List() ([]PendingRequest, error)
- func (s PendingStore) PlanStatus(id string) (PlanStatus, error)
- func (s PendingStore) Resolve(req PendingRequest, verdict Verdict, scope Scope) (Resolution, error)
- func (s PendingStore) Status(id string) (StatusResult, error)
- func (s PendingStore) Wait(id string, timeout time.Duration) (StatusResult, error)
- func (s PendingStore) WaitPlan(id string, timeout time.Duration) (PlanStatus, error)
- type PlanManifest
- type PlanMember
- type PlanStatus
- type Resolution
- type RuntimeConfig
- type Scope
- type SessionStore
- func (s SessionStore) Claim(sessionID string, host string, command string, stdinSHA256 string, ...) (Grant, bool, error)
- func (s SessionStore) Commit(sessionID string, reqID string) error
- func (s SessionStore) End(sessionID string) error
- func (s SessionStore) Grant(sessionID string, host string, scope Scope, matcher Matcher, ...) (Grant, error)
- func (s SessionStore) Peek(sessionID string, host string, command string, stdinSHA256 string) (Grant, bool, error)
- func (s SessionStore) Release(sessionID string, reqID string) error
- type StatusResult
- type Verdict
Constants ¶
const ( DefaultSessionTTL = 12 * time.Hour DefaultWaitTimeout = 10 * time.Minute )
const ( ChannelCLI = "cli" ChannelTUI = "tui" ChannelExit = "exit" ChannelPlan = "plan" )
Variables ¶
var ( ErrInvalidPlanID = errors.New("invalid plan id") ErrPlanNotFound = errors.New("plan not found") ErrPlanScope = errors.New("plan approvals support --once or --session only") ErrPlanNoPending = errors.New("plan has no pending requests") ErrPlansDirUnset = errors.New("plan store directory is not configured") )
var ( ErrInvalidID = errors.New("invalid approval id") ErrPendingNotFound = errors.New("approval request not found") ErrAlreadyResolved = errors.New("approval request already resolved") ErrCorruptResolution = errors.New("corrupt approval resolution") )
var ErrInvalidUTF8Command = errors.New("approval command is not valid UTF-8")
ErrInvalidUTF8Command rejects commands that are not valid UTF-8. Go's regexp has no way to express a single invalid byte: `\xF3` in a pattern denotes the *rune* U+00F3 (encoded C3 B3), not the byte 0xF3. A matcher generated from such a command would therefore fail to match the approved command while matching a different one, so these commands are rejected outright rather than approved with a matcher that cannot mean what it says.
var ErrNULCommand = errors.New("approval command contains NUL")
Functions ¶
func CheckHostGrantRule ¶
CheckHostGrantRule reports whether a persisted approval rule is still safe to honor. Beyond the string invariant it rejects patterns generated before '+' was escaped: those act as quantifiers and authorize commands the operator never approved.
func RequestDigest ¶
func RequestDigest(req PendingRequest, scope Scope) string
Types ¶
type ApplyOptions ¶
type ApplyResult ¶
type ApplyResult struct {
Request PendingRequest `json:"request"`
Resolution Resolution `json:"resolution"`
Grant *Grant `json:"grant,omitempty"`
RuleName string `json:"rule_name,omitempty"`
}
func ApplyDecision ¶
func ApplyDecision(opts ApplyOptions, id string, verdict Verdict, scope Scope) (ApplyResult, error)
func ApplyPlanDecision ¶ added in v0.10.0
func ApplyPlanDecision(opts ApplyOptions, id string, verdict Verdict, scope Scope) ([]ApplyResult, error)
ApplyPlanDecision adjudicates every still-pending member of a plan with one verdict. Approvals are capped at once/session: host promotion widens policy permanently and must stay a deliberate per-command decision.
type Authorization ¶
type Authorization struct {
Status AuthorizationStatus
Decision policy.Decision
GrantScope Scope
GrantMatcher string
ApprovalMatcher Matcher
}
func Authorize ¶
func Authorize(cfg policy.Config, inv inventory.Inventory, sessionStore SessionStore, runtime RuntimeConfig, sessionID string, host string, command string, stdinSHA256 string, reqID string) (Authorization, error)
Authorize decides one run request. A matching once grant is claimed under reqID (two-phase consumption): the caller must settle the claim with SessionStore.Commit once the command reaches the remote, or SessionStore.Release if it verifiably never executed. stdinSHA256 is empty for runs without stdin; when set, grants must carry the same stdin hash, persistent host approval rules never match, and the candidate matcher is forced to exact and non-promotable.
func PreflightAuthorize ¶
func PreflightAuthorize(cfg policy.Config, inv inventory.Inventory, sessionStore SessionStore, runtime RuntimeConfig, sessionID string, host string, command string, stdinSHA256 string) (Authorization, error)
PreflightAuthorize is the side-effect-free variant used to preview a batch before executing any of it.
type AuthorizationStatus ¶
type AuthorizationStatus string
const ( AuthAllow AuthorizationStatus = "allow" AuthAllowByGrant AuthorizationStatus = "allow_by_grant" AuthHardDeny AuthorizationStatus = "hard_deny" AuthNeedsApproval AuthorizationStatus = "needs_approval" )
type Grant ¶
type Grant struct {
Scope Scope `json:"scope"`
Kind MatcherKind `json:"kind"`
Regex string `json:"regex"`
Prefix []string `json:"prefix,omitempty"`
SourceCmd string `json:"source_cmd"`
Host string `json:"host"`
GrantedTS string `json:"granted_ts"`
ExpiresTS string `json:"expires_ts"`
ApprovalID string `json:"approval_id"`
ReqID string `json:"req_id"`
Channel string `json:"channel"`
// StdinSHA256 binds the grant to one exact stdin payload. Empty means the
// approved command had no stdin; a grant never matches a run whose stdin
// hash differs from the one the operator approved.
StdinSHA256 string `json:"stdin_sha256,omitempty"`
// ClaimReqID/ClaimTS implement two-phase once-grant consumption: Claim marks
// the grant as reserved by one run request; Commit deletes it once the command
// reached the remote; Release restores it when the command never executed.
// A claim never expires by wall clock: a crash between claim and settle leaves
// the grant unusable (fail-closed), same as a consumed grant.
ClaimReqID string `json:"claim_req_id,omitempty"`
ClaimTS string `json:"claim_ts,omitempty"`
}
type HostGrantMode ¶
type HostGrantMode string
const ( HostGrantExact HostGrantMode = "exact" HostGrantSafePrefix HostGrantMode = "safe-prefix" HostGrantPrefix HostGrantMode = "prefix" )
type Matcher ¶
type Matcher struct {
Kind MatcherKind `json:"kind"`
Regex string `json:"regex"`
Prefix []string `json:"prefix,omitempty"`
Promotable bool `json:"promotable"`
SourceCmd string `json:"source_cmd"`
}
Matcher is the reusable command matcher that can be stored in session grants or generated host rules.
func Generalize ¶
func Generalize(command string, mode HostGrantMode) (Matcher, error)
type MatcherKind ¶
type MatcherKind string
const ( MatcherExact MatcherKind = "exact" MatcherPrefix MatcherKind = "prefix" )
type PendingRequest ¶
type PendingRequest struct {
Version int `json:"version"`
ID string `json:"id"`
ReqID string `json:"req_id"`
SessionID string `json:"session_id"`
Host string `json:"host"`
Cmd string `json:"cmd"`
CmdSHA256 string `json:"cmd_sha256"`
Candidate Matcher `json:"candidate_matcher"`
MatcherSHA256 string `json:"matcher_sha256"`
Kind string `json:"kind"`
Promotable bool `json:"promotable"`
TS string `json:"ts"`
ProposedScopes []Scope `json:"proposed_scope"`
// StdinSHA256/StdinBytes describe the stdin payload the run would feed the
// command. The content itself never enters the approval store; the operator
// adjudicates on hash + size, and the resulting grant is pinned to the hash.
StdinSHA256 string `json:"stdin_sha256,omitempty"`
StdinBytes int64 `json:"stdin_bytes,omitempty"`
// PlanID/PlanSeq/PlanTotal tag requests minted by one `plan submit` so the
// operator can review and adjudicate the batch as a unit. Authoritative plan
// membership lives in the plan manifest (plans/<id>.json); these fields are
// display metadata on the requests this submit created.
PlanID string `json:"plan_id,omitempty"`
PlanSeq int `json:"plan_seq,omitempty"`
PlanTotal int `json:"plan_total,omitempty"`
}
type PendingStore ¶
type PendingStore struct {
PendingDir string
ResponsesDir string
PlansDir string
Now func() time.Time
}
func (PendingStore) Create ¶
func (s PendingStore) Create(req PendingRequest) (PendingRequest, error)
func (PendingStore) CreatePlan ¶ added in v0.10.0
func (s PendingStore) CreatePlan(manifest PlanManifest) (PlanManifest, error)
func (PendingStore) Get ¶
func (s PendingStore) Get(id string) (PendingRequest, error)
func (PendingStore) GetPlan ¶ added in v0.10.0
func (s PendingStore) GetPlan(id string) (PlanManifest, error)
func (PendingStore) List ¶
func (s PendingStore) List() ([]PendingRequest, error)
func (PendingStore) PlanStatus ¶ added in v0.10.0
func (s PendingStore) PlanStatus(id string) (PlanStatus, error)
PlanStatus resolves every member's current status. A member whose pending file has been reaped after resolution counts as expired — fail-closed, the plan never reports approved from unknowable members — but expiry is kept distinct from denied so an approved-then-reaped plan is not misreported as operator-rejected.
func (PendingStore) Resolve ¶
func (s PendingStore) Resolve(req PendingRequest, verdict Verdict, scope Scope) (Resolution, error)
func (PendingStore) Status ¶
func (s PendingStore) Status(id string) (StatusResult, error)
func (PendingStore) Wait ¶
func (s PendingStore) Wait(id string, timeout time.Duration) (StatusResult, error)
func (PendingStore) WaitPlan ¶ added in v0.10.0
func (s PendingStore) WaitPlan(id string, timeout time.Duration) (PlanStatus, error)
WaitPlan polls until every member is resolved or the timeout elapses, mirroring PendingStore.Wait for single approvals.
type PlanManifest ¶ added in v0.10.0
type PlanManifest struct {
Version int `json:"version"`
ID string `json:"id"`
SessionID string `json:"session_id"`
Host string `json:"host"`
TS string `json:"ts"`
MemberIDs []string `json:"member_ids"`
}
PlanManifest is the authoritative membership record for one submitted plan, written once (O_EXCL) at submit time. Member requests resolve individually through the ordinary pending/response stores.
type PlanMember ¶ added in v0.10.0
type PlanMember struct {
ApprovalID string `json:"approval_id"`
Status string `json:"status"` // pending | approved | denied | expired
Scope Scope `json:"scope,omitempty"`
Request *PendingRequest `json:"request,omitempty"`
}
PlanMember pairs one member request with its current resolution status.
type PlanStatus ¶ added in v0.10.0
type PlanStatus struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
Host string `json:"host"`
Status string `json:"status"` // pending | approved | denied | expired
Pending int `json:"pending"`
Approved int `json:"approved"`
Denied int `json:"denied"`
Expired int `json:"expired,omitempty"`
Members []PlanMember `json:"members"`
}
PlanStatus is the aggregate view returned by plan status/wait.
type Resolution ¶
type RuntimeConfig ¶
type RuntimeConfig struct {
Enabled bool
HostGrantMode HostGrantMode
SessionTTL time.Duration
WaitTimeout time.Duration
}
func RuntimeConfigFromPolicy ¶
func RuntimeConfigFromPolicy(cfg policy.Approval, envValue string) (RuntimeConfig, error)
type SessionStore ¶
func (SessionStore) Claim ¶ added in v0.10.0
func (s SessionStore) Claim(sessionID string, host string, command string, stdinSHA256 string, reqID string) (Grant, bool, error)
Claim matches a grant for one run request. A matching once grant is marked as claimed by reqID (in the same lock, so two concurrent runs can never claim the same once grant); session grants match without side effects. The caller must settle every once claim with Commit or Release.
func (SessionStore) Commit ¶ added in v0.10.0
func (s SessionStore) Commit(sessionID string, reqID string) error
Commit consumes every once grant claimed by reqID. Call it as soon as the command has been handed to the remote: from that point re-running requires a fresh approval.
func (SessionStore) End ¶
func (s SessionStore) End(sessionID string) error
func (SessionStore) Peek ¶
func (s SessionStore) Peek(sessionID string, host string, command string, stdinSHA256 string) (Grant, bool, error)
Peek reports whether a grant would authorize the command without reserving or consuming anything. Once grants already claimed by an in-flight run are invisible: they can no longer authorize a different request.
func (SessionStore) Release ¶ added in v0.10.0
func (s SessionStore) Release(sessionID string, reqID string) error
Release restores every once grant claimed by reqID. Call it only when the command verifiably never executed (local cancel, transport failure before the remote ran it, audit append failure before execution).
type StatusResult ¶
type StatusResult struct {
ID string `json:"id"`
Verdict Verdict `json:"verdict,omitempty"`
Scope Scope `json:"scope,omitempty"`
Status string `json:"status"`
Request *PendingRequest `json:"request,omitempty"`
}