Move auth package under registry package
Signed-off-by: Stephen J Day <stephen.day@docker.com>master
							parent
							
								
									3822e685a0
								
							
						
					
					
						commit
						c3b07952ad
					
				| 
						 | 
				
			
			@ -0,0 +1,142 @@
 | 
			
		|||
// Package auth defines a standard interface for request access controllers.
 | 
			
		||||
//
 | 
			
		||||
// An access controller has a simple interface with a single `Authorized`
 | 
			
		||||
// method which checks that a given request is authorized to perform one or
 | 
			
		||||
// more actions on one or more resources. This method should return a non-nil
 | 
			
		||||
// error if the requset is not authorized.
 | 
			
		||||
//
 | 
			
		||||
// An implementation registers its access controller by name with a constructor
 | 
			
		||||
// which accepts an options map for configuring the access controller.
 | 
			
		||||
//
 | 
			
		||||
//		options := map[string]interface{}{"sillySecret": "whysosilly?"}
 | 
			
		||||
// 		accessController, _ := auth.GetAccessController("silly", options)
 | 
			
		||||
//
 | 
			
		||||
// This `accessController` can then be used in a request handler like so:
 | 
			
		||||
//
 | 
			
		||||
// 		func updateOrder(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
//			orderNumber := r.FormValue("orderNumber")
 | 
			
		||||
//			resource := auth.Resource{Type: "customerOrder", Name: orderNumber}
 | 
			
		||||
// 			access := auth.Access{Resource: resource, Action: "update"}
 | 
			
		||||
//
 | 
			
		||||
// 			if ctx, err := accessController.Authorized(ctx, access); err != nil {
 | 
			
		||||
//				if challenge, ok := err.(auth.Challenge) {
 | 
			
		||||
//					// Let the challenge write the response.
 | 
			
		||||
//					challenge.ServeHTTP(w, r)
 | 
			
		||||
//				} else {
 | 
			
		||||
//					// Some other error.
 | 
			
		||||
//				}
 | 
			
		||||
//			}
 | 
			
		||||
// 		}
 | 
			
		||||
//
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// UserInfo carries information about
 | 
			
		||||
// an autenticated/authorized client.
 | 
			
		||||
type UserInfo struct {
 | 
			
		||||
	Name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resource describes a resource by type and name.
 | 
			
		||||
type Resource struct {
 | 
			
		||||
	Type string
 | 
			
		||||
	Name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Access describes a specific action that is
 | 
			
		||||
// requested or allowed for a given recource.
 | 
			
		||||
type Access struct {
 | 
			
		||||
	Resource
 | 
			
		||||
	Action string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Challenge is a special error type which is used for HTTP 401 Unauthorized
 | 
			
		||||
// responses and is able to write the response with WWW-Authenticate challenge
 | 
			
		||||
// header values based on the error.
 | 
			
		||||
type Challenge interface {
 | 
			
		||||
	error
 | 
			
		||||
	// ServeHTTP prepares the request to conduct the appropriate challenge
 | 
			
		||||
	// response. For most implementations, simply calling ServeHTTP should be
 | 
			
		||||
	// sufficient. Because no body is written, users may write a custom body after
 | 
			
		||||
	// calling ServeHTTP, but any headers must be written before the call and may
 | 
			
		||||
	// be overwritten.
 | 
			
		||||
	ServeHTTP(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AccessController controls access to registry resources based on a request
 | 
			
		||||
// and required access levels for a request. Implementations can support both
 | 
			
		||||
// complete denial and http authorization challenges.
 | 
			
		||||
type AccessController interface {
 | 
			
		||||
	// Authorized returns a non-nil error if the context is granted access and
 | 
			
		||||
	// returns a new authorized context. If one or more Access structs are
 | 
			
		||||
	// provided, the requested access will be compared with what is available
 | 
			
		||||
	// to the context. The given context will contain a "http.request" key with
 | 
			
		||||
	// a `*http.Request` value. If the error is non-nil, access should always
 | 
			
		||||
	// be denied. The error may be of type Challenge, in which case the caller
 | 
			
		||||
	// may have the Challenge handle the request or choose what action to take
 | 
			
		||||
	// based on the Challenge header or response status. The returned context
 | 
			
		||||
	// object should have a "auth.user" value set to a UserInfo struct.
 | 
			
		||||
	Authorized(ctx context.Context, access ...Access) (context.Context, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithUser returns a context with the authorized user info.
 | 
			
		||||
func WithUser(ctx context.Context, user UserInfo) context.Context {
 | 
			
		||||
	return userInfoContext{
 | 
			
		||||
		Context: ctx,
 | 
			
		||||
		user:    user,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type userInfoContext struct {
 | 
			
		||||
	context.Context
 | 
			
		||||
	user UserInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (uic userInfoContext) Value(key interface{}) interface{} {
 | 
			
		||||
	switch key {
 | 
			
		||||
	case "auth.user":
 | 
			
		||||
		return uic.user
 | 
			
		||||
	case "auth.user.name":
 | 
			
		||||
		return uic.user.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return uic.Context.Value(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InitFunc is the type of an AccessController factory function and is used
 | 
			
		||||
// to register the contsructor for different AccesController backends.
 | 
			
		||||
type InitFunc func(options map[string]interface{}) (AccessController, error)
 | 
			
		||||
 | 
			
		||||
var accessControllers map[string]InitFunc
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	accessControllers = make(map[string]InitFunc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Register is used to register an InitFunc for
 | 
			
		||||
// an AccessController backend with the given name.
 | 
			
		||||
func Register(name string, initFunc InitFunc) error {
 | 
			
		||||
	if _, exists := accessControllers[name]; exists {
 | 
			
		||||
		return fmt.Errorf("name already registered: %s", name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessControllers[name] = initFunc
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAccessController constructs an AccessController
 | 
			
		||||
// with the given options using the named backend.
 | 
			
		||||
func GetAccessController(name string, options map[string]interface{}) (AccessController, error) {
 | 
			
		||||
	if initFunc, exists := accessControllers[name]; exists {
 | 
			
		||||
		return initFunc(options)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, fmt.Errorf("no access controller registered with name: %s", name)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,96 @@
 | 
			
		|||
// Package silly provides a simple authentication scheme that checks for the
 | 
			
		||||
// existence of an Authorization header and issues access if is present and
 | 
			
		||||
// non-empty.
 | 
			
		||||
//
 | 
			
		||||
// This package is present as an example implementation of a minimal
 | 
			
		||||
// auth.AccessController and for testing. This is not suitable for any kind of
 | 
			
		||||
// production security.
 | 
			
		||||
package silly
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
	ctxu "github.com/docker/distribution/context"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// accessController provides a simple implementation of auth.AccessController
 | 
			
		||||
// that simply checks for a non-empty Authorization header. It is useful for
 | 
			
		||||
// demonstration and testing.
 | 
			
		||||
type accessController struct {
 | 
			
		||||
	realm   string
 | 
			
		||||
	service string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ auth.AccessController = &accessController{}
 | 
			
		||||
 | 
			
		||||
func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
 | 
			
		||||
	realm, present := options["realm"]
 | 
			
		||||
	if _, ok := realm.(string); !present || !ok {
 | 
			
		||||
		return nil, fmt.Errorf(`"realm" must be set for silly access controller`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service, present := options["service"]
 | 
			
		||||
	if _, ok := service.(string); !present || !ok {
 | 
			
		||||
		return nil, fmt.Errorf(`"service" must be set for silly access controller`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &accessController{realm: realm.(string), service: service.(string)}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Authorized simply checks for the existence of the authorization header,
 | 
			
		||||
// responding with a bearer challenge if it doesn't exist.
 | 
			
		||||
func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) {
 | 
			
		||||
	req, err := ctxu.GetRequest(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if req.Header.Get("Authorization") == "" {
 | 
			
		||||
		challenge := challenge{
 | 
			
		||||
			realm:   ac.realm,
 | 
			
		||||
			service: ac.service,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(accessRecords) > 0 {
 | 
			
		||||
			var scopes []string
 | 
			
		||||
			for _, access := range accessRecords {
 | 
			
		||||
				scopes = append(scopes, fmt.Sprintf("%s:%s:%s", access.Type, access.Resource.Name, access.Action))
 | 
			
		||||
			}
 | 
			
		||||
			challenge.scope = strings.Join(scopes, " ")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, &challenge
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return context.WithValue(ctx, "auth.user", auth.UserInfo{Name: "silly"}), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type challenge struct {
 | 
			
		||||
	realm   string
 | 
			
		||||
	service string
 | 
			
		||||
	scope   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	header := fmt.Sprintf("Bearer realm=%q,service=%q", ch.realm, ch.service)
 | 
			
		||||
 | 
			
		||||
	if ch.scope != "" {
 | 
			
		||||
		header = fmt.Sprintf("%s,scope=%q", header, ch.scope)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Authorization", header)
 | 
			
		||||
	w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ch *challenge) Error() string {
 | 
			
		||||
	return fmt.Sprintf("silly authentication challenge: %#v", ch)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// init registers the silly auth backend.
 | 
			
		||||
func init() {
 | 
			
		||||
	auth.Register("silly", auth.InitFunc(newAccessController))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
package silly
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSillyAccessController(t *testing.T) {
 | 
			
		||||
	ac := &accessController{
 | 
			
		||||
		realm:   "test-realm",
 | 
			
		||||
		service: "test-service",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		ctx := context.WithValue(nil, "http.request", r)
 | 
			
		||||
		authCtx, err := ac.Authorized(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			switch err := err.(type) {
 | 
			
		||||
			case auth.Challenge:
 | 
			
		||||
				err.ServeHTTP(w, r)
 | 
			
		||||
				return
 | 
			
		||||
			default:
 | 
			
		||||
				t.Fatalf("unexpected error authorizing request: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		userInfo, ok := authCtx.Value("auth.user").(auth.UserInfo)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			t.Fatal("silly accessController did not set auth.user context")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if userInfo.Name != "silly" {
 | 
			
		||||
			t.Fatalf("expected user name %q, got %q", "silly", userInfo.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w.WriteHeader(http.StatusNoContent)
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Get(server.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error during GET: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	// Request should not be authorized
 | 
			
		||||
	if resp.StatusCode != http.StatusUnauthorized {
 | 
			
		||||
		t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusUnauthorized)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", server.URL, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error creating new request: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Authorization", "seriously, anything")
 | 
			
		||||
 | 
			
		||||
	resp, err = http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error during GET: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	// Request should not be authorized
 | 
			
		||||
	if resp.StatusCode != http.StatusNoContent {
 | 
			
		||||
		t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,274 @@
 | 
			
		|||
package token
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
	ctxu "github.com/docker/distribution/context"
 | 
			
		||||
	"github.com/docker/libtrust"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// accessSet maps a typed, named resource to
 | 
			
		||||
// a set of actions requested or authorized.
 | 
			
		||||
type accessSet map[auth.Resource]actionSet
 | 
			
		||||
 | 
			
		||||
// newAccessSet constructs an accessSet from
 | 
			
		||||
// a variable number of auth.Access items.
 | 
			
		||||
func newAccessSet(accessItems ...auth.Access) accessSet {
 | 
			
		||||
	accessSet := make(accessSet, len(accessItems))
 | 
			
		||||
 | 
			
		||||
	for _, access := range accessItems {
 | 
			
		||||
		resource := auth.Resource{
 | 
			
		||||
			Type: access.Type,
 | 
			
		||||
			Name: access.Name,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		set, exists := accessSet[resource]
 | 
			
		||||
		if !exists {
 | 
			
		||||
			set = newActionSet()
 | 
			
		||||
			accessSet[resource] = set
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		set.add(access.Action)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return accessSet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// contains returns whether or not the given access is in this accessSet.
 | 
			
		||||
func (s accessSet) contains(access auth.Access) bool {
 | 
			
		||||
	actionSet, ok := s[access.Resource]
 | 
			
		||||
	if ok {
 | 
			
		||||
		return actionSet.contains(access.Action)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// scopeParam returns a collection of scopes which can
 | 
			
		||||
// be used for a WWW-Authenticate challenge parameter.
 | 
			
		||||
// See https://tools.ietf.org/html/rfc6750#section-3
 | 
			
		||||
func (s accessSet) scopeParam() string {
 | 
			
		||||
	scopes := make([]string, 0, len(s))
 | 
			
		||||
 | 
			
		||||
	for resource, actionSet := range s {
 | 
			
		||||
		actions := strings.Join(actionSet.keys(), ",")
 | 
			
		||||
		scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return strings.Join(scopes, " ")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Errors used and exported by this package.
 | 
			
		||||
var (
 | 
			
		||||
	ErrInsufficientScope = errors.New("insufficient scope")
 | 
			
		||||
	ErrTokenRequired     = errors.New("authorization token required")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// authChallenge implements the auth.Challenge interface.
 | 
			
		||||
type authChallenge struct {
 | 
			
		||||
	err       error
 | 
			
		||||
	realm     string
 | 
			
		||||
	service   string
 | 
			
		||||
	accessSet accessSet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Error returns the internal error string for this authChallenge.
 | 
			
		||||
func (ac *authChallenge) Error() string {
 | 
			
		||||
	return ac.err.Error()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Status returns the HTTP Response Status Code for this authChallenge.
 | 
			
		||||
func (ac *authChallenge) Status() int {
 | 
			
		||||
	return http.StatusUnauthorized
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// challengeParams constructs the value to be used in
 | 
			
		||||
// the WWW-Authenticate response challenge header.
 | 
			
		||||
// See https://tools.ietf.org/html/rfc6750#section-3
 | 
			
		||||
func (ac *authChallenge) challengeParams() string {
 | 
			
		||||
	str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service)
 | 
			
		||||
 | 
			
		||||
	if scope := ac.accessSet.scopeParam(); scope != "" {
 | 
			
		||||
		str = fmt.Sprintf("%s,scope=%q", str, scope)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
 | 
			
		||||
		str = fmt.Sprintf("%s,error=%q", str, "invalid_token")
 | 
			
		||||
	} else if ac.err == ErrInsufficientScope {
 | 
			
		||||
		str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return str
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetHeader sets the WWW-Authenticate value for the given header.
 | 
			
		||||
func (ac *authChallenge) SetHeader(header http.Header) {
 | 
			
		||||
	header.Add("WWW-Authenticate", ac.challengeParams())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeHttp handles writing the challenge response
 | 
			
		||||
// by setting the challenge header and status code.
 | 
			
		||||
func (ac *authChallenge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	ac.SetHeader(w.Header())
 | 
			
		||||
	w.WriteHeader(ac.Status())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// accessController implements the auth.AccessController interface.
 | 
			
		||||
type accessController struct {
 | 
			
		||||
	realm       string
 | 
			
		||||
	issuer      string
 | 
			
		||||
	service     string
 | 
			
		||||
	rootCerts   *x509.CertPool
 | 
			
		||||
	trustedKeys map[string]libtrust.PublicKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// tokenAccessOptions is a convenience type for handling
 | 
			
		||||
// options to the contstructor of an accessController.
 | 
			
		||||
type tokenAccessOptions struct {
 | 
			
		||||
	realm          string
 | 
			
		||||
	issuer         string
 | 
			
		||||
	service        string
 | 
			
		||||
	rootCertBundle string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// checkOptions gathers the necessary options
 | 
			
		||||
// for an accessController from the given map.
 | 
			
		||||
func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
 | 
			
		||||
	var opts tokenAccessOptions
 | 
			
		||||
 | 
			
		||||
	keys := []string{"realm", "issuer", "service", "rootCertBundle"}
 | 
			
		||||
	vals := make([]string, 0, len(keys))
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		val, ok := options[key].(string)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
 | 
			
		||||
		}
 | 
			
		||||
		vals = append(vals, val)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
 | 
			
		||||
 | 
			
		||||
	return opts, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAccessController creates an accessController using the given options.
 | 
			
		||||
func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
 | 
			
		||||
	config, err := checkOptions(options)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fp, err := os.Open(config.rootCertBundle)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fp.Close()
 | 
			
		||||
 | 
			
		||||
	rawCertBundle, err := ioutil.ReadAll(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var rootCerts []*x509.Certificate
 | 
			
		||||
	pemBlock, rawCertBundle := pem.Decode(rawCertBundle)
 | 
			
		||||
	for pemBlock != nil {
 | 
			
		||||
		cert, err := x509.ParseCertificate(pemBlock.Bytes)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rootCerts = append(rootCerts, cert)
 | 
			
		||||
 | 
			
		||||
		pemBlock, rawCertBundle = pem.Decode(rawCertBundle)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(rootCerts) == 0 {
 | 
			
		||||
		return nil, errors.New("token auth requires at least one token signing root certificate")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootPool := x509.NewCertPool()
 | 
			
		||||
	trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
 | 
			
		||||
	for _, rootCert := range rootCerts {
 | 
			
		||||
		rootPool.AddCert(rootCert)
 | 
			
		||||
		pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		trustedKeys[pubKey.KeyID()] = pubKey
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &accessController{
 | 
			
		||||
		realm:       config.realm,
 | 
			
		||||
		issuer:      config.issuer,
 | 
			
		||||
		service:     config.service,
 | 
			
		||||
		rootCerts:   rootPool,
 | 
			
		||||
		trustedKeys: trustedKeys,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Authorized handles checking whether the given request is authorized
 | 
			
		||||
// for actions on resources described by the given access items.
 | 
			
		||||
func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
 | 
			
		||||
	challenge := &authChallenge{
 | 
			
		||||
		realm:     ac.realm,
 | 
			
		||||
		service:   ac.service,
 | 
			
		||||
		accessSet: newAccessSet(accessItems...),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := ctxu.GetRequest(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parts := strings.Split(req.Header.Get("Authorization"), " ")
 | 
			
		||||
 | 
			
		||||
	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
 | 
			
		||||
		challenge.err = ErrTokenRequired
 | 
			
		||||
		return nil, challenge
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rawToken := parts[1]
 | 
			
		||||
 | 
			
		||||
	token, err := NewToken(rawToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		challenge.err = err
 | 
			
		||||
		return nil, challenge
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	verifyOpts := VerifyOptions{
 | 
			
		||||
		TrustedIssuers:    []string{ac.issuer},
 | 
			
		||||
		AcceptedAudiences: []string{ac.service},
 | 
			
		||||
		Roots:             ac.rootCerts,
 | 
			
		||||
		TrustedKeys:       ac.trustedKeys,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = token.Verify(verifyOpts); err != nil {
 | 
			
		||||
		challenge.err = err
 | 
			
		||||
		return nil, challenge
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessSet := token.accessSet()
 | 
			
		||||
	for _, access := range accessItems {
 | 
			
		||||
		if !accessSet.contains(access) {
 | 
			
		||||
			challenge.err = ErrInsufficientScope
 | 
			
		||||
			return nil, challenge
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// init handles registering the token auth backend.
 | 
			
		||||
func init() {
 | 
			
		||||
	auth.Register("token", auth.InitFunc(newAccessController))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
package token
 | 
			
		||||
 | 
			
		||||
// StringSet is a useful type for looking up strings.
 | 
			
		||||
type stringSet map[string]struct{}
 | 
			
		||||
 | 
			
		||||
// NewStringSet creates a new StringSet with the given strings.
 | 
			
		||||
func newStringSet(keys ...string) stringSet {
 | 
			
		||||
	ss := make(stringSet, len(keys))
 | 
			
		||||
	ss.add(keys...)
 | 
			
		||||
	return ss
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add inserts the given keys into this StringSet.
 | 
			
		||||
func (ss stringSet) add(keys ...string) {
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		ss[key] = struct{}{}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Contains returns whether the given key is in this StringSet.
 | 
			
		||||
func (ss stringSet) contains(key string) bool {
 | 
			
		||||
	_, ok := ss[key]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Keys returns a slice of all keys in this StringSet.
 | 
			
		||||
func (ss stringSet) keys() []string {
 | 
			
		||||
	keys := make([]string, 0, len(ss))
 | 
			
		||||
 | 
			
		||||
	for key := range ss {
 | 
			
		||||
		keys = append(keys, key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return keys
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,343 @@
 | 
			
		|||
package token
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	log "github.com/Sirupsen/logrus"
 | 
			
		||||
	"github.com/docker/libtrust"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// TokenSeparator is the value which separates the header, claims, and
 | 
			
		||||
	// signature in the compact serialization of a JSON Web Token.
 | 
			
		||||
	TokenSeparator = "."
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Errors used by token parsing and verification.
 | 
			
		||||
var (
 | 
			
		||||
	ErrMalformedToken = errors.New("malformed token")
 | 
			
		||||
	ErrInvalidToken   = errors.New("invalid token")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ResourceActions stores allowed actions on a named and typed resource.
 | 
			
		||||
type ResourceActions struct {
 | 
			
		||||
	Type    string   `json:"type"`
 | 
			
		||||
	Name    string   `json:"name"`
 | 
			
		||||
	Actions []string `json:"actions"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClaimSet describes the main section of a JSON Web Token.
 | 
			
		||||
type ClaimSet struct {
 | 
			
		||||
	// Public claims
 | 
			
		||||
	Issuer     string `json:"iss"`
 | 
			
		||||
	Subject    string `json:"sub"`
 | 
			
		||||
	Audience   string `json:"aud"`
 | 
			
		||||
	Expiration int64  `json:"exp"`
 | 
			
		||||
	NotBefore  int64  `json:"nbf"`
 | 
			
		||||
	IssuedAt   int64  `json:"iat"`
 | 
			
		||||
	JWTID      string `json:"jti"`
 | 
			
		||||
 | 
			
		||||
	// Private claims
 | 
			
		||||
	Access []*ResourceActions `json:"access"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Header describes the header section of a JSON Web Token.
 | 
			
		||||
type Header struct {
 | 
			
		||||
	Type       string          `json:"typ"`
 | 
			
		||||
	SigningAlg string          `json:"alg"`
 | 
			
		||||
	KeyID      string          `json:"kid,omitempty"`
 | 
			
		||||
	X5c        []string        `json:"x5c,omitempty"`
 | 
			
		||||
	RawJWK     json.RawMessage `json:"jwk,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Token describes a JSON Web Token.
 | 
			
		||||
type Token struct {
 | 
			
		||||
	Raw       string
 | 
			
		||||
	Header    *Header
 | 
			
		||||
	Claims    *ClaimSet
 | 
			
		||||
	Signature []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VerifyOptions is used to specify
 | 
			
		||||
// options when verifying a JSON Web Token.
 | 
			
		||||
type VerifyOptions struct {
 | 
			
		||||
	TrustedIssuers    []string
 | 
			
		||||
	AcceptedAudiences []string
 | 
			
		||||
	Roots             *x509.CertPool
 | 
			
		||||
	TrustedKeys       map[string]libtrust.PublicKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewToken parses the given raw token string
 | 
			
		||||
// and constructs an unverified JSON Web Token.
 | 
			
		||||
func NewToken(rawToken string) (*Token, error) {
 | 
			
		||||
	parts := strings.Split(rawToken, TokenSeparator)
 | 
			
		||||
	if len(parts) != 3 {
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		rawHeader, rawClaims   = parts[0], parts[1]
 | 
			
		||||
		headerJSON, claimsJSON []byte
 | 
			
		||||
		err                    error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Errorf("error while unmarshalling raw token: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if headerJSON, err = joseBase64UrlDecode(rawHeader); err != nil {
 | 
			
		||||
		err = fmt.Errorf("unable to decode header: %s", err)
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if claimsJSON, err = joseBase64UrlDecode(rawClaims); err != nil {
 | 
			
		||||
		err = fmt.Errorf("unable to decode claims: %s", err)
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := new(Token)
 | 
			
		||||
	token.Header = new(Header)
 | 
			
		||||
	token.Claims = new(ClaimSet)
 | 
			
		||||
 | 
			
		||||
	token.Raw = strings.Join(parts[:2], TokenSeparator)
 | 
			
		||||
	if token.Signature, err = joseBase64UrlDecode(parts[2]); err != nil {
 | 
			
		||||
		err = fmt.Errorf("unable to decode signature: %s", err)
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = json.Unmarshal(headerJSON, token.Header); err != nil {
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = json.Unmarshal(claimsJSON, token.Claims); err != nil {
 | 
			
		||||
		return nil, ErrMalformedToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return token, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify attempts to verify this token using the given options.
 | 
			
		||||
// Returns a nil error if the token is valid.
 | 
			
		||||
func (t *Token) Verify(verifyOpts VerifyOptions) error {
 | 
			
		||||
	// Verify that the Issuer claim is a trusted authority.
 | 
			
		||||
	if !contains(verifyOpts.TrustedIssuers, t.Claims.Issuer) {
 | 
			
		||||
		log.Errorf("token from untrusted issuer: %q", t.Claims.Issuer)
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify that the Audience claim is allowed.
 | 
			
		||||
	if !contains(verifyOpts.AcceptedAudiences, t.Claims.Audience) {
 | 
			
		||||
		log.Errorf("token intended for another audience: %q", t.Claims.Audience)
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify that the token is currently usable and not expired.
 | 
			
		||||
	currentUnixTime := time.Now().Unix()
 | 
			
		||||
	if !(t.Claims.NotBefore <= currentUnixTime && currentUnixTime <= t.Claims.Expiration) {
 | 
			
		||||
		log.Errorf("token not to be used before %d or after %d - currently %d", t.Claims.NotBefore, t.Claims.Expiration, currentUnixTime)
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify the token signature.
 | 
			
		||||
	if len(t.Signature) == 0 {
 | 
			
		||||
		log.Error("token has no signature")
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify that the signing key is trusted.
 | 
			
		||||
	signingKey, err := t.VerifySigningKey(verifyOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error(err)
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Finally, verify the signature of the token using the key which signed it.
 | 
			
		||||
	if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil {
 | 
			
		||||
		log.Errorf("unable to verify token signature: %s", err)
 | 
			
		||||
		return ErrInvalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VerifySigningKey attempts to get the key which was used to sign this token.
 | 
			
		||||
// The token header should contain either of these 3 fields:
 | 
			
		||||
//      `x5c` - The x509 certificate chain for the signing key. Needs to be
 | 
			
		||||
//              verified.
 | 
			
		||||
//      `jwk` - The JSON Web Key representation of the signing key.
 | 
			
		||||
//              May contain its own `x5c` field which needs to be verified.
 | 
			
		||||
//      `kid` - The unique identifier for the key. This library interprets it
 | 
			
		||||
//              as a libtrust fingerprint. The key itself can be looked up in
 | 
			
		||||
//              the trustedKeys field of the given verify options.
 | 
			
		||||
// Each of these methods are tried in that order of preference until the
 | 
			
		||||
// signing key is found or an error is returned.
 | 
			
		||||
func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey libtrust.PublicKey, err error) {
 | 
			
		||||
	// First attempt to get an x509 certificate chain from the header.
 | 
			
		||||
	var (
 | 
			
		||||
		x5c    = t.Header.X5c
 | 
			
		||||
		rawJWK = t.Header.RawJWK
 | 
			
		||||
		keyID  = t.Header.KeyID
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case len(x5c) > 0:
 | 
			
		||||
		signingKey, err = parseAndVerifyCertChain(x5c, verifyOpts.Roots)
 | 
			
		||||
	case len(rawJWK) > 0:
 | 
			
		||||
		signingKey, err = parseAndVerifyRawJWK(rawJWK, verifyOpts)
 | 
			
		||||
	case len(keyID) > 0:
 | 
			
		||||
		signingKey = verifyOpts.TrustedKeys[keyID]
 | 
			
		||||
		if signingKey == nil {
 | 
			
		||||
			err = fmt.Errorf("token signed by untrusted key with ID: %q", keyID)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		err = errors.New("unable to get token signing key")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseAndVerifyCertChain(x5c []string, roots *x509.CertPool) (leafKey libtrust.PublicKey, err error) {
 | 
			
		||||
	if len(x5c) == 0 {
 | 
			
		||||
		return nil, errors.New("empty x509 certificate chain")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ensure the first element is encoded correctly.
 | 
			
		||||
	leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to decode leaf certificate: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// And that it is a valid x509 certificate.
 | 
			
		||||
	leafCert, err := x509.ParseCertificate(leafCertDer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to parse leaf certificate: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The rest of the certificate chain are intermediate certificates.
 | 
			
		||||
	intermediates := x509.NewCertPool()
 | 
			
		||||
	for i := 1; i < len(x5c); i++ {
 | 
			
		||||
		intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("unable to decode intermediate certificate: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("unable to parse intermediate certificate: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		intermediates.AddCert(intermediateCert)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	verifyOpts := x509.VerifyOptions{
 | 
			
		||||
		Intermediates: intermediates,
 | 
			
		||||
		Roots:         roots,
 | 
			
		||||
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: this call returns certificate chains which we ignore for now, but
 | 
			
		||||
	// we should check them for revocations if we have the ability later.
 | 
			
		||||
	if _, err = leafCert.Verify(verifyOpts); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to verify certificate chain: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the public key from the leaf certificate.
 | 
			
		||||
	leafCryptoKey, ok := leafCert.PublicKey.(crypto.PublicKey)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, errors.New("unable to get leaf cert public key value")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	leafKey, err = libtrust.FromCryptoPublicKey(leafCryptoKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to make libtrust public key from leaf certificate: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseAndVerifyRawJWK(rawJWK json.RawMessage, verifyOpts VerifyOptions) (pubKey libtrust.PublicKey, err error) {
 | 
			
		||||
	pubKey, err = libtrust.UnmarshalPublicKeyJWK([]byte(rawJWK))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to decode raw JWK value: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check to see if the key includes a certificate chain.
 | 
			
		||||
	x5cVal, ok := pubKey.GetExtendedField("x5c").([]interface{})
 | 
			
		||||
	if !ok {
 | 
			
		||||
		// The JWK should be one of the trusted root keys.
 | 
			
		||||
		if _, trusted := verifyOpts.TrustedKeys[pubKey.KeyID()]; !trusted {
 | 
			
		||||
			return nil, errors.New("untrusted JWK with no certificate chain")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// The JWK is one of the trusted keys.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ensure each item in the chain is of the correct type.
 | 
			
		||||
	x5c := make([]string, len(x5cVal))
 | 
			
		||||
	for i, val := range x5cVal {
 | 
			
		||||
		certString, ok := val.(string)
 | 
			
		||||
		if !ok || len(certString) == 0 {
 | 
			
		||||
			return nil, errors.New("malformed certificate chain")
 | 
			
		||||
		}
 | 
			
		||||
		x5c[i] = certString
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ensure that the x509 certificate chain can
 | 
			
		||||
	// be verified up to one of our trusted roots.
 | 
			
		||||
	leafKey, err := parseAndVerifyCertChain(x5c, verifyOpts.Roots)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("could not verify JWK certificate chain: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify that the public key in the leaf cert *is* the signing key.
 | 
			
		||||
	if pubKey.KeyID() != leafKey.KeyID() {
 | 
			
		||||
		return nil, errors.New("leaf certificate public key ID does not match JWK key ID")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// accessSet returns a set of actions available for the resource
 | 
			
		||||
// actions listed in the `access` section of this token.
 | 
			
		||||
func (t *Token) accessSet() accessSet {
 | 
			
		||||
	if t.Claims == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessSet := make(accessSet, len(t.Claims.Access))
 | 
			
		||||
 | 
			
		||||
	for _, resourceActions := range t.Claims.Access {
 | 
			
		||||
		resource := auth.Resource{
 | 
			
		||||
			Type: resourceActions.Type,
 | 
			
		||||
			Name: resourceActions.Name,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		set, exists := accessSet[resource]
 | 
			
		||||
		if !exists {
 | 
			
		||||
			set = newActionSet()
 | 
			
		||||
			accessSet[resource] = set
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, action := range resourceActions.Actions {
 | 
			
		||||
			set.add(action)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return accessSet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Token) compactRaw() string {
 | 
			
		||||
	return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,386 @@
 | 
			
		|||
package token
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
	"github.com/docker/libtrust"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func makeRootKeys(numKeys int) ([]libtrust.PrivateKey, error) {
 | 
			
		||||
	keys := make([]libtrust.PrivateKey, 0, numKeys)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < numKeys; i++ {
 | 
			
		||||
		key, err := libtrust.GenerateECP256PrivateKey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		keys = append(keys, key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return keys, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeSigningKeyWithChain(rootKey libtrust.PrivateKey, depth int) (libtrust.PrivateKey, error) {
 | 
			
		||||
	if depth == 0 {
 | 
			
		||||
		// Don't need to build a chain.
 | 
			
		||||
		return rootKey, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		x5c       = make([]string, depth)
 | 
			
		||||
		parentKey = rootKey
 | 
			
		||||
		key       libtrust.PrivateKey
 | 
			
		||||
		cert      *x509.Certificate
 | 
			
		||||
		err       error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for depth > 0 {
 | 
			
		||||
		if key, err = libtrust.GenerateECP256PrivateKey(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if cert, err = libtrust.GenerateCACert(parentKey, key); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		depth--
 | 
			
		||||
		x5c[depth] = base64.StdEncoding.EncodeToString(cert.Raw)
 | 
			
		||||
		parentKey = key
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key.AddExtendedField("x5c", x5c)
 | 
			
		||||
 | 
			
		||||
	return key, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeRootCerts(rootKeys []libtrust.PrivateKey) ([]*x509.Certificate, error) {
 | 
			
		||||
	certs := make([]*x509.Certificate, 0, len(rootKeys))
 | 
			
		||||
 | 
			
		||||
	for _, key := range rootKeys {
 | 
			
		||||
		cert, err := libtrust.GenerateCACert(key, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		certs = append(certs, cert)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return certs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeTrustedKeyMap(rootKeys []libtrust.PrivateKey) map[string]libtrust.PublicKey {
 | 
			
		||||
	trustedKeys := make(map[string]libtrust.PublicKey, len(rootKeys))
 | 
			
		||||
 | 
			
		||||
	for _, key := range rootKeys {
 | 
			
		||||
		trustedKeys[key.KeyID()] = key.PublicKey()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return trustedKeys
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int) (*Token, error) {
 | 
			
		||||
	signingKey, err := makeSigningKeyWithChain(rootKey, depth)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to amke signing key with chain: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rawJWK, err := signingKey.PublicKey().MarshalJSON()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to marshal signing key to JSON: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	joseHeader := &Header{
 | 
			
		||||
		Type:       "JWT",
 | 
			
		||||
		SigningAlg: "ES256",
 | 
			
		||||
		RawJWK:     json.RawMessage(rawJWK),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
 | 
			
		||||
	randomBytes := make([]byte, 15)
 | 
			
		||||
	if _, err = rand.Read(randomBytes); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claimSet := &ClaimSet{
 | 
			
		||||
		Issuer:     issuer,
 | 
			
		||||
		Subject:    "foo",
 | 
			
		||||
		Audience:   audience,
 | 
			
		||||
		Expiration: now.Add(5 * time.Minute).Unix(),
 | 
			
		||||
		NotBefore:  now.Unix(),
 | 
			
		||||
		IssuedAt:   now.Unix(),
 | 
			
		||||
		JWTID:      base64.URLEncoding.EncodeToString(randomBytes),
 | 
			
		||||
		Access:     access,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var joseHeaderBytes, claimSetBytes []byte
 | 
			
		||||
 | 
			
		||||
	if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to marshal jose header: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	if claimSetBytes, err = json.Marshal(claimSet); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to marshal claim set: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	encodedJoseHeader := joseBase64UrlEncode(joseHeaderBytes)
 | 
			
		||||
	encodedClaimSet := joseBase64UrlEncode(claimSetBytes)
 | 
			
		||||
	encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
 | 
			
		||||
 | 
			
		||||
	var signatureBytes []byte
 | 
			
		||||
	if signatureBytes, _, err = signingKey.Sign(strings.NewReader(encodingToSign), crypto.SHA256); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to sign jwt payload: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	signature := joseBase64UrlEncode(signatureBytes)
 | 
			
		||||
	tokenString := fmt.Sprintf("%s.%s", encodingToSign, signature)
 | 
			
		||||
 | 
			
		||||
	return NewToken(tokenString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This test makes 4 tokens with a varying number of intermediate
 | 
			
		||||
// certificates ranging from no intermediate chain to a length of 3
 | 
			
		||||
// intermediates.
 | 
			
		||||
func TestTokenVerify(t *testing.T) {
 | 
			
		||||
	var (
 | 
			
		||||
		numTokens = 4
 | 
			
		||||
		issuer    = "test-issuer"
 | 
			
		||||
		audience  = "test-audience"
 | 
			
		||||
		access    = []*ResourceActions{
 | 
			
		||||
			{
 | 
			
		||||
				Type:    "repository",
 | 
			
		||||
				Name:    "foo/bar",
 | 
			
		||||
				Actions: []string{"pull", "push"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	rootKeys, err := makeRootKeys(numTokens)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootCerts, err := makeRootCerts(rootKeys)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootPool := x509.NewCertPool()
 | 
			
		||||
	for _, rootCert := range rootCerts {
 | 
			
		||||
		rootPool.AddCert(rootCert)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	trustedKeys := makeTrustedKeyMap(rootKeys)
 | 
			
		||||
 | 
			
		||||
	tokens := make([]*Token, 0, numTokens)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < numTokens; i++ {
 | 
			
		||||
		token, err := makeTestToken(issuer, audience, access, rootKeys[i], i)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		tokens = append(tokens, token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	verifyOps := VerifyOptions{
 | 
			
		||||
		TrustedIssuers:    []string{issuer},
 | 
			
		||||
		AcceptedAudiences: []string{audience},
 | 
			
		||||
		Roots:             rootPool,
 | 
			
		||||
		TrustedKeys:       trustedKeys,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, token := range tokens {
 | 
			
		||||
		if err := token.Verify(verifyOps); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) {
 | 
			
		||||
	rootCerts, err := makeRootCerts(rootKeys)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tempFile, err := ioutil.TempFile("", "rootCertBundle")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer tempFile.Close()
 | 
			
		||||
 | 
			
		||||
	for _, cert := range rootCerts {
 | 
			
		||||
		if err = pem.Encode(tempFile, &pem.Block{
 | 
			
		||||
			Type:  "CERTIFICATE",
 | 
			
		||||
			Bytes: cert.Raw,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			os.Remove(tempFile.Name())
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tempFile.Name(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestAccessController tests complete integration of the token auth package.
 | 
			
		||||
// It starts by mocking the options for a token auth accessController which
 | 
			
		||||
// it creates. It then tries a few mock requests:
 | 
			
		||||
// 		- don't supply a token; should error with challenge
 | 
			
		||||
//		- supply an invalid token; should error with challenge
 | 
			
		||||
// 		- supply a token with insufficient access; should error with challenge
 | 
			
		||||
//		- supply a valid token; should not error
 | 
			
		||||
func TestAccessController(t *testing.T) {
 | 
			
		||||
	// Make 2 keys; only the first is to be a trusted root key.
 | 
			
		||||
	rootKeys, err := makeRootKeys(2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootCertBundleFilename, err := writeTempRootCerts(rootKeys[:1])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer os.Remove(rootCertBundleFilename)
 | 
			
		||||
 | 
			
		||||
	realm := "https://auth.example.com/token/"
 | 
			
		||||
	issuer := "test-issuer.example.com"
 | 
			
		||||
	service := "test-service.example.com"
 | 
			
		||||
 | 
			
		||||
	options := map[string]interface{}{
 | 
			
		||||
		"realm":          realm,
 | 
			
		||||
		"issuer":         issuer,
 | 
			
		||||
		"service":        service,
 | 
			
		||||
		"rootCertBundle": rootCertBundleFilename,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessController, err := newAccessController(options)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 1. Make a mock http.Request with no token.
 | 
			
		||||
	req, err := http.NewRequest("GET", "http://example.com/foo", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testAccess := auth.Access{
 | 
			
		||||
		Resource: auth.Resource{
 | 
			
		||||
			Type: "foo",
 | 
			
		||||
			Name: "bar",
 | 
			
		||||
		},
 | 
			
		||||
		Action: "baz",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.WithValue(nil, "http.request", req)
 | 
			
		||||
	authCtx, err := accessController.Authorized(ctx, testAccess)
 | 
			
		||||
	challenge, ok := err.(auth.Challenge)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("accessController did not return a challenge")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if challenge.Error() != ErrTokenRequired.Error() {
 | 
			
		||||
		t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if authCtx != nil {
 | 
			
		||||
		t.Fatalf("expected nil auth context but got %s", authCtx)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2. Supply an invalid token.
 | 
			
		||||
	token, err := makeTestToken(
 | 
			
		||||
		issuer, service,
 | 
			
		||||
		[]*ResourceActions{{
 | 
			
		||||
			Type:    testAccess.Type,
 | 
			
		||||
			Name:    testAccess.Name,
 | 
			
		||||
			Actions: []string{testAccess.Action},
 | 
			
		||||
		}},
 | 
			
		||||
		rootKeys[1], 1, // Everything is valid except the key which signed it.
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
 | 
			
		||||
 | 
			
		||||
	authCtx, err = accessController.Authorized(ctx, testAccess)
 | 
			
		||||
	challenge, ok = err.(auth.Challenge)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("accessController did not return a challenge")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if challenge.Error() != ErrInvalidToken.Error() {
 | 
			
		||||
		t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if authCtx != nil {
 | 
			
		||||
		t.Fatalf("expected nil auth context but got %s", authCtx)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. Supply a token with insufficient access.
 | 
			
		||||
	token, err = makeTestToken(
 | 
			
		||||
		issuer, service,
 | 
			
		||||
		[]*ResourceActions{}, // No access specified.
 | 
			
		||||
		rootKeys[0], 1,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
 | 
			
		||||
 | 
			
		||||
	authCtx, err = accessController.Authorized(ctx, testAccess)
 | 
			
		||||
	challenge, ok = err.(auth.Challenge)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("accessController did not return a challenge")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if challenge.Error() != ErrInsufficientScope.Error() {
 | 
			
		||||
		t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if authCtx != nil {
 | 
			
		||||
		t.Fatalf("expected nil auth context but got %s", authCtx)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4. Supply the token we need, or deserve, or whatever.
 | 
			
		||||
	token, err = makeTestToken(
 | 
			
		||||
		issuer, service,
 | 
			
		||||
		[]*ResourceActions{{
 | 
			
		||||
			Type:    testAccess.Type,
 | 
			
		||||
			Name:    testAccess.Name,
 | 
			
		||||
			Actions: []string{testAccess.Action},
 | 
			
		||||
		}},
 | 
			
		||||
		rootKeys[0], 1,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
 | 
			
		||||
 | 
			
		||||
	authCtx, err = accessController.Authorized(ctx, testAccess)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("accessController returned unexpected error: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userInfo, ok := authCtx.Value("auth.user").(auth.UserInfo)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("token accessController did not set auth.user context")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userInfo.Name != "foo" {
 | 
			
		||||
		t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
package token
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// joseBase64UrlEncode encodes the given data using the standard base64 url
 | 
			
		||||
// encoding format but with all trailing '=' characters ommitted in accordance
 | 
			
		||||
// with the jose specification.
 | 
			
		||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
 | 
			
		||||
func joseBase64UrlEncode(b []byte) string {
 | 
			
		||||
	return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// joseBase64UrlDecode decodes the given string using the standard base64 url
 | 
			
		||||
// decoder but first adds the appropriate number of trailing '=' characters in
 | 
			
		||||
// accordance with the jose specification.
 | 
			
		||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
 | 
			
		||||
func joseBase64UrlDecode(s string) ([]byte, error) {
 | 
			
		||||
	switch len(s) % 4 {
 | 
			
		||||
	case 0:
 | 
			
		||||
	case 2:
 | 
			
		||||
		s += "=="
 | 
			
		||||
	case 3:
 | 
			
		||||
		s += "="
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, errors.New("illegal base64url string")
 | 
			
		||||
	}
 | 
			
		||||
	return base64.URLEncoding.DecodeString(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// actionSet is a special type of stringSet.
 | 
			
		||||
type actionSet struct {
 | 
			
		||||
	stringSet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newActionSet(actions ...string) actionSet {
 | 
			
		||||
	return actionSet{newStringSet(actions...)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Contains calls StringSet.Contains() for
 | 
			
		||||
// either "*" or the given action string.
 | 
			
		||||
func (s actionSet) contains(action string) bool {
 | 
			
		||||
	return s.stringSet.contains("*") || s.stringSet.contains(action)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// contains returns true if q is found in ss.
 | 
			
		||||
func contains(ss []string, q string) bool {
 | 
			
		||||
	for _, s := range ss {
 | 
			
		||||
		if s == q {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.google.com/p/go-uuid/uuid"
 | 
			
		||||
	"github.com/docker/distribution/registry/api/v2"
 | 
			
		||||
	"github.com/docker/distribution/auth"
 | 
			
		||||
	"github.com/docker/distribution/registry/auth"
 | 
			
		||||
	"github.com/docker/distribution/configuration"
 | 
			
		||||
	ctxu "github.com/docker/distribution/context"
 | 
			
		||||
	"github.com/docker/distribution/storage"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import (
 | 
			
		|||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/distribution/registry/api/v2"
 | 
			
		||||
	_ "github.com/docker/distribution/auth/silly"
 | 
			
		||||
	_ "github.com/docker/distribution/registry/auth/silly"
 | 
			
		||||
	"github.com/docker/distribution/configuration"
 | 
			
		||||
	"github.com/docker/distribution/storage"
 | 
			
		||||
	"github.com/docker/distribution/storagedriver/inmemory"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue