github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/auth/token/accesscontroller.go (about)

     1  package token
     2  
     3  import (
     4  	"crypto"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/docker/distribution/context"
    15  	"github.com/docker/distribution/registry/auth"
    16  	"github.com/docker/libtrust"
    17  )
    18  
    19  // accessSet maps a typed, named resource to
    20  // a set of actions requested or authorized.
    21  type accessSet map[auth.Resource]actionSet
    22  
    23  // newAccessSet constructs an accessSet from
    24  // a variable number of auth.Access items.
    25  func newAccessSet(accessItems ...auth.Access) accessSet {
    26  	accessSet := make(accessSet, len(accessItems))
    27  
    28  	for _, access := range accessItems {
    29  		resource := auth.Resource{
    30  			Type: access.Type,
    31  			Name: access.Name,
    32  		}
    33  
    34  		set, exists := accessSet[resource]
    35  		if !exists {
    36  			set = newActionSet()
    37  			accessSet[resource] = set
    38  		}
    39  
    40  		set.add(access.Action)
    41  	}
    42  
    43  	return accessSet
    44  }
    45  
    46  // contains returns whether or not the given access is in this accessSet.
    47  func (s accessSet) contains(access auth.Access) bool {
    48  	actionSet, ok := s[access.Resource]
    49  	if ok {
    50  		return actionSet.contains(access.Action)
    51  	}
    52  
    53  	return false
    54  }
    55  
    56  // scopeParam returns a collection of scopes which can
    57  // be used for a WWW-Authenticate challenge parameter.
    58  // See https://tools.ietf.org/html/rfc6750#section-3
    59  func (s accessSet) scopeParam() string {
    60  	scopes := make([]string, 0, len(s))
    61  
    62  	for resource, actionSet := range s {
    63  		actions := strings.Join(actionSet.keys(), ",")
    64  		scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions))
    65  	}
    66  
    67  	return strings.Join(scopes, " ")
    68  }
    69  
    70  // Errors used and exported by this package.
    71  var (
    72  	ErrInsufficientScope = errors.New("insufficient scope")
    73  	ErrTokenRequired     = errors.New("authorization token required")
    74  )
    75  
    76  // authChallenge implements the auth.Challenge interface.
    77  type authChallenge struct {
    78  	err       error
    79  	realm     string
    80  	service   string
    81  	accessSet accessSet
    82  }
    83  
    84  var _ auth.Challenge = authChallenge{}
    85  
    86  // Error returns the internal error string for this authChallenge.
    87  func (ac authChallenge) Error() string {
    88  	return ac.err.Error()
    89  }
    90  
    91  // Status returns the HTTP Response Status Code for this authChallenge.
    92  func (ac authChallenge) Status() int {
    93  	return http.StatusUnauthorized
    94  }
    95  
    96  // challengeParams constructs the value to be used in
    97  // the WWW-Authenticate response challenge header.
    98  // See https://tools.ietf.org/html/rfc6750#section-3
    99  func (ac authChallenge) challengeParams() string {
   100  	str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service)
   101  
   102  	if scope := ac.accessSet.scopeParam(); scope != "" {
   103  		str = fmt.Sprintf("%s,scope=%q", str, scope)
   104  	}
   105  
   106  	if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
   107  		str = fmt.Sprintf("%s,error=%q", str, "invalid_token")
   108  	} else if ac.err == ErrInsufficientScope {
   109  		str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
   110  	}
   111  
   112  	return str
   113  }
   114  
   115  // SetChallenge sets the WWW-Authenticate value for the response.
   116  func (ac authChallenge) SetHeaders(w http.ResponseWriter) {
   117  	w.Header().Add("WWW-Authenticate", ac.challengeParams())
   118  }
   119  
   120  // accessController implements the auth.AccessController interface.
   121  type accessController struct {
   122  	realm       string
   123  	issuer      string
   124  	service     string
   125  	rootCerts   *x509.CertPool
   126  	trustedKeys map[string]libtrust.PublicKey
   127  }
   128  
   129  // tokenAccessOptions is a convenience type for handling
   130  // options to the contstructor of an accessController.
   131  type tokenAccessOptions struct {
   132  	realm          string
   133  	issuer         string
   134  	service        string
   135  	rootCertBundle string
   136  }
   137  
   138  // checkOptions gathers the necessary options
   139  // for an accessController from the given map.
   140  func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
   141  	var opts tokenAccessOptions
   142  
   143  	keys := []string{"realm", "issuer", "service", "rootcertbundle"}
   144  	vals := make([]string, 0, len(keys))
   145  	for _, key := range keys {
   146  		val, ok := options[key].(string)
   147  		if !ok {
   148  			return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
   149  		}
   150  		vals = append(vals, val)
   151  	}
   152  
   153  	opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
   154  
   155  	return opts, nil
   156  }
   157  
   158  // newAccessController creates an accessController using the given options.
   159  func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
   160  	config, err := checkOptions(options)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	fp, err := os.Open(config.rootCertBundle)
   166  	if err != nil {
   167  		return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
   168  	}
   169  	defer fp.Close()
   170  
   171  	rawCertBundle, err := ioutil.ReadAll(fp)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
   174  	}
   175  
   176  	var rootCerts []*x509.Certificate
   177  	pemBlock, rawCertBundle := pem.Decode(rawCertBundle)
   178  	for pemBlock != nil {
   179  		cert, err := x509.ParseCertificate(pemBlock.Bytes)
   180  		if err != nil {
   181  			return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err)
   182  		}
   183  
   184  		rootCerts = append(rootCerts, cert)
   185  
   186  		pemBlock, rawCertBundle = pem.Decode(rawCertBundle)
   187  	}
   188  
   189  	if len(rootCerts) == 0 {
   190  		return nil, errors.New("token auth requires at least one token signing root certificate")
   191  	}
   192  
   193  	rootPool := x509.NewCertPool()
   194  	trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
   195  	for _, rootCert := range rootCerts {
   196  		rootPool.AddCert(rootCert)
   197  		pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey))
   198  		if err != nil {
   199  			return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err)
   200  		}
   201  		trustedKeys[pubKey.KeyID()] = pubKey
   202  	}
   203  
   204  	return &accessController{
   205  		realm:       config.realm,
   206  		issuer:      config.issuer,
   207  		service:     config.service,
   208  		rootCerts:   rootPool,
   209  		trustedKeys: trustedKeys,
   210  	}, nil
   211  }
   212  
   213  // Authorized handles checking whether the given request is authorized
   214  // for actions on resources described by the given access items.
   215  func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
   216  	challenge := &authChallenge{
   217  		realm:     ac.realm,
   218  		service:   ac.service,
   219  		accessSet: newAccessSet(accessItems...),
   220  	}
   221  
   222  	req, err := context.GetRequest(ctx)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	parts := strings.Split(req.Header.Get("Authorization"), " ")
   228  
   229  	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
   230  		challenge.err = ErrTokenRequired
   231  		return nil, challenge
   232  	}
   233  
   234  	rawToken := parts[1]
   235  
   236  	token, err := NewToken(rawToken)
   237  	if err != nil {
   238  		challenge.err = err
   239  		return nil, challenge
   240  	}
   241  
   242  	verifyOpts := VerifyOptions{
   243  		TrustedIssuers:    []string{ac.issuer},
   244  		AcceptedAudiences: []string{ac.service},
   245  		Roots:             ac.rootCerts,
   246  		TrustedKeys:       ac.trustedKeys,
   247  	}
   248  
   249  	if err = token.Verify(verifyOpts); err != nil {
   250  		challenge.err = err
   251  		return nil, challenge
   252  	}
   253  
   254  	accessSet := token.accessSet()
   255  	for _, access := range accessItems {
   256  		if !accessSet.contains(access) {
   257  			challenge.err = ErrInsufficientScope
   258  			return nil, challenge
   259  		}
   260  	}
   261  
   262  	return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil
   263  }
   264  
   265  // init handles registering the token auth backend.
   266  func init() {
   267  	auth.Register("token", auth.InitFunc(newAccessController))
   268  }