go-crypt/trust/approval.go
2026-02-20 04:02:47 +00:00

174 lines
4.4 KiB
Go

package trust
import (
"fmt"
"sync"
"time"
)
// ApprovalStatus represents the state of an approval request.
type ApprovalStatus int
const (
// ApprovalPending means the request is awaiting review.
ApprovalPending ApprovalStatus = iota
// ApprovalApproved means the request was approved.
ApprovalApproved
// ApprovalDenied means the request was denied.
ApprovalDenied
)
// String returns the human-readable name of the approval status.
func (s ApprovalStatus) String() string {
switch s {
case ApprovalPending:
return "pending"
case ApprovalApproved:
return "approved"
case ApprovalDenied:
return "denied"
default:
return fmt.Sprintf("unknown(%d)", int(s))
}
}
// ApprovalRequest represents a queued capability approval request.
type ApprovalRequest struct {
// ID is the unique identifier for this request.
ID string
// Agent is the name of the requesting agent.
Agent string
// Cap is the capability being requested.
Cap Capability
// Repo is the optional repo context for repo-scoped capabilities.
Repo string
// Status is the current approval status.
Status ApprovalStatus
// Reason is a human-readable explanation from the reviewer.
Reason string
// RequestedAt is when the request was created.
RequestedAt time.Time
// ReviewedAt is when the request was reviewed (zero if pending).
ReviewedAt time.Time
// ReviewedBy is the name of the admin who reviewed the request.
ReviewedBy string
}
// ApprovalQueue manages pending approval requests for NeedsApproval decisions.
type ApprovalQueue struct {
mu sync.RWMutex
requests map[string]*ApprovalRequest
nextID int
}
// NewApprovalQueue creates an empty approval queue.
func NewApprovalQueue() *ApprovalQueue {
return &ApprovalQueue{
requests: make(map[string]*ApprovalRequest),
}
}
// Submit creates a new approval request and returns its ID.
// Returns an error if the agent name or capability is empty.
func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) {
if agent == "" {
return "", fmt.Errorf("trust.ApprovalQueue.Submit: agent name is required")
}
if cap == "" {
return "", fmt.Errorf("trust.ApprovalQueue.Submit: capability is required")
}
q.mu.Lock()
defer q.mu.Unlock()
q.nextID++
id := fmt.Sprintf("approval-%d", q.nextID)
q.requests[id] = &ApprovalRequest{
ID: id,
Agent: agent,
Cap: cap,
Repo: repo,
Status: ApprovalPending,
RequestedAt: time.Now(),
}
return id, nil
}
// Approve marks a pending request as approved. Returns an error if the
// request is not found or is not in pending status.
func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error {
q.mu.Lock()
defer q.mu.Unlock()
req, ok := q.requests[id]
if !ok {
return fmt.Errorf("trust.ApprovalQueue.Approve: request %q not found", id)
}
if req.Status != ApprovalPending {
return fmt.Errorf("trust.ApprovalQueue.Approve: request %q is already %s", id, req.Status)
}
req.Status = ApprovalApproved
req.ReviewedBy = reviewedBy
req.Reason = reason
req.ReviewedAt = time.Now()
return nil
}
// Deny marks a pending request as denied. Returns an error if the
// request is not found or is not in pending status.
func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error {
q.mu.Lock()
defer q.mu.Unlock()
req, ok := q.requests[id]
if !ok {
return fmt.Errorf("trust.ApprovalQueue.Deny: request %q not found", id)
}
if req.Status != ApprovalPending {
return fmt.Errorf("trust.ApprovalQueue.Deny: request %q is already %s", id, req.Status)
}
req.Status = ApprovalDenied
req.ReviewedBy = reviewedBy
req.Reason = reason
req.ReviewedAt = time.Now()
return nil
}
// Get returns the approval request with the given ID, or nil if not found.
func (q *ApprovalQueue) Get(id string) *ApprovalRequest {
q.mu.RLock()
defer q.mu.RUnlock()
req, ok := q.requests[id]
if !ok {
return nil
}
// Return a copy to prevent mutation.
copy := *req
return &copy
}
// Pending returns all requests with ApprovalPending status.
func (q *ApprovalQueue) Pending() []ApprovalRequest {
q.mu.RLock()
defer q.mu.RUnlock()
var out []ApprovalRequest
for _, req := range q.requests {
if req.Status == ApprovalPending {
out = append(out, *req)
}
}
return out
}
// Len returns the total number of requests in the queue.
func (q *ApprovalQueue) Len() int {
q.mu.RLock()
defer q.mu.RUnlock()
return len(q.requests)
}