diff --git a/internal/apiapproval/approval.go b/internal/apiapproval/approval.go index 022f658..ae2cd94 100644 --- a/internal/apiapproval/approval.go +++ b/internal/apiapproval/approval.go @@ -50,6 +50,7 @@ type Broker struct { timeout time.Duration now func() time.Time nextID func() string + notify func() } type pendingRequest struct { @@ -108,6 +109,15 @@ func (b *Broker) Pending() []Request { return requests } +func (b *Broker) SetChangeNotifier(notify func()) { + if b == nil { + return + } + b.mu.Lock() + defer b.mu.Unlock() + b.notify = notify +} + func (b *Broker) Request(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (Result, error) { if b == nil { return Result{}, ErrRequestTimedOut @@ -128,12 +138,20 @@ func (b *Broker) Request(ctx context.Context, token apitokens.Token, op apitoken b.mu.Lock() b.pending[pending.request.ID] = pending + notify := b.notify b.mu.Unlock() + if notify != nil { + notify() + } defer func() { b.mu.Lock() delete(b.pending, pending.request.ID) + notify := b.notify b.mu.Unlock() + if notify != nil { + notify() + } }() timer := time.NewTimer(b.timeout) diff --git a/internal/apiapproval/approval_test.go b/internal/apiapproval/approval_test.go index 9d1230d..ab1a719 100644 --- a/internal/apiapproval/approval_test.go +++ b/internal/apiapproval/approval_test.go @@ -3,6 +3,7 @@ package apiapproval import ( "context" "errors" + "slices" "testing" "time" @@ -120,6 +121,36 @@ func TestBrokerTimesOutPendingRequests(t *testing.T) { } } +func TestBrokerNotifiesWhenPendingRequestsChange(t *testing.T) { + t.Parallel() + + broker := NewBroker(time.Minute) + changes := make(chan int, 4) + broker.SetChangeNotifier(func() { + changes <- len(broker.Pending()) + }) + + errCh := make(chan error, 1) + go func() { + _, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}) + errCh <- err + }() + + waitForPending(t, broker, 1) + if _, _, err := broker.Resolve(broker.Pending()[0].ID, OutcomeAllowOnce); err != nil { + t.Fatalf("Resolve(allow once) error = %v", err) + } + if err := <-errCh; err != nil { + t.Fatalf("Request() error = %v, want nil", err) + } + + got := []int{<-changes, <-changes} + slices.Sort(got) + if !slices.Equal(got, []int{0, 1}) { + t.Fatalf("change notifications = %v, want [0 1]", got) + } +} + func waitForPending(t *testing.T, broker *Broker, want int) { t.Helper() diff --git a/internal/appui/runtime.go b/internal/appui/runtime.go index 5aeffac..95cbd88 100644 --- a/internal/appui/runtime.go +++ b/internal/appui/runtime.go @@ -76,6 +76,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error { ui.state.AuditLog = ui.auditLog ui.grpcAddress = host.Address() ui.state.Approvals = &uiApprovalManager{server: host.Server()} + host.Server().ApprovalBroker().SetChangeNotifier(ui.invalidate) defer func() { _ = host.Stop() }() } for {