diff --git a/metadata/namespaces.go b/metadata/namespaces.go index 3cbbfd8be..b2b429500 100644 --- a/metadata/namespaces.go +++ b/metadata/namespaces.go @@ -21,6 +21,10 @@ func (s *namespaceStore) Create(ctx context.Context, namespace string, labels ma return err } + if err := namespaces.Validate(namespace); err != nil { + return err + } + // provides the already exists error. bkt, err := topbkt.CreateBucket([]byte(namespace)) if err != nil { diff --git a/namespaces/context.go b/namespaces/context.go index 31c2a0410..401acaaf0 100644 --- a/namespaces/context.go +++ b/namespaces/context.go @@ -37,7 +37,9 @@ func NamespaceFromEnv(ctx context.Context) context.Context { return WithNamespace(ctx, namespace) } -// Namespace returns the namespace from the context +// Namespace returns the namespace from the context. +// +// The namespace is not guaranteed to be valid. func Namespace(ctx context.Context) (string, bool) { namespace, ok := ctx.Value(namespaceKey{}).(string) if !ok { @@ -52,12 +54,16 @@ func IsNamespaceRequired(err error) bool { return errors.Cause(err) == errNamespaceRequired } -// NamespaceRequired returns the namespace or an error +// NamespaceRequired returns the valid namepace from the context or an error. func NamespaceRequired(ctx context.Context) (string, error) { namespace, ok := Namespace(ctx) if !ok || namespace == "" { return "", errNamespaceRequired } + if err := Validate(namespace); err != nil { + return "", err + } + return namespace, nil } diff --git a/namespaces/validate.go b/namespaces/validate.go new file mode 100644 index 000000000..033ed4b85 --- /dev/null +++ b/namespaces/validate.go @@ -0,0 +1,52 @@ +package namespaces + +import ( + "regexp" + + "github.com/pkg/errors" +) + +const ( + label = `[a-z][a-z0-9]+(?:[-]+[a-z0-9]+)*` +) + +func reGroup(s string) string { + return `(?:` + s + `)` +} + +func reAnchor(s string) string { + return `^` + s + `$` +} + +var ( + // namespaceRe validates that a namespace matches valid namespaces. + // + // Rules for domains, defined in RFC 1035, section 2.3.1, are used for + // namespaces. + namespaceRe = regexp.MustCompile(reAnchor(label + reGroup("[.]"+reGroup(label)) + "*")) + + errNamespaceInvalid = errors.Errorf("invalid namespace, must match %v", namespaceRe) +) + +// IsNamespacesValid return true if the error was due to an invalid namespace +// name. +func IsNamespaceInvalid(err error) bool { + return errors.Cause(err) == errNamespaceInvalid +} + +// Validate return nil if the string s is a valid namespace name. +// +// Namespaces must be valid domain names according to RFC 1035, section 2.3.1. +// To enforce case insensitvity, all characters must be lower case. +// +// In general, namespaces that pass this validation, should be safe for use as +// a domain name or filesystem path component. +// +// Typically, this function is used through NamespacesRequired, rather than +// directly. +func Validate(s string) error { + if !namespaceRe.MatchString(s) { + return errors.Wrapf(errNamespaceInvalid, "namespace %q", s) + } + return nil +} diff --git a/namespaces/validate_test.go b/namespaces/validate_test.go new file mode 100644 index 000000000..d15b096b9 --- /dev/null +++ b/namespaces/validate_test.go @@ -0,0 +1,97 @@ +package namespaces + +import ( + "testing" + + "github.com/pkg/errors" +) + +func TestValidNamespaces(t *testing.T) { + for _, testcase := range []struct { + name string + input string + err error + }{ + { + name: "Default", + input: "default", + }, + { + name: "Hyphen", + input: "default-default", + }, + { + name: "DoubleHyphen", + input: "default--default", + }, + { + name: "containerD", + input: "containerd.io", + }, + { + name: "SwarmKit", + input: "swarmkit.docker.io", + }, + { + name: "Punycode", + input: "zn--e9.org", // or something like it! + }, + { + name: "LeadingPeriod", + input: ".foo..foo", + err: errNamespaceInvalid, + }, + { + name: "Path", + input: "foo/foo", + err: errNamespaceInvalid, + }, + { + name: "ParentDir", + input: "foo/..", + err: errNamespaceInvalid, + }, + { + name: "RepeatedPeriod", + input: "foo..foo", + err: errNamespaceInvalid, + }, + { + name: "OutOfPlaceHyphenEmbedded", + input: "foo.-boo", + err: errNamespaceInvalid, + }, + { + name: "OutOfPlaceHyphen", + input: "-foo.boo", + err: errNamespaceInvalid, + }, + { + name: "OutOfPlaceHyphenEnd", + input: "foo.boo", + err: errNamespaceInvalid, + }, + { + name: "Underscores", + input: "foo_foo.boo_underscores", // boo-urns? + err: errNamespaceInvalid, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + if err := Validate(testcase.input); err != nil { + if errors.Cause(err) != testcase.err { + if testcase.err == nil { + t.Fatalf("unexpected error: %v != nil", err) + } else { + t.Fatalf("expected error %v to be %v", err, testcase.err) + } + } else { + t.Logf("invalid %q detected as invalid: %v", testcase.input, err) + return + } + + t.Logf("%q is a valid namespace", testcase.input) + } + }) + } +} diff --git a/services/containers/helpers.go b/services/containers/helpers.go index 69c8f5df8..d6193583a 100644 --- a/services/containers/helpers.go +++ b/services/containers/helpers.go @@ -4,6 +4,7 @@ import ( api "github.com/containerd/containerd/api/services/containers" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/metadata" + "github.com/containerd/containerd/namespaces" "github.com/gogo/protobuf/types" specs "github.com/opencontainers/runtime-spec/specs-go" "google.golang.org/grpc" @@ -57,6 +58,10 @@ func mapGRPCError(err error, id string) error { return grpc.Errorf(codes.NotFound, "container %v not found", id) case metadata.IsExists(err): return grpc.Errorf(codes.AlreadyExists, "container %v already exists", id) + case namespaces.IsNamespaceRequired(err): + return grpc.Errorf(codes.InvalidArgument, "namespace required, please set %q header", namespaces.GRPCHeader) + case namespaces.IsNamespaceInvalid(err): + return grpc.Errorf(codes.InvalidArgument, err.Error()) } return err diff --git a/services/images/helpers.go b/services/images/helpers.go index 064f78726..f2d9aed56 100644 --- a/services/images/helpers.go +++ b/services/images/helpers.go @@ -85,6 +85,8 @@ func mapGRPCError(err error, id string) error { return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id) case namespaces.IsNamespaceRequired(err): return grpc.Errorf(codes.InvalidArgument, "namespace required, please set %q header", namespaces.GRPCHeader) + case namespaces.IsNamespaceInvalid(err): + return grpc.Errorf(codes.InvalidArgument, err.Error()) } return err