diff --git a/client/client_test.go b/client/client_test.go index d3d74b7aa9..6545936db3 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -140,7 +140,7 @@ func fullTestServer(t *testing.T) *httptest.Server { ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) - return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil)) + return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil, nil)) } // server that returns some particular error code all the time diff --git a/cmd/notary-server/config.go b/cmd/notary-server/config.go index 38857bc160..81dbe1ebe8 100644 --- a/cmd/notary-server/config.go +++ b/cmd/notary-server/config.go @@ -3,7 +3,9 @@ package main import ( "crypto/tls" "fmt" + "path" "strconv" + "strings" "time" "github.com/Sirupsen/logrus" @@ -21,9 +23,21 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/spf13/viper" "golang.org/x/net/context" - "gopkg.in/dancannon/gorethink.v2" + gorethink "gopkg.in/dancannon/gorethink.v2" ) +// gets the required gun prefixes accepted by this server +func getRequiredGunPrefixes(configuration *viper.Viper) ([]string, error) { + prefixes := configuration.GetStringSlice("repositories.gun_prefixes") + for _, prefix := range prefixes { + p := path.Clean(strings.TrimSpace(prefix)) + if p+"/" != prefix || strings.HasPrefix(p, "/") || strings.HasPrefix(p, "..") { + return nil, fmt.Errorf("invalid GUN prefix %s", prefix) + } + } + return prefixes, nil +} + // get the address for the HTTP server, and parses the optional TLS // configuration for the server - if no TLS configuration is specified, // TLS is not enabled. @@ -217,6 +231,11 @@ func parseServerConfig(configFilePath string, hRegister healthRegister) (context } logrus.SetLevel(lvl) + prefixes, err := getRequiredGunPrefixes(config) + if err != nil { + return nil, server.Config{}, err + } + // parse bugsnag config bugsnagConf, err := utils.ParseBugsnag(config) if err != nil { @@ -252,6 +271,7 @@ func parseServerConfig(configFilePath string, hRegister healthRegister) (context Trust: trust, AuthMethod: config.GetString("auth.type"), AuthOpts: config.Get("auth.options"), + RepoPrefixes: prefixes, CurrentCacheControlConfig: currentCache, ConsistentCacheControlConfig: consistentCache, }, nil diff --git a/cmd/notary-server/main_test.go b/cmd/notary-server/main_test.go index 6302313209..16fe4b9277 100644 --- a/cmd/notary-server/main_test.go +++ b/cmd/notary-server/main_test.go @@ -403,6 +403,32 @@ func TestGetCacheConfig(t *testing.T) { } } +func TestGetGUNPRefixes(t *testing.T) { + valids := map[string][]string{ + `{}`: nil, + `{"repositories": {"gun_prefixes": []}}`: nil, + `{"repositories": {}}`: nil, + `{"repositories": {"gun_prefixes": ["hello/"]}}`: {"hello/"}, + } + invalids := []string{ + `{"repositories": {"gun_prefixes": " / "}}`, + `{"repositories": {"gun_prefixes": "nope"}}`, + `{"repositories": {"gun_prefixes": ["nope"]}}`, + `{"repositories": {"gun_prefixes": ["/nope/"]}}`, + `{"repositories": {"gun_prefixes": ["../nope/"]}}`, + } + + for valid, expected := range valids { + prefixes, err := getRequiredGunPrefixes(configure(valid)) + require.NoError(t, err) + require.Equal(t, expected, prefixes) + } + for _, invalid := range invalids { + _, err := getRequiredGunPrefixes(configure(invalid)) + require.Error(t, err, "expected error with %s", invalid) + } +} + // For sanity, make sure we can always parse the sample config func TestSampleConfig(t *testing.T) { var registerCalled = 0 diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 52a2fe8ead..60bf4241d6 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -73,7 +73,7 @@ func setupServerHandler(metaStore storage.MetaStore) http.Handler { ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass"))) - return server.RootHandler(nil, ctx, cryptoService, nil, nil) + return server.RootHandler(nil, ctx, cryptoService, nil, nil, nil) } // makes a testing notary-server diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 996ab0c104..1c44811f11 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -327,7 +327,7 @@ func setUpRepo(t *testing.T, tempBaseDir, gun string, ret passphrase.Retriever) ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(ret)) - ts := httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil)) + ts := httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil, nil)) repo, err := client.NewNotaryRepository( tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) diff --git a/docs/reference/server-config.md b/docs/reference/server-config.md index c527248e24..9af3865161 100644 --- a/docs/reference/server-config.md +++ b/docs/reference/server-config.md @@ -58,12 +58,15 @@ learn more about the configuration section corresponding to that key: "api_key": "c9d60ae4c7e70c4b6c4ebd3e8056d2b8", "release_stage": "production" } - } + }, "caching": { "max_age": { "current_metadata": 300, "consistent_metadata": 31536000, } + }, + "repositories": { + "gun_prefixes": ["docker.io/", "my-own-registry.com/"] } } @@ -329,6 +332,33 @@ Example: +## repositories section (optional) + +Example: + +```json +"repositories": { + "gun_prefixes": ["docker.io/", "my-own-registry.com/"] +} +``` + + + + + + + + + + + + +
ParameterRequiredDescription
gun_prefixesnoA list of GUN prefixes that will be accepted by this + server. POST operations on an image beginning with any other prefix + will be rejected with a 400, and GET/DELETE operations will be rejected + with a 404. +
+ ## Related information * [Notary Signer Configuration File](signer-config.md) diff --git a/server/errors/errors.go b/server/errors/errors.go index ad92ccf89d..3ccb7d3f36 100644 --- a/server/errors/errors.go +++ b/server/errors/errors.go @@ -83,8 +83,14 @@ var ( ErrNoKeyAlgorithm = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NO_KEYALGORITHM", Message: "The server does not have a key algorithm configured.", - Description: "No key algorihtm has been configured for the server and it has been asked to perform an operation that requires generation.", + Description: "No key algorithm has been configured for the server and it has been asked to perform an operation that requires generation.", HTTPStatusCode: http.StatusInternalServerError, }) + ErrInvalidGUN = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "INVALID_GUN", + Message: "The server does not support actions on images of this name.", + Description: "The server does not support actions on images of this name", + HTTPStatusCode: http.StatusBadRequest, + }) ErrUnknown = errcode.ErrorCodeUnknown ) diff --git a/server/integration_test.go b/server/integration_test.go index cb09c3e733..99379d3c11 100644 --- a/server/integration_test.go +++ b/server/integration_test.go @@ -24,7 +24,7 @@ func TestValidationErrorFormat(t *testing.T) { context.Background(), "metaStore", storage.NewMemStorage()) ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) - handler := RootHandler(nil, ctx, signed.NewEd25519(), nil, nil) + handler := RootHandler(nil, ctx, signed.NewEd25519(), nil, nil, nil) server := httptest.NewServer(handler) defer server.Close() diff --git a/server/server.go b/server/server.go index 05ac52195e..7a359ba5c7 100644 --- a/server/server.go +++ b/server/server.go @@ -5,11 +5,14 @@ import ( "fmt" "net" "net/http" + "strings" "github.com/Sirupsen/logrus" "github.com/docker/distribution/health" + "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/auth" "github.com/docker/notary" + "github.com/docker/notary/server/errors" "github.com/docker/notary/server/handlers" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" @@ -38,6 +41,7 @@ type Config struct { Trust signed.CryptoService AuthMethod string AuthOpts interface{} + RepoPrefixes []string ConsistentCacheControlConfig utils.CacheControlConfig CurrentCacheControlConfig utils.CacheControlConfig } @@ -74,8 +78,11 @@ func Run(ctx context.Context, conf Config) error { } svr := http.Server{ - Addr: conf.Addr, - Handler: RootHandler(ac, ctx, conf.Trust, conf.ConsistentCacheControlConfig, conf.CurrentCacheControlConfig), + Addr: conf.Addr, + Handler: RootHandler( + ac, ctx, conf.Trust, + conf.ConsistentCacheControlConfig, conf.CurrentCacheControlConfig, + conf.RepoPrefixes), } logrus.Info("Starting on ", conf.Addr) @@ -85,41 +92,98 @@ func Run(ctx context.Context, conf Config) error { return err } +// assumes that required prefixes is not empty +func filterImagePrefixes(requiredPrefixes []string, err error, handler http.Handler) http.Handler { + if len(requiredPrefixes) == 0 { + return handler + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + imageName := mux.Vars(r)["imageName"] + + for _, prefix := range requiredPrefixes { + if strings.HasPrefix(imageName, prefix) { + handler.ServeHTTP(w, r) + return + } + } + + errcode.ServeJSON(w, err) + }) +} + +type _serverEndpoint struct { + OperationName string + ServerHandler utils.ContextHandler + ErrorIfGUNInvalid error + IncludeCacheHeaders bool + CacheControlConfig utils.CacheControlConfig + PermissionsRequired []string +} + // RootHandler returns the handler that routes all the paths from / for the // server. func RootHandler(ac auth.AccessController, ctx context.Context, trust signed.CryptoService, - consistent, current utils.CacheControlConfig) http.Handler { + consistent, current utils.CacheControlConfig, repoPrefixes []string) http.Handler { - hand := utils.RootHandlerFactory(ac, ctx, trust) + authWrapper := utils.RootHandlerFactory(ac, ctx, trust) + + createHandler := func(opts _serverEndpoint) http.Handler { + var wrapped http.Handler + wrapped = authWrapper(opts.ServerHandler, opts.PermissionsRequired...) + if opts.IncludeCacheHeaders { + wrapped = utils.WrapWithCacheHandler(opts.CacheControlConfig, wrapped) + } + wrapped = filterImagePrefixes(repoPrefixes, opts.ErrorIfGUNInvalid, wrapped) + return prometheus.InstrumentHandlerWithOpts(prometheusOpts(opts.OperationName), wrapped) + } + + invalidGUNErr := errors.ErrInvalidGUN.WithDetail(fmt.Sprintf("Require GUNs with prefix: %v", repoPrefixes)) + notFoundError := errors.ErrMetadataNotFound.WithDetail(nil) r := mux.NewRouter() - r.Methods("GET").Path("/v2/").Handler(hand(handlers.MainHandler)) - r.Methods("POST").Path("/v2/{imageName:.*}/_trust/tuf/").Handler( - prometheus.InstrumentHandlerWithOpts( - prometheusOpts("UpdateTuf"), - hand(handlers.AtomicUpdateHandler, "push", "pull"))) - r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.{checksum:[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}}.json").Handler( - prometheus.InstrumentHandlerWithOpts( - prometheusOpts("GetRoleByHash"), - utils.WrapWithCacheHandler(consistent, hand(handlers.GetHandler, "pull")))) - r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler( - prometheus.InstrumentHandlerWithOpts( - prometheusOpts("GetRole"), - utils.WrapWithCacheHandler(current, hand(handlers.GetHandler, "pull")))) + r.Methods("GET").Path("/v2/").Handler(authWrapper(handlers.MainHandler)) + + r.Methods("POST").Path("/v2/{imageName:.*}/_trust/tuf/").Handler(createHandler(_serverEndpoint{ + OperationName: "UpdateTuf", + ErrorIfGUNInvalid: invalidGUNErr, + ServerHandler: handlers.AtomicUpdateHandler, + PermissionsRequired: []string{"push", "pull"}, + })) + r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.{checksum:[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}}.json").Handler(createHandler(_serverEndpoint{ + OperationName: "GetRoleByHash", + ErrorIfGUNInvalid: notFoundError, + IncludeCacheHeaders: true, + CacheControlConfig: consistent, + ServerHandler: handlers.GetHandler, + PermissionsRequired: []string{"pull"}, + })) + r.Methods("GET").Path("/v2/{imageName:.*}/_trust/tuf/{tufRole:root|targets(?:/[^/\\s]+)*|snapshot|timestamp}.json").Handler(createHandler(_serverEndpoint{ + OperationName: "GetRole", + ErrorIfGUNInvalid: notFoundError, + IncludeCacheHeaders: true, + CacheControlConfig: current, + ServerHandler: handlers.GetHandler, + PermissionsRequired: []string{"pull"}, + })) r.Methods("GET").Path( - "/v2/{imageName:.*}/_trust/tuf/{tufRole:snapshot|timestamp}.key").Handler( - prometheus.InstrumentHandlerWithOpts( - prometheusOpts("GetKey"), - hand(handlers.GetKeyHandler, "push", "pull"))) - r.Methods("DELETE").Path("/v2/{imageName:.*}/_trust/tuf/").Handler( - prometheus.InstrumentHandlerWithOpts( - prometheusOpts("DeleteTuf"), - hand(handlers.DeleteHandler, "push", "pull"))) + "/v2/{imageName:.*}/_trust/tuf/{tufRole:snapshot|timestamp}.key").Handler(createHandler(_serverEndpoint{ + OperationName: "GetKey", + ErrorIfGUNInvalid: notFoundError, + ServerHandler: handlers.GetKeyHandler, + PermissionsRequired: []string{"push", "pull"}, + })) + r.Methods("DELETE").Path("/v2/{imageName:.*}/_trust/tuf/").Handler(createHandler(_serverEndpoint{ + OperationName: "DeleteTuf", + ErrorIfGUNInvalid: notFoundError, + ServerHandler: handlers.DeleteHandler, + PermissionsRequired: []string{"push", "pull"}, + })) r.Methods("GET").Path("/_notary_server/health").HandlerFunc(health.StatusHandler) r.Methods("GET").Path("/metrics").Handler(prometheus.Handler()) r.Methods("GET", "POST", "PUT", "HEAD", "DELETE").Path("/{other:.*}").Handler( - hand(handlers.NotFoundHandler)) + authWrapper(handlers.NotFoundHandler)) return r } diff --git a/server/server_test.go b/server/server_test.go index 9d902f966c..0cb9deb141 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -14,10 +14,13 @@ import ( "testing" _ "github.com/docker/distribution/registry/auth/silly" + "github.com/docker/notary" "github.com/docker/notary/server/storage" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" "github.com/docker/notary/tuf/testutils" + tufutils "github.com/docker/notary/tuf/utils" "github.com/docker/notary/utils" "github.com/stretchr/testify/require" "golang.org/x/net/context" @@ -55,9 +58,98 @@ func TestRunReservedPort(t *testing.T) { ) } +func TestRepoPrefixMatches(t *testing.T) { + gun := "docker.io/notary" + meta, cs, err := testutils.NewRepoMetadata(gun) + require.NoError(t, err) + + ctx := context.WithValue(context.Background(), "metaStore", storage.NewMemStorage()) + ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) + + snChecksumBytes := sha256.Sum256(meta[data.CanonicalSnapshotRole]) + + // successful gets + handler := RootHandler(nil, ctx, cs, nil, nil, []string{"docker.io"}) + ts := httptest.NewServer(handler) + + url := fmt.Sprintf("%s/v2/%s/_trust/tuf/", ts.URL, gun) + uploader, err := store.NewHTTPStore(url, "", "json", "key", http.DefaultTransport) + require.NoError(t, err) + + // uploading is cool + require.NoError(t, uploader.SetMultiMeta(meta)) + // getting is cool + _, err = uploader.GetMeta(data.CanonicalSnapshotRole, notary.MaxDownloadSize) + require.NoError(t, err) + + _, err = uploader.GetMeta( + tufutils.ConsistentName(data.CanonicalSnapshotRole, snChecksumBytes[:]), notary.MaxDownloadSize) + require.NoError(t, err) + + _, err = uploader.GetKey(data.CanonicalTimestampRole) + require.NoError(t, err) + + // the httpstore doesn't actually delete all, so we do it manually + req, err := http.NewRequest("DELETE", url, nil) + require.NoError(t, err) + res, err := http.DefaultTransport.RoundTrip(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) +} + +func TestRepoPrefixDoesNotMatch(t *testing.T) { + gun := "docker.io/notary" + meta, cs, err := testutils.NewRepoMetadata(gun) + require.NoError(t, err) + s := storage.NewMemStorage() + + ctx := context.WithValue(context.Background(), "metaStore", s) + ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) + + snChecksumBytes := sha256.Sum256(meta[data.CanonicalSnapshotRole]) + + // successful gets + handler := RootHandler(nil, ctx, cs, nil, nil, []string{"nope"}) + ts := httptest.NewServer(handler) + + url := fmt.Sprintf("%s/v2/%s/_trust/tuf/", ts.URL, gun) + uploader, err := store.NewHTTPStore(url, "", "json", "key", http.DefaultTransport) + require.NoError(t, err) + + require.Error(t, uploader.SetMultiMeta(meta)) + + // update the storage so we don't fail just because the metadata is missing + for _, roleName := range data.BaseRoles { + require.NoError(t, s.UpdateCurrent(gun, storage.MetaUpdate{ + Role: roleName, + Data: meta[roleName], + Version: 1, + })) + } + + _, err = uploader.GetMeta(data.CanonicalSnapshotRole, notary.MaxDownloadSize) + require.Error(t, err) + + _, err = uploader.GetMeta( + tufutils.ConsistentName(data.CanonicalSnapshotRole, snChecksumBytes[:]), notary.MaxDownloadSize) + require.Error(t, err) + + _, err = uploader.GetKey(data.CanonicalTimestampRole) + require.Error(t, err) + + // the httpstore doesn't actually delete all, so we do it manually + req, err := http.NewRequest("DELETE", url, nil) + require.NoError(t, err) + res, err := http.DefaultTransport.RoundTrip(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusNotFound, res.StatusCode) +} + func TestMetricsEndpoint(t *testing.T) { handler := RootHandler(nil, context.Background(), signed.NewEd25519(), - nil, nil) + nil, nil, nil) ts := httptest.NewServer(handler) defer ts.Close() @@ -72,7 +164,7 @@ func TestGetKeysEndpoint(t *testing.T) { context.Background(), "metaStore", storage.NewMemStorage()) ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) - handler := RootHandler(nil, ctx, signed.NewEd25519(), nil, nil) + handler := RootHandler(nil, ctx, signed.NewEd25519(), nil, nil, nil) ts := httptest.NewServer(handler) defer ts.Close() @@ -144,7 +236,7 @@ func TestGetRoleByHash(t *testing.T) { ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) - handler := RootHandler(nil, ctx, signed.NewEd25519(), ccc, ccc) + handler := RootHandler(nil, ctx, signed.NewEd25519(), ccc, ccc, nil) serv := httptest.NewServer(handler) defer serv.Close() @@ -188,7 +280,7 @@ func TestGetCurrentRole(t *testing.T) { ctx = context.WithValue(ctx, "keyAlgorithm", data.ED25519Key) ccc := utils.NewCacheControlConfig(10, false) - handler := RootHandler(nil, ctx, signed.NewEd25519(), ccc, ccc) + handler := RootHandler(nil, ctx, signed.NewEd25519(), ccc, ccc, nil) serv := httptest.NewServer(handler) defer serv.Close() diff --git a/utils/http.go b/utils/http.go index 95245d9fb2..b1f7b56429 100644 --- a/utils/http.go +++ b/utils/http.go @@ -15,14 +15,14 @@ import ( "golang.org/x/net/context" ) -// contextHandler defines an alterate HTTP handler interface which takes in +// ContextHandler defines an alterate 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 +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 + handler ContextHandler auth auth.AccessController actions []string context context.Context @@ -34,8 +34,8 @@ type rootHandler struct { // Context creator and authorizer. The returned factory allows creating // new rootHandlers from the alternate http handler contextHandler and // a scope. -func RootHandlerFactory(auth auth.AccessController, ctx context.Context, trust signed.CryptoService) func(contextHandler, ...string) *rootHandler { - return func(handler contextHandler, actions ...string) *rootHandler { +func RootHandlerFactory(auth auth.AccessController, ctx context.Context, trust signed.CryptoService) func(ContextHandler, ...string) *rootHandler { + return func(handler ContextHandler, actions ...string) *rootHandler { return &rootHandler{ handler: handler, auth: auth,