vendor: update notary and deps for new trust commands
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
202
vendor/github.com/docker/distribution/registry/auth/auth.go
generated
vendored
Normal file
202
vendor/github.com/docker/distribution/registry/auth/auth.go
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
||||
// Package auth defines a standard interface for request access controllers.
|
||||
//
|
||||
// An access controller has a simple interface with a single `Authorized`
|
||||
// method which checks that a given request is authorized to perform one or
|
||||
// more actions on one or more resources. This method should return a non-nil
|
||||
// error if the request is not authorized.
|
||||
//
|
||||
// An implementation registers its access controller by name with a constructor
|
||||
// which accepts an options map for configuring the access controller.
|
||||
//
|
||||
// options := map[string]interface{}{"sillySecret": "whysosilly?"}
|
||||
// accessController, _ := auth.GetAccessController("silly", options)
|
||||
//
|
||||
// This `accessController` can then be used in a request handler like so:
|
||||
//
|
||||
// func updateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
// orderNumber := r.FormValue("orderNumber")
|
||||
// resource := auth.Resource{Type: "customerOrder", Name: orderNumber}
|
||||
// access := auth.Access{Resource: resource, Action: "update"}
|
||||
//
|
||||
// if ctx, err := accessController.Authorized(ctx, access); err != nil {
|
||||
// if challenge, ok := err.(auth.Challenge) {
|
||||
// // Let the challenge write the response.
|
||||
// challenge.SetHeaders(w)
|
||||
// w.WriteHeader(http.StatusUnauthorized)
|
||||
// return
|
||||
// } else {
|
||||
// // Some other error.
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// UserKey is used to get the user object from
|
||||
// a user context
|
||||
UserKey = "auth.user"
|
||||
|
||||
// UserNameKey is used to get the user name from
|
||||
// a user context
|
||||
UserNameKey = "auth.user.name"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidCredential is returned when the auth token does not authenticate correctly.
|
||||
ErrInvalidCredential = errors.New("invalid authorization credential")
|
||||
|
||||
// ErrAuthenticationFailure returned when authentication fails.
|
||||
ErrAuthenticationFailure = errors.New("authentication failure")
|
||||
)
|
||||
|
||||
// UserInfo carries information about
|
||||
// an autenticated/authorized client.
|
||||
type UserInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Resource describes a resource by type and name.
|
||||
type Resource struct {
|
||||
Type string
|
||||
Class string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Access describes a specific action that is
|
||||
// requested or allowed for a given resource.
|
||||
type Access struct {
|
||||
Resource
|
||||
Action string
|
||||
}
|
||||
|
||||
// Challenge is a special error type which is used for HTTP 401 Unauthorized
|
||||
// responses and is able to write the response with WWW-Authenticate challenge
|
||||
// header values based on the error.
|
||||
type Challenge interface {
|
||||
error
|
||||
|
||||
// SetHeaders prepares the request to conduct a challenge response by
|
||||
// adding the an HTTP challenge header on the response message. Callers
|
||||
// are expected to set the appropriate HTTP status code (e.g. 401)
|
||||
// themselves.
|
||||
SetHeaders(w http.ResponseWriter)
|
||||
}
|
||||
|
||||
// AccessController controls access to registry resources based on a request
|
||||
// and required access levels for a request. Implementations can support both
|
||||
// complete denial and http authorization challenges.
|
||||
type AccessController interface {
|
||||
// Authorized returns a non-nil error if the context is granted access and
|
||||
// returns a new authorized context. If one or more Access structs are
|
||||
// provided, the requested access will be compared with what is available
|
||||
// to the context. The given context will contain a "http.request" key with
|
||||
// a `*http.Request` value. If the error is non-nil, access should always
|
||||
// be denied. The error may be of type Challenge, in which case the caller
|
||||
// may have the Challenge handle the request or choose what action to take
|
||||
// based on the Challenge header or response status. The returned context
|
||||
// object should have a "auth.user" value set to a UserInfo struct.
|
||||
Authorized(ctx context.Context, access ...Access) (context.Context, error)
|
||||
}
|
||||
|
||||
// CredentialAuthenticator is an object which is able to authenticate credentials
|
||||
type CredentialAuthenticator interface {
|
||||
AuthenticateUser(username, password string) error
|
||||
}
|
||||
|
||||
// WithUser returns a context with the authorized user info.
|
||||
func WithUser(ctx context.Context, user UserInfo) context.Context {
|
||||
return userInfoContext{
|
||||
Context: ctx,
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
|
||||
type userInfoContext struct {
|
||||
context.Context
|
||||
user UserInfo
|
||||
}
|
||||
|
||||
func (uic userInfoContext) Value(key interface{}) interface{} {
|
||||
switch key {
|
||||
case UserKey:
|
||||
return uic.user
|
||||
case UserNameKey:
|
||||
return uic.user.Name
|
||||
}
|
||||
|
||||
return uic.Context.Value(key)
|
||||
}
|
||||
|
||||
// WithResources returns a context with the authorized resources.
|
||||
func WithResources(ctx context.Context, resources []Resource) context.Context {
|
||||
return resourceContext{
|
||||
Context: ctx,
|
||||
resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
type resourceContext struct {
|
||||
context.Context
|
||||
resources []Resource
|
||||
}
|
||||
|
||||
type resourceKey struct{}
|
||||
|
||||
func (rc resourceContext) Value(key interface{}) interface{} {
|
||||
if key == (resourceKey{}) {
|
||||
return rc.resources
|
||||
}
|
||||
|
||||
return rc.Context.Value(key)
|
||||
}
|
||||
|
||||
// AuthorizedResources returns the list of resources which have
|
||||
// been authorized for this request.
|
||||
func AuthorizedResources(ctx context.Context) []Resource {
|
||||
if resources, ok := ctx.Value(resourceKey{}).([]Resource); ok {
|
||||
return resources
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitFunc is the type of an AccessController factory function and is used
|
||||
// to register the constructor for different AccesController backends.
|
||||
type InitFunc func(options map[string]interface{}) (AccessController, error)
|
||||
|
||||
var accessControllers map[string]InitFunc
|
||||
|
||||
func init() {
|
||||
accessControllers = make(map[string]InitFunc)
|
||||
}
|
||||
|
||||
// Register is used to register an InitFunc for
|
||||
// an AccessController backend with the given name.
|
||||
func Register(name string, initFunc InitFunc) error {
|
||||
if _, exists := accessControllers[name]; exists {
|
||||
return fmt.Errorf("name already registered: %s", name)
|
||||
}
|
||||
|
||||
accessControllers[name] = initFunc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccessController constructs an AccessController
|
||||
// with the given options using the named backend.
|
||||
func GetAccessController(name string, options map[string]interface{}) (AccessController, error) {
|
||||
if initFunc, exists := accessControllers[name]; exists {
|
||||
return initFunc(options)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no access controller registered with name: %s", name)
|
||||
}
|
||||
277
vendor/github.com/docker/notary/utils/configuration.go
generated
vendored
Normal file
277
vendor/github.com/docker/notary/utils/configuration.go
generated
vendored
Normal file
@ -0,0 +1,277 @@
|
||||
// Common configuration elements that may be resused
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
bugsnag_hook "github.com/Shopify/logrus-bugsnag"
|
||||
"github.com/bugsnag/bugsnag-go"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/docker/notary"
|
||||
)
|
||||
|
||||
// Storage is a configuration about what storage backend a server should use
|
||||
type Storage struct {
|
||||
Backend string
|
||||
Source string
|
||||
}
|
||||
|
||||
// RethinkDBStorage is configuration about a RethinkDB backend service
|
||||
type RethinkDBStorage struct {
|
||||
Storage
|
||||
CA string
|
||||
Cert string
|
||||
DBName string
|
||||
Key string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// GetPathRelativeToConfig gets a configuration key which is a path, and if
|
||||
// it is not empty or an absolute path, returns the absolute path relative
|
||||
// to the configuration file
|
||||
func GetPathRelativeToConfig(configuration *viper.Viper, key string) string {
|
||||
configFile := configuration.ConfigFileUsed()
|
||||
p := configuration.GetString(key)
|
||||
if p == "" || filepath.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(configFile), p))
|
||||
}
|
||||
|
||||
// ParseServerTLS tries to parse out valid server TLS options from a Viper.
|
||||
// The cert/key files are relative to the config file used to populate the instance
|
||||
// of viper.
|
||||
func ParseServerTLS(configuration *viper.Viper, tlsRequired bool) (*tls.Config, error) {
|
||||
// unmarshalling into objects does not seem to pick up env vars
|
||||
tlsOpts := tlsconfig.Options{
|
||||
CertFile: GetPathRelativeToConfig(configuration, "server.tls_cert_file"),
|
||||
KeyFile: GetPathRelativeToConfig(configuration, "server.tls_key_file"),
|
||||
CAFile: GetPathRelativeToConfig(configuration, "server.client_ca_file"),
|
||||
ExclusiveRootPools: true,
|
||||
}
|
||||
if tlsOpts.CAFile != "" {
|
||||
tlsOpts.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
if !tlsRequired {
|
||||
cert, key, ca := tlsOpts.CertFile, tlsOpts.KeyFile, tlsOpts.CAFile
|
||||
if cert == "" && key == "" && ca == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if (cert == "" && key != "") || (cert != "" && key == "") || (cert == "" && key == "" && ca != "") {
|
||||
return nil, fmt.Errorf(
|
||||
"either include both a cert and key file, or no TLS information at all to disable TLS")
|
||||
}
|
||||
}
|
||||
|
||||
return tlsconfig.Server(tlsOpts)
|
||||
}
|
||||
|
||||
// ParseLogLevel tries to parse out a log level from a Viper. If there is no
|
||||
// configuration, defaults to the provided error level
|
||||
func ParseLogLevel(configuration *viper.Viper, defaultLevel logrus.Level) (
|
||||
logrus.Level, error) {
|
||||
|
||||
logStr := configuration.GetString("logging.level")
|
||||
if logStr == "" {
|
||||
return defaultLevel, nil
|
||||
}
|
||||
return logrus.ParseLevel(logStr)
|
||||
}
|
||||
|
||||
// ParseSQLStorage tries to parse out Storage from a Viper. If backend and
|
||||
// URL are not provided, returns a nil pointer. Storage is required (if
|
||||
// a backend is not provided, an error will be returned.)
|
||||
func ParseSQLStorage(configuration *viper.Viper) (*Storage, error) {
|
||||
store := Storage{
|
||||
Backend: configuration.GetString("storage.backend"),
|
||||
Source: configuration.GetString("storage.db_url"),
|
||||
}
|
||||
|
||||
switch {
|
||||
case store.Backend != notary.MySQLBackend && store.Backend != notary.SQLiteBackend && store.Backend != notary.PostgresBackend:
|
||||
return nil, fmt.Errorf(
|
||||
"%s is not a supported SQL backend driver",
|
||||
store.Backend,
|
||||
)
|
||||
case store.Source == "":
|
||||
return nil, fmt.Errorf(
|
||||
"must provide a non-empty database source for %s",
|
||||
store.Backend,
|
||||
)
|
||||
case store.Backend == notary.MySQLBackend:
|
||||
urlConfig, err := mysql.ParseDSN(store.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse the database source for %s",
|
||||
store.Backend,
|
||||
)
|
||||
}
|
||||
|
||||
urlConfig.ParseTime = true
|
||||
store.Source = urlConfig.FormatDSN()
|
||||
}
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
// ParseRethinkDBStorage tries to parse out Storage from a Viper. If backend and
|
||||
// URL are not provided, returns a nil pointer. Storage is required (if
|
||||
// a backend is not provided, an error will be returned.)
|
||||
func ParseRethinkDBStorage(configuration *viper.Viper) (*RethinkDBStorage, error) {
|
||||
store := RethinkDBStorage{
|
||||
Storage: Storage{
|
||||
Backend: configuration.GetString("storage.backend"),
|
||||
Source: configuration.GetString("storage.db_url"),
|
||||
},
|
||||
CA: GetPathRelativeToConfig(configuration, "storage.tls_ca_file"),
|
||||
Cert: GetPathRelativeToConfig(configuration, "storage.client_cert_file"),
|
||||
Key: GetPathRelativeToConfig(configuration, "storage.client_key_file"),
|
||||
DBName: configuration.GetString("storage.database"),
|
||||
Username: configuration.GetString("storage.username"),
|
||||
Password: configuration.GetString("storage.password"),
|
||||
}
|
||||
|
||||
switch {
|
||||
case store.Backend != notary.RethinkDBBackend:
|
||||
return nil, fmt.Errorf(
|
||||
"%s is not a supported RethinkDB backend driver",
|
||||
store.Backend,
|
||||
)
|
||||
case store.Source == "":
|
||||
return nil, fmt.Errorf(
|
||||
"must provide a non-empty host:port for %s",
|
||||
store.Backend,
|
||||
)
|
||||
case store.CA == "":
|
||||
return nil, fmt.Errorf(
|
||||
"cowardly refusal to connect to %s without a CA cert",
|
||||
store.Backend,
|
||||
)
|
||||
case store.Cert == "" || store.Key == "":
|
||||
return nil, fmt.Errorf(
|
||||
"cowardly refusal to connect to %s without a client cert and key",
|
||||
store.Backend,
|
||||
)
|
||||
case store.DBName == "":
|
||||
return nil, fmt.Errorf(
|
||||
"%s requires a specific database to connect to",
|
||||
store.Backend,
|
||||
)
|
||||
case store.Username == "":
|
||||
return nil, fmt.Errorf(
|
||||
"%s requires a username to connect to the db",
|
||||
store.Backend,
|
||||
)
|
||||
}
|
||||
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
// ParseBugsnag tries to parse out a Bugsnag Configuration from a Viper.
|
||||
// If no values are provided, returns a nil pointer.
|
||||
func ParseBugsnag(configuration *viper.Viper) (*bugsnag.Configuration, error) {
|
||||
// can't unmarshal because we can't add tags to the bugsnag.Configuration
|
||||
// struct
|
||||
bugconf := bugsnag.Configuration{
|
||||
APIKey: configuration.GetString("reporting.bugsnag.api_key"),
|
||||
ReleaseStage: configuration.GetString("reporting.bugsnag.release_stage"),
|
||||
Endpoint: configuration.GetString("reporting.bugsnag.endpoint"),
|
||||
}
|
||||
if bugconf.APIKey == "" && bugconf.ReleaseStage == "" && bugconf.Endpoint == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if bugconf.APIKey == "" {
|
||||
return nil, fmt.Errorf("must provide an API key for bugsnag")
|
||||
}
|
||||
return &bugconf, nil
|
||||
}
|
||||
|
||||
// utilities for setting up/acting on common configurations
|
||||
|
||||
// SetupViper sets up an instance of viper to also look at environment
|
||||
// variables
|
||||
func SetupViper(v *viper.Viper, envPrefix string) {
|
||||
v.SetEnvPrefix(envPrefix)
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
}
|
||||
|
||||
// SetUpBugsnag configures bugsnag and sets up a logrus hook
|
||||
func SetUpBugsnag(config *bugsnag.Configuration) error {
|
||||
if config != nil {
|
||||
bugsnag.Configure(*config)
|
||||
hook, err := bugsnag_hook.NewBugsnagHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.AddHook(hook)
|
||||
logrus.Debug("Adding logrus hook for Bugsnag")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseViper tries to parse out a Viper from a configuration file.
|
||||
func ParseViper(v *viper.Viper, configFile string) error {
|
||||
filename := filepath.Base(configFile)
|
||||
ext := filepath.Ext(configFile)
|
||||
configPath := filepath.Dir(configFile)
|
||||
|
||||
v.SetConfigType(strings.TrimPrefix(ext, "."))
|
||||
v.SetConfigName(strings.TrimSuffix(filename, ext))
|
||||
v.AddConfigPath(configPath)
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("Could not read config at :%s, viper error: %v", configFile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdjustLogLevel increases/decreases the log level, return error if the operation is invaild.
|
||||
func AdjustLogLevel(increment bool) error {
|
||||
lvl := logrus.GetLevel()
|
||||
|
||||
// The log level seems not possible, in the foreseeable future,
|
||||
// out of range [Panic, Debug]
|
||||
if increment {
|
||||
if lvl == logrus.DebugLevel {
|
||||
return fmt.Errorf("log level can not be set higher than %s", "Debug")
|
||||
}
|
||||
lvl++
|
||||
} else {
|
||||
if lvl == logrus.PanicLevel {
|
||||
return fmt.Errorf("log level can not be set lower than %s", "Panic")
|
||||
}
|
||||
lvl--
|
||||
}
|
||||
|
||||
logrus.SetLevel(lvl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupSignalTrap is a utility to trap supported signals hand handle them (currently by increasing logging)
|
||||
func SetupSignalTrap(handler func(os.Signal)) chan os.Signal {
|
||||
if len(notary.NotarySupportedSignals) == 0 {
|
||||
return nil
|
||||
|
||||
}
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, notary.NotarySupportedSignals...)
|
||||
go func() {
|
||||
for {
|
||||
handler(<-c)
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
29
vendor/github.com/docker/notary/utils/configuration_nowindows.go
generated
vendored
Normal file
29
vendor/github.com/docker/notary/utils/configuration_nowindows.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
// +build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LogLevelSignalHandle will increase/decrease the logging level via the signal we get.
|
||||
func LogLevelSignalHandle(sig os.Signal) {
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
if err := AdjustLogLevel(true); err != nil {
|
||||
fmt.Printf("Attempt to increase log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err)
|
||||
return
|
||||
}
|
||||
case syscall.SIGUSR2:
|
||||
if err := AdjustLogLevel(false); err != nil {
|
||||
fmt.Printf("Attempt to decrease log level failed, will remain at %s level, error: %s\n", logrus.GetLevel(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Successfully setting log level to", logrus.GetLevel())
|
||||
}
|
||||
9
vendor/github.com/docker/notary/utils/configuration_windows.go
generated
vendored
Normal file
9
vendor/github.com/docker/notary/utils/configuration_windows.go
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
// LogLevelSignalHandle will do nothing, because we aren't currently supporting signal handling in windows
|
||||
func LogLevelSignalHandle(sig os.Signal) {
|
||||
}
|
||||
252
vendor/github.com/docker/notary/utils/http.go
generated
vendored
Normal file
252
vendor/github.com/docker/notary/utils/http.go
generated
vendored
Normal file
@ -0,0 +1,252 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/auth"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
)
|
||||
|
||||
// ContextHandler defines an alternate HTTP handler interface which takes in
|
||||
// a context for authorization and returns an HTTP application error.
|
||||
type ContextHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
// rootHandler is an implementation of an HTTP request handler which handles
|
||||
// authorization and calling out to the defined alternate http handler.
|
||||
type rootHandler struct {
|
||||
handler ContextHandler
|
||||
auth auth.AccessController
|
||||
actions []string
|
||||
context context.Context
|
||||
trust signed.CryptoService
|
||||
}
|
||||
|
||||
// AuthWrapper wraps a Handler with and Auth requirement
|
||||
type AuthWrapper func(ContextHandler, ...string) *rootHandler
|
||||
|
||||
// RootHandlerFactory creates a new rootHandler factory using the given
|
||||
// Context creator and authorizer. The returned factory allows creating
|
||||
// new rootHandlers from the alternate http handler contextHandler and
|
||||
// a scope.
|
||||
func RootHandlerFactory(ctx context.Context, auth auth.AccessController, trust signed.CryptoService) func(ContextHandler, ...string) *rootHandler {
|
||||
return func(handler ContextHandler, actions ...string) *rootHandler {
|
||||
return &rootHandler{
|
||||
handler: handler,
|
||||
auth: auth,
|
||||
actions: actions,
|
||||
context: ctx,
|
||||
trust: trust,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP serves an HTTP request and implements the http.Handler interface.
|
||||
func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
err error
|
||||
ctx = ctxu.WithRequest(root.context, r)
|
||||
log = ctxu.GetRequestLogger(ctx)
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
ctx, w = ctxu.WithResponseWriter(ctx, w)
|
||||
ctx = ctxu.WithLogger(ctx, log)
|
||||
ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, root.trust)
|
||||
|
||||
defer func(ctx context.Context) {
|
||||
ctxu.GetResponseLogger(ctx).Info("response completed")
|
||||
}(ctx)
|
||||
|
||||
if root.auth != nil {
|
||||
ctx = context.WithValue(ctx, notary.CtxKeyRepo, vars["gun"])
|
||||
if ctx, err = root.doAuth(ctx, vars["gun"], w); err != nil {
|
||||
// errors have already been logged/output to w inside doAuth
|
||||
// just return
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := root.handler(ctx, w, r); err != nil {
|
||||
serveError(log, w, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serveError(log ctxu.Logger, w http.ResponseWriter, err error) {
|
||||
if httpErr, ok := err.(errcode.Error); ok {
|
||||
// info level logging for non-5XX http errors
|
||||
httpErrCode := httpErr.ErrorCode().Descriptor().HTTPStatusCode
|
||||
if httpErrCode >= http.StatusInternalServerError {
|
||||
// error level logging for 5XX http errors
|
||||
log.Errorf("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
|
||||
} else {
|
||||
log.Infof("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
|
||||
}
|
||||
}
|
||||
e := errcode.ServeJSON(w, err)
|
||||
if e != nil {
|
||||
log.Error(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (root *rootHandler) doAuth(ctx context.Context, gun string, w http.ResponseWriter) (context.Context, error) {
|
||||
var access []auth.Access
|
||||
if gun == "" {
|
||||
access = buildCatalogRecord(root.actions...)
|
||||
} else {
|
||||
access = buildAccessRecords(gun, root.actions...)
|
||||
}
|
||||
|
||||
log := ctxu.GetRequestLogger(ctx)
|
||||
var authCtx context.Context
|
||||
var err error
|
||||
if authCtx, err = root.auth.Authorized(ctx, access...); err != nil {
|
||||
if challenge, ok := err.(auth.Challenge); ok {
|
||||
// Let the challenge write the response.
|
||||
challenge.SetHeaders(w)
|
||||
|
||||
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(access)); err != nil {
|
||||
log.Errorf("failed to serve challenge response: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized)
|
||||
return nil, err
|
||||
}
|
||||
return authCtx, nil
|
||||
}
|
||||
|
||||
func buildAccessRecords(repo string, actions ...string) []auth.Access {
|
||||
requiredAccess := make([]auth.Access, 0, len(actions))
|
||||
for _, action := range actions {
|
||||
requiredAccess = append(requiredAccess, auth.Access{
|
||||
Resource: auth.Resource{
|
||||
Type: "repository",
|
||||
Name: repo,
|
||||
},
|
||||
Action: action,
|
||||
})
|
||||
}
|
||||
return requiredAccess
|
||||
}
|
||||
|
||||
// buildCatalogRecord returns the only valid format for the catalog
|
||||
// resource. Only admins can get this access level from the token
|
||||
// server.
|
||||
func buildCatalogRecord(actions ...string) []auth.Access {
|
||||
requiredAccess := []auth.Access{{
|
||||
Resource: auth.Resource{
|
||||
Type: "registry",
|
||||
Name: "catalog",
|
||||
},
|
||||
Action: "*",
|
||||
}}
|
||||
|
||||
return requiredAccess
|
||||
}
|
||||
|
||||
// CacheControlConfig is an interface for something that knows how to set cache
|
||||
// control headers
|
||||
type CacheControlConfig interface {
|
||||
// SetHeaders will actually set the cache control headers on a Headers object
|
||||
SetHeaders(headers http.Header)
|
||||
}
|
||||
|
||||
// NewCacheControlConfig returns CacheControlConfig interface for either setting
|
||||
// cache control or disabling cache control entirely
|
||||
func NewCacheControlConfig(maxAgeInSeconds int, mustRevalidate bool) CacheControlConfig {
|
||||
if maxAgeInSeconds > 0 {
|
||||
return PublicCacheControl{MustReValidate: mustRevalidate, MaxAgeInSeconds: maxAgeInSeconds}
|
||||
}
|
||||
return NoCacheControl{}
|
||||
}
|
||||
|
||||
// PublicCacheControl is a set of options that we will set to enable cache control
|
||||
type PublicCacheControl struct {
|
||||
MustReValidate bool
|
||||
MaxAgeInSeconds int
|
||||
}
|
||||
|
||||
// SetHeaders sets the public headers with an optional must-revalidate header
|
||||
func (p PublicCacheControl) SetHeaders(headers http.Header) {
|
||||
cacheControlValue := fmt.Sprintf("public, max-age=%v, s-maxage=%v",
|
||||
p.MaxAgeInSeconds, p.MaxAgeInSeconds)
|
||||
|
||||
if p.MustReValidate {
|
||||
cacheControlValue = fmt.Sprintf("%s, must-revalidate", cacheControlValue)
|
||||
}
|
||||
headers.Set("Cache-Control", cacheControlValue)
|
||||
// delete the Pragma directive, because the only valid value in HTTP is
|
||||
// "no-cache"
|
||||
headers.Del("Pragma")
|
||||
if headers.Get("Last-Modified") == "" {
|
||||
SetLastModifiedHeader(headers, time.Time{})
|
||||
}
|
||||
}
|
||||
|
||||
// NoCacheControl is an object which represents a directive to cache nothing
|
||||
type NoCacheControl struct{}
|
||||
|
||||
// SetHeaders sets the public headers cache-control headers and pragma to no-cache
|
||||
func (n NoCacheControl) SetHeaders(headers http.Header) {
|
||||
headers.Set("Cache-Control", "max-age=0, no-cache, no-store")
|
||||
headers.Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
// cacheControlResponseWriter wraps an existing response writer, and if Write is
|
||||
// called, will try to set the cache control headers if it can
|
||||
type cacheControlResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
config CacheControlConfig
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// WriteHeader stores the header before writing it, so we can tell if it's been set
|
||||
// to a non-200 status code
|
||||
func (c *cacheControlResponseWriter) WriteHeader(statusCode int) {
|
||||
c.statusCode = statusCode
|
||||
c.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// Write will set the cache headers if they haven't already been set and if the status
|
||||
// code has either not been set or set to 200
|
||||
func (c *cacheControlResponseWriter) Write(data []byte) (int, error) {
|
||||
if c.statusCode == http.StatusOK || c.statusCode == 0 {
|
||||
headers := c.ResponseWriter.Header()
|
||||
if headers.Get("Cache-Control") == "" {
|
||||
c.config.SetHeaders(headers)
|
||||
}
|
||||
}
|
||||
return c.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
type cacheControlHandler struct {
|
||||
http.Handler
|
||||
config CacheControlConfig
|
||||
}
|
||||
|
||||
func (c cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.Handler.ServeHTTP(&cacheControlResponseWriter{ResponseWriter: w, config: c.config}, r)
|
||||
}
|
||||
|
||||
// WrapWithCacheHandler wraps another handler in one that can add cache control headers
|
||||
// given a 200 response
|
||||
func WrapWithCacheHandler(ccc CacheControlConfig, handler http.Handler) http.Handler {
|
||||
if ccc != nil {
|
||||
return cacheControlHandler{Handler: handler, config: ccc}
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// SetLastModifiedHeader takes a time and uses it to set the LastModified header using
|
||||
// the right date format
|
||||
func SetLastModifiedHeader(headers http.Header, lmt time.Time) {
|
||||
headers.Set("Last-Modified", lmt.Format(time.RFC1123))
|
||||
}
|
||||
240
vendor/github.com/docker/notary/utils/keys.go
generated
vendored
Normal file
240
vendor/github.com/docker/notary/utils/keys.go
generated
vendored
Normal file
@ -0,0 +1,240 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/notary"
|
||||
tufdata "github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Exporter is a simple interface for the two functions we need from the Storage interface
|
||||
type Exporter interface {
|
||||
Get(string) ([]byte, error)
|
||||
ListFiles() []string
|
||||
}
|
||||
|
||||
// Importer is a simple interface for the one function we need from the Storage interface
|
||||
type Importer interface {
|
||||
Set(string, []byte) error
|
||||
}
|
||||
|
||||
// ExportKeysByGUN exports all keys filtered to a GUN
|
||||
func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error {
|
||||
keys := s.ListFiles()
|
||||
sort.Strings(keys) // ensure consistency. ListFiles has no order guarantee
|
||||
for _, loc := range keys {
|
||||
keyFile, err := s.Get(loc)
|
||||
if err != nil {
|
||||
logrus.Warn("Could not parse key file at ", loc)
|
||||
continue
|
||||
}
|
||||
block, _ := pem.Decode(keyFile)
|
||||
keyGun := block.Headers["gun"]
|
||||
if keyGun == gun { // must be full GUN match
|
||||
if err := ExportKeys(to, s, loc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportKeysByID exports all keys matching the given ID
|
||||
func ExportKeysByID(to io.Writer, s Exporter, ids []string) error {
|
||||
want := make(map[string]struct{})
|
||||
for _, id := range ids {
|
||||
want[id] = struct{}{}
|
||||
}
|
||||
keys := s.ListFiles()
|
||||
for _, k := range keys {
|
||||
id := filepath.Base(k)
|
||||
if _, ok := want[id]; ok {
|
||||
if err := ExportKeys(to, s, k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportKeys copies a key from the store to the io.Writer
|
||||
func ExportKeys(to io.Writer, s Exporter, from string) error {
|
||||
// get PEM block
|
||||
k, err := s.Get(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse PEM blocks if there are more than one
|
||||
for block, rest := pem.Decode(k); block != nil; block, rest = pem.Decode(rest) {
|
||||
// add from path in a header for later import
|
||||
block.Headers["path"] = from
|
||||
// write serialized PEM
|
||||
err = pem.Encode(to, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportKeys expects an io.Reader containing one or more PEM blocks.
|
||||
// It reads PEM blocks one at a time until pem.Decode returns a nil
|
||||
// block.
|
||||
// Each block is written to the subpath indicated in the "path" PEM
|
||||
// header. If the file already exists, the file is truncated. Multiple
|
||||
// adjacent PEMs with the same "path" header are appended together.
|
||||
func ImportKeys(from io.Reader, to []Importer, fallbackRole string, fallbackGUN string, passRet notary.PassRetriever) error {
|
||||
// importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function
|
||||
// it is very rough, but it may help while reading this piece of code
|
||||
data, err := ioutil.ReadAll(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
writeTo string
|
||||
toWrite []byte
|
||||
)
|
||||
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) {
|
||||
handleLegacyPath(block)
|
||||
setFallbacks(block, fallbackGUN, fallbackRole)
|
||||
|
||||
loc, err := checkValidity(block)
|
||||
if err != nil {
|
||||
// already logged in checkValidity
|
||||
continue
|
||||
}
|
||||
|
||||
// the path header is not of any use once we've imported the key so strip it away
|
||||
delete(block.Headers, "path")
|
||||
|
||||
// we are now all set for import but let's first encrypt the key
|
||||
blockBytes := pem.EncodeToMemory(block)
|
||||
// check if key is encrypted, note: if it is encrypted at this point, it will have had a path header
|
||||
if privKey, err := utils.ParsePEMPrivateKey(blockBytes, ""); err == nil {
|
||||
// Key is not encrypted- ask for a passphrase and encrypt this key
|
||||
var chosenPassphrase string
|
||||
for attempts := 0; ; attempts++ {
|
||||
var giveup bool
|
||||
chosenPassphrase, giveup, err = passRet(loc, block.Headers["role"], true, attempts)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if giveup || attempts > 10 {
|
||||
return errors.New("maximum number of passphrase attempts exceeded")
|
||||
}
|
||||
}
|
||||
blockBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, tufdata.RoleName(block.Headers["role"]), tufdata.GUN(block.Headers["gun"]), chosenPassphrase)
|
||||
if err != nil {
|
||||
return errors.New("failed to encrypt key with given passphrase")
|
||||
}
|
||||
}
|
||||
|
||||
if loc != writeTo {
|
||||
// next location is different from previous one. We've finished aggregating
|
||||
// data for the previous file. If we have data, write the previous file,
|
||||
// clear toWrite and set writeTo to the next path we're going to write
|
||||
if toWrite != nil {
|
||||
if err = importToStores(to, writeTo, toWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// set up for aggregating next file's data
|
||||
toWrite = nil
|
||||
writeTo = loc
|
||||
}
|
||||
|
||||
toWrite = append(toWrite, blockBytes...)
|
||||
}
|
||||
if toWrite != nil { // close out final iteration if there's data left
|
||||
return importToStores(to, writeTo, toWrite)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleLegacyPath(block *pem.Block) {
|
||||
// if there is a legacy path then we set the gun header from this path
|
||||
// this is the case when a user attempts to import a key bundle generated by an older client
|
||||
if rawPath := block.Headers["path"]; rawPath != "" && rawPath != filepath.Base(rawPath) {
|
||||
// this is a legacy filepath and we should try to deduce the gun name from it
|
||||
pathWOFileName := filepath.Dir(rawPath)
|
||||
if strings.HasPrefix(pathWOFileName, notary.NonRootKeysSubdir) {
|
||||
// remove the notary keystore-specific segment of the path, and any potential leading or trailing slashes
|
||||
gunName := strings.Trim(strings.TrimPrefix(pathWOFileName, notary.NonRootKeysSubdir), "/")
|
||||
if gunName != "" {
|
||||
block.Headers["gun"] = gunName
|
||||
}
|
||||
}
|
||||
block.Headers["path"] = filepath.Base(rawPath)
|
||||
}
|
||||
}
|
||||
|
||||
func setFallbacks(block *pem.Block, fallbackGUN, fallbackRole string) {
|
||||
if block.Headers["gun"] == "" {
|
||||
if fallbackGUN != "" {
|
||||
block.Headers["gun"] = fallbackGUN
|
||||
}
|
||||
}
|
||||
|
||||
if block.Headers["role"] == "" {
|
||||
if fallbackRole == "" {
|
||||
block.Headers["role"] = notary.DefaultImportRole
|
||||
} else {
|
||||
block.Headers["role"] = fallbackRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkValidity ensures the fields in the pem headers are valid and parses out the location.
|
||||
// While importing a collection of keys, errors from this function should result in only the
|
||||
// current pem block being skipped.
|
||||
func checkValidity(block *pem.Block) (string, error) {
|
||||
// A root key or a delegations key should not have a gun
|
||||
// Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun
|
||||
switch block.Headers["role"] {
|
||||
case tufdata.CanonicalSnapshotRole.String(), tufdata.CanonicalTargetsRole.String(), tufdata.CanonicalTimestampRole.String():
|
||||
// check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to
|
||||
if block.Headers["gun"] == "" {
|
||||
logrus.Warnf("failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to", block.Headers["path"])
|
||||
return "", errors.New("invalid key pem block")
|
||||
}
|
||||
default:
|
||||
delete(block.Headers, "gun")
|
||||
}
|
||||
|
||||
loc, ok := block.Headers["path"]
|
||||
// only if the path isn't specified do we get into this parsing path logic
|
||||
if !ok || loc == "" {
|
||||
// if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun)
|
||||
// parse key for the keyID which we will save it by.
|
||||
// if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by
|
||||
|
||||
decodedKey, err := utils.ParsePEMPrivateKey(pem.EncodeToMemory(block), "")
|
||||
if err != nil {
|
||||
logrus.Warn("failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header")
|
||||
return "", errors.New("invalid key pem block")
|
||||
}
|
||||
loc = decodedKey.ID()
|
||||
}
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
func importToStores(to []Importer, path string, bytes []byte) error {
|
||||
var err error
|
||||
for _, i := range to {
|
||||
if err = i.Set(path, bytes); err != nil {
|
||||
logrus.Errorf("failed to import key to store: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user