diff --git a/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation.go b/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation.go index ae893607c6b..66512edefe3 100644 --- a/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation.go +++ b/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation.go @@ -17,12 +17,111 @@ limitations under the License. package validation import ( + "strings" + + machinery "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/pod-security-admission/admission/api" + admissionapi "k8s.io/pod-security-admission/admission/api" + "k8s.io/pod-security-admission/api" ) -func ValidatePodSecurityConfiguration(configuration *api.PodSecurityConfiguration) field.ErrorList { +// ValidatePodSecurityConfiguration validates a given PodSecurityConfiguration. +func ValidatePodSecurityConfiguration(configuration *admissionapi.PodSecurityConfiguration) field.ErrorList { allErrs := field.ErrorList{} - // TODO: validate default levels and versions + + // validate defaults + allErrs = append(allErrs, validateLevel(field.NewPath("defaults", "enforce"), configuration.Defaults.Enforce)...) + allErrs = append(allErrs, validateVersion(field.NewPath("defaults", "enforce-version"), configuration.Defaults.EnforceVersion)...) + allErrs = append(allErrs, validateLevel(field.NewPath("defaults", "warn"), configuration.Defaults.Warn)...) + allErrs = append(allErrs, validateVersion(field.NewPath("defaults", "warn-version"), configuration.Defaults.WarnVersion)...) + allErrs = append(allErrs, validateLevel(field.NewPath("defaults", "audit"), configuration.Defaults.Audit)...) + allErrs = append(allErrs, validateVersion(field.NewPath("defaults", "audit-version"), configuration.Defaults.AuditVersion)...) + + // validate exemptions + allErrs = append(allErrs, validateNamespaces(configuration)...) + allErrs = append(allErrs, validateRuntimeClasses(configuration)...) + allErrs = append(allErrs, validateUsernames(configuration)...) + return allErrs } + +// validateLevel validates a level +func validateLevel(p *field.Path, value string) field.ErrorList { + errs := field.ErrorList{} + _, err := api.ParseLevel(value) + if err != nil { + errs = append(errs, field.Invalid(p, value, err.Error())) + } + return errs +} + +// validateVersion validates a version +func validateVersion(p *field.Path, value string) field.ErrorList { + errs := field.ErrorList{} + _, err := api.ParseVersion(value) + if err != nil { + errs = append(errs, field.Invalid(p, value, err.Error())) + } + return errs +} + +func validateNamespaces(configuration *admissionapi.PodSecurityConfiguration) field.ErrorList { + errs := field.ErrorList{} + validSet := sets.NewString() + for i, ns := range configuration.Exemptions.Namespaces { + err := machinery.ValidateNamespaceName(ns, false) + if len(err) > 0 { + path := field.NewPath("exemptions", "namespaces").Index(i) + errs = append(errs, field.Invalid(path, ns, strings.Join(err, ", "))) + continue + } + if validSet.Has(ns) { + path := field.NewPath("exemptions", "namespaces").Index(i) + errs = append(errs, field.Duplicate(path, ns)) + continue + } + validSet.Insert(ns) + } + return errs +} + +func validateRuntimeClasses(configuration *admissionapi.PodSecurityConfiguration) field.ErrorList { + errs := field.ErrorList{} + validSet := sets.NewString() + for i, rc := range configuration.Exemptions.RuntimeClasses { + err := machinery.NameIsDNSSubdomain(rc, false) + if len(err) > 0 { + path := field.NewPath("exemptions", "runtimeClasses").Index(i) + errs = append(errs, field.Invalid(path, rc, strings.Join(err, ", "))) + continue + } + if validSet.Has(rc) { + path := field.NewPath("exemptions", "runtimeClasses").Index(i) + errs = append(errs, field.Duplicate(path, rc)) + continue + } + validSet.Insert(rc) + } + return errs +} + +func validateUsernames(configuration *admissionapi.PodSecurityConfiguration) field.ErrorList { + errs := field.ErrorList{} + validSet := sets.NewString() + for i, uname := range configuration.Exemptions.Usernames { + if uname == "" { + path := field.NewPath("exemptions", "usernames").Index(i) + errs = append(errs, field.Invalid(path, uname, "username must not be empty")) + continue + } + if validSet.Has(uname) { + path := field.NewPath("exemptions", "usernames").Index(i) + errs = append(errs, field.Duplicate(path, uname)) + continue + } + validSet.Insert(uname) + } + + return errs +} diff --git a/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation_test.go b/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation_test.go index 10aebbd6245..df8a17446d9 100644 --- a/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation_test.go +++ b/staging/src/k8s.io/pod-security-admission/admission/api/validation/validation_test.go @@ -15,3 +15,129 @@ limitations under the License. */ package validation + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/pod-security-admission/admission/api" +) + +type ( + test struct { + configuration api.PodSecurityConfiguration + expectedErrList field.ErrorList + } +) + +const ( + invalidValueUppercase = "TEST" + invalidValueChars = "_&$%#" + invalidValueTooLong = "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest" + invalidValueEmpty = "" + validValue = "testing" +) + +func TestValidatePodSecurityConfiguration(t *testing.T) { + tests := []test{ + // defaults + { + expectedErrList: field.ErrorList{ + field.Invalid(exemptionsPath("namespaces", 0), invalidValueEmpty, "..."), + field.Invalid(exemptionsPath("namespaces", 1), invalidValueChars, "..."), + field.Invalid(exemptionsPath("namespaces", 2), invalidValueUppercase, "..."), + field.Invalid(exemptionsPath("namespaces", 3), invalidValueTooLong, "..."), + field.Duplicate(exemptionsPath("namespaces", 5), validValue), + field.Invalid(exemptionsPath("runtimeClasses", 0), invalidValueEmpty, "..."), + field.Invalid(exemptionsPath("runtimeClasses", 1), invalidValueChars, "..."), + field.Invalid(exemptionsPath("runtimeClasses", 2), invalidValueUppercase, "..."), + field.Duplicate(exemptionsPath("runtimeClasses", 4), validValue), + field.Invalid(exemptionsPath("usernames", 0), invalidValueEmpty, "..."), + field.Duplicate(exemptionsPath("usernames", 2), validValue), + }, + configuration: api.PodSecurityConfiguration{ + Defaults: api.PodSecurityDefaults{ + Enforce: "privileged", + EnforceVersion: "v1.22", + Audit: "baseline", + AuditVersion: "v1.25", + Warn: "restricted", + WarnVersion: "latest", + }, + Exemptions: api.PodSecurityExemptions{ + Namespaces: []string{ + invalidValueEmpty, + invalidValueChars, + invalidValueUppercase, + invalidValueTooLong, + validValue, + validValue, + }, + RuntimeClasses: []string{ + invalidValueEmpty, + invalidValueChars, + invalidValueUppercase, + validValue, + validValue, + }, + Usernames: []string{ + invalidValueEmpty, + validValue, + validValue, + }, + }, + }, + }, + { + expectedErrList: field.ErrorList{ + field.Invalid(defaultsPath("enforce"), "baslein", "..."), + field.Invalid(defaultsPath("enforce-version"), "v.122", "..."), + field.Invalid(defaultsPath("warn"), "", "..."), + field.Invalid(defaultsPath("warn-version"), "", "..."), + field.Invalid(defaultsPath("audit"), "lorum", "..."), + field.Invalid(defaultsPath("audit-version"), "ipsum", "..."), + }, + configuration: api.PodSecurityConfiguration{ + Defaults: api.PodSecurityDefaults{ + Enforce: "baslein", + EnforceVersion: "v.122", + Audit: "lorum", + AuditVersion: "ipsum", + Warn: "", + WarnVersion: "", + }, + Exemptions: api.PodSecurityExemptions{}, + }, + }, + } + + for _, test := range tests { + errList := ValidatePodSecurityConfiguration(&test.configuration) + if len(errList) != len(test.expectedErrList) { + t.Errorf("expected %d errs, got %d", len(test.expectedErrList), len(errList)) + } + + for i, expected := range test.expectedErrList { + if expected.Type.String() != errList[i].Type.String() { + t.Errorf("expected err type %s, got %s", + expected.Type.String(), + errList[i].Type.String()) + } + if expected.BadValue != errList[i].BadValue { + t.Errorf("expected bad value '%s', got '%s'", + expected.BadValue, + errList[i].BadValue) + } + } + } +} + +// defaultsPath returns the appropriate defaults path +func defaultsPath(child string) *field.Path { + return field.NewPath("defaults", child) +} + +// exemptionsPath returns the appropriate defaults path +func exemptionsPath(child string, i int) *field.Path { + return field.NewPath("exemptions", child).Index(i) +}