summaryrefslogtreecommitdiffstats
path: root/pkg/v1/google/auth.go
blob: 11ae3979695d5a88a8b6a67220c888c902f5472a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"time"

	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/logs"
	"golang.org/x/oauth2"
	googauth "golang.org/x/oauth2/google"
)

const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"

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

// NewEnvAuthenticator returns an authn.Authenticator that generates access
// tokens from the environment we're running in.
//
// See: https://godoc.org/golang.org/x/oauth2/google#FindDefaultCredentials
func NewEnvAuthenticator() (authn.Authenticator, error) {
	ts, err := googauth.DefaultTokenSource(context.Background(), cloudPlatformScope)
	if err != nil {
		return nil, err
	}

	token, err := ts.Token()
	if err != nil {
		return nil, err
	}

	return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil
}

// NewGcloudAuthenticator returns an oauth2.TokenSource that generates access
// tokens by shelling out to the gcloud sdk.
func NewGcloudAuthenticator() (authn.Authenticator, error) {
	if _, err := exec.LookPath("gcloud"); err != nil {
		// gcloud is not available, fall back to anonymous
		logs.Warn.Println("gcloud binary not found")
		return authn.Anonymous, nil
	}

	ts := gcloudSource{GetGcloudCmd}

	// Attempt to fetch a token to ensure gcloud is installed and we can run it.
	token, err := ts.Token()
	if err != nil {
		return nil, err
	}

	return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil
}

// NewJSONKeyAuthenticator returns a Basic authenticator which uses Service Account
// as a way of authenticating with Google Container Registry.
// More information: https://cloud.google.com/container-registry/docs/advanced-authentication#json_key_file
func NewJSONKeyAuthenticator(serviceAccountJSON string) authn.Authenticator {
	return &authn.Basic{
		Username: "_json_key",
		Password: serviceAccountJSON,
	}
}

// NewTokenAuthenticator returns an oauth2.TokenSource that generates access
// tokens by using the Google SDK to produce JWT tokens from a Service Account.
// More information: https://godoc.org/golang.org/x/oauth2/google#JWTAccessTokenSourceFromJSON
func NewTokenAuthenticator(serviceAccountJSON string, scope string) (authn.Authenticator, error) {
	ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), scope)
	if err != nil {
		return nil, err
	}

	return &tokenSourceAuth{oauth2.ReuseTokenSource(nil, ts)}, nil
}

// NewTokenSourceAuthenticator converts an oauth2.TokenSource into an authn.Authenticator.
func NewTokenSourceAuthenticator(ts oauth2.TokenSource) authn.Authenticator {
	return &tokenSourceAuth{ts}
}

// tokenSourceAuth turns an oauth2.TokenSource into an authn.Authenticator.
type tokenSourceAuth struct {
	oauth2.TokenSource
}

// Authorization implements authn.Authenticator.
func (tsa *tokenSourceAuth) Authorization() (*authn.AuthConfig, error) {
	token, err := tsa.Token()
	if err != nil {
		return nil, err
	}

	return &authn.AuthConfig{
		Username: "_token",
		Password: token.AccessToken,
	}, nil
}

// gcloudOutput represents the output of the gcloud command we invoke.
//
// `gcloud config config-helper --format=json(credential)` looks something like:
//
//	{
//	  "credential": {
//	    "access_token": "supersecretaccesstoken",
//	    "token_expiry": "2018-12-02T04:08:13Z"
//	  }
//	}
type gcloudOutput struct {
	Credential struct {
		AccessToken string `json:"access_token"`
		TokenExpiry string `json:"token_expiry"`
	} `json:"credential"`
}

type gcloudSource struct {
	// This is passed in so that we mock out gcloud and test Token.
	exec func() *exec.Cmd
}

// Token implements oauath2.TokenSource.
func (gs gcloudSource) Token() (*oauth2.Token, error) {
	cmd := gs.exec()
	var out bytes.Buffer
	cmd.Stdout = &out

	// Don't attempt to interpret stderr, just pass it through.
	cmd.Stderr = logs.Warn.Writer()

	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("error executing `gcloud config config-helper`: %w", err)
	}

	creds := gcloudOutput{}
	if err := json.Unmarshal(out.Bytes(), &creds); err != nil {
		return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %w", err)
	}

	expiry, err := time.Parse(time.RFC3339, creds.Credential.TokenExpiry)
	if err != nil {
		return nil, fmt.Errorf("failed to parse gcloud token expiry: %w", err)
	}

	token := oauth2.Token{
		AccessToken: creds.Credential.AccessToken,
		Expiry:      expiry,
	}

	return &token, nil
}