202 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			202 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"flag"
 | 
						|
	"net/http"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/Sirupsen/logrus"
 | 
						|
	"github.com/docker/distribution/context"
 | 
						|
	"github.com/docker/distribution/registry/api/errcode"
 | 
						|
	"github.com/docker/distribution/registry/auth"
 | 
						|
	_ "github.com/docker/distribution/registry/auth/htpasswd"
 | 
						|
	"github.com/docker/libtrust"
 | 
						|
	"github.com/gorilla/mux"
 | 
						|
)
 | 
						|
 | 
						|
func main() {
 | 
						|
	var (
 | 
						|
		issuer = &TokenIssuer{}
 | 
						|
		pkFile string
 | 
						|
		addr   string
 | 
						|
		debug  bool
 | 
						|
		err    error
 | 
						|
 | 
						|
		passwdFile string
 | 
						|
		realm      string
 | 
						|
 | 
						|
		cert    string
 | 
						|
		certKey string
 | 
						|
	)
 | 
						|
 | 
						|
	flag.StringVar(&issuer.Issuer, "issuer", "distribution-token-server", "Issuer string for token")
 | 
						|
	flag.StringVar(&pkFile, "key", "", "Private key file")
 | 
						|
	flag.StringVar(&addr, "addr", "localhost:8080", "Address to listen on")
 | 
						|
	flag.BoolVar(&debug, "debug", false, "Debug mode")
 | 
						|
 | 
						|
	flag.StringVar(&passwdFile, "passwd", ".htpasswd", "Passwd file")
 | 
						|
	flag.StringVar(&realm, "realm", "", "Authentication realm")
 | 
						|
 | 
						|
	flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS")
 | 
						|
	flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS")
 | 
						|
 | 
						|
	flag.Parse()
 | 
						|
 | 
						|
	if debug {
 | 
						|
		logrus.SetLevel(logrus.DebugLevel)
 | 
						|
	}
 | 
						|
 | 
						|
	if pkFile == "" {
 | 
						|
		issuer.SigningKey, err = libtrust.GenerateECP256PrivateKey()
 | 
						|
		if err != nil {
 | 
						|
			logrus.Fatalf("Error generating private key: %v", err)
 | 
						|
		}
 | 
						|
		logrus.Debugf("Using newly generated key with id %s", issuer.SigningKey.KeyID())
 | 
						|
	} else {
 | 
						|
		issuer.SigningKey, err = libtrust.LoadKeyFile(pkFile)
 | 
						|
		if err != nil {
 | 
						|
			logrus.Fatalf("Error loading key file %s: %v", pkFile, err)
 | 
						|
		}
 | 
						|
		logrus.Debugf("Loaded private key with id %s", issuer.SigningKey.KeyID())
 | 
						|
	}
 | 
						|
 | 
						|
	if realm == "" {
 | 
						|
		logrus.Fatalf("Must provide realm")
 | 
						|
	}
 | 
						|
 | 
						|
	ac, err := auth.GetAccessController("htpasswd", map[string]interface{}{
 | 
						|
		"realm": realm,
 | 
						|
		"path":  passwdFile,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		logrus.Fatalf("Error initializing access controller: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	ctx := context.Background()
 | 
						|
 | 
						|
	ts := &tokenServer{
 | 
						|
		issuer:           issuer,
 | 
						|
		accessController: ac,
 | 
						|
	}
 | 
						|
 | 
						|
	router := mux.NewRouter()
 | 
						|
	router.Path("/token/").Methods("GET").Handler(handlerWithContext(ctx, ts.getToken))
 | 
						|
 | 
						|
	if cert == "" {
 | 
						|
		err = http.ListenAndServe(addr, router)
 | 
						|
	} else if certKey == "" {
 | 
						|
		logrus.Fatalf("Must provide certficate (-tlscert) and key (-tlskey)")
 | 
						|
	} else {
 | 
						|
		err = http.ListenAndServeTLS(addr, cert, certKey, router)
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		logrus.Infof("Error serving: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
// handlerWithContext wraps the given context-aware handler by setting up the
 | 
						|
// request context from a base context.
 | 
						|
func handlerWithContext(ctx context.Context, handler func(context.Context, http.ResponseWriter, *http.Request)) http.Handler {
 | 
						|
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		ctx := context.WithRequest(ctx, r)
 | 
						|
		logger := context.GetRequestLogger(ctx)
 | 
						|
		ctx = context.WithLogger(ctx, logger)
 | 
						|
 | 
						|
		handler(ctx, w, r)
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func handleError(ctx context.Context, err error, w http.ResponseWriter) {
 | 
						|
	ctx, w = context.WithResponseWriter(ctx, w)
 | 
						|
 | 
						|
	if serveErr := errcode.ServeJSON(w, err); serveErr != nil {
 | 
						|
		context.GetResponseLogger(ctx).Errorf("error sending error response: %v", serveErr)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	context.GetResponseLogger(ctx).Info("application error")
 | 
						|
}
 | 
						|
 | 
						|
type tokenServer struct {
 | 
						|
	issuer           *TokenIssuer
 | 
						|
	accessController auth.AccessController
 | 
						|
}
 | 
						|
 | 
						|
// getToken handles authenticating the request and authorizing access to the
 | 
						|
// requested scopes.
 | 
						|
func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *http.Request) {
 | 
						|
	context.GetLogger(ctx).Info("getToken")
 | 
						|
 | 
						|
	params := r.URL.Query()
 | 
						|
	service := params.Get("service")
 | 
						|
	scopeSpecifiers := params["scope"]
 | 
						|
 | 
						|
	requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers)
 | 
						|
 | 
						|
	authorizedCtx, err := ts.accessController.Authorized(ctx, requestedAccessList...)
 | 
						|
	if err != nil {
 | 
						|
		challenge, ok := err.(auth.Challenge)
 | 
						|
		if !ok {
 | 
						|
			handleError(ctx, err, w)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Get response context.
 | 
						|
		ctx, w = context.WithResponseWriter(ctx, w)
 | 
						|
 | 
						|
		challenge.SetHeaders(w)
 | 
						|
		handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail(challenge.Error()), w)
 | 
						|
 | 
						|
		context.GetResponseLogger(ctx).Info("get token authentication challenge")
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
	ctx = authorizedCtx
 | 
						|
 | 
						|
	username := context.GetStringValue(ctx, "auth.user.name")
 | 
						|
 | 
						|
	ctx = context.WithValue(ctx, "acctSubject", username)
 | 
						|
	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "acctSubject"))
 | 
						|
 | 
						|
	context.GetLogger(ctx).Info("authenticated client")
 | 
						|
 | 
						|
	ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList)
 | 
						|
	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess"))
 | 
						|
 | 
						|
	scopePrefix := username + "/"
 | 
						|
	grantedAccessList := make([]auth.Access, 0, len(requestedAccessList))
 | 
						|
	for _, access := range requestedAccessList {
 | 
						|
		if access.Type != "repository" {
 | 
						|
			context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if !strings.HasPrefix(access.Name, scopePrefix) {
 | 
						|
			context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		grantedAccessList = append(grantedAccessList, access)
 | 
						|
	}
 | 
						|
 | 
						|
	ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList)
 | 
						|
	ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess"))
 | 
						|
 | 
						|
	token, err := ts.issuer.CreateJWT(username, service, grantedAccessList)
 | 
						|
	if err != nil {
 | 
						|
		handleError(ctx, err, w)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	context.GetLogger(ctx).Info("authorized client")
 | 
						|
 | 
						|
	// Get response context.
 | 
						|
	ctx, w = context.WithResponseWriter(ctx, w)
 | 
						|
 | 
						|
	w.Header().Set("Content-Type", "application/json")
 | 
						|
	json.NewEncoder(w).Encode(map[string]string{"token": token})
 | 
						|
 | 
						|
	context.GetResponseLogger(ctx).Info("get token complete")
 | 
						|
}
 |