Skip to content

Commit 39d1148

Browse files
authored
Add Context support to auth methods (#1949)
Signed-off-by: Jon Johnson <[email protected]>
1 parent ff385a9 commit 39d1148

File tree

11 files changed

+100
-33
lines changed

11 files changed

+100
-33
lines changed

cmd/crane/cmd/auth.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ $ curl -H "$(crane auth token -H ubuntu)" https://index.docker.io/v2/library/ubu
7373
return err
7474
}
7575

76-
auth, err := o.Keychain.Resolve(repo)
76+
auth, err := authn.Resolve(cmd.Context(), o.Keychain, repo)
7777
if err != nil {
7878
return err
7979
}
@@ -152,7 +152,7 @@ func NewCmdAuthGet(options []crane.Option, argv ...string) *cobra.Command {
152152
Short: "Implements a credential helper",
153153
Example: eg,
154154
Args: cobra.MaximumNArgs(1),
155-
RunE: func(_ *cobra.Command, args []string) error {
155+
RunE: func(cmd *cobra.Command, args []string) error {
156156
registryAddr := ""
157157
if len(args) == 1 {
158158
registryAddr = args[0]
@@ -168,7 +168,7 @@ func NewCmdAuthGet(options []crane.Option, argv ...string) *cobra.Command {
168168
if err != nil {
169169
return err
170170
}
171-
authorizer, err := crane.GetOptions(options...).Keychain.Resolve(reg)
171+
authorizer, err := authn.Resolve(cmd.Context(), crane.GetOptions(options...).Keychain, reg)
172172
if err != nil {
173173
return err
174174
}
@@ -182,7 +182,7 @@ func NewCmdAuthGet(options []crane.Option, argv ...string) *cobra.Command {
182182
os.Exit(1)
183183
}
184184

185-
auth, err := authorizer.Authorization()
185+
auth, err := authn.Authorization(cmd.Context(), authorizer)
186186
if err != nil {
187187
return err
188188
}

pkg/authn/authn.go

+17
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package authn
1616

1717
import (
18+
"context"
1819
"encoding/base64"
1920
"encoding/json"
2021
"fmt"
@@ -27,6 +28,22 @@ type Authenticator interface {
2728
Authorization() (*AuthConfig, error)
2829
}
2930

31+
// ContextAuthenticator is like Authenticator, but allows for context to be passed in.
32+
type ContextAuthenticator interface {
33+
// Authorization returns the value to use in an http transport's Authorization header.
34+
AuthorizationContext(context.Context) (*AuthConfig, error)
35+
}
36+
37+
// Authorization calls AuthorizationContext with ctx if the given [Authenticator] implements [ContextAuthenticator],
38+
// otherwise it calls Resolve with the given [Resource].
39+
func Authorization(ctx context.Context, authn Authenticator) (*AuthConfig, error) {
40+
if actx, ok := authn.(ContextAuthenticator); ok {
41+
return actx.AuthorizationContext(ctx)
42+
}
43+
44+
return authn.Authorization()
45+
}
46+
3047
// AuthConfig contains authorization information for connecting to a Registry
3148
// Inlined what we use from github.com/docker/cli/cli/config/types
3249
type AuthConfig struct {

pkg/authn/keychain.go

+37-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package authn
1616

1717
import (
18+
"context"
1819
"os"
1920
"path/filepath"
2021
"sync"
@@ -45,6 +46,11 @@ type Keychain interface {
4546
Resolve(Resource) (Authenticator, error)
4647
}
4748

49+
// ContextKeychain is like Keychain, but allows for context to be passed in.
50+
type ContextKeychain interface {
51+
ResolveContext(context.Context, Resource) (Authenticator, error)
52+
}
53+
4854
// defaultKeychain implements Keychain with the semantics of the standard Docker
4955
// credential keychain.
5056
type defaultKeychain struct {
@@ -62,8 +68,23 @@ const (
6268
DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
6369
)
6470

65-
// Resolve implements Keychain.
71+
// Resolve calls ResolveContext with ctx if the given [Keychain] implements [ContextKeychain],
72+
// otherwise it calls Resolve with the given [Resource].
73+
func Resolve(ctx context.Context, keychain Keychain, target Resource) (Authenticator, error) {
74+
if rctx, ok := keychain.(ContextKeychain); ok {
75+
return rctx.ResolveContext(ctx, target)
76+
}
77+
78+
return keychain.Resolve(target)
79+
}
80+
81+
// ResolveContext implements ContextKeychain.
6682
func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
83+
return dk.ResolveContext(context.Background(), target)
84+
}
85+
86+
// Resolve implements Keychain.
87+
func (dk *defaultKeychain) ResolveContext(ctx context.Context, target Resource) (Authenticator, error) {
6788
dk.mu.Lock()
6889
defer dk.mu.Unlock()
6990

@@ -180,6 +201,10 @@ func NewKeychainFromHelper(h Helper) Keychain { return wrapper{h} }
180201
type wrapper struct{ h Helper }
181202

182203
func (w wrapper) Resolve(r Resource) (Authenticator, error) {
204+
return w.ResolveContext(context.Background(), r)
205+
}
206+
207+
func (w wrapper) ResolveContext(ctx context.Context, r Resource) (Authenticator, error) {
183208
u, p, err := w.h.Get(r.RegistryStr())
184209
if err != nil {
185210
return Anonymous, nil
@@ -206,8 +231,12 @@ type refreshingKeychain struct {
206231
}
207232

208233
func (r *refreshingKeychain) Resolve(target Resource) (Authenticator, error) {
234+
return r.ResolveContext(context.Background(), target)
235+
}
236+
237+
func (r *refreshingKeychain) ResolveContext(ctx context.Context, target Resource) (Authenticator, error) {
209238
last := time.Now()
210-
auth, err := r.keychain.Resolve(target)
239+
auth, err := Resolve(ctx, r.keychain, target)
211240
if err != nil || auth == Anonymous {
212241
return auth, err
213242
}
@@ -236,17 +265,21 @@ type refreshing struct {
236265
}
237266

238267
func (r *refreshing) Authorization() (*AuthConfig, error) {
268+
return r.AuthorizationContext(context.Background())
269+
}
270+
271+
func (r *refreshing) AuthorizationContext(ctx context.Context) (*AuthConfig, error) {
239272
r.Lock()
240273
defer r.Unlock()
241274
if r.cached == nil || r.expired() {
242275
r.last = r.now()
243-
auth, err := r.keychain.Resolve(r.target)
276+
auth, err := Resolve(ctx, r.keychain, r.target)
244277
if err != nil {
245278
return nil, err
246279
}
247280
r.cached = auth
248281
}
249-
return r.cached.Authorization()
282+
return Authorization(ctx, r.cached)
250283
}
251284

252285
func (r *refreshing) now() time.Time {

pkg/authn/multikeychain.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package authn
1616

17+
import "context"
18+
1719
type multiKeychain struct {
1820
keychains []Keychain
1921
}
@@ -28,8 +30,12 @@ func NewMultiKeychain(kcs ...Keychain) Keychain {
2830

2931
// Resolve implements Keychain.
3032
func (mk *multiKeychain) Resolve(target Resource) (Authenticator, error) {
33+
return mk.ResolveContext(context.Background(), target)
34+
}
35+
36+
func (mk *multiKeychain) ResolveContext(ctx context.Context, target Resource) (Authenticator, error) {
3137
for _, kc := range mk.keychains {
32-
auth, err := kc.Resolve(target)
38+
auth, err := Resolve(ctx, kc, target)
3339
if err != nil {
3440
return nil, err
3541
}

pkg/v1/google/auth.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,23 @@ import (
3131
const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
3232

3333
// GetGcloudCmd is exposed so we can test this.
34-
var GetGcloudCmd = func() *exec.Cmd {
34+
var GetGcloudCmd = func(ctx context.Context) *exec.Cmd {
3535
// This is odd, but basically what docker-credential-gcr does.
3636
//
3737
// config-helper is undocumented, but it's purportedly the only supported way
3838
// of accessing tokens (`gcloud auth print-access-token` is discouraged).
3939
//
4040
// --force-auth-refresh means we are getting a token that is valid for about
4141
// an hour (we reuse it until it's expired).
42-
return exec.Command("gcloud", "config", "config-helper", "--force-auth-refresh", "--format=json(credential)")
42+
return exec.CommandContext(ctx, "gcloud", "config", "config-helper", "--force-auth-refresh", "--format=json(credential)")
4343
}
4444

4545
// NewEnvAuthenticator returns an authn.Authenticator that generates access
4646
// tokens from the environment we're running in.
4747
//
4848
// See: https://godoc.org/golang.org/x/oauth2/google#FindDefaultCredentials
49-
func NewEnvAuthenticator() (authn.Authenticator, error) {
50-
ts, err := googauth.DefaultTokenSource(context.Background(), cloudPlatformScope)
49+
func NewEnvAuthenticator(ctx context.Context) (authn.Authenticator, error) {
50+
ts, err := googauth.DefaultTokenSource(ctx, cloudPlatformScope)
5151
if err != nil {
5252
return nil, err
5353
}
@@ -62,14 +62,14 @@ func NewEnvAuthenticator() (authn.Authenticator, error) {
6262

6363
// NewGcloudAuthenticator returns an oauth2.TokenSource that generates access
6464
// tokens by shelling out to the gcloud sdk.
65-
func NewGcloudAuthenticator() (authn.Authenticator, error) {
65+
func NewGcloudAuthenticator(ctx context.Context) (authn.Authenticator, error) {
6666
if _, err := exec.LookPath("gcloud"); err != nil {
6767
// gcloud is not available, fall back to anonymous
6868
logs.Warn.Println("gcloud binary not found")
6969
return authn.Anonymous, nil
7070
}
7171

72-
ts := gcloudSource{GetGcloudCmd}
72+
ts := gcloudSource{ctx, GetGcloudCmd}
7373

7474
// Attempt to fetch a token to ensure gcloud is installed and we can run it.
7575
token, err := ts.Token()
@@ -143,13 +143,15 @@ type gcloudOutput struct {
143143
}
144144

145145
type gcloudSource struct {
146+
ctx context.Context
147+
146148
// This is passed in so that we mock out gcloud and test Token.
147-
exec func() *exec.Cmd
149+
exec func(ctx context.Context) *exec.Cmd
148150
}
149151

150152
// Token implements oauath2.TokenSource.
151153
func (gs gcloudSource) Token() (*oauth2.Token, error) {
152-
cmd := gs.exec()
154+
cmd := gs.exec(gs.ctx)
153155
var out bytes.Buffer
154156
cmd.Stdout = &out
155157

pkg/v1/google/auth_test.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package google
1919

2020
import (
2121
"bytes"
22+
"context"
2223
"fmt"
2324
"os"
2425
"os/exec"
@@ -84,15 +85,16 @@ func TestMain(m *testing.M) {
8485
}
8586
}
8687

87-
func newGcloudCmdMock(env string) func() *exec.Cmd {
88-
return func() *exec.Cmd {
89-
cmd := exec.Command(os.Args[0])
88+
func newGcloudCmdMock(env string) func(context.Context) *exec.Cmd {
89+
return func(ctx context.Context) *exec.Cmd {
90+
cmd := exec.CommandContext(ctx, os.Args[0])
9091
cmd.Env = []string{fmt.Sprintf("GO_TEST_MODE=%s", env)}
9192
return cmd
9293
}
9394
}
9495

9596
func TestGcloudErrors(t *testing.T) {
97+
ctx := context.Background()
9698
cases := []struct {
9799
env string
98100

@@ -113,7 +115,7 @@ func TestGcloudErrors(t *testing.T) {
113115
t.Run(tc.env, func(t *testing.T) {
114116
GetGcloudCmd = newGcloudCmdMock(tc.env)
115117

116-
if _, err := NewGcloudAuthenticator(); err == nil {
118+
if _, err := NewGcloudAuthenticator(ctx); err == nil {
117119
t.Errorf("wanted error, got nil")
118120
} else if got := err.Error(); !strings.HasPrefix(got, tc.wantPrefix) {
119121
t.Errorf("wanted error prefix %q, got %q", tc.wantPrefix, got)
@@ -123,13 +125,14 @@ func TestGcloudErrors(t *testing.T) {
123125
}
124126

125127
func TestGcloudSuccess(t *testing.T) {
128+
ctx := context.Background()
126129
// Stupid coverage to make sure it doesn't panic.
127130
var b bytes.Buffer
128131
logs.Debug.SetOutput(&b)
129132

130133
GetGcloudCmd = newGcloudCmdMock("success")
131134

132-
auth, err := NewGcloudAuthenticator()
135+
auth, err := NewGcloudAuthenticator(ctx)
133136
if err != nil {
134137
t.Fatalf("NewGcloudAuthenticator got error %v", err)
135138
}
@@ -263,7 +266,7 @@ func TestNewEnvAuthenticatorFailure(t *testing.T) {
263266
}
264267

265268
// Expect error.
266-
_, err := NewEnvAuthenticator()
269+
_, err := NewEnvAuthenticator(context.Background())
267270
if err == nil {
268271
t.Errorf("expected err, got nil")
269272
}

pkg/v1/google/keychain.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package google
1616

1717
import (
18+
"context"
1819
"strings"
1920
"sync"
2021

@@ -52,26 +53,31 @@ type googleKeychain struct {
5253
// In general, we don't worry about that here because we expect to use the same
5354
// gcloud configuration in the scope of this one process.
5455
func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
56+
return gk.ResolveContext(context.Background(), target)
57+
}
58+
59+
// ResolveContext implements authn.ContextKeychain.
60+
func (gk *googleKeychain) ResolveContext(ctx context.Context, target authn.Resource) (authn.Authenticator, error) {
5561
// Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback.
5662
if !isGoogle(target.RegistryStr()) {
5763
return authn.Anonymous, nil
5864
}
5965

6066
gk.once.Do(func() {
61-
gk.auth = resolve()
67+
gk.auth = resolve(ctx)
6268
})
6369

6470
return gk.auth, nil
6571
}
6672

67-
func resolve() authn.Authenticator {
68-
auth, envErr := NewEnvAuthenticator()
73+
func resolve(ctx context.Context) authn.Authenticator {
74+
auth, envErr := NewEnvAuthenticator(ctx)
6975
if envErr == nil && auth != authn.Anonymous {
7076
logs.Debug.Println("google.Keychain: using Application Default Credentials")
7177
return auth
7278
}
7379

74-
auth, gErr := NewGcloudAuthenticator()
80+
auth, gErr := NewGcloudAuthenticator(ctx)
7581
if gErr == nil && auth != authn.Anonymous {
7682
logs.Debug.Println("google.Keychain: using gcloud fallback")
7783
return auth

pkg/v1/remote/fetcher.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type fetcher struct {
4747
func makeFetcher(ctx context.Context, target resource, o *options) (*fetcher, error) {
4848
auth := o.auth
4949
if o.keychain != nil {
50-
kauth, err := o.keychain.Resolve(target)
50+
kauth, err := authn.Resolve(ctx, o.keychain, target)
5151
if err != nil {
5252
return nil, err
5353
}

pkg/v1/remote/transport/basic.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ var _ http.RoundTripper = (*basicTransport)(nil)
3333
// RoundTrip implements http.RoundTripper
3434
func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
3535
if bt.auth != authn.Anonymous {
36-
auth, err := bt.auth.Authorization()
36+
auth, err := authn.Authorization(in.Context(), bt.auth)
3737
if err != nil {
3838
return nil, err
3939
}

pkg/v1/remote/transport/bearer.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func Exchange(ctx context.Context, reg name.Registry, auth authn.Authenticator,
4949
if err != nil {
5050
return nil, err
5151
}
52-
authcfg, err := auth.Authorization()
52+
authcfg, err := authn.Authorization(ctx, auth)
5353
if err != nil {
5454
return nil, err
5555
}
@@ -190,7 +190,7 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
190190
// The basic token exchange is attempted first, falling back to the oauth flow.
191191
// If the IdentityToken is set, this indicates that we should start with the oauth flow.
192192
func (bt *bearerTransport) refresh(ctx context.Context) error {
193-
auth, err := bt.basic.Authorization()
193+
auth, err := authn.Authorization(ctx, bt.basic)
194194
if err != nil {
195195
return err
196196
}
@@ -295,7 +295,7 @@ func canonicalAddress(host, scheme string) (address string) {
295295

296296
// https://docs.docker.com/registry/spec/auth/oauth/
297297
func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
298-
auth, err := bt.basic.Authorization()
298+
auth, err := authn.Authorization(ctx, bt.basic)
299299
if err != nil {
300300
return nil, err
301301
}

0 commit comments

Comments
 (0)