@@ -11,9 +11,13 @@ import (
11
11
"encoding/base64"
12
12
"fmt"
13
13
"os"
14
+ "regexp"
14
15
"strings"
15
16
"sync"
17
+ "time"
16
18
19
+ "github.com/aws/aws-sdk-go-v2/aws"
20
+ "github.com/aws/aws-sdk-go-v2/service/ecr"
17
21
"github.com/docker/cli/cli/config/configfile"
18
22
"github.com/docker/distribution/reference"
19
23
"github.com/docker/docker/api/types"
@@ -27,7 +31,7 @@ import (
27
31
// RegistryAuthenticator can provide authentication for some registries
28
32
type RegistryAuthenticator interface {
29
33
// Authenticate attempts to provide authentication for Docker registry access
30
- Authenticate (registry string ) (auth * Authentication , err error )
34
+ Authenticate (ctx context. Context , registry string ) (auth * Authentication , err error )
31
35
}
32
36
33
37
// NewDockerConfigFileAuth reads a docker config file to provide authentication
@@ -91,7 +95,7 @@ func (a *DockerConfigFileAuth) loadFromFile(fn string) (err error) {
91
95
}
92
96
93
97
// Authenticate attempts to provide an encoded authentication string for Docker registry access
94
- func (a * DockerConfigFileAuth ) Authenticate (registry string ) (auth * Authentication , err error ) {
98
+ func (a * DockerConfigFileAuth ) Authenticate (ctx context. Context , registry string ) (auth * Authentication , err error ) {
95
99
ac , err := a .C .GetAuthConfig (registry )
96
100
if err != nil {
97
101
return nil , err
@@ -108,9 +112,100 @@ func (a *DockerConfigFileAuth) Authenticate(registry string) (auth *Authenticati
108
112
}, nil
109
113
}
110
114
115
+ // CompositeAuth returns the first non-empty authentication of any of its consitutents
116
+ type CompositeAuth []RegistryAuthenticator
117
+
118
+ func (ca CompositeAuth ) Authenticate (ctx context.Context , registry string ) (auth * Authentication , err error ) {
119
+ for _ , ath := range ca {
120
+ res , err := ath .Authenticate (ctx , registry )
121
+ if err != nil {
122
+ return nil , err
123
+ }
124
+ if ! res .Empty () {
125
+ return res , nil
126
+ }
127
+ }
128
+ return & Authentication {}, nil
129
+ }
130
+
131
+ func NewECRAuthenticator (ecrc * ecr.Client ) * ECRAuthenticator {
132
+ return & ECRAuthenticator {
133
+ ecrc : ecrc ,
134
+ }
135
+ }
136
+
137
+ type ECRAuthenticator struct {
138
+ ecrc * ecr.Client
139
+
140
+ ecrAuth string
141
+ ecrAuthLastRefreshTime time.Time
142
+ ecrAuthLock sync.Mutex
143
+ }
144
+
145
+ const (
146
+ // ECR tokens are valid for 12h [1], and we want to ensure we refresh at least twice a day before full expiry.
147
+ //
148
+ // [1] https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
149
+ ecrTokenRefreshTime = 4 * time .Hour
150
+ )
151
+
152
+ func (ath * ECRAuthenticator ) Authenticate (ctx context.Context , registry string ) (auth * Authentication , err error ) {
153
+ if ! isECRRegistry (registry ) {
154
+ return nil , nil
155
+ }
156
+
157
+ ath .ecrAuthLock .Lock ()
158
+ defer ath .ecrAuthLock .Unlock ()
159
+ if time .Since (ath .ecrAuthLastRefreshTime ) > ecrTokenRefreshTime {
160
+ tknout , err := ath .ecrc .GetAuthorizationToken (ctx , & ecr.GetAuthorizationTokenInput {})
161
+ if err != nil {
162
+ return nil , err
163
+ }
164
+ if len (tknout .AuthorizationData ) == 0 {
165
+ return nil , fmt .Errorf ("no ECR authorization data received" )
166
+ }
167
+
168
+ pwd , err := base64 .StdEncoding .DecodeString (aws .ToString (tknout .AuthorizationData [0 ].AuthorizationToken ))
169
+ if err != nil {
170
+ return nil , err
171
+ }
172
+
173
+ ath .ecrAuth = string (pwd )
174
+ ath .ecrAuthLastRefreshTime = time .Now ()
175
+ log .Debug ("refreshed ECR token" )
176
+ }
177
+
178
+ segs := strings .Split (ath .ecrAuth , ":" )
179
+ if len (segs ) != 2 {
180
+ return nil , fmt .Errorf ("cannot understand ECR token. Expected 2 segments, got %d" , len (segs ))
181
+ }
182
+ return & Authentication {
183
+ Username : segs [0 ],
184
+ Password : segs [1 ],
185
+ Auth : base64 .StdEncoding .EncodeToString ([]byte (ath .ecrAuth )),
186
+ }, nil
187
+ }
188
+
111
189
// Authentication represents docker usable authentication
112
190
type Authentication types.AuthConfig
113
191
192
+ func (a * Authentication ) Empty () bool {
193
+ if a == nil {
194
+ return true
195
+ }
196
+ if a .Auth == "" && a .Password == "" {
197
+ return true
198
+ }
199
+ return false
200
+ }
201
+
202
+ var ecrRegistryRegexp = regexp .MustCompile (`\d{12}.dkr.ecr.\w+-\w+-\w+.amazonaws.com` )
203
+
204
+ // isECRRegistry returns true if the registry domain is an ECR registry
205
+ func isECRRegistry (domain string ) bool {
206
+ return ecrRegistryRegexp .MatchString (domain )
207
+ }
208
+
114
209
// AllowedAuthFor describes for which repositories authentication may be provided for
115
210
type AllowedAuthFor struct {
116
211
All bool
@@ -197,7 +292,7 @@ func (r Resolver) ResolveRequestAuth(auth *api.BuildRegistryAuth) (authFor Allow
197
292
}
198
293
199
294
// GetAuthFor computes the base64 encoded auth format for a Docker image pull/push
200
- func (a AllowedAuthFor ) GetAuthFor (auth RegistryAuthenticator , refstr string ) (res * Authentication , err error ) {
295
+ func (a AllowedAuthFor ) GetAuthFor (ctx context. Context , auth RegistryAuthenticator , refstr string ) (res * Authentication , err error ) {
201
296
if auth == nil {
202
297
return
203
298
}
@@ -211,20 +306,28 @@ func (a AllowedAuthFor) GetAuthFor(auth RegistryAuthenticator, refstr string) (r
211
306
// If we haven't found authentication using the built-in way, we'll resort to additional auth
212
307
// the user sent us.
213
308
defer func () {
214
- if err == nil && res == nil {
215
- res = a .additionalAuth (reg )
309
+ if err != nil || ! res .Empty () {
310
+ return
311
+ }
216
312
217
- if res != nil {
218
- log .WithField ("reg" , reg ).Debug ("found additional auth" )
219
- }
313
+ log .WithField ("reg" , reg ).Debug ("checking for additional auth" )
314
+ res = a .additionalAuth (reg )
315
+
316
+ if res != nil {
317
+ log .WithField ("reg" , reg ).Debug ("found additional auth" )
220
318
}
221
319
}()
222
320
223
321
var regAllowed bool
224
- if a .IsAllowAll () {
322
+ switch {
323
+ case a .IsAllowAll ():
225
324
// free for all
226
325
regAllowed = true
227
- } else {
326
+ case isECRRegistry (reg ):
327
+ // We allow ECR registries by default to support private ECR registries OOTB.
328
+ // The AWS IAM permissions dictate what users actually have access to.
329
+ regAllowed = true
330
+ default :
228
331
for _ , a := range a .Explicit {
229
332
if a == reg {
230
333
regAllowed = true
@@ -237,7 +340,7 @@ func (a AllowedAuthFor) GetAuthFor(auth RegistryAuthenticator, refstr string) (r
237
340
return nil , nil
238
341
}
239
342
240
- return auth .Authenticate (reg )
343
+ return auth .Authenticate (ctx , reg )
241
344
}
242
345
243
346
func (a AllowedAuthFor ) additionalAuth (domain string ) * Authentication {
0 commit comments