...

Source file src/github.com/docker/distribution/registry/auth/token/accesscontroller.go

Documentation: github.com/docker/distribution/registry/auth/token

     1  package token
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"crypto/x509"
     7  	"encoding/pem"
     8  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  
    15  	dcontext "github.com/docker/distribution/context"
    16  	"github.com/docker/distribution/registry/auth"
    17  	"github.com/docker/libtrust"
    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  	autoRedirect bool
    82  	service      string
    83  	accessSet    accessSet
    84  }
    85  
    86  var _ auth.Challenge = authChallenge{}
    87  
    88  // Error returns the internal error string for this authChallenge.
    89  func (ac authChallenge) Error() string {
    90  	return ac.err.Error()
    91  }
    92  
    93  // Status returns the HTTP Response Status Code for this authChallenge.
    94  func (ac authChallenge) Status() int {
    95  	return http.StatusUnauthorized
    96  }
    97  
    98  // challengeParams constructs the value to be used in
    99  // the WWW-Authenticate response challenge header.
   100  // See https://tools.ietf.org/html/rfc6750#section-3
   101  func (ac authChallenge) challengeParams(r *http.Request) string {
   102  	var realm string
   103  	if ac.autoRedirect {
   104  		realm = fmt.Sprintf("https://%s/auth/token", r.Host)
   105  	} else {
   106  		realm = ac.realm
   107  	}
   108  	str := fmt.Sprintf("Bearer realm=%q,service=%q", realm, ac.service)
   109  
   110  	if scope := ac.accessSet.scopeParam(); scope != "" {
   111  		str = fmt.Sprintf("%s,scope=%q", str, scope)
   112  	}
   113  
   114  	if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
   115  		str = fmt.Sprintf("%s,error=%q", str, "invalid_token")
   116  	} else if ac.err == ErrInsufficientScope {
   117  		str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
   118  	}
   119  
   120  	return str
   121  }
   122  
   123  // SetChallenge sets the WWW-Authenticate value for the response.
   124  func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
   125  	w.Header().Add("WWW-Authenticate", ac.challengeParams(r))
   126  }
   127  
   128  // accessController implements the auth.AccessController interface.
   129  type accessController struct {
   130  	realm        string
   131  	autoRedirect bool
   132  	issuer       string
   133  	service      string
   134  	rootCerts    *x509.CertPool
   135  	trustedKeys  map[string]libtrust.PublicKey
   136  }
   137  
   138  // tokenAccessOptions is a convenience type for handling
   139  // options to the contstructor of an accessController.
   140  type tokenAccessOptions struct {
   141  	realm          string
   142  	autoRedirect   bool
   143  	issuer         string
   144  	service        string
   145  	rootCertBundle string
   146  }
   147  
   148  // checkOptions gathers the necessary options
   149  // for an accessController from the given map.
   150  func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
   151  	var opts tokenAccessOptions
   152  
   153  	keys := []string{"realm", "issuer", "service", "rootcertbundle"}
   154  	vals := make([]string, 0, len(keys))
   155  	for _, key := range keys {
   156  		val, ok := options[key].(string)
   157  		if !ok {
   158  			return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
   159  		}
   160  		vals = append(vals, val)
   161  	}
   162  
   163  	opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
   164  
   165  	autoRedirectVal, ok := options["autoredirect"]
   166  	if ok {
   167  		autoRedirect, ok := autoRedirectVal.(bool)
   168  		if !ok {
   169  			return opts, fmt.Errorf("token auth requires a valid option bool: autoredirect")
   170  		}
   171  		opts.autoRedirect = autoRedirect
   172  	}
   173  
   174  	return opts, nil
   175  }
   176  
   177  // newAccessController creates an accessController using the given options.
   178  func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
   179  	config, err := checkOptions(options)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	fp, err := os.Open(config.rootCertBundle)
   185  	if err != nil {
   186  		return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
   187  	}
   188  	defer fp.Close()
   189  
   190  	rawCertBundle, err := ioutil.ReadAll(fp)
   191  	if err != nil {
   192  		return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
   193  	}
   194  
   195  	var rootCerts []*x509.Certificate
   196  	pemBlock, rawCertBundle := pem.Decode(rawCertBundle)
   197  	for pemBlock != nil {
   198  		if pemBlock.Type == "CERTIFICATE" {
   199  			cert, err := x509.ParseCertificate(pemBlock.Bytes)
   200  			if err != nil {
   201  				return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err)
   202  			}
   203  
   204  			rootCerts = append(rootCerts, cert)
   205  		}
   206  
   207  		pemBlock, rawCertBundle = pem.Decode(rawCertBundle)
   208  	}
   209  
   210  	if len(rootCerts) == 0 {
   211  		return nil, errors.New("token auth requires at least one token signing root certificate")
   212  	}
   213  
   214  	rootPool := x509.NewCertPool()
   215  	trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
   216  	for _, rootCert := range rootCerts {
   217  		rootPool.AddCert(rootCert)
   218  		pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey))
   219  		if err != nil {
   220  			return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err)
   221  		}
   222  		trustedKeys[pubKey.KeyID()] = pubKey
   223  	}
   224  
   225  	return &accessController{
   226  		realm:        config.realm,
   227  		autoRedirect: config.autoRedirect,
   228  		issuer:       config.issuer,
   229  		service:      config.service,
   230  		rootCerts:    rootPool,
   231  		trustedKeys:  trustedKeys,
   232  	}, nil
   233  }
   234  
   235  // Authorized handles checking whether the given request is authorized
   236  // for actions on resources described by the given access items.
   237  func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
   238  	challenge := &authChallenge{
   239  		realm:        ac.realm,
   240  		autoRedirect: ac.autoRedirect,
   241  		service:      ac.service,
   242  		accessSet:    newAccessSet(accessItems...),
   243  	}
   244  
   245  	req, err := dcontext.GetRequest(ctx)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	parts := strings.Split(req.Header.Get("Authorization"), " ")
   251  
   252  	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
   253  		challenge.err = ErrTokenRequired
   254  		return nil, challenge
   255  	}
   256  
   257  	rawToken := parts[1]
   258  
   259  	token, err := NewToken(rawToken)
   260  	if err != nil {
   261  		challenge.err = err
   262  		return nil, challenge
   263  	}
   264  
   265  	verifyOpts := VerifyOptions{
   266  		TrustedIssuers:    []string{ac.issuer},
   267  		AcceptedAudiences: []string{ac.service},
   268  		Roots:             ac.rootCerts,
   269  		TrustedKeys:       ac.trustedKeys,
   270  	}
   271  
   272  	if err = token.Verify(verifyOpts); err != nil {
   273  		challenge.err = err
   274  		return nil, challenge
   275  	}
   276  
   277  	accessSet := token.accessSet()
   278  	for _, access := range accessItems {
   279  		if !accessSet.contains(access) {
   280  			challenge.err = ErrInsufficientScope
   281  			return nil, challenge
   282  		}
   283  	}
   284  
   285  	ctx = auth.WithResources(ctx, token.resources())
   286  
   287  	return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil
   288  }
   289  
   290  // init handles registering the token auth backend.
   291  func init() {
   292  	auth.Register("token", auth.InitFunc(newAccessController))
   293  }
   294  

View as plain text