1
0
mirror of https://github.com/docker/docs.git synced 2026-11-05 00:14:17 +03:00

Merge pull request #729 from cyli/server-gun-prefixes

Server gun prefix requirement feature
This commit is contained in:
Ying Li
2016-05-23 18:59:21 -07:00
11 changed files with 280 additions and 42 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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{})

View File

@@ -58,12 +58,15 @@ learn more about the configuration section corresponding to that key:
"api_key": "c9d60ae4c7e70c4b6c4ebd3e8056d2b8",
"release_stage": "production"
}
}
},
<a href="#caching-section-optional">"caching"</a>: {
"max_age": {
"current_metadata": 300,
"consistent_metadata": 31536000,
}
},
<a href="#repositories-section-optional">"repositories"</a>: {
"gun_prefixes": ["docker.io/", "my-own-registry.com/"]
}
}
</code></pre>
@@ -329,6 +332,33 @@ Example:
</tr>
</table>
## repositories section (optional)
Example:
```json
"repositories": {
"gun_prefixes": ["docker.io/", "my-own-registry.com/"]
}
```
<table>
<tr>
<th>Parameter</th>
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td valign="top"><code>gun_prefixes</code></td>
<td valign="top">no</td>
<td valign="top">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.
</td>
</tr>
</table>
## Related information
* [Notary Signer Configuration File](signer-config.md)

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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,