forked from TrueCloudLab/distribution
2c3d738a05
The auth package has been updated to use "golang.org/x/net/context" for passing information between the application and the auth backend. AccessControllers should now set a "auth.user" context value to a AuthUser struct containing a single "Name" field for now with possible, optional, values in the future. The "silly" auth backend always sets the name to "silly", while the "token" auth backend will set the name to match the "subject" claim of the JWT. Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
273 lines
7.3 KiB
Go
273 lines
7.3 KiB
Go
package token
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/auth"
|
|
"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 := auth.RequestFromContext(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 context.WithValue(ctx, "auth.user", auth.UserInfo{Name: token.Claims.Subject}), nil
|
|
}
|
|
|
|
// init handles registering the token auth backend.
|
|
func init() {
|
|
auth.Register("token", auth.InitFunc(newAccessController))
|
|
}
|