Documentation
¶
Overview ¶
Package hubclient is a lock's client for lockhub, the approval hub: it pushes signed tickets, long-polls the hub's decisions and turns an approval into a plan that runs once.
Every request but Hello is signed with the lock instance's key (the Lock-Instance, Lock-Time, Lock-Nonce and Lock-Sig headers, see ticket.RequestMessage); every decision is verified with the hub key the lock pinned, never one the hub sends. A screen leaves the lock only as a render.MaskedScreen.
The client fails closed: a hub that cannot be reached leaves the plan pending, and nothing falls back to another approver. A ticket the hub did not acknowledge is pushed again by the next Submit of its plan or by Sync.
Index ¶
- Constants
- Variables
- func HubKeyCommand(w io.Writer, home string, args []string) error
- func InstanceKey(home, instance string) (ed25519.PrivateKey, error)
- func KeyDir(home string) string
- func ReadHubKey(path string) (ed25519.PublicKey, error)
- func ValidInstance(name string) bool
- type Client
- func (c *Client) Begin(st *plans.Store, ticketID string, caller plans.Caller, recheck Recheck) (*plans.Plan, *ticket.Decision, error)
- func (c *Client) Finish(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) error
- func (c *Client) Hello(ctx context.Context) (Hello, error)
- func (c *Client) Poll(ctx context.Context, wait time.Duration) (int, error)
- func (c *Client) Submit(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) (string, error)
- func (c *Client) Sync(ctx context.Context, st *plans.Store) error
- func (c *Client) Watch(ctx context.Context, st *plans.Store, report func(error))
- func (c *Client) Withdraw(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ...) error
- type Config
- type Hello
- type HubError
- type Recheck
Constants ¶
const ( // MaxPending bounds the pending tickets of an instance. MaxPending = 20 // RateN new tickets per RateWindow at most. RateN = 10 RateWindow = 5 * time.Minute // MaxWait bounds a long poll (the hub's own bound). MaxWait = 60 * time.Second // MaxInbox bounds the decisions kept for plans not yet run. MaxInbox = plans.MaxPlans )
Defaults.
Variables ¶
var ( // ErrUnreachable: the hub did not answer, or answered 5xx. A ticket // signed for the plan stays pending. ErrUnreachable = errors.New("hubclient: hub unreachable") // ErrRateLimited: too many pending tickets or too many new tickets in // the rate window, here or at the hub; no ticket was opened. ErrRateLimited = errors.New("hubclient: too many tickets") // ErrPending: no decision on the ticket yet. ErrPending = errors.New("hubclient: no decision yet") // ErrDenied: the human denied the ticket; the plan is Denied. ErrDenied = errors.New("hubclient: denied") // ErrBadDecision: the hub sent a decision that does not verify under // the pinned hub key or is not for this instance; it was dropped. ErrBadDecision = errors.New("hubclient: decision refused") )
Functions ¶
func HubKeyCommand ¶
HubKeyCommand is a lock's `hub-key --instance NAME`: it prints the public key of the instance (InstanceKey of home, "" the user's, created on the first call) in base64 std on one line, for `lock up` to pin at the hub.
func InstanceKey ¶
func InstanceKey(home, instance string) (ed25519.PrivateKey, error)
InstanceKey returns the Ed25519 key of instance, kept in KeyDir(home)/<instance>.key (0600 in a 0700 directory, the seed in base64 std on one line); home "" is the user's home. The first call creates it; a key file group or others can read is refused.
func ReadHubKey ¶
ReadHubKey reads the pinned hub key: an Ed25519 public key in base64 std on one line (the file `lock up` mounts at /run/lockfamily/hub.pub).
func ValidInstance ¶
ValidInstance reports whether name is a valid lock instance name.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client talks to one hub for one lock instance. It is safe for concurrent use.
func (*Client) Begin ¶
func (c *Client) Begin(st *plans.Store, ticketID string, caller plans.Caller, recheck Recheck) (*plans.Plan, *ticket.Decision, error)
Begin turns the decision on ticketID into a run of its plan, for caller (the agent that made the plan): the decision verifies under the pinned hub key and matches the ticket the lock signed (ticket.Decision.Match), is still usable, and the plan's binding has not moved (recheck, plans.Store.Check). The plan comes back Executing under the decision's nonce, consumed: the product runs it, then calls Finish.
Begin also returns the verified decision, for the lock's audit (who approved, on which facade, the hub's signature), with the plan or with the error of a decision that was matched but not acted on.
No decision yet is ErrPending; a denial ends the plan Denied and is ErrDenied with the human's reason; a decision too old or past the ticket ends it Expired.
func (*Client) Finish ¶
func (c *Client) Finish(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, ok bool, reason string) error
Finish ends an executing plan (plans.Store.Finish) and tells the hub its ticket was consumed. The plan is finished even when the hub cannot be told; the error says so.
func (*Client) Poll ¶
Poll long-polls the hub once, waiting up to wait (at most MaxWait) for decisions after the cursor, keeps those that verify under the pinned hub key for this instance, and advances and saves the cursor. It returns how many decisions it kept; decisions it dropped make ErrBadDecision.
func (*Client) Submit ¶
func (c *Client) Submit(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, t *ticket.Ticket, screen render.MaskedScreen) (string, error)
Submit asks the hub for a decision on plan planID of caller and returns the ticket id. t is the product's ticket for the plan (NewTicket; its plan hash and policy fingerprint the plan's); Submit sets its instance, key id, screen and touched kinds from screen, signs it, records it in st and pushes it.
Submit is idempotent: a plan already pending returns its ticket, and a pending plan of caller with the same plan hash lends its ticket instead of opening a new one. A screen MaskScreen did not build is refused (render.ErrUnmasked). With the hub unreachable, Submit returns the ticket id with ErrUnreachable: the plan is pending, and the ticket goes out on a later Submit or Sync.
func (*Client) Sync ¶
Sync pushes every pending ticket the hub has not acknowledged since this client started: after a restart of the lock or of the hub.
func (*Client) Watch ¶
Watch polls until ctx ends, backing off while the hub is unreachable and resending unacknowledged tickets (Sync) when it answers again. Errors go to report (nil: dropped).
func (*Client) Withdraw ¶
func (c *Client) Withdraw(ctx context.Context, st *plans.Store, planID string, caller plans.Caller, reason string) error
Withdraw ends a plan not yet run as Withdrawn and withdraws its ticket at the hub. The plan is withdrawn even when the hub cannot be told; the error says so.
type Config ¶
type Config struct {
// URL is the hub's base URL ("http://lockhub:8443").
URL string
// Instance is the lock instance's name (Lock-Instance, Source.Instance).
Instance string
// Key is the instance's Ed25519 key: it signs tickets and requests.
Key ed25519.PrivateKey
// HubKey is the pinned hub key decisions must verify under.
HubKey ed25519.PublicKey
// StateFile keeps the decision cursor and the decisions not yet acted
// on, in the lock's volume (written 0600).
StateFile string
// ApprovalTTL is the instance's approval lifetime (ticket.ClampTTL; 0
// keeps each risk class's).
ApprovalTTL time.Duration
// MaxPending, RateN and RateWindow override the defaults when > 0.
MaxPending int
RateN int
RateWindow time.Duration
// HTTP is the HTTP client (nil: one without a global timeout; long
// polls are bounded by MaxWait). Redirects are refused unless it sets
// its own CheckRedirect.
HTTP *http.Client
// PlainHTTP (the flag --hub-plain-http) allows an http:// URL to a hub
// on a network of its own (lock up's): a service name without a dot or
// a private address only. Off, http is refused but to a loopback
// address; use https. Requests and decisions are signed either way.
PlainHTTP bool
// Now is the clock (nil: time.Now).
Now func() time.Time
}
Config configures a Client. URL, Instance, Key, HubKey and StateFile are required: there is no discovery and no unpinned hub.
type Hello ¶
type Hello struct {
Name string `json:"name"`
Version string `json:"version"`
API int `json:"api"`
}
Hello is the hub's answer to GET /v1/hello.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package hubtest is a fake lockhub for the tests of locks: it speaks the lock API v1 (signed requests, tickets, decisions with a cursor) on an httptest server, and lets a test decide tickets.
|
Package hubtest is a fake lockhub for the tests of locks: it speaks the lock API v1 (signed requests, tickets, decisions with a cursor) on an httptest server, and lets a test decide tickets. |