diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index c7fd8d60742..e8ccd0efbf9 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -36,6 +36,7 @@ import ( _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label" _ "k8s.io/kubernetes/plugin/pkg/admission/resourcequota" + _ "k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy" _ "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny" _ "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount" ) diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission.go b/plugin/pkg/admission/security/podsecuritypolicy/admission.go new file mode 100644 index 00000000000..acf8602a200 --- /dev/null +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission.go @@ -0,0 +1,286 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "fmt" + "io" + "strings" + + "github.com/golang/glog" + + admission "k8s.io/kubernetes/pkg/admission" + api "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/client/cache" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/runtime" + psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + sc "k8s.io/kubernetes/pkg/securitycontext" + "k8s.io/kubernetes/pkg/serviceaccount" + "k8s.io/kubernetes/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/watch" +) + +const ( + PluginName = "PodSecurityPolicy" +) + +func init() { + admission.RegisterPlugin(PluginName, func(client clientset.Interface, config io.Reader) (admission.Interface, error) { + plugin := NewPlugin(client, psp.NewSimpleStrategyFactory(), getMatchingPolicies, false) + plugin.Run() + return plugin, nil + }) +} + +// PSPMatchFn allows plugging in how PSPs are matched against user information. +type PSPMatchFn func(store cache.Store, user user.Info, sa user.Info) ([]*extensions.PodSecurityPolicy, error) + +// podSecurityPolicyPlugin holds state for and implements the admission plugin. +type podSecurityPolicyPlugin struct { + *admission.Handler + client clientset.Interface + strategyFactory psp.StrategyFactory + pspMatcher PSPMatchFn + failOnNoPolicies bool + + reflector *cache.Reflector + stopChan chan struct{} + store cache.Store +} + +var _ admission.Interface = &podSecurityPolicyPlugin{} + +// NewPlugin creates a new PSP admission plugin. +func NewPlugin(kclient clientset.Interface, strategyFactory psp.StrategyFactory, pspMatcher PSPMatchFn, failOnNoPolicies bool) *podSecurityPolicyPlugin { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + reflector := cache.NewReflector( + &cache.ListWatch{ + ListFunc: func(options api.ListOptions) (runtime.Object, error) { + return kclient.Extensions().PodSecurityPolicies().List(options) + }, + WatchFunc: func(options api.ListOptions) (watch.Interface, error) { + return kclient.Extensions().PodSecurityPolicies().Watch(options) + }, + }, + &extensions.PodSecurityPolicy{}, + store, + 0, + ) + + return &podSecurityPolicyPlugin{ + Handler: admission.NewHandler(admission.Create, admission.Update), + client: kclient, + strategyFactory: strategyFactory, + pspMatcher: pspMatcher, + failOnNoPolicies: failOnNoPolicies, + + store: store, + reflector: reflector, + } +} + +func (a *podSecurityPolicyPlugin) Run() { + if a.stopChan == nil { + a.stopChan = make(chan struct{}) + } + a.reflector.RunUntil(a.stopChan) +} +func (a *podSecurityPolicyPlugin) Stop() { + if a.stopChan != nil { + close(a.stopChan) + a.stopChan = nil + } +} + +// Admit determines if the pod should be admitted based on the requested security context +// and the available PSPs. +// +// 1. Find available PSPs. +// 2. Create the providers, includes setting pre-allocated values if necessary. +// 3. Try to generate and validate a PSP with providers. If we find one then admit the pod +// with the validated PSP. If we don't find any reject the pod and give all errors from the +// failed attempts. +func (c *podSecurityPolicyPlugin) Admit(a admission.Attributes) error { + if a.GetResource().GroupResource() != api.Resource("pods") { + return nil + } + + if len(a.GetSubresource()) != 0 { + return nil + } + + pod, ok := a.GetObject().(*api.Pod) + // if we can't convert then we don't handle this object so just return + if !ok { + return nil + } + + // get all constraints that are usable by the user + glog.V(4).Infof("getting pod security policies for pod %s (generate: %s)", pod.Name, pod.GenerateName) + var saInfo user.Info + if len(pod.Spec.ServiceAccountName) > 0 { + saInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "") + } + + matchedPolicies, err := c.pspMatcher(c.store, a.GetUserInfo(), saInfo) + if err != nil { + return admission.NewForbidden(a, err) + } + + // if we have no policies and want to succeed then return. Otherwise we'll end up with no + // providers and fail with "unable to validate against any pod security policy" below. + if len(matchedPolicies) == 0 && !c.failOnNoPolicies { + return nil + } + + providers, errs := c.createProvidersFromPolicies(matchedPolicies, pod.Namespace) + logProviders(pod, providers, errs) + + if len(providers) == 0 { + return admission.NewForbidden(a, fmt.Errorf("no providers available to validate pod request")) + } + + // all containers in a single pod must validate under a single provider or we will reject the request + validationErrs := field.ErrorList{} + for _, provider := range providers { + if errs := assignSecurityContext(provider, pod, field.NewPath(fmt.Sprintf("provider %s: ", provider.GetPSPName()))); len(errs) > 0 { + validationErrs = append(validationErrs, errs...) + continue + } + + // the entire pod validated, annotate and accept the pod + glog.V(4).Infof("pod %s (generate: %s) validated against provider %s", pod.Name, pod.GenerateName, provider.GetPSPName()) + if pod.ObjectMeta.Annotations == nil { + pod.ObjectMeta.Annotations = map[string]string{} + } + pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = provider.GetPSPName() + return nil + } + + // we didn't validate against any provider, reject the pod and give the errors for each attempt + glog.V(4).Infof("unable to validate pod %s (generate: %s) against any pod security policy: %v", pod.Name, pod.GenerateName, validationErrs) + return admission.NewForbidden(a, fmt.Errorf("unable to validate against any pod security policy: %v", validationErrs)) +} + +// assignSecurityContext creates a security context for each container in the pod +// and validates that the sc falls within the psp constraints. All containers must validate against +// the same psp or is not considered valid. +func assignSecurityContext(provider psp.Provider, pod *api.Pod, fldPath *field.Path) field.ErrorList { + generatedSCs := make([]*api.SecurityContext, len(pod.Spec.Containers)) + + errs := field.ErrorList{} + + psc, err := provider.CreatePodSecurityContext(pod) + if err != nil { + errs = append(errs, field.Invalid(field.NewPath("spec", "securityContext"), pod.Spec.SecurityContext, err.Error())) + } + + // save the original PSC and validate the generated PSC. Leave the generated PSC + // set for container generation/validation. We will reset to original post container + // validation. + originalPSC := pod.Spec.SecurityContext + pod.Spec.SecurityContext = psc + errs = append(errs, provider.ValidatePodSecurityContext(pod, field.NewPath("spec", "securityContext"))...) + + // Note: this is not changing the original container, we will set container SCs later so long + // as all containers validated under the same PSP. + for i, containerCopy := range pod.Spec.Containers { + // We will determine the effective security context for the container and validate against that + // since that is how the sc provider will eventually apply settings in the runtime. + // This results in an SC that is based on the Pod's PSC with the set fields from the container + // overriding pod level settings. + containerCopy.SecurityContext = sc.DetermineEffectiveSecurityContext(pod, &containerCopy) + + sc, err := provider.CreateContainerSecurityContext(pod, &containerCopy) + if err != nil { + errs = append(errs, field.Invalid(field.NewPath("spec", "containers").Index(i).Child("securityContext"), "", err.Error())) + continue + } + generatedSCs[i] = sc + + containerCopy.SecurityContext = sc + errs = append(errs, provider.ValidateContainerSecurityContext(pod, &containerCopy, field.NewPath("spec", "containers").Index(i).Child("securityContext"))...) + } + + if len(errs) > 0 { + // ensure psc is not mutated if there are errors + pod.Spec.SecurityContext = originalPSC + return errs + } + + // if we've reached this code then we've generated and validated an SC for every container in the + // pod so let's apply what we generated. Note: the psc is already applied. + for i, sc := range generatedSCs { + pod.Spec.Containers[i].SecurityContext = sc + } + return nil +} + +// createProvidersFromPolicies creates providers from the constraints supplied. +func (c *podSecurityPolicyPlugin) createProvidersFromPolicies(psps []*extensions.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) { + var ( + // collected providers + providers []psp.Provider + // collected errors to return + errs []error + ) + + for _, constraint := range psps { + provider, err := psp.NewSimpleProvider(constraint, namespace, c.strategyFactory) + if err != nil { + errs = append(errs, fmt.Errorf("error creating provider for PSP %s: %v", constraint.Name, err)) + continue + } + providers = append(providers, provider) + } + return providers, errs +} + +// getMatchingPolicies returns policies from the store. For now this returns everything +// in the future it can filter based on UserInfo and permissions. +func getMatchingPolicies(store cache.Store, user user.Info, sa user.Info) ([]*extensions.PodSecurityPolicy, error) { + matchedPolicies := make([]*extensions.PodSecurityPolicy, 0) + + for _, c := range store.List() { + constraint, ok := c.(*extensions.PodSecurityPolicy) + if !ok { + return nil, errors.NewInternalError(fmt.Errorf("error converting object from store to a pod security policy: %v", c)) + } + matchedPolicies = append(matchedPolicies, constraint) + } + + return matchedPolicies, nil +} + +// logProviders logs what providers were found for the pod as well as any errors that were encountered +// while creating providers. +func logProviders(pod *api.Pod, providers []psp.Provider, providerCreationErrs []error) { + names := make([]string, len(providers)) + for i, p := range providers { + names[i] = p.GetPSPName() + } + glog.V(4).Infof("validating pod %s (generate: %s) against providers %s", pod.Name, pod.GenerateName, strings.Join(names, ",")) + + for _, err := range providerCreationErrs { + glog.V(4).Infof("provider creation error: %v", err) + } +} diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go new file mode 100644 index 00000000000..8292f24bae6 --- /dev/null +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go @@ -0,0 +1,1191 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "fmt" + "reflect" + "strings" + "testing" + + kadmission "k8s.io/kubernetes/pkg/admission" + kapi "k8s.io/kubernetes/pkg/api" + extensions "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/client/cache" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + clientsetfake "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + diff "k8s.io/kubernetes/pkg/util/diff" +) + +func NewTestAdmission(store cache.Store, kclient clientset.Interface) kadmission.Interface { + return &podSecurityPolicyPlugin{ + Handler: kadmission.NewHandler(kadmission.Create), + client: kclient, + store: store, + strategyFactory: kpsp.NewSimpleStrategyFactory(), + pspMatcher: getMatchingPolicies, + } +} + +func TestAdmitPrivileged(t *testing.T) { + createPodWithPriv := func(priv bool) *kapi.Pod { + pod := goodPod() + pod.Spec.Containers[0].SecurityContext.Privileged = &priv + return pod + } + + nonPrivilegedPSP := restrictivePSP() + nonPrivilegedPSP.Name = "non-priv" + nonPrivilegedPSP.Spec.Privileged = false + + privilegedPSP := restrictivePSP() + privilegedPSP.Name = "priv" + privilegedPSP.Spec.Privileged = true + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedPriv bool + expectedPSP string + }{ + "pod without priv request allowed under non priv PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{nonPrivilegedPSP}, + shouldPass: true, + expectedPriv: false, + expectedPSP: nonPrivilegedPSP.Name, + }, + "pod without priv request allowed under priv PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{privilegedPSP}, + shouldPass: true, + expectedPriv: false, + expectedPSP: privilegedPSP.Name, + }, + "pod with priv request denied by non priv PSP": { + pod: createPodWithPriv(true), + psps: []*extensions.PodSecurityPolicy{nonPrivilegedPSP}, + shouldPass: false, + }, + "pod with priv request allowed by priv PSP": { + pod: createPodWithPriv(true), + psps: []*extensions.PodSecurityPolicy{nonPrivilegedPSP, privilegedPSP}, + shouldPass: true, + expectedPriv: true, + expectedPSP: privilegedPSP.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.Containers[0].SecurityContext.Privileged == nil || + *v.pod.Spec.Containers[0].SecurityContext.Privileged != v.expectedPriv { + t.Errorf("%s expected privileged to be %t", k, v.expectedPriv) + } + } + } +} + +func TestAdmitCaps(t *testing.T) { + createPodWithCaps := func(caps *kapi.Capabilities) *kapi.Pod { + pod := goodPod() + pod.Spec.Containers[0].SecurityContext.Capabilities = caps + return pod + } + + restricted := restrictivePSP() + + allowsFooInAllowed := restrictivePSP() + allowsFooInAllowed.Name = "allowCapInAllowed" + allowsFooInAllowed.Spec.AllowedCapabilities = []kapi.Capability{"foo"} + + allowsFooInRequired := restrictivePSP() + allowsFooInRequired.Name = "allowCapInRequired" + allowsFooInRequired.Spec.DefaultAddCapabilities = []kapi.Capability{"foo"} + + requiresFooToBeDropped := restrictivePSP() + requiresFooToBeDropped.Name = "requireDrop" + requiresFooToBeDropped.Spec.RequiredDropCapabilities = []kapi.Capability{"foo"} + + tc := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedCapabilities *kapi.Capabilities + expectedPSP string + }{ + // UC 1: if a PSP does not define allowed or required caps then a pod requesting a cap + // should be rejected. + "should reject cap add when not allowed or required": { + pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}), + psps: []*extensions.PodSecurityPolicy{restricted}, + shouldPass: false, + }, + // UC 2: if a PSP allows a cap in the allowed field it should accept the pod request + // to add the cap. + "should accept cap add when in allowed": { + pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}), + psps: []*extensions.PodSecurityPolicy{restricted, allowsFooInAllowed}, + shouldPass: true, + expectedPSP: allowsFooInAllowed.Name, + }, + // UC 3: if a PSP requires a cap then it should accept the pod request + // to add the cap. + "should accept cap add when in required": { + pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}), + psps: []*extensions.PodSecurityPolicy{restricted, allowsFooInRequired}, + shouldPass: true, + expectedPSP: allowsFooInRequired.Name, + }, + // UC 4: if a PSP requires a cap to be dropped then it should fail both + // in the verification of adds and verification of drops + "should reject cap add when requested cap is required to be dropped": { + pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}), + psps: []*extensions.PodSecurityPolicy{restricted, requiresFooToBeDropped}, + shouldPass: false, + }, + // UC 5: if a PSP requires a cap to be dropped it should accept + // a manual request to drop the cap. + "should accept cap drop when cap is required to be dropped": { + pod: createPodWithCaps(&kapi.Capabilities{Drop: []kapi.Capability{"foo"}}), + psps: []*extensions.PodSecurityPolicy{requiresFooToBeDropped}, + shouldPass: true, + expectedPSP: requiresFooToBeDropped.Name, + }, + // UC 6: required add is defaulted + "required add is defaulted": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{allowsFooInRequired}, + shouldPass: true, + expectedCapabilities: &kapi.Capabilities{ + Add: []kapi.Capability{"foo"}, + }, + expectedPSP: allowsFooInRequired.Name, + }, + // UC 7: required drop is defaulted + "required drop is defaulted": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{requiresFooToBeDropped}, + shouldPass: true, + expectedCapabilities: &kapi.Capabilities{ + Drop: []kapi.Capability{"foo"}, + }, + expectedPSP: requiresFooToBeDropped.Name, + }, + } + + for k, v := range tc { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.expectedCapabilities != nil { + if !reflect.DeepEqual(v.expectedCapabilities, v.pod.Spec.Containers[0].SecurityContext.Capabilities) { + t.Errorf("%s resulted in caps that were not expected - expected: %v, received: %v", k, v.expectedCapabilities, v.pod.Spec.Containers[0].SecurityContext.Capabilities) + } + } + } +} + +func TestAdmitVolumes(t *testing.T) { + val := reflect.ValueOf(kapi.VolumeSource{}) + + for i := 0; i < val.NumField(); i++ { + // reflectively create the volume source + fieldVal := val.Type().Field(i) + + volumeSource := kapi.VolumeSource{} + volumeSourceVolume := reflect.New(fieldVal.Type.Elem()) + + reflect.ValueOf(&volumeSource).Elem().FieldByName(fieldVal.Name).Set(volumeSourceVolume) + volume := kapi.Volume{VolumeSource: volumeSource} + + // sanity check before moving on + fsType, err := psputil.GetVolumeFSType(volume) + if err != nil { + t.Errorf("error getting FSType for %s: %s", fieldVal.Name, err.Error()) + continue + } + + // add the volume to the pod + pod := goodPod() + pod.Spec.Volumes = []kapi.Volume{volume} + + // create a PSP that allows no volumes + psp := restrictivePSP() + + // expect a denial for this PSP + testPSPAdmit(fmt.Sprintf("%s denial", string(fsType)), []*extensions.PodSecurityPolicy{psp}, pod, false, "", t) + + // now add the fstype directly to the psp and it should validate + psp.Spec.Volumes = []extensions.FSType{fsType} + testPSPAdmit(fmt.Sprintf("%s direct accept", string(fsType)), []*extensions.PodSecurityPolicy{psp}, pod, true, psp.Name, t) + + // now change the psp to allow any volumes and the pod should still validate + psp.Spec.Volumes = []extensions.FSType{extensions.All} + testPSPAdmit(fmt.Sprintf("%s wildcard accept", string(fsType)), []*extensions.PodSecurityPolicy{psp}, pod, true, psp.Name, t) + } +} + +func TestAdmitHostNetwork(t *testing.T) { + createPodWithHostNetwork := func(hostNetwork bool) *kapi.Pod { + pod := goodPod() + pod.Spec.SecurityContext.HostNetwork = hostNetwork + return pod + } + + noHostNetwork := restrictivePSP() + noHostNetwork.Name = "no-hostnetwork" + noHostNetwork.Spec.HostNetwork = false + + hostNetwork := restrictivePSP() + hostNetwork.Name = "hostnetwork" + hostNetwork.Spec.HostNetwork = true + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedHostNetwork bool + expectedPSP string + }{ + "pod without hostnetwork request allowed under noHostNetwork PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{noHostNetwork}, + shouldPass: true, + expectedHostNetwork: false, + expectedPSP: noHostNetwork.Name, + }, + "pod without hostnetwork request allowed under hostNetwork PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{hostNetwork}, + shouldPass: true, + expectedHostNetwork: false, + expectedPSP: hostNetwork.Name, + }, + "pod with hostnetwork request denied by noHostNetwork PSP": { + pod: createPodWithHostNetwork(true), + psps: []*extensions.PodSecurityPolicy{noHostNetwork}, + shouldPass: false, + }, + "pod with hostnetwork request allowed by hostNetwork PSP": { + pod: createPodWithHostNetwork(true), + psps: []*extensions.PodSecurityPolicy{noHostNetwork, hostNetwork}, + shouldPass: true, + expectedHostNetwork: true, + expectedPSP: hostNetwork.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.SecurityContext.HostNetwork != v.expectedHostNetwork { + t.Errorf("%s expected hostNetwork to be %t", k, v.expectedHostNetwork) + } + } + } +} + +func TestAdmitHostPorts(t *testing.T) { + createPodWithHostPorts := func(port int32) *kapi.Pod { + pod := goodPod() + pod.Spec.Containers[0].Ports = []kapi.ContainerPort{ + {HostPort: port}, + } + return pod + } + + noHostPorts := restrictivePSP() + noHostPorts.Name = "noHostPorts" + + hostPorts := restrictivePSP() + hostPorts.Name = "hostPorts" + hostPorts.Spec.HostPorts = []extensions.HostPortRange{ + {Min: 1, Max: 10}, + } + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedPSP string + }{ + "host port out of range": { + pod: createPodWithHostPorts(11), + psps: []*extensions.PodSecurityPolicy{hostPorts}, + shouldPass: false, + }, + "host port in range": { + pod: createPodWithHostPorts(5), + psps: []*extensions.PodSecurityPolicy{hostPorts}, + shouldPass: true, + expectedPSP: hostPorts.Name, + }, + "no host ports with range": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{hostPorts}, + shouldPass: true, + expectedPSP: hostPorts.Name, + }, + "no host ports without range": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{noHostPorts}, + shouldPass: true, + expectedPSP: noHostPorts.Name, + }, + "host ports without range": { + pod: createPodWithHostPorts(5), + psps: []*extensions.PodSecurityPolicy{noHostPorts}, + shouldPass: false, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + } +} + +func TestAdmitHostPID(t *testing.T) { + createPodWithHostPID := func(hostPID bool) *kapi.Pod { + pod := goodPod() + pod.Spec.SecurityContext.HostPID = hostPID + return pod + } + + noHostPID := restrictivePSP() + noHostPID.Name = "no-hostpid" + noHostPID.Spec.HostPID = false + + hostPID := restrictivePSP() + hostPID.Name = "hostpid" + hostPID.Spec.HostPID = true + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedHostPID bool + expectedPSP string + }{ + "pod without hostpid request allowed under noHostPID PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{noHostPID}, + shouldPass: true, + expectedHostPID: false, + expectedPSP: noHostPID.Name, + }, + "pod without hostpid request allowed under hostPID PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{hostPID}, + shouldPass: true, + expectedHostPID: false, + expectedPSP: hostPID.Name, + }, + "pod with hostpid request denied by noHostPID PSP": { + pod: createPodWithHostPID(true), + psps: []*extensions.PodSecurityPolicy{noHostPID}, + shouldPass: false, + }, + "pod with hostpid request allowed by hostPID PSP": { + pod: createPodWithHostPID(true), + psps: []*extensions.PodSecurityPolicy{noHostPID, hostPID}, + shouldPass: true, + expectedHostPID: true, + expectedPSP: hostPID.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.SecurityContext.HostPID != v.expectedHostPID { + t.Errorf("%s expected hostPID to be %t", k, v.expectedHostPID) + } + } + } +} + +func TestAdmitHostIPC(t *testing.T) { + createPodWithHostIPC := func(hostIPC bool) *kapi.Pod { + pod := goodPod() + pod.Spec.SecurityContext.HostIPC = hostIPC + return pod + } + + noHostIPC := restrictivePSP() + noHostIPC.Name = "no-hostIPC" + noHostIPC.Spec.HostIPC = false + + hostIPC := restrictivePSP() + hostIPC.Name = "hostIPC" + hostIPC.Spec.HostIPC = true + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedHostIPC bool + expectedPSP string + }{ + "pod without hostIPC request allowed under noHostIPC PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{noHostIPC}, + shouldPass: true, + expectedHostIPC: false, + expectedPSP: noHostIPC.Name, + }, + "pod without hostIPC request allowed under hostIPC PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{hostIPC}, + shouldPass: true, + expectedHostIPC: false, + expectedPSP: hostIPC.Name, + }, + "pod with hostIPC request denied by noHostIPC PSP": { + pod: createPodWithHostIPC(true), + psps: []*extensions.PodSecurityPolicy{noHostIPC}, + shouldPass: false, + }, + "pod with hostIPC request allowed by hostIPC PSP": { + pod: createPodWithHostIPC(true), + psps: []*extensions.PodSecurityPolicy{noHostIPC, hostIPC}, + shouldPass: true, + expectedHostIPC: true, + expectedPSP: hostIPC.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.SecurityContext.HostIPC != v.expectedHostIPC { + t.Errorf("%s expected hostIPC to be %t", k, v.expectedHostIPC) + } + } + } +} + +func TestAdmitSELinux(t *testing.T) { + createPodWithSELinux := func(opts *kapi.SELinuxOptions) *kapi.Pod { + pod := goodPod() + // doesn't matter if we set it here or on the container, the + // admission controller uses DetermineEffectiveSC to get the defaulting + // behavior so it can validate what will be applied at runtime + pod.Spec.SecurityContext.SELinuxOptions = opts + return pod + } + + runAsAny := restrictivePSP() + runAsAny.Name = "runAsAny" + runAsAny.Spec.SELinux.Rule = extensions.SELinuxStrategyRunAsAny + + mustRunAs := restrictivePSP() + mustRunAs.Name = "mustRunAs" + mustRunAs.Spec.SELinux.SELinuxOptions.Level = "level" + mustRunAs.Spec.SELinux.SELinuxOptions.Role = "role" + mustRunAs.Spec.SELinux.SELinuxOptions.Type = "type" + mustRunAs.Spec.SELinux.SELinuxOptions.User = "user" + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedSELinux *kapi.SELinuxOptions + expectedPSP string + }{ + "runAsAny with no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedSELinux: nil, + expectedPSP: runAsAny.Name, + }, + "runAsAny with pod request": { + pod: createPodWithSELinux(&kapi.SELinuxOptions{User: "foo"}), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedSELinux: &kapi.SELinuxOptions{User: "foo"}, + expectedPSP: runAsAny.Name, + }, + "mustRunAs with bad pod request": { + pod: createPodWithSELinux(&kapi.SELinuxOptions{User: "foo"}), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: false, + }, + "mustRunAs with no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedSELinux: mustRunAs.Spec.SELinux.SELinuxOptions, + expectedPSP: mustRunAs.Name, + }, + "mustRunAs with good pod request": { + pod: createPodWithSELinux(&kapi.SELinuxOptions{Level: "level", Role: "role", Type: "type", User: "user"}), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedSELinux: mustRunAs.Spec.SELinux.SELinuxOptions, + expectedPSP: mustRunAs.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions == nil && v.expectedSELinux == nil { + // ok, don't need to worry about identifying specific diffs + continue + } + if v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions == nil && v.expectedSELinux != nil { + t.Errorf("%s expected selinux to be: %v but found nil", k, v.expectedSELinux) + continue + } + if v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions != nil && v.expectedSELinux == nil { + t.Errorf("%s expected selinux to be nil but found: %v", k, *v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions) + continue + } + if !reflect.DeepEqual(*v.expectedSELinux, *v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions) { + t.Errorf("%s expected selinux to be: %v but found %v", k, *v.expectedSELinux, *v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions) + } + } + } +} + +func TestAdmitRunAsUser(t *testing.T) { + createPodWithRunAsUser := func(user int64) *kapi.Pod { + pod := goodPod() + // doesn't matter if we set it here or on the container, the + // admission controller uses DetermineEffectiveSC to get the defaulting + // behavior so it can validate what will be applied at runtime + pod.Spec.SecurityContext.RunAsUser = &user + return pod + } + + runAsAny := restrictivePSP() + runAsAny.Name = "runAsAny" + runAsAny.Spec.RunAsUser.Rule = extensions.RunAsUserStrategyRunAsAny + + mustRunAs := restrictivePSP() + mustRunAs.Name = "mustRunAs" + + runAsNonRoot := restrictivePSP() + runAsNonRoot.Name = "runAsNonRoot" + runAsNonRoot.Spec.RunAsUser.Rule = extensions.RunAsUserStrategyMustRunAsNonRoot + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedRunAsUser *int + expectedPSP string + }{ + "runAsAny no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedRunAsUser: nil, + expectedPSP: runAsAny.Name, + }, + "runAsAny pod request": { + pod: createPodWithRunAsUser(1), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedRunAsUser: intPtr(1), + expectedPSP: runAsAny.Name, + }, + "mustRunAs pod request out of range": { + pod: createPodWithRunAsUser(1), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: false, + }, + "mustRunAs pod request in range": { + pod: createPodWithRunAsUser(999), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedRunAsUser: intPtr(int(mustRunAs.Spec.RunAsUser.Ranges[0].Min)), + expectedPSP: mustRunAs.Name, + }, + "mustRunAs no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedRunAsUser: intPtr(int(mustRunAs.Spec.RunAsUser.Ranges[0].Min)), + expectedPSP: mustRunAs.Name, + }, + "runAsNonRoot no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{runAsNonRoot}, + shouldPass: true, + expectedRunAsUser: nil, + expectedPSP: runAsNonRoot.Name, + }, + "runAsNonRoot pod request root": { + pod: createPodWithRunAsUser(0), + psps: []*extensions.PodSecurityPolicy{runAsNonRoot}, + shouldPass: false, + }, + "runAsNonRoot pod request non-root": { + pod: createPodWithRunAsUser(1), + psps: []*extensions.PodSecurityPolicy{runAsNonRoot}, + shouldPass: true, + expectedRunAsUser: intPtr(1), + expectedPSP: runAsNonRoot.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.Containers[0].SecurityContext.RunAsUser == nil && v.expectedRunAsUser == nil { + // ok, don't need to worry about identifying specific diffs + continue + } + if v.pod.Spec.Containers[0].SecurityContext.RunAsUser == nil && v.expectedRunAsUser != nil { + t.Errorf("%s expected RunAsUser to be: %v but found nil", k, v.expectedRunAsUser) + continue + } + if v.pod.Spec.Containers[0].SecurityContext.RunAsUser != nil && v.expectedRunAsUser == nil { + t.Errorf("%s expected RunAsUser to be nil but found: %v", k, *v.pod.Spec.Containers[0].SecurityContext.RunAsUser) + continue + } + if int64(*v.expectedRunAsUser) != *v.pod.Spec.Containers[0].SecurityContext.RunAsUser { + t.Errorf("%s expected RunAsUser to be: %v but found %v", k, *v.expectedRunAsUser, *v.pod.Spec.Containers[0].SecurityContext.RunAsUser) + } + } + } +} + +func TestAdmitSupplementalGroups(t *testing.T) { + createPodWithSupGroup := func(group int64) *kapi.Pod { + pod := goodPod() + // doesn't matter if we set it here or on the container, the + // admission controller uses DetermineEffectiveSC to get the defaulting + // behavior so it can validate what will be applied at runtime + pod.Spec.SecurityContext.SupplementalGroups = []int64{group} + return pod + } + + runAsAny := restrictivePSP() + runAsAny.Name = "runAsAny" + runAsAny.Spec.SupplementalGroups.Rule = extensions.SupplementalGroupsStrategyRunAsAny + + mustRunAs := restrictivePSP() + mustRunAs.Name = "mustRunAs" + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedSupGroups []int64 + expectedPSP string + }{ + "runAsAny no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedSupGroups: []int64{}, + expectedPSP: runAsAny.Name, + }, + "runAsAny pod request": { + pod: createPodWithSupGroup(1), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedSupGroups: []int64{1}, + expectedPSP: runAsAny.Name, + }, + "mustRunAs no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedSupGroups: []int64{mustRunAs.Spec.SupplementalGroups.Ranges[0].Min}, + expectedPSP: mustRunAs.Name, + }, + "mustRunAs bad pod request": { + pod: createPodWithSupGroup(1), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: false, + }, + "mustRunAs good pod request": { + pod: createPodWithSupGroup(999), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedSupGroups: []int64{999}, + expectedPSP: mustRunAs.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.SecurityContext.SupplementalGroups == nil && v.expectedSupGroups != nil { + t.Errorf("%s expected SupplementalGroups to be: %v but found nil", k, v.expectedSupGroups) + continue + } + if v.pod.Spec.SecurityContext.SupplementalGroups != nil && v.expectedSupGroups == nil { + t.Errorf("%s expected SupplementalGroups to be nil but found: %v", k, v.pod.Spec.SecurityContext.SupplementalGroups) + continue + } + if !reflect.DeepEqual(v.expectedSupGroups, v.pod.Spec.SecurityContext.SupplementalGroups) { + t.Errorf("%s expected SupplementalGroups to be: %v but found %v", k, v.expectedSupGroups, v.pod.Spec.SecurityContext.SupplementalGroups) + } + } + } +} + +func TestAdmitFSGroup(t *testing.T) { + createPodWithFSGroup := func(group int64) *kapi.Pod { + pod := goodPod() + // doesn't matter if we set it here or on the container, the + // admission controller uses DetermineEffectiveSC to get the defaulting + // behavior so it can validate what will be applied at runtime + pod.Spec.SecurityContext.FSGroup = &group + return pod + } + + runAsAny := restrictivePSP() + runAsAny.Name = "runAsAny" + runAsAny.Spec.FSGroup.Rule = extensions.FSGroupStrategyRunAsAny + + mustRunAs := restrictivePSP() + mustRunAs.Name = "mustRunAs" + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedFSGroup *int64 + expectedPSP string + }{ + "runAsAny no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedFSGroup: nil, + expectedPSP: runAsAny.Name, + }, + "runAsAny pod request": { + pod: createPodWithFSGroup(1), + psps: []*extensions.PodSecurityPolicy{runAsAny}, + shouldPass: true, + expectedFSGroup: int64Ptr(1), + expectedPSP: runAsAny.Name, + }, + "mustRunAs no pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedFSGroup: &mustRunAs.Spec.SupplementalGroups.Ranges[0].Min, + expectedPSP: mustRunAs.Name, + }, + "mustRunAs bad pod request": { + pod: createPodWithFSGroup(1), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: false, + }, + "mustRunAs good pod request": { + pod: createPodWithFSGroup(999), + psps: []*extensions.PodSecurityPolicy{mustRunAs}, + shouldPass: true, + expectedFSGroup: int64Ptr(999), + expectedPSP: mustRunAs.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.SecurityContext.FSGroup == nil && v.expectedFSGroup == nil { + // ok, don't need to worry about identifying specific diffs + continue + } + if v.pod.Spec.SecurityContext.FSGroup == nil && v.expectedFSGroup != nil { + t.Errorf("%s expected FSGroup to be: %v but found nil", k, *v.expectedFSGroup) + continue + } + if v.pod.Spec.SecurityContext.FSGroup != nil && v.expectedFSGroup == nil { + t.Errorf("%s expected FSGroup to be nil but found: %v", k, *v.pod.Spec.SecurityContext.FSGroup) + continue + } + if *v.expectedFSGroup != *v.pod.Spec.SecurityContext.FSGroup { + t.Errorf("%s expected FSGroup to be: %v but found %v", k, *v.expectedFSGroup, *v.pod.Spec.SecurityContext.FSGroup) + } + } + } +} + +func TestAdmitReadOnlyRootFilesystem(t *testing.T) { + createPodWithRORFS := func(rorfs bool) *kapi.Pod { + pod := goodPod() + pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &rorfs + return pod + } + + noRORFS := restrictivePSP() + noRORFS.Name = "no-rorfs" + noRORFS.Spec.ReadOnlyRootFilesystem = false + + rorfs := restrictivePSP() + rorfs.Name = "rorfs" + rorfs.Spec.ReadOnlyRootFilesystem = true + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedRORFS bool + expectedPSP string + }{ + "no-rorfs allows pod request with rorfs": { + pod: createPodWithRORFS(true), + psps: []*extensions.PodSecurityPolicy{noRORFS}, + shouldPass: true, + expectedRORFS: true, + expectedPSP: noRORFS.Name, + }, + "no-rorfs allows pod request without rorfs": { + pod: createPodWithRORFS(false), + psps: []*extensions.PodSecurityPolicy{noRORFS}, + shouldPass: true, + expectedRORFS: false, + expectedPSP: noRORFS.Name, + }, + "rorfs rejects pod request without rorfs": { + pod: createPodWithRORFS(false), + psps: []*extensions.PodSecurityPolicy{rorfs}, + shouldPass: false, + }, + "rorfs defaults nil pod request": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{rorfs}, + shouldPass: true, + expectedRORFS: true, + expectedPSP: rorfs.Name, + }, + "rorfs accepts pod request with rorfs": { + pod: createPodWithRORFS(true), + psps: []*extensions.PodSecurityPolicy{rorfs}, + shouldPass: true, + expectedRORFS: true, + expectedPSP: rorfs.Name, + }, + } + + for k, v := range tests { + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + if v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem == nil || + *v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem != v.expectedRORFS { + t.Errorf("%s expected ReadOnlyRootFilesystem to be %t but found %#v", k, v.expectedRORFS, v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem) + } + } + } +} + +func testPSPAdmit(testCaseName string, psps []*extensions.PodSecurityPolicy, pod *kapi.Pod, shouldPass bool, expectedPSP string, t *testing.T) { + namespace := createNamespaceForTest() + serviceAccount := createSAForTest() + tc := clientsetfake.NewSimpleClientset(namespace, serviceAccount) + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + + for _, psp := range psps { + store.Add(psp) + } + + plugin := NewTestAdmission(store, tc) + + attrs := kadmission.NewAttributesRecord(pod, kapi.Kind("Pod").WithVersion("version"), "namespace", "", kapi.Resource("pods").WithVersion("version"), "", kadmission.Create, &user.DefaultInfo{}) + err := plugin.Admit(attrs) + + if shouldPass && err != nil { + t.Errorf("%s expected no errors but received %v", testCaseName, err) + } + + if shouldPass && err == nil { + if pod.Annotations[psputil.ValidatedPSPAnnotation] != expectedPSP { + t.Errorf("%s expected to validate under %s but found %s", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation]) + } + } + + if !shouldPass && err == nil { + t.Errorf("%s expected errors but received none", testCaseName) + } +} + +func TestAssignSecurityContext(t *testing.T) { + // psp that will deny privileged container requests and has a default value for a field (uid) + psp := restrictivePSP() + provider, err := kpsp.NewSimpleProvider(psp, "namespace", kpsp.NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("failed to create provider: %v", err) + } + + createContainer := func(priv bool) kapi.Container { + return kapi.Container{ + SecurityContext: &kapi.SecurityContext{ + Privileged: &priv, + }, + } + } + + // these are set up such that the containers always have a nil uid. If the case should not + // validate then the uids should not have been updated by the strategy. If the case should + // validate then uids should be set. This is ensuring that we're hanging on to the old SC + // as we generate/validate and only updating the original container if the entire pod validates + testCases := map[string]struct { + pod *kapi.Pod + shouldValidate bool + expectedUID *int64 + }{ + "pod and container SC is not changed when invalid": { + pod: &kapi.Pod{ + Spec: kapi.PodSpec{ + SecurityContext: &kapi.PodSecurityContext{}, + Containers: []kapi.Container{createContainer(true)}, + }, + }, + shouldValidate: false, + }, + "must validate all containers": { + pod: &kapi.Pod{ + Spec: kapi.PodSpec{ + // good container and bad container + SecurityContext: &kapi.PodSecurityContext{}, + Containers: []kapi.Container{createContainer(false), createContainer(true)}, + }, + }, + shouldValidate: false, + }, + "pod validates": { + pod: &kapi.Pod{ + Spec: kapi.PodSpec{ + SecurityContext: &kapi.PodSecurityContext{}, + Containers: []kapi.Container{createContainer(false)}, + }, + }, + shouldValidate: true, + }, + } + + for k, v := range testCases { + errs := assignSecurityContext(provider, v.pod, nil) + if v.shouldValidate && len(errs) > 0 { + t.Errorf("%s expected to validate but received errors %v", k, errs) + continue + } + if !v.shouldValidate && len(errs) == 0 { + t.Errorf("%s expected validation errors but received none", k) + continue + } + + // if we shouldn't have validated ensure that uid is not set on the containers + if !v.shouldValidate { + for _, c := range v.pod.Spec.Containers { + if c.SecurityContext.RunAsUser != nil { + t.Errorf("%s had non-nil UID %d. UID should not be set on test cases that don't validate", k, *c.SecurityContext.RunAsUser) + } + } + } + + // if we validated then the pod sc should be updated now with the defaults from the psp + if v.shouldValidate { + for _, c := range v.pod.Spec.Containers { + if *c.SecurityContext.RunAsUser != 999 { + t.Errorf("%s expected uid to be defaulted to 999 but found %v", k, *c.SecurityContext.RunAsUser) + } + } + } + } +} + +func TestCreateProvidersFromConstraints(t *testing.T) { + testCases := map[string]struct { + // use a generating function so we can test for non-mutation + psp func() *extensions.PodSecurityPolicy + expectedErr string + }{ + "valid psp": { + psp: func() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: kapi.ObjectMeta{ + Name: "valid psp", + }, + Spec: extensions.PodSecurityPolicySpec{ + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyRunAsAny, + }, + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyRunAsAny, + }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, + }, + } + }, + }, + "bad psp strategy options": { + psp: func() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: kapi.ObjectMeta{ + Name: "bad psp user options", + }, + Spec: extensions.PodSecurityPolicySpec{ + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyRunAsAny, + }, + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyMustRunAs, + }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, + }, + } + }, + expectedErr: "MustRunAsRange requires at least one range", + }, + } + + for k, v := range testCases { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + + tc := clientsetfake.NewSimpleClientset() + admit := &podSecurityPolicyPlugin{ + Handler: kadmission.NewHandler(kadmission.Create, kadmission.Update), + client: tc, + store: store, + strategyFactory: kpsp.NewSimpleStrategyFactory(), + } + + psp := v.psp() + _, errs := admit.createProvidersFromPolicies([]*extensions.PodSecurityPolicy{psp}, "namespace") + + if !reflect.DeepEqual(psp, v.psp()) { + diff := diff.ObjectDiff(psp, v.psp()) + t.Errorf("%s createProvidersFromPolicies mutated policy. diff:\n%s", k, diff) + } + if len(v.expectedErr) > 0 && len(errs) != 1 { + t.Errorf("%s expected a single error '%s' but received %v", k, v.expectedErr, errs) + continue + } + if len(v.expectedErr) == 0 && len(errs) != 0 { + t.Errorf("%s did not expect an error but received %v", k, errs) + continue + } + + // check that we got the error we expected + if len(v.expectedErr) > 0 { + if !strings.Contains(errs[0].Error(), v.expectedErr) { + t.Errorf("%s expected error '%s' but received %v", k, v.expectedErr, errs[0]) + } + } + } +} + +func restrictivePSP() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: kapi.ObjectMeta{ + Name: "restrictive", + }, + Spec: extensions.PodSecurityPolicySpec{ + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 999, Max: 999}, + }, + }, + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyMustRunAs, + SELinuxOptions: &kapi.SELinuxOptions{ + Level: "s9:z0,z1", + }, + }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 999, Max: 999}, + }, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 999, Max: 999}, + }, + }, + }, + } +} + +func createNamespaceForTest() *kapi.Namespace { + return &kapi.Namespace{ + ObjectMeta: kapi.ObjectMeta{ + Name: "default", + }, + } +} + +func createSAForTest() *kapi.ServiceAccount { + return &kapi.ServiceAccount{ + ObjectMeta: kapi.ObjectMeta{ + Name: "default", + }, + } +} + +// goodPod is empty and should not be used directly for testing since we're providing +// two different PSPs. Since no values are specified it would be allowed to match any +// psp when defaults are filled in. +func goodPod() *kapi.Pod { + return &kapi.Pod{ + Spec: kapi.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &kapi.PodSecurityContext{}, + Containers: []kapi.Container{ + { + SecurityContext: &kapi.SecurityContext{}, + }, + }, + }, + } +} + +func intPtr(i int) *int { + return &i +} + +func int64Ptr(i int) *int64 { + i64 := int64(i) + return &i64 +}