Skip to content

Commit

Permalink
Add support for anonymous registry requests
Browse files Browse the repository at this point in the history
  • Loading branch information
liggitt committed Jul 19, 2016
1 parent 9f5bf70 commit d179662
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 44 deletions.
28 changes: 28 additions & 0 deletions pkg/cmd/dockerregistry/dockerregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"time"

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

tokenPath := "/openshift/token"

// If needed, generate and populate the token realm URL in the config.
// Must be done prior to instantiating the app, so our auth provider has the config available.
_, usingOpenShiftAuth := config.Auth[server.OpenShiftAuth]
_, hasTokenRealm := config.Auth[server.OpenShiftAuth][server.TokenRealmKey].(string)
if usingOpenShiftAuth && !hasTokenRealm {
registryHost := os.Getenv(server.DockerRegistryURLEnvVar)
if len(registryHost) == 0 {
log.Fatalf("%s is required", server.DockerRegistryURLEnvVar)
}
tokenURL := &url.URL{Scheme: "https", Host: registryHost, Path: tokenPath}
if len(config.HTTP.TLS.Certificate) == 0 {
tokenURL.Scheme = "http"
}

if config.Auth[server.OpenShiftAuth] == nil {
config.Auth[server.OpenShiftAuth] = configuration.Parameters{}
}
config.Auth[server.OpenShiftAuth][server.TokenRealmKey] = tokenURL.String()
}

ctx := context.Background()
ctx, err = configureLogging(ctx, config)
if err != nil {
Expand All @@ -58,6 +81,11 @@ func Execute(configFile io.Reader) {

app := handlers.NewApp(ctx, config)

// Add a token handling endpoint
if usingOpenShiftAuth {
app.NewRoute().Methods("GET").PathPrefix(tokenPath).Handler(server.NewTokenHandler(ctx, server.DefaultRegistryClient))
}

// TODO add https scheme
adminRouter := app.NewRoute().PathPrefix("/admin/").Subrouter()

Expand Down
101 changes: 76 additions & 25 deletions pkg/dockerregistry/server/auth.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package server

import (
"encoding/base64"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -34,6 +33,13 @@ func (d deferredErrors) Empty() bool {
return len(d) == 0
}

const (
OpenShiftAuth = "openshift"

RealmKey = "realm"
TokenRealmKey = "token-realm"
)

// DefaultRegistryClient is exposed for testing the registry with fake client.
var DefaultRegistryClient = NewRegistryClient(clientcmd.NewConfig().BindToFile())

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

func init() {
registryauth.Register("openshift", registryauth.InitFunc(newAccessController))
registryauth.Register(OpenShiftAuth, registryauth.InitFunc(newAccessController))
}

type contextKey int
Expand Down Expand Up @@ -96,8 +102,9 @@ func DeferredErrorsFrom(ctx context.Context) (deferredErrors, bool) {
}

type AccessController struct {
realm string
config restclient.Config
realm string
tokenRealm string
config restclient.Config
}

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

var _ registryauth.Challenge = &authChallenge{}

type tokenAuthChallenge struct {
realm string
service string
err error
}

var _ registryauth.Challenge = &tokenAuthChallenge{}

// Errors used and exported by this package.
var (
// Challenging errors
ErrTokenRequired = errors.New("authorization header with basic token required")
ErrTokenInvalid = errors.New("failed to decode basic token")
ErrOpenShiftTokenRequired = errors.New("expected bearer token as password for basic token to registry")
ErrOpenShiftAccessDenied = errors.New("access denied")
ErrTokenRequired = errors.New("authorization header required")
ErrTokenInvalid = errors.New("failed to decode credentials")
ErrOpenShiftAccessDenied = errors.New("access denied")

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

func newAccessController(options map[string]interface{}) (registryauth.AccessController, error) {
log.Info("Using Origin Auth handler")
realm, ok := options["realm"].(string)
realm, ok := options[RealmKey].(string)
if !ok {
// Default to openshift if not present
realm = "origin"
}
return &AccessController{realm: realm, config: DefaultRegistryClient.SafeClientConfig()}, nil

tokenRealm, _ := options[TokenRealmKey].(string)

return &AccessController{realm: realm, tokenRealm: tokenRealm, config: DefaultRegistryClient.SafeClientConfig()}, nil
}

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

// Error returns the internal error string for this authChallenge.
func (ac *tokenAuthChallenge) Error() string {
return ac.err.Error()
}

// SetHeaders sets the bearer challenge header on the response.
func (ac *tokenAuthChallenge) SetHeaders(w http.ResponseWriter) {
// WWW-Authenticate response challenge header.
// See https://docs.docker.com/registry/spec/auth/token/#/how-to-authenticate and https://tools.ietf.org/html/rfc6750#section-3
str := fmt.Sprintf("Bearer realm=%q", ac.realm)
if ac.service != "" {
str += fmt.Sprintf(",service=%q", ac.service)
}
w.Header().Set("WWW-Authenticate", str)
}

// wrapErr wraps errors related to authorization in an authChallenge error that will present a WWW-Authenticate challenge response
func (ac *AccessController) wrapErr(err error) error {
switch err {
case ErrTokenRequired, ErrTokenInvalid, ErrOpenShiftTokenRequired, ErrOpenShiftAccessDenied:
case ErrTokenRequired:
// Challenge for errors that involve missing tokens
if len(ac.tokenRealm) > 0 {
// Direct to token auth if we've been given a place to direct to
return &tokenAuthChallenge{realm: ac.tokenRealm, err: err}
} else {
// Otherwise just send the basic challenge
return &authChallenge{realm: ac.realm, err: err}
}
case ErrTokenInvalid, ErrOpenShiftAccessDenied:
// Challenge for errors that involve tokens or access denied
return &authChallenge{realm: ac.realm, err: err}
case ErrNamespaceRequired, ErrUnsupportedAction, ErrUnsupportedResource:
Expand All @@ -175,7 +217,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
return nil, ac.wrapErr(err)
}

bearerToken, err := getToken(ctx, req)
bearerToken, err := getOpenShiftAPIToken(ctx, req)
if err != nil {
return nil, ac.wrapErr(err)
}
Expand Down Expand Up @@ -301,26 +343,35 @@ func getNamespaceName(resourceName string) (string, string, error) {
return ns, name, nil
}

func getToken(ctx context.Context, req *http.Request) (string, error) {
func getOpenShiftAPIToken(ctx context.Context, req *http.Request) (string, error) {
token := ""

authParts := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
if len(authParts) != 2 {
return "", ErrTokenRequired
}
basicToken := authParts[1]

payload, err := base64.StdEncoding.DecodeString(basicToken)
if err != nil {
context.GetLogger(ctx).Errorf("Basic token decode failed: %s", err)
return "", ErrTokenInvalid
}
switch strings.ToLower(authParts[0]) {
case "bearer":
// This is either a direct API token, or a token issued by our docker token handler
token = authParts[1]
// Recognize the token issued to anonymous users by our docker token handler
if token == anonymousToken {
token = ""
}

case "basic":
_, password, ok := req.BasicAuth()
if !ok || len(password) == 0 {
return "", ErrTokenInvalid
}
token = password

osAuthParts := strings.SplitN(string(payload), ":", 2)
if len(osAuthParts) != 2 {
return "", ErrOpenShiftTokenRequired
default:
return "", ErrTokenRequired
}

bearerToken := osAuthParts[1]
return bearerToken, nil
return token, nil
}

func verifyOpenShiftUser(ctx context.Context, client client.UsersInterface) error {
Expand Down
Loading

0 comments on commit d179662

Please sign in to comment.