Skip to content

Commit b71ea57

Browse files
committed
fix: make cancellation interrupt auth and plugin waits
1 parent d6d6675 commit b71ea57

19 files changed

Lines changed: 230 additions & 30 deletions

internal/auth/context.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
func requestWithContext(req *http.Request, ctx context.Context) *http.Request {
9+
if req == nil || ctx == nil || req.Context() == ctx {
10+
return req
11+
}
12+
return req.WithContext(ctx)
13+
}

internal/auth/external_tool.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ func (a *ExternalTool) OnRequest(req *http.Request, params map[string]string) er
7373
}
7474

7575
func (a *ExternalTool) run(ctx context.Context, req *http.Request, params map[string]string) error {
76+
if ctx == nil {
77+
ctx = context.Background()
78+
}
7679
commandLine := params["commandline"]
7780
if commandLine == "" {
7881
return fmt.Errorf("external-tool auth: missing required param \"commandline\"")
@@ -180,7 +183,7 @@ func (a *ExternalTool) Authenticate(ctx context.Context, req *http.Request, ac A
180183
if stderr == nil {
181184
stderr = ac.Stderr
182185
}
183-
if req.Context() != nil {
186+
if ctx == nil && req.Context() != nil {
184187
ctx = req.Context()
185188
}
186189
return (&ExternalTool{Stderr: stderr, Timeout: a.Timeout}).run(ctx, req, ac.Params)

internal/auth/oauth_authcode.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/rand"
66
"crypto/sha256"
77
"encoding/base64"
8+
"errors"
89
"fmt"
910
"html"
1011
"io"
@@ -152,6 +153,7 @@ func (h *AuthorizationCode) Authenticate(ctx context.Context, req *http.Request,
152153
h2.Prompt = ac.Prompter.Prompt
153154
h2.CanPrompt = true
154155
}
156+
req = requestWithContext(req, ctx)
155157
return h2.authenticateRequest(req, authParams(ac), ac.Force)
156158
}
157159

@@ -373,6 +375,9 @@ func (h *AuthorizationCode) doBrowserFlow(ctx context.Context, params map[string
373375
case err = <-errCh:
374376
return CachedToken{}, fmt.Errorf("callback error: %w", err)
375377
case <-ctx2.Done():
378+
if errors.Is(ctx2.Err(), context.Canceled) {
379+
return CachedToken{}, ctx2.Err()
380+
}
376381
return CachedToken{}, fmt.Errorf("timed out waiting for authorization callback")
377382
}
378383
}

internal/auth/oauth_authcode_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,42 @@ func TestAuthCode_BrowserFlow_ImmediateCallbackDuringOpenBrowser(t *testing.T) {
296296
}
297297
}
298298

299+
func TestAuthCodeAuthenticateUsesExplicitContext(t *testing.T) {
300+
ctx, cancel := context.WithCancel(context.Background())
301+
cancel()
302+
h := &AuthorizationCode{
303+
HTTPClient: testHTTPClient(func(r *http.Request) (*http.Response, error) {
304+
if err := r.Context().Err(); err != nil {
305+
return nil, err
306+
}
307+
return testResponse(200, "application/json", `{"access_token":"unexpected-token","token_type":"bearer","expires_in":3600}`), nil
308+
}),
309+
OpenBrowser: func(raw string) error {
310+
callbackURL, state := mustCallbackURL(t, raw)
311+
resp, err := http.Get(fmt.Sprintf("%s/?state=%s&code=late-code", callbackURL, url.QueryEscape(state)))
312+
if err != nil {
313+
return err
314+
}
315+
defer resp.Body.Close()
316+
return nil
317+
},
318+
}
319+
320+
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
321+
err := h.Authenticate(ctx, req, AuthContext{Params: map[string]string{
322+
"client_id": "id1",
323+
"authorize_url": "https://auth.example.com/authorize",
324+
"token_url": "https://auth.example.com/token",
325+
"redirect_port": availablePort(t),
326+
}})
327+
if !errors.Is(err, context.Canceled) {
328+
t.Fatalf("expected context cancellation, got %v", err)
329+
}
330+
if got := req.Header.Get("Authorization"); got != "" {
331+
t.Fatalf("Authorization = %q, want empty", got)
332+
}
333+
}
334+
299335
func TestAuthCode_BrowserFlow_TwoStrayPreflightsDoNotDeadlock(t *testing.T) {
300336
h := &AuthorizationCode{
301337
HTTPClient: testHTTPClient(func(r *http.Request) (*http.Response, error) {

internal/auth/oauth_client_creds.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (h *ClientCredentials) Authenticate(ctx context.Context, req *http.Request,
5252
if ac.HTTPClient != nil {
5353
h2.HTTPClient = ac.HTTPClient
5454
}
55+
req = requestWithContext(req, ctx)
5556
return h2.authenticateRequest(req, authParams(ac), ac.Force)
5657
}
5758

internal/auth/oauth_device_code.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ func (h *DeviceCode) runFlow(ctx context.Context, params map[string]string, devi
221221
select {
222222
case <-pollCtx.Done():
223223
timer.Stop()
224+
if errors.Is(pollCtx.Err(), context.Canceled) {
225+
return CachedToken{}, pollCtx.Err()
226+
}
224227
return CachedToken{}, fmt.Errorf("timed out waiting for device authorization")
225228
case <-timer.C:
226229
}
@@ -303,6 +306,7 @@ func (h *DeviceCode) Authenticate(ctx context.Context, req *http.Request, ac Aut
303306
if ac.Stderr != nil {
304307
h2.Stderr = ac.Stderr
305308
}
309+
req = requestWithContext(req, ctx)
306310
return h2.authenticateRequest(req, authParams(ac), ac.Force)
307311
}
308312

internal/cli/api_auth.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (c *CLI) runAPIAuthList(cmd *cobra.Command, args []string) error {
9292
if err != nil {
9393
return err
9494
}
95-
set, hasOps := c.cachedOperationSetForAPI(apiName, apiCfg, profileName)
95+
set, hasOps := c.cachedOperationSetForAPI(requestContext(cmd), apiName, apiCfg, profileName)
9696
_, profileReady, profileErr := c.profileAuthReadiness(apiName, profileName, prof)
9797
coverage := operationAuthCoverage{}
9898
if hasOps {
@@ -204,7 +204,7 @@ func (c *CLI) runAPIAuthAdd(cmd *cobra.Command, args []string) error {
204204
if prof.Credentials[credentialID] == nil {
205205
prof.Credentials[credentialID] = &config.CredentialConfig{}
206206
}
207-
defaultNeeds := c.cachedCredentialDefaultNeeds(apiName, apiCfg, profileName, credentialID)
207+
defaultNeeds := c.cachedCredentialDefaultNeeds(requestContext(cmd), apiName, apiCfg, profileName, credentialID)
208208
if prof.Credentials[credentialID].Auth == nil {
209209
if authCfg, ok, err := c.cachedAuthConfigForCredential(apiName, apiCfg, credentialID); err != nil {
210210
return err
@@ -298,7 +298,7 @@ func (c *CLI) runAPIAuthInspect(cmd *cobra.Command, args []string) error {
298298
}
299299

300300
func (c *CLI) runAPIAuthInspectOperation(cmd *cobra.Command, apiName, profileName string, apiCfg *config.APIConfig, prof *config.ProfileConfig, operationName, rawHeader string, redact bool) error {
301-
op, ok, err := c.cachedOperationForAPI(apiName, apiCfg, profileName, operationName)
301+
op, ok, err := c.cachedOperationForAPI(requestContext(cmd), apiName, apiCfg, profileName, operationName)
302302
if err != nil {
303303
return err
304304
}
@@ -471,7 +471,7 @@ func (c *CLI) operationAuthInspectionRequest(cmd *cobra.Command, apiName, profil
471471
if err != nil {
472472
return nil, err
473473
}
474-
req, _ := http.NewRequest("GET", "http://example.com", nil)
474+
req, _ := http.NewRequestWithContext(requestContext(cmd), "GET", "http://example.com", nil)
475475
for _, item := range selected {
476476
step, err := c.operationAuthStep(apiName, profileName, item, authOpts)
477477
if err != nil {
@@ -510,7 +510,7 @@ func (c *CLI) authInspectionRequest(cmd *cobra.Command, apiName, profileName str
510510
if err != nil {
511511
return nil, err
512512
}
513-
req, _ := http.NewRequest("GET", "http://example.com", nil)
513+
req, _ := http.NewRequestWithContext(requestContext(cmd), "GET", "http://example.com", nil)
514514
if err := handler.Authenticate(requestContext(cmd), req, c.authContext(requestContext(cmd), apiName, profileName, params, resolved.CacheKey, false)); err != nil {
515515
return nil, fmt.Errorf("building auth inspection: %w", err)
516516
}
@@ -636,11 +636,11 @@ func (c *CLI) apiProfileForAuth(apiName, profileName string, create bool) (*conf
636636
return apiCfg, apiCfg.Profiles[profileName], nil
637637
}
638638

639-
func (c *CLI) cachedOperationSetForAPI(apiName string, apiCfg *config.APIConfig, profileName string) (spec.OperationSet, bool) {
639+
func (c *CLI) cachedOperationSetForAPI(ctx context.Context, apiName string, apiCfg *config.APIConfig, profileName string) (spec.OperationSet, bool) {
640640
if set, _, ok := c.cachedOperationSetStatusForAPI(apiName, apiCfg, profileName); ok {
641641
return set, true
642642
}
643-
set, ok, _ := c.operationSetForAPI(context.Background(), apiName, apiCfg, profileName, false)
643+
set, ok, _ := c.operationSetForAPI(ctx, apiName, apiCfg, profileName, false)
644644
return set, ok
645645
}
646646

@@ -708,8 +708,8 @@ func (c *CLI) operationSetForAPI(ctx context.Context, apiName string, apiCfg *co
708708
return set, true, nil
709709
}
710710

711-
func (c *CLI) cachedOperationForAPI(apiName string, apiCfg *config.APIConfig, profileName, value string) (spec.Operation, bool, error) {
712-
set, ok := c.cachedOperationSetForAPI(apiName, apiCfg, profileName)
711+
func (c *CLI) cachedOperationForAPI(ctx context.Context, apiName string, apiCfg *config.APIConfig, profileName, value string) (spec.Operation, bool, error) {
712+
set, ok := c.cachedOperationSetForAPI(ctx, apiName, apiCfg, profileName)
713713
if !ok {
714714
return spec.Operation{}, false, nil
715715
}
@@ -858,8 +858,8 @@ func authRequirementKindSupported(kind string) bool {
858858
}
859859
}
860860

861-
func (c *CLI) cachedCredentialDefaultNeeds(apiName string, apiCfg *config.APIConfig, profileName, credentialID string) []string {
862-
set, ok := c.cachedOperationSetForAPI(apiName, apiCfg, profileName)
861+
func (c *CLI) cachedCredentialDefaultNeeds(ctx context.Context, apiName string, apiCfg *config.APIConfig, profileName, credentialID string) []string {
862+
set, ok := c.cachedOperationSetForAPI(ctx, apiName, apiCfg, profileName)
863863
if !ok {
864864
return nil
865865
}

internal/cli/auth.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"crypto/sha256"
77
"encoding/hex"
8+
"errors"
89
"fmt"
910
"log"
1011
"net/http"
@@ -312,8 +313,11 @@ func (c *CLI) runSecretCommand(commandLine string) (string, error) {
312313
var stderr bytes.Buffer
313314
cmd.Stderr = &limitedWriter{w: &stderr, limit: 4096}
314315
out, err := cmd.Output()
315-
if ctx.Err() != nil {
316-
return "", fmt.Errorf("secret command timed out or was canceled")
316+
if ctxErr := ctx.Err(); ctxErr != nil {
317+
if errors.Is(ctxErr, context.Canceled) {
318+
return "", ctxErr
319+
}
320+
return "", fmt.Errorf("secret command timed out: %w", ctxErr)
317321
}
318322
if err != nil {
319323
excerpt := strings.TrimSpace(stderr.String())

internal/cli/auth_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func TestCachedOperationSetForAPIUsesEffectiveProfileBase(t *testing.T) {
6767
t.Fatalf("Discover: %v", err)
6868
}
6969

70-
got, ok := c.cachedOperationSetForAPI("svc", apiCfg, "default")
70+
got, ok := c.cachedOperationSetForAPI(context.Background(), "svc", apiCfg, "default")
7171
if !ok {
7272
t.Fatal("expected cached profile operation set")
7373
}

internal/cli/cli.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"sort"
1616
"strings"
17+
"sync"
1718
"syscall"
1819
"time"
1920

@@ -830,17 +831,24 @@ func signalAwareContext() (context.Context, context.CancelFunc) {
830831
ctx, cancelCause := context.WithCancelCause(context.Background())
831832
sigCh := make(chan os.Signal, 1)
832833
done := make(chan struct{})
834+
var stopOnce sync.Once
835+
stopSignals := func() {
836+
stopOnce.Do(func() {
837+
signal.Stop(sigCh)
838+
close(done)
839+
})
840+
}
833841
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
834842
go func() {
835843
select {
836844
case sig := <-sigCh:
837845
cancelCause(signalCancelError{signal: sig})
846+
stopSignals()
838847
case <-done:
839848
}
840849
}()
841850
cancel := func() {
842-
signal.Stop(sigCh)
843-
close(done)
851+
stopSignals()
844852
cancelCause(nil)
845853
}
846854
return ctx, cancel

0 commit comments

Comments
 (0)