123 lines
2.6 KiB
Go
123 lines
2.6 KiB
Go
// Package bypass handles bypass request submission and resolution for toolsrvclient.
|
|
// Bypass requests are submitted by sandboxed execution, exposed via 9P for
|
|
// external approval (olliesrv), and resolved when the approver responds.
|
|
package bypass
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// Request represents a bypass request.
|
|
type Request struct {
|
|
ID string `json:"id"`
|
|
Cmd string `json:"cmd"`
|
|
Cwd string `json:"cwd"`
|
|
Env map[string]string `json:"env,omitempty"`
|
|
|
|
// Resolution state (not serialized)
|
|
done chan struct{}
|
|
approved bool
|
|
err error
|
|
cancelled bool
|
|
}
|
|
|
|
var (
|
|
pending = make(chan *Request, 16)
|
|
mu sync.Mutex
|
|
requests = make(map[string]*Request)
|
|
)
|
|
|
|
// Submit submits a bypass request and blocks until resolved or context cancelled.
|
|
// Returns (approved, error). If denied, approved is false and error is nil.
|
|
// If context is cancelled, returns (false, ctx.Err()).
|
|
func markCancelled(req *Request) {
|
|
mu.Lock()
|
|
if current, ok := requests[req.ID]; ok && current == req {
|
|
delete(requests, req.ID)
|
|
req.cancelled = true
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
func Submit(ctx context.Context, cmd, cwd string, env map[string]string) (bool, error) {
|
|
req := &Request{
|
|
ID: nextID(),
|
|
Cmd: cmd,
|
|
Cwd: cwd,
|
|
Env: env,
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
mu.Lock()
|
|
requests[req.ID] = req
|
|
mu.Unlock()
|
|
|
|
select {
|
|
case pending <- req:
|
|
case <-ctx.Done():
|
|
markCancelled(req)
|
|
return false, ctx.Err()
|
|
}
|
|
|
|
select {
|
|
case <-req.done:
|
|
case <-ctx.Done():
|
|
markCancelled(req)
|
|
return false, ctx.Err()
|
|
}
|
|
|
|
mu.Lock()
|
|
delete(requests, req.ID)
|
|
mu.Unlock()
|
|
|
|
return req.approved, req.err
|
|
}
|
|
|
|
// NextPending blocks until a request is available and returns it.
|
|
// Used by the 9P pending file to expose requests to the approver.
|
|
// Returns nil if context is cancelled before a request arrives.
|
|
func NextPending(ctx context.Context) *Request {
|
|
for {
|
|
select {
|
|
case req := <-pending:
|
|
mu.Lock()
|
|
_, active := requests[req.ID]
|
|
mu.Unlock()
|
|
if active {
|
|
return req
|
|
}
|
|
case <-ctx.Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve completes a pending request with the given decision.
|
|
// Returns false if the request ID is not found (already resolved or invalid).
|
|
func Resolve(id string, approved bool, errMsg string) bool {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
req, ok := requests[id]
|
|
if !ok {
|
|
return false
|
|
}
|
|
delete(requests, id)
|
|
req.approved = approved
|
|
if errMsg != "" {
|
|
req.err = errors.New(errMsg)
|
|
}
|
|
close(req.done)
|
|
return true
|
|
}
|
|
|
|
func nextID() string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|