Skip to content

Commit 5380585

Browse files
committed
namespaces: enforce a character set for namespaces
Signed-off-by: Stephen J Day <stephen.day@docker.com>
1 parent 753f1a6 commit 5380585

File tree

6 files changed

+168
-2
lines changed

6 files changed

+168
-2
lines changed

metadata/namespaces.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ func (s *namespaceStore) Create(ctx context.Context, namespace string, labels ma
2121
return err
2222
}
2323

24+
if err := namespaces.Validate(namespace); err != nil {
25+
return err
26+
}
27+
2428
// provides the already exists error.
2529
bkt, err := topbkt.CreateBucket([]byte(namespace))
2630
if err != nil {

namespaces/context.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ func NamespaceFromEnv(ctx context.Context) context.Context {
3737
return WithNamespace(ctx, namespace)
3838
}
3939

40-
// Namespace returns the namespace from the context
40+
// Namespace returns the namespace from the context.
41+
//
42+
// The namespace is not guaranteed to be valid.
4143
func Namespace(ctx context.Context) (string, bool) {
4244
namespace, ok := ctx.Value(namespaceKey{}).(string)
4345
if !ok {
@@ -52,12 +54,16 @@ func IsNamespaceRequired(err error) bool {
5254
return errors.Cause(err) == errNamespaceRequired
5355
}
5456

55-
// NamespaceRequired returns the namespace or an error
57+
// NamespaceRequired returns the valid namepace from the context or an error.
5658
func NamespaceRequired(ctx context.Context) (string, error) {
5759
namespace, ok := Namespace(ctx)
5860
if !ok || namespace == "" {
5961
return "", errNamespaceRequired
6062
}
6163

64+
if err := Validate(namespace); err != nil {
65+
return "", err
66+
}
67+
6268
return namespace, nil
6369
}

namespaces/validate.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package namespaces
2+
3+
import (
4+
"regexp"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
const (
10+
label = `[a-z][a-z0-9]+(?:[-]+[a-z0-9]+)*`
11+
)
12+
13+
func reGroup(s string) string {
14+
return `(?:` + s + `)`
15+
}
16+
17+
func reAnchor(s string) string {
18+
return `^` + s + `$`
19+
}
20+
21+
var (
22+
// namespaceRe validates that a namespace matches valid namespaces.
23+
//
24+
// Rules for domains, defined in RFC 1035, section 2.3.1, are used for
25+
// namespaces.
26+
namespaceRe = regexp.MustCompile(reAnchor(label + reGroup("[.]"+reGroup(label)) + "*"))
27+
28+
errNamespaceInvalid = errors.Errorf("invalid namespace, must match %v", namespaceRe)
29+
)
30+
31+
// IsNamespacesValid return true if the error was due to an invalid namespace
32+
// name.
33+
func IsNamespaceInvalid(err error) bool {
34+
return errors.Cause(err) == errNamespaceInvalid
35+
}
36+
37+
// Validate return nil if the string s is a valid namespace name.
38+
//
39+
// Namespaces must be valid domain names according to RFC 1035, section 2.3.1.
40+
// To enforce case insensitvity, all characters must be lower case.
41+
//
42+
// In general, namespaces that pass this validation, should be safe for use as
43+
// a domain name or filesystem path component.
44+
//
45+
// Typically, this function is used through NamespacesRequired, rather than
46+
// directly.
47+
func Validate(s string) error {
48+
if !namespaceRe.MatchString(s) {
49+
return errors.Wrapf(errNamespaceInvalid, "namespace %q", s)
50+
}
51+
return nil
52+
}

namespaces/validate_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package namespaces
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
func TestValidNamespaces(t *testing.T) {
10+
for _, testcase := range []struct {
11+
name string
12+
input string
13+
err error
14+
}{
15+
{
16+
name: "Default",
17+
input: "default",
18+
},
19+
{
20+
name: "Hyphen",
21+
input: "default-default",
22+
},
23+
{
24+
name: "DoubleHyphen",
25+
input: "default--default",
26+
},
27+
{
28+
name: "containerD",
29+
input: "containerd.io",
30+
},
31+
{
32+
name: "SwarmKit",
33+
input: "swarmkit.docker.io",
34+
},
35+
{
36+
name: "Punycode",
37+
input: "zn--e9.org", // or something like it!
38+
},
39+
{
40+
name: "LeadingPeriod",
41+
input: ".foo..foo",
42+
err: errNamespaceInvalid,
43+
},
44+
{
45+
name: "Path",
46+
input: "foo/foo",
47+
err: errNamespaceInvalid,
48+
},
49+
{
50+
name: "ParentDir",
51+
input: "foo/..",
52+
err: errNamespaceInvalid,
53+
},
54+
{
55+
name: "RepeatedPeriod",
56+
input: "foo..foo",
57+
err: errNamespaceInvalid,
58+
},
59+
{
60+
name: "OutOfPlaceHyphenEmbedded",
61+
input: "foo.-boo",
62+
err: errNamespaceInvalid,
63+
},
64+
{
65+
name: "OutOfPlaceHyphen",
66+
input: "-foo.boo",
67+
err: errNamespaceInvalid,
68+
},
69+
{
70+
name: "OutOfPlaceHyphenEnd",
71+
input: "foo.boo",
72+
err: errNamespaceInvalid,
73+
},
74+
{
75+
name: "Underscores",
76+
input: "foo_foo.boo_underscores", // boo-urns?
77+
err: errNamespaceInvalid,
78+
},
79+
} {
80+
t.Run(testcase.name, func(t *testing.T) {
81+
if err := Validate(testcase.input); err != nil {
82+
if errors.Cause(err) != testcase.err {
83+
if testcase.err == nil {
84+
t.Fatalf("unexpected error: %v != nil", err)
85+
} else {
86+
t.Fatalf("expected error %v to be %v", err, testcase.err)
87+
}
88+
} else {
89+
t.Logf("invalid %q detected as invalid: %v", testcase.input, err)
90+
return
91+
}
92+
93+
t.Logf("%q is a valid namespace", testcase.input)
94+
}
95+
})
96+
}
97+
}

services/containers/helpers.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
api "github.com/containerd/containerd/api/services/containers"
55
"github.com/containerd/containerd/containers"
66
"github.com/containerd/containerd/metadata"
7+
"github.com/containerd/containerd/namespaces"
78
"github.com/gogo/protobuf/types"
89
specs "github.com/opencontainers/runtime-spec/specs-go"
910
"google.golang.org/grpc"
@@ -57,6 +58,10 @@ func mapGRPCError(err error, id string) error {
5758
return grpc.Errorf(codes.NotFound, "container %v not found", id)
5859
case metadata.IsExists(err):
5960
return grpc.Errorf(codes.AlreadyExists, "container %v already exists", id)
61+
case namespaces.IsNamespaceRequired(err):
62+
return grpc.Errorf(codes.InvalidArgument, "namespace required, please set %q header", namespaces.GRPCHeader)
63+
case namespaces.IsNamespaceInvalid(err):
64+
return grpc.Errorf(codes.InvalidArgument, err.Error())
6065
}
6166

6267
return err

services/images/helpers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ func mapGRPCError(err error, id string) error {
8585
return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id)
8686
case namespaces.IsNamespaceRequired(err):
8787
return grpc.Errorf(codes.InvalidArgument, "namespace required, please set %q header", namespaces.GRPCHeader)
88+
case namespaces.IsNamespaceInvalid(err):
89+
return grpc.Errorf(codes.InvalidArgument, err.Error())
8890
}
8991

9092
return err

0 commit comments

Comments
 (0)