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