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/"]
+}
+```
+
+
+
+ | Parameter |
+ Required |
+ Description |
+
+
+ gun_prefixes |
+ no |
+ A 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,