Skip to content

Commit d179662

Browse files
committed
Add support for anonymous registry requests
1 parent 9f5bf70 commit d179662

File tree

5 files changed

+271
-44
lines changed

5 files changed

+271
-44
lines changed

pkg/cmd/dockerregistry/dockerregistry.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"io/ioutil"
99
"net/http"
10+
"net/url"
1011
"os"
1112
"time"
1213

@@ -46,6 +47,28 @@ func Execute(configFile io.Reader) {
4647
log.Fatalf("Error parsing configuration file: %s", err)
4748
}
4849

50+
tokenPath := "/openshift/token"
51+
52+
// If needed, generate and populate the token realm URL in the config.
53+
// Must be done prior to instantiating the app, so our auth provider has the config available.
54+
_, usingOpenShiftAuth := config.Auth[server.OpenShiftAuth]
55+
_, hasTokenRealm := config.Auth[server.OpenShiftAuth][server.TokenRealmKey].(string)
56+
if usingOpenShiftAuth && !hasTokenRealm {
57+
registryHost := os.Getenv(server.DockerRegistryURLEnvVar)
58+
if len(registryHost) == 0 {
59+
log.Fatalf("%s is required", server.DockerRegistryURLEnvVar)
60+
}
61+
tokenURL := &url.URL{Scheme: "https", Host: registryHost, Path: tokenPath}
62+
if len(config.HTTP.TLS.Certificate) == 0 {
63+
tokenURL.Scheme = "http"
64+
}
65+
66+
if config.Auth[server.OpenShiftAuth] == nil {
67+
config.Auth[server.OpenShiftAuth] = configuration.Parameters{}
68+
}
69+
config.Auth[server.OpenShiftAuth][server.TokenRealmKey] = tokenURL.String()
70+
}
71+
4972
ctx := context.Background()
5073
ctx, err = configureLogging(ctx, config)
5174
if err != nil {
@@ -58,6 +81,11 @@ func Execute(configFile io.Reader) {
5881

5982
app := handlers.NewApp(ctx, config)
6083

84+
// Add a token handling endpoint
85+
if usingOpenShiftAuth {
86+
app.NewRoute().Methods("GET").PathPrefix(tokenPath).Handler(server.NewTokenHandler(ctx, server.DefaultRegistryClient))
87+
}
88+
6189
// TODO add https scheme
6290
adminRouter := app.NewRoute().PathPrefix("/admin/").Subrouter()
6391

pkg/dockerregistry/server/auth.go

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package server
22

33
import (
4-
"encoding/base64"
54
"errors"
65
"fmt"
76
"net/http"
@@ -34,6 +33,13 @@ func (d deferredErrors) Empty() bool {
3433
return len(d) == 0
3534
}
3635

36+
const (
37+
OpenShiftAuth = "openshift"
38+
39+
RealmKey = "realm"
40+
TokenRealmKey = "token-realm"
41+
)
42+
3743
// DefaultRegistryClient is exposed for testing the registry with fake client.
3844
var DefaultRegistryClient = NewRegistryClient(clientcmd.NewConfig().BindToFile())
3945

@@ -58,7 +64,7 @@ func (r *RegistryClient) SafeClientConfig() restclient.Config {
5864
}
5965

6066
func init() {
61-
registryauth.Register("openshift", registryauth.InitFunc(newAccessController))
67+
registryauth.Register(OpenShiftAuth, registryauth.InitFunc(newAccessController))
6268
}
6369

6470
type contextKey int
@@ -96,8 +102,9 @@ func DeferredErrorsFrom(ctx context.Context) (deferredErrors, bool) {
96102
}
97103

98104
type AccessController struct {
99-
realm string
100-
config restclient.Config
105+
realm string
106+
tokenRealm string
107+
config restclient.Config
101108
}
102109

103110
var _ registryauth.AccessController = &AccessController{}
@@ -109,13 +116,20 @@ type authChallenge struct {
109116

110117
var _ registryauth.Challenge = &authChallenge{}
111118

119+
type tokenAuthChallenge struct {
120+
realm string
121+
service string
122+
err error
123+
}
124+
125+
var _ registryauth.Challenge = &tokenAuthChallenge{}
126+
112127
// Errors used and exported by this package.
113128
var (
114129
// Challenging errors
115-
ErrTokenRequired = errors.New("authorization header with basic token required")
116-
ErrTokenInvalid = errors.New("failed to decode basic token")
117-
ErrOpenShiftTokenRequired = errors.New("expected bearer token as password for basic token to registry")
118-
ErrOpenShiftAccessDenied = errors.New("access denied")
130+
ErrTokenRequired = errors.New("authorization header required")
131+
ErrTokenInvalid = errors.New("failed to decode credentials")
132+
ErrOpenShiftAccessDenied = errors.New("access denied")
119133

120134
// Non-challenging errors
121135
ErrNamespaceRequired = errors.New("repository namespace required")
@@ -125,12 +139,15 @@ var (
125139

126140
func newAccessController(options map[string]interface{}) (registryauth.AccessController, error) {
127141
log.Info("Using Origin Auth handler")
128-
realm, ok := options["realm"].(string)
142+
realm, ok := options[RealmKey].(string)
129143
if !ok {
130144
// Default to openshift if not present
131145
realm = "origin"
132146
}
133-
return &AccessController{realm: realm, config: DefaultRegistryClient.SafeClientConfig()}, nil
147+
148+
tokenRealm, _ := options[TokenRealmKey].(string)
149+
150+
return &AccessController{realm: realm, tokenRealm: tokenRealm, config: DefaultRegistryClient.SafeClientConfig()}, nil
134151
}
135152

136153
// Error returns the internal error string for this authChallenge.
@@ -149,10 +166,35 @@ func (ac *authChallenge) SetHeaders(w http.ResponseWriter) {
149166
w.Header().Set("WWW-Authenticate", str)
150167
}
151168

169+
// Error returns the internal error string for this authChallenge.
170+
func (ac *tokenAuthChallenge) Error() string {
171+
return ac.err.Error()
172+
}
173+
174+
// SetHeaders sets the bearer challenge header on the response.
175+
func (ac *tokenAuthChallenge) SetHeaders(w http.ResponseWriter) {
176+
// WWW-Authenticate response challenge header.
177+
// See https://docs.docker.com/registry/spec/auth/token/#/how-to-authenticate and https://tools.ietf.org/html/rfc6750#section-3
178+
str := fmt.Sprintf("Bearer realm=%q", ac.realm)
179+
if ac.service != "" {
180+
str += fmt.Sprintf(",service=%q", ac.service)
181+
}
182+
w.Header().Set("WWW-Authenticate", str)
183+
}
184+
152185
// wrapErr wraps errors related to authorization in an authChallenge error that will present a WWW-Authenticate challenge response
153186
func (ac *AccessController) wrapErr(err error) error {
154187
switch err {
155-
case ErrTokenRequired, ErrTokenInvalid, ErrOpenShiftTokenRequired, ErrOpenShiftAccessDenied:
188+
case ErrTokenRequired:
189+
// Challenge for errors that involve missing tokens
190+
if len(ac.tokenRealm) > 0 {
191+
// Direct to token auth if we've been given a place to direct to
192+
return &tokenAuthChallenge{realm: ac.tokenRealm, err: err}
193+
} else {
194+
// Otherwise just send the basic challenge
195+
return &authChallenge{realm: ac.realm, err: err}
196+
}
197+
case ErrTokenInvalid, ErrOpenShiftAccessDenied:
156198
// Challenge for errors that involve tokens or access denied
157199
return &authChallenge{realm: ac.realm, err: err}
158200
case ErrNamespaceRequired, ErrUnsupportedAction, ErrUnsupportedResource:
@@ -175,7 +217,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
175217
return nil, ac.wrapErr(err)
176218
}
177219

178-
bearerToken, err := getToken(ctx, req)
220+
bearerToken, err := getOpenShiftAPIToken(ctx, req)
179221
if err != nil {
180222
return nil, ac.wrapErr(err)
181223
}
@@ -301,26 +343,35 @@ func getNamespaceName(resourceName string) (string, string, error) {
301343
return ns, name, nil
302344
}
303345

304-
func getToken(ctx context.Context, req *http.Request) (string, error) {
346+
func getOpenShiftAPIToken(ctx context.Context, req *http.Request) (string, error) {
347+
token := ""
348+
305349
authParts := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
306-
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
350+
if len(authParts) != 2 {
307351
return "", ErrTokenRequired
308352
}
309-
basicToken := authParts[1]
310353

311-
payload, err := base64.StdEncoding.DecodeString(basicToken)
312-
if err != nil {
313-
context.GetLogger(ctx).Errorf("Basic token decode failed: %s", err)
314-
return "", ErrTokenInvalid
315-
}
354+
switch strings.ToLower(authParts[0]) {
355+
case "bearer":
356+
// This is either a direct API token, or a token issued by our docker token handler
357+
token = authParts[1]
358+
// Recognize the token issued to anonymous users by our docker token handler
359+
if token == anonymousToken {
360+
token = ""
361+
}
362+
363+
case "basic":
364+
_, password, ok := req.BasicAuth()
365+
if !ok || len(password) == 0 {
366+
return "", ErrTokenInvalid
367+
}
368+
token = password
316369

317-
osAuthParts := strings.SplitN(string(payload), ":", 2)
318-
if len(osAuthParts) != 2 {
319-
return "", ErrOpenShiftTokenRequired
370+
default:
371+
return "", ErrTokenRequired
320372
}
321373

322-
bearerToken := osAuthParts[1]
323-
return bearerToken, nil
374+
return token, nil
324375
}
325376

326377
func verifyOpenShiftUser(ctx context.Context, client client.UsersInterface) error {

0 commit comments

Comments
 (0)