Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/image-builder-mk3/pkg/auth/auth.go
2500 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package auth
6
7
import (
8
"bytes"
9
"context"
10
"crypto/sha256"
11
"encoding/base64"
12
"fmt"
13
"os"
14
"regexp"
15
"strings"
16
"sync"
17
"time"
18
19
"github.com/aws/aws-sdk-go-v2/aws"
20
"github.com/aws/aws-sdk-go-v2/service/ecr"
21
"github.com/distribution/reference"
22
"github.com/docker/cli/cli/config/configfile"
23
"github.com/docker/docker/api/types/registry"
24
"github.com/opentracing/opentracing-go"
25
"golang.org/x/xerrors"
26
27
"github.com/gitpod-io/gitpod/common-go/log"
28
"github.com/gitpod-io/gitpod/common-go/tracing"
29
"github.com/gitpod-io/gitpod/common-go/watch"
30
"github.com/gitpod-io/gitpod/image-builder/api"
31
)
32
33
// RegistryAuthenticator can provide authentication for some registries
34
type RegistryAuthenticator interface {
35
// Authenticate attempts to provide authentication for Docker registry access
36
Authenticate(ctx context.Context, registry string) (auth *Authentication, err error)
37
}
38
39
// NewDockerConfigFileAuth reads a docker config file to provide authentication
40
func NewDockerConfigFileAuth(fn string) (*DockerConfigFileAuth, error) {
41
res := &DockerConfigFileAuth{}
42
err := res.loadFromFile(fn)
43
if err != nil {
44
return nil, err
45
}
46
47
err = watch.File(context.Background(), fn, func() {
48
res.loadFromFile(fn)
49
})
50
if err != nil {
51
log.WithError(err).WithField("path", fn).Error("error watching file")
52
return nil, err
53
}
54
55
return res, nil
56
}
57
58
// DockerConfigFileAuth uses a Docker config file to provide authentication
59
type DockerConfigFileAuth struct {
60
C *configfile.ConfigFile
61
62
hash string
63
mu sync.RWMutex
64
}
65
66
func (a *DockerConfigFileAuth) loadFromFile(fn string) (err error) {
67
defer func() {
68
if err != nil {
69
err = fmt.Errorf("error loading Docker config from %s: %w", fn, err)
70
log.WithError(err).WithField("path", fn).Error("failed loading from file")
71
}
72
}()
73
74
cntnt, err := os.ReadFile(fn)
75
if err != nil {
76
return err
77
}
78
hash := sha256.New()
79
_, _ = hash.Write(cntnt)
80
newHash := fmt.Sprintf("%x", hash.Sum(nil))
81
if a.hash == newHash {
82
log.Infof("nothing has changed: %s", fn)
83
return nil
84
}
85
86
log.WithField("path", fn).Info("reloading auth from Docker config")
87
88
cfg := configfile.New(fn)
89
err = cfg.LoadFromReader(bytes.NewReader(cntnt))
90
if err != nil {
91
return err
92
}
93
94
a.mu.Lock()
95
defer a.mu.Unlock()
96
a.C = cfg
97
a.hash = newHash
98
99
log.Infof("file has changed: %s", fn)
100
return nil
101
}
102
103
// Authenticate attempts to provide an encoded authentication string for Docker registry access
104
func (a *DockerConfigFileAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
105
ac, err := a.C.GetAuthConfig(registry)
106
if err != nil {
107
log.WithError(err).WithField("registry", registry).Error("failed DockerConfigFileAuth Authenticate")
108
return nil, err
109
}
110
111
return &Authentication{
112
Username: ac.Username,
113
Password: ac.Password,
114
Auth: ac.Auth,
115
Email: ac.Email,
116
ServerAddress: ac.ServerAddress,
117
IdentityToken: ac.IdentityToken,
118
RegistryToken: ac.RegistryToken,
119
}, nil
120
}
121
122
// CompositeAuth returns the first non-empty authentication of any of its consitutents
123
type CompositeAuth []RegistryAuthenticator
124
125
func (ca CompositeAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
126
for _, ath := range ca {
127
res, err := ath.Authenticate(ctx, registry)
128
if err != nil {
129
log.WithError(err).WithField("registry", registry).Errorf("failed CompositeAuth Authenticate")
130
return nil, err
131
}
132
if !res.Empty() {
133
return res, nil
134
} else {
135
log.WithField("registry", registry).Warn("response was empty for CompositeAuth authenticate")
136
}
137
}
138
return &Authentication{}, nil
139
}
140
141
func NewECRAuthenticator(ecrc *ecr.Client) *ECRAuthenticator {
142
return &ECRAuthenticator{
143
ecrc: ecrc,
144
}
145
}
146
147
type ECRAuthenticator struct {
148
ecrc *ecr.Client
149
150
ecrAuth string
151
ecrAuthLastRefreshTime time.Time
152
ecrAuthLock sync.Mutex
153
}
154
155
const (
156
// ECR tokens are valid for 12h [1], and we want to ensure we refresh at least twice a day before full expiry.
157
//
158
// [1] https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
159
ecrTokenRefreshTime = 4 * time.Hour
160
)
161
162
func (ath *ECRAuthenticator) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
163
if !isECRRegistry(registry) {
164
return nil, nil
165
}
166
167
defer func() {
168
if err != nil {
169
err = fmt.Errorf("error with ECR authenticate: %w", err)
170
log.WithError(err).WithField("registry", registry).Error("failed ECR authenticate")
171
}
172
}()
173
174
ath.ecrAuthLock.Lock()
175
defer ath.ecrAuthLock.Unlock()
176
if time.Since(ath.ecrAuthLastRefreshTime) > ecrTokenRefreshTime {
177
tknout, err := ath.ecrc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
178
if err != nil {
179
return nil, err
180
}
181
if len(tknout.AuthorizationData) == 0 {
182
err = fmt.Errorf("no ECR authorization data received")
183
return nil, err
184
}
185
186
pwd, err := base64.StdEncoding.DecodeString(aws.ToString(tknout.AuthorizationData[0].AuthorizationToken))
187
if err != nil {
188
return nil, err
189
}
190
191
ath.ecrAuth = string(pwd)
192
ath.ecrAuthLastRefreshTime = time.Now()
193
log.Info("refreshed ECR token")
194
} else {
195
log.Info("no ECR token refresh necessary")
196
}
197
198
segs := strings.Split(ath.ecrAuth, ":")
199
if len(segs) != 2 {
200
err = fmt.Errorf("cannot understand ECR token. Expected 2 segments, got %d", len(segs))
201
return nil, err
202
}
203
return &Authentication{
204
Username: segs[0],
205
Password: segs[1],
206
Auth: base64.StdEncoding.EncodeToString([]byte(ath.ecrAuth)),
207
}, nil
208
}
209
210
// Authentication represents docker usable authentication
211
type Authentication registry.AuthConfig
212
213
func (a *Authentication) Empty() bool {
214
if a == nil {
215
return true
216
}
217
if a.Auth == "" && a.Password == "" {
218
return true
219
}
220
return false
221
}
222
223
var ecrRegistryRegexp = regexp.MustCompile(`\d{12}.dkr.ecr.\w+-\w+-\w+.amazonaws.com`)
224
225
const DummyECRRegistryDomain = "000000000000.dkr.ecr.dummy-host-zone.amazonaws.com"
226
227
// isECRRegistry returns true if the registry domain is an ECR registry
228
func isECRRegistry(domain string) bool {
229
return ecrRegistryRegexp.MatchString(domain)
230
}
231
232
// AllowedAuthFor describes for which repositories authentication may be provided for
233
type AllowedAuthFor struct {
234
All bool
235
Explicit []string
236
Additional map[string]string
237
}
238
239
// AllowedAuthForAll means auth for all repositories is allowed
240
func AllowedAuthForAll() AllowedAuthFor { return AllowedAuthFor{true, nil, nil} }
241
242
// AllowedAuthForNone means auth for no repositories is allowed
243
func AllowedAuthForNone() AllowedAuthFor { return AllowedAuthFor{false, nil, nil} }
244
245
// IsAllowNone returns true if we are to allow authentication for no repos
246
func (a AllowedAuthFor) IsAllowNone() bool {
247
return !a.All && len(a.Explicit) == 0
248
}
249
250
// IsAllowAll returns true if we are to allow authentication for all repos
251
func (a AllowedAuthFor) IsAllowAll() bool {
252
return a.All
253
}
254
255
// Elevate adds a ref to the list of authenticated repositories
256
func (a AllowedAuthFor) Elevate(ref string) AllowedAuthFor {
257
pref, _ := reference.ParseNormalizedNamed(ref)
258
if pref == nil {
259
log.WithField("ref", ref).Debug("cannot elevate auth for invalid image ref")
260
return a
261
}
262
263
return AllowedAuthFor{a.All, append(a.Explicit, reference.Domain(pref)), a.Additional}
264
}
265
266
// ExplicitlyAll produces an AllowedAuthFor that allows authentication for all
267
// registries, yet carries the original Explicit list which affects GetAuthForImageBuild
268
func (a AllowedAuthFor) ExplicitlyAll() AllowedAuthFor {
269
return AllowedAuthFor{
270
All: true,
271
Explicit: a.Explicit,
272
}
273
}
274
275
// Resolver resolves an auth request determining which authentication is actually allowed
276
type Resolver struct {
277
BaseImageRepository string
278
WorkspaceImageRepository string
279
}
280
281
// ResolveRequestAuth computes the allowed authentication for a build based on its request
282
func (r Resolver) ResolveRequestAuth(ctx context.Context, auth *api.BuildRegistryAuth) (authFor AllowedAuthFor) {
283
span, _ := opentracing.StartSpanFromContext(ctx, "ResolveRequestAuth")
284
var err error
285
defer tracing.FinishSpan(span, &err)
286
287
// by default we allow nothing
288
authFor = AllowedAuthForNone()
289
if auth == nil {
290
return
291
}
292
293
switch ath := auth.Mode.(type) {
294
case *api.BuildRegistryAuth_Total:
295
if ath.Total.AllowAll {
296
authFor = AllowedAuthForAll()
297
} else {
298
authFor = AllowedAuthForNone()
299
}
300
case *api.BuildRegistryAuth_Selective:
301
var explicit []string
302
if ath.Selective.AllowBaserep {
303
ref, _ := reference.ParseNormalizedNamed(r.BaseImageRepository)
304
explicit = append(explicit, reference.Domain(ref))
305
}
306
if ath.Selective.AllowWorkspacerep {
307
ref, _ := reference.ParseNormalizedNamed(r.WorkspaceImageRepository)
308
explicit = append(explicit, reference.Domain(ref))
309
}
310
explicit = append(explicit, ath.Selective.AnyOf...)
311
authFor = AllowedAuthFor{false, explicit, nil}
312
default:
313
authFor = AllowedAuthForNone()
314
}
315
316
authFor.Additional = auth.Additional
317
318
return
319
}
320
321
// GetAuthFor computes the base64 encoded auth format for a Docker image pull/push
322
func (a AllowedAuthFor) GetAuthFor(ctx context.Context, auth RegistryAuthenticator, refstr string) (res *Authentication, err error) {
323
if auth == nil {
324
return
325
}
326
327
ref, err := reference.ParseNormalizedNamed(refstr)
328
if err != nil {
329
log.WithError(err).Errorf("failed parsing normalized name")
330
return nil, xerrors.Errorf("cannot parse image ref: %v", err)
331
}
332
reg := reference.Domain(ref)
333
334
// If we haven't found authentication using the built-in way, we'll resort to additional auth
335
// the user sent us.
336
defer func() {
337
if err != nil || !res.Empty() {
338
return
339
}
340
341
log.WithField("reg", reg).Debug("checking for additional auth")
342
res = a.additionalAuth(reg)
343
344
if res != nil {
345
log.WithField("reg", reg).Debug("found additional auth")
346
}
347
}()
348
349
var regAllowed bool
350
switch {
351
case a.IsAllowAll():
352
// free for all
353
regAllowed = true
354
case isECRRegistry(reg):
355
// We allow ECR registries by default to support private ECR registries OOTB.
356
// The AWS IAM permissions dictate what users actually have access to.
357
regAllowed = true
358
default:
359
for _, a := range a.Explicit {
360
if a == reg {
361
regAllowed = true
362
break
363
}
364
}
365
}
366
if !regAllowed {
367
log.WithField("reg", reg).WithField("ref", ref).WithField("a", a).Warn("registry not allowed - you may want to add this to the list of allowed registries in your installation config")
368
return nil, nil
369
}
370
371
return auth.Authenticate(ctx, reg)
372
}
373
374
func (a AllowedAuthFor) additionalAuth(domain string) *Authentication {
375
ath, ok := a.Additional[domain]
376
if !ok {
377
return nil
378
}
379
380
res := &Authentication{
381
Auth: ath,
382
}
383
dec, err := base64.StdEncoding.DecodeString(ath)
384
if err == nil {
385
segs := strings.Split(string(dec), ":")
386
numSegs := len(segs)
387
388
if numSegs > 1 {
389
res.Username = strings.Join(segs[:numSegs-1], ":")
390
res.Password = segs[numSegs-1]
391
}
392
} else {
393
log.Errorf("failed getting additional auth")
394
}
395
return res
396
}
397
398
// ImageBuildAuth is the format image builds needs
399
type ImageBuildAuth map[string]registry.AuthConfig
400
401
// GetImageBuildAuthFor produces authentication in the format an image builds needs
402
func (a AllowedAuthFor) GetImageBuildAuthFor(ctx context.Context, auth RegistryAuthenticator, additionalRegistries []string, blocklist []string) (res ImageBuildAuth) {
403
res = make(ImageBuildAuth)
404
for reg := range a.Additional {
405
var blocked bool
406
for _, blk := range blocklist {
407
if blk == reg {
408
blocked = true
409
break
410
}
411
}
412
if blocked {
413
continue
414
}
415
ath := a.additionalAuth(reg)
416
res[reg] = registry.AuthConfig(*ath)
417
}
418
for _, reg := range additionalRegistries {
419
ath, err := auth.Authenticate(ctx, reg)
420
if err != nil {
421
log.WithError(err).WithField("registry", reg).Warn("cannot get authentication for additional registry for image build")
422
continue
423
}
424
if ath.Empty() {
425
continue
426
}
427
res[reg] = registry.AuthConfig(*ath)
428
}
429
430
return
431
}
432
433