diff --git a/pkg/security/securitycontextconstraints/selinux/mustrunas.go b/pkg/security/securitycontextconstraints/selinux/mustrunas.go index 1ccbb7a451cc..5e82942e6021 100644 --- a/pkg/security/securitycontextconstraints/selinux/mustrunas.go +++ b/pkg/security/securitycontextconstraints/selinux/mustrunas.go @@ -2,6 +2,10 @@ package selinux import ( "fmt" + "reflect" + "sort" + "strconv" + "strings" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/api" @@ -48,7 +52,7 @@ func (s *mustRunAs) Validate(pod *api.Pod, container *api.Container) field.Error } seLinuxOptionsPath := field.NewPath("seLinuxOptions") seLinux := container.SecurityContext.SELinuxOptions - if seLinux.Level != s.opts.SELinuxOptions.Level { + if !equalLevels(s.opts.SELinuxOptions.Level, seLinux.Level) { detail := fmt.Sprintf("seLinuxOptions.level on %s does not match required level. Found %s, wanted %s", container.Name, seLinux.Level, s.opts.SELinuxOptions.Level) allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("level"), seLinux.Level, detail)) } @@ -67,3 +71,70 @@ func (s *mustRunAs) Validate(pod *api.Pod, container *api.Container) field.Error return allErrs } + +func equalLevels(expectedLevel, actualLevel string) bool { + if expectedLevel == actualLevel { + return true + } + + // "s0:c6,c0" => [ "s0", "c6,c0" ] + expectedParts := strings.SplitN(expectedLevel, ":", 2) + actualParts := strings.SplitN(actualLevel, ":", 2) + if len(expectedParts) != 2 || len(expectedParts) != len(actualParts) { + return false + } + + // "s0-s0" => [ "s0" ] + expectedSensitivity := parseSensitivity(expectedParts[0]) + actualSensitivity := parseSensitivity(actualParts[0]) + if !reflect.DeepEqual(expectedSensitivity, actualSensitivity) { + return false + } + + // "c6,c0" => [ "c0", "c6" ] + expectedCategories := parseCategories(expectedParts[1]) + actualCategories := parseCategories(actualParts[1]) + + return reflect.DeepEqual(expectedCategories, actualCategories) +} + +func parseSensitivity(sensitivity string) []string { + // "s0-s0" => [ "s0" ] + if strings.IndexByte(sensitivity, '-') > -1 { + sensitivityRange := strings.SplitN(sensitivity, "-", 2) + if sensitivityRange[0] == sensitivityRange[1] { + return []string{sensitivityRange[0]} + } + } + + return []string{sensitivity} +} + +func parseCategories(categories string) []string { + parts := strings.Split(categories, ",") + + // "c0.c3" => [ "c0", "c1", "c2", c3" ] + if len(parts) == 1 && strings.IndexByte(categories, '.') > -1 { + categoryRange := strings.SplitN(categories, ".", 2) + if len(categoryRange) == 2 && categoryRange[0][0] == 'c' && categoryRange[1][0] == 'c' { + begin := strings.TrimPrefix(categoryRange[0], "c") + end := strings.TrimPrefix(categoryRange[1], "c") + + // bitSize 16 because we expect that categories will be in a range [0, 1024) + from, err1 := strconv.ParseInt(begin, 10, 16) + to, err2 := strconv.ParseInt(end, 10, 16) + if err1 == nil && err2 == nil && from < to { + parts = make([]string, to-from+1) + for i := from; i <= to; i++ { + parts[i] = fmt.Sprintf("c%d", i) + } + } + } + } + + // although sorting digits as strings is leading to wrong order, + // it doesn't matter because we only need to sort both parts in a similar way + sort.Strings(parts) + + return parts +} diff --git a/pkg/security/securitycontextconstraints/selinux/mustrunas_test.go b/pkg/security/securitycontextconstraints/selinux/mustrunas_test.go index 1000230749ca..e4650cdcd96f 100644 --- a/pkg/security/securitycontextconstraints/selinux/mustrunas_test.go +++ b/pkg/security/securitycontextconstraints/selinux/mustrunas_test.go @@ -62,7 +62,7 @@ func TestMustRunAsValidate(t *testing.T) { return &api.SELinuxOptions{ User: "user", Role: "role", - Level: "level", + Level: "s0:c6,c0", Type: "type", } } @@ -79,43 +79,76 @@ func TestMustRunAsValidate(t *testing.T) { seType := newValidOpts() seType.Type = "invalid" + levelWithIdenticalSensitivity := newValidOpts() + levelWithIdenticalSensitivity.Level = "s0-s0:c6,c0" + + levelWithDifferentOrderOfCategories := newValidOpts() + levelWithDifferentOrderOfCategories.Level = "s0:c0,c6" + + levelWithAbbreviatedCategories := newValidOpts() + levelWithAbbreviatedCategories.Level = "s0:c0.c3" + + levelWithContiguousSeriesOfCategories := newValidOpts() + levelWithContiguousSeriesOfCategories.Level = "s0:c0,c1,c2,c3" + tests := map[string]struct { - seLinux *api.SELinuxOptions - expectedMsg string + seLinux *api.SELinuxOptions + expectedSeLinux *api.SELinuxOptions + expectedMsg string }{ "invalid role": { - seLinux: role, - expectedMsg: "does not match required role", + seLinux: role, + expectedSeLinux: newValidOpts(), + expectedMsg: "does not match required role", }, "invalid user": { - seLinux: user, - expectedMsg: "does not match required user", + seLinux: user, + expectedSeLinux: newValidOpts(), + expectedMsg: "does not match required user", }, "invalid level": { - seLinux: level, - expectedMsg: "does not match required level", + seLinux: level, + expectedSeLinux: newValidOpts(), + expectedMsg: "does not match required level", }, "invalid type": { - seLinux: seType, - expectedMsg: "does not match required type", + seLinux: seType, + expectedSeLinux: newValidOpts(), + expectedMsg: "does not match required type", }, "valid": { - seLinux: newValidOpts(), - expectedMsg: "", + seLinux: newValidOpts(), + expectedSeLinux: newValidOpts(), + expectedMsg: "", + }, + "valid level with identical sensitivity": { // "s0:c6,c0" == "s0-s0:c6,c0" + seLinux: levelWithIdenticalSensitivity, + expectedSeLinux: newValidOpts(), + expectedMsg: "", + }, + "valid level with different order of categories": { // "s0:c6,c0" == "s0:c0,c6" + seLinux: levelWithDifferentOrderOfCategories, + expectedSeLinux: newValidOpts(), + expectedMsg: "", + }, + "valid level with abbreviated categories": { // "s0:c0.c3" == "s0:c0,c1,c2,c3" + seLinux: levelWithAbbreviatedCategories, + expectedSeLinux: levelWithContiguousSeriesOfCategories, + expectedMsg: "", }, - } - - opts := &securityapi.SELinuxContextStrategyOptions{ - SELinuxOptions: newValidOpts(), } for name, tc := range tests { + opts := &securityapi.SELinuxContextStrategyOptions{ + SELinuxOptions: tc.expectedSeLinux, + } mustRunAs, err := NewMustRunAs(opts) if err != nil { t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err) continue } container := &api.Container{ + Name: "selinux-testing-container", SecurityContext: &api.SecurityContext{ SELinuxOptions: tc.seLinux, }, @@ -124,20 +157,20 @@ func TestMustRunAsValidate(t *testing.T) { errs := mustRunAs.Validate(nil, container) //should've passed but didn't if len(tc.expectedMsg) == 0 && len(errs) > 0 { - t.Errorf("%s expected no errors but received %v", name, errs) + t.Errorf("%q expected no errors but received %v", name, errs) } //should've failed but didn't if len(tc.expectedMsg) != 0 && len(errs) == 0 { - t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg) + t.Errorf("%q expected error %s but received no errors", name, tc.expectedMsg) } //failed with additional messages if len(tc.expectedMsg) != 0 && len(errs) > 1 { - t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs) + t.Errorf("%q expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs) } //check that we got the right message if len(tc.expectedMsg) != 0 && len(errs) == 1 { if !strings.Contains(errs[0].Error(), tc.expectedMsg) { - t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs) + t.Errorf("%q expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs) } } }