Skip to content

Commit

Permalink
SCC: recognize that SELinux levels can be logically equivalent.
Browse files Browse the repository at this point in the history
For example: s0:c0,c6 is the same as s0:c6,c0
Also add support for categories ranges which can be expressed with
shorthand of c0.c6 and sensitivity with identical levels.
  • Loading branch information
php-coder committed Sep 19, 2017
1 parent 3fddedc commit 391aba5
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 22 deletions.
73 changes: 72 additions & 1 deletion pkg/security/securitycontextconstraints/selinux/mustrunas.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package selinux

import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/api"
Expand Down Expand Up @@ -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))
}
Expand All @@ -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
}
75 changes: 54 additions & 21 deletions pkg/security/securitycontextconstraints/selinux/mustrunas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestMustRunAsValidate(t *testing.T) {
return &api.SELinuxOptions{
User: "user",
Role: "role",
Level: "level",
Level: "s0:c6,c0",
Type: "type",
}
}
Expand All @@ -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,
},
Expand All @@ -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)
}
}
}
Expand Down

0 comments on commit 391aba5

Please sign in to comment.