func (e *exists) Admit(a admission.Attributes) (err error) { defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) if err != nil { return err } mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) if err != nil { return err } if mapping.Scope.Name() != meta.RESTScopeNameNamespace { return nil } namespace := &api.Namespace{ ObjectMeta: api.ObjectMeta{ Name: a.GetNamespace(), Namespace: "", }, Status: api.NamespaceStatus{}, } _, exists, err := e.store.Get(namespace) if err != nil { return err } if exists { return nil } obj := a.GetObject() name := "Unknown" if obj != nil { name, _ = meta.NewAccessor().Name(obj) } return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s does not exist", a.GetNamespace())) }
// forbidden renders a simple forbidden error func forbidden(reason, apiVersion string, w http.ResponseWriter, req *http.Request) { // the api version can be empty for two basic reasons: // 1. malformed API request // 2. not an API request at all // In these cases, just assume the latest version that will work better than nothing if len(apiVersion) == 0 { apiVersion = klatest.Version } // Reason is an opaque string that describes why access is allowed or forbidden (forbidden by the time we reach here). // We don't have direct access to kind or name (not that those apply either in the general case) // We create a NewForbidden to stay close the API, but then we override the message to get a serialization // that makes sense when a human reads it. forbiddenError, _ := kapierror.NewForbidden("", "", errors.New("")).(*kapierror.StatusError) forbiddenError.ErrStatus.Message = reason // Not all API versions in valid API requests will have a matching codec in kubernetes. If we can't find one, // just default to the latest kube codec. codec := klatest.Codec if requestedCodec, err := klatest.InterfacesFor(apiVersion); err == nil { codec = requestedCodec } formatted := &bytes.Buffer{} output, err := codec.Encode(&forbiddenError.ErrStatus) if err != nil { fmt.Fprintf(formatted, "%s", forbiddenError.Error()) } else { _ = json.Indent(formatted, output, "", " ") } w.Header().Set("Content-Type", restful.MIME_JSON) w.WriteHeader(http.StatusForbidden) w.Write(formatted.Bytes()) }
// Validate validates a new image stream. func (s Strategy) Validate(ctx kapi.Context, obj runtime.Object) fielderrors.ValidationErrorList { stream := obj.(*api.ImageStream) user, ok := kapi.UserFrom(ctx) if !ok { return fielderrors.ValidationErrorList{kerrors.NewForbidden("imageStream", stream.Name, fmt.Errorf("unable to update an ImageStream without a user on the context"))} } errs := s.tagVerifier.Verify(nil, stream, user) errs = append(errs, s.tagsChanged(nil, stream)...) errs = append(errs, validation.ValidateImageStream(stream)...) return errs }
// List retrieves a list of Projects that match label. func (s *REST) List(ctx kapi.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { user, ok := kapi.UserFrom(ctx) if !ok { return nil, kerrors.NewForbidden("Project", "", fmt.Errorf("unable to list projects without a user on the context")) } namespaceList, err := s.lister.List(user) if err != nil { return nil, err } return convertNamespaceList(namespaceList), nil }
// Admit will deny any SecurityContext that defines options that were not previously available in the api.Container // struct (Capabilities and Privileged) func (p *plugin) Admit(a admission.Attributes) (err error) { if a.GetResource() != string(api.ResourcePods) { return nil } pod, ok := a.GetObject().(*api.Pod) if !ok { return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") } for _, v := range pod.Spec.Containers { if v.SecurityContext != nil { if v.SecurityContext.SELinuxOptions != nil { return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden")) } if v.SecurityContext.RunAsUser != nil { return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden")) } } } return nil }
// Admit enforces that pod and its project node label selectors matches at least a node in the cluster. func (p *podNodeEnvironment) Admit(a admission.Attributes) (err error) { // ignore anything except create or update of pods if !(a.GetOperation() == admission.Create || a.GetOperation() == admission.Update) { return nil } resource := a.GetResource() if resource != "pods" { return nil } obj := a.GetObject() pod, ok := obj.(*kapi.Pod) if !ok { return nil } name := pod.Name projects, err := projectcache.GetProjectCache() if err != nil { return err } namespace, err := projects.GetNamespaceObject(a.GetNamespace()) if err != nil { return apierrors.NewForbidden(resource, name, err) } projectNodeSelector, err := projects.GetNodeSelectorMap(namespace) if err != nil { return err } if labelselector.Conflicts(projectNodeSelector, pod.Spec.NodeSelector) { return apierrors.NewForbidden(resource, name, fmt.Errorf("pod node label selector conflicts with its project node label selector")) } // modify pod node selector = project node selector + current pod node selector pod.Spec.NodeSelector = labelselector.Merge(projectNodeSelector, pod.Spec.NodeSelector) return nil }
// NewForbidden is a utility function to return a well-formatted admission control error response func NewForbidden(a Attributes, internalError error) error { // do not double wrap an error of same type if apierrors.IsForbidden(internalError) { return internalError } name := "Unknown" kind := a.GetKind() obj := a.GetObject() if obj != nil { objectMeta, err := api.ObjectMetaFor(obj) if err != nil { return apierrors.NewForbidden(kind, name, internalError) } // this is necessary because name object name generation has not occurred yet if len(objectMeta.Name) > 0 { name = objectMeta.Name } else if len(objectMeta.GenerateName) > 0 { name = objectMeta.GenerateName } } return apierrors.NewForbidden(kind, name, internalError) }
func TestErrors(t *testing.T) { o := testclient.NewObjects(kapi.Scheme, kapi.Scheme) o.Add(&kapi.List{ Items: []runtime.Object{ &(errors.NewNotFound("DeploymentConfigList", "").(*errors.StatusError).ErrStatus), &(errors.NewForbidden("DeploymentConfigList", "", nil).(*errors.StatusError).ErrStatus), }, }) oc, _ := NewFixtureClients(o) _, err := oc.DeploymentConfigs("test").List(labels.Everything(), fields.Everything()) if !errors.IsNotFound(err) { t.Fatalf("unexpected error: %v", err) } t.Logf("error: %#v", err.(*errors.StatusError).Status()) _, err = oc.DeploymentConfigs("test").List(labels.Everything(), fields.Everything()) if !errors.IsForbidden(err) { t.Fatalf("unexpected error: %v", err) } }
func TestErrors(t *testing.T) { o := NewObjects(api.Scheme, api.Scheme) o.Add(&api.List{ Items: []runtime.Object{ // This first call to List will return this error &(errors.NewNotFound("ServiceList", "").(*errors.StatusError).ErrStatus), // The second call to List will return this error &(errors.NewForbidden("ServiceList", "", nil).(*errors.StatusError).ErrStatus), }, }) client := &Fake{ReactFn: ObjectReaction(o, latest.RESTMapper)} _, err := client.Services("test").List(labels.Everything()) if !errors.IsNotFound(err) { t.Fatalf("unexpected error: %v", err) } t.Logf("error: %#v", err.(*errors.StatusError).Status()) _, err = client.Services("test").List(labels.Everything()) if !errors.IsForbidden(err) { t.Fatalf("unexpected error: %v", err) } }
func (l *lifecycle) Admit(a admission.Attributes) (err error) { // prevent deletion of immortal namespaces if a.GetOperation() == admission.Delete { if a.GetKind() == "Namespace" && l.immortalNamespaces.Has(a.GetName()) { return errors.NewForbidden(a.GetKind(), a.GetName(), fmt.Errorf("namespace can never be deleted")) } return nil } defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) if err != nil { return admission.NewForbidden(a, err) } mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) if err != nil { return admission.NewForbidden(a, err) } if mapping.Scope.Name() != meta.RESTScopeNameNamespace { return nil } namespaceObj, exists, err := l.store.Get(&api.Namespace{ ObjectMeta: api.ObjectMeta{ Name: a.GetNamespace(), Namespace: "", }, }) if err != nil { return admission.NewForbidden(a, err) } if !exists { return nil } namespace := namespaceObj.(*api.Namespace) if namespace.Status.Phase != api.NamespaceTerminating { return nil } return admission.NewForbidden(a, fmt.Errorf("Unable to create new content in namespace %s because it is being terminated.", a.GetNamespace())) }
// Get retrieves the item from etcd. func (r *REST) Get(ctx kapi.Context, name string) (runtime.Object, error) { // "~" means the currently authenticated user if name == "~" { user, ok := kapi.UserFrom(ctx) if !ok || user.GetName() == "" { return nil, kerrs.NewForbidden("user", "~", errors.New("requests to ~ must be authenticated")) } name = user.GetName() // remove the known virtual groups from the list if they are present contextGroups := kutil.NewStringSet(user.GetGroups()...) contextGroups.Delete(bootstrappolicy.UnauthenticatedGroup, bootstrappolicy.AuthenticatedGroup) if ok, _ := validation.ValidateUserName(name, false); !ok { // The user the authentication layer has identified cannot possibly be a persisted user // Return an API representation of the virtual user return &api.User{ObjectMeta: kapi.ObjectMeta{Name: name}, Groups: contextGroups.List()}, nil } obj, err := r.Etcd.Get(ctx, name) if err == nil { return obj, nil } if !kerrs.IsNotFound(err) { return nil, err } return &api.User{ObjectMeta: kapi.ObjectMeta{Name: name}, Groups: contextGroups.List()}, nil } if ok, details := validation.ValidateUserName(name, false); !ok { return nil, fielderrors.NewFieldInvalid("metadata.name", name, details) } return r.Etcd.Get(ctx, name) }
// Admit admits resources into cluster that do not violate any defined LimitRange in the namespace func (l *limitRanger) Admit(a admission.Attributes) (err error) { // ignore deletes if a.GetOperation() == "DELETE" { return nil } obj := a.GetObject() resource := a.GetResource() name := "Unknown" if obj != nil { name, _ = meta.NewAccessor().Name(obj) } key := &api.LimitRange{ ObjectMeta: api.ObjectMeta{ Namespace: a.GetNamespace(), Name: "", }, } items, err := l.indexer.Index("namespace", key) if err != nil { return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing limit ranges", a.GetOperation(), resource)) } if len(items) == 0 { return nil } // ensure it meets each prescribed min/max for i := range items { limitRange := items[i].(*api.LimitRange) err = l.limitFunc(limitRange, a.GetResource(), a.GetObject()) if err != nil { return err } } return nil }
func (r *REST) List(ctx kapi.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { userInfo, exists := kapi.UserFrom(ctx) if !exists { return nil, errors.New("a user must be provided") } // the caller might not have permission to run a subject access review (he has it by default, but it could have been removed). // So we'll escalate for the subject access review to determine rights accessReview := &authorizationapi.SubjectAccessReview{ Verb: "create", Resource: "projectrequests", User: userInfo.GetName(), Groups: util.NewStringSet(userInfo.GetGroups()...), } accessReviewResponse, err := r.openshiftClient.ClusterSubjectAccessReviews().Create(accessReview) if err != nil { return nil, err } if accessReviewResponse.Allowed { return &kapi.Status{Status: kapi.StatusSuccess}, nil } forbiddenError, _ := kapierror.NewForbidden("ProjectRequest", "", errors.New("you may not request a new project via this API.")).(*kapierror.StatusError) if len(r.message) > 0 { forbiddenError.ErrStatus.Message = r.message forbiddenError.ErrStatus.Details = &kapi.StatusDetails{ Kind: "ProjectRequest", Causes: []kapi.StatusCause{ {Message: r.message}, }, } } else { forbiddenError.ErrStatus.Message = "You may not request a new project via this API." } return nil, forbiddenError }
// Admit enforces that a namespace must exist in order to associate content with it. // Admit enforces that a namespace that is terminating cannot accept new content being associated with it. func (e *lifecycle) Admit(a admission.Attributes) (err error) { if len(a.GetNamespace()) == 0 { return nil } defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) if err != nil { glog.V(4).Infof("Ignoring life-cycle enforcement for resource %v; no associated default version and kind could be found.", a.GetResource()) return nil } mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) if err != nil { return admission.NewForbidden(a, err) } if mapping.Scope.Name() != meta.RESTScopeNameNamespace { return nil } // we want to allow someone to delete something in case it was phantom created somehow if a.GetOperation() == "DELETE" { return nil } name := "Unknown" obj := a.GetObject() if obj != nil { name, _ = meta.NewAccessor().Name(obj) } projects, err := cache.GetProjectCache() if err != nil { return admission.NewForbidden(a, err) } namespace, err := projects.GetNamespaceObject(a.GetNamespace()) if err != nil { return admission.NewForbidden(a, err) } if a.GetOperation() != "CREATE" { return nil } if namespace.Status.Phase == kapi.NamespaceTerminating && !e.creatableResources.Has(strings.ToLower(a.GetResource())) { return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s is terminating", a.GetNamespace())) } // in case of concurrency issues, we will retry this logic numRetries := 10 interval := time.Duration(rand.Int63n(90)+int64(10)) * time.Millisecond for retry := 1; retry <= numRetries; retry++ { // associate this namespace with openshift _, err = projectutil.Associate(e.client, namespace) if err == nil { break } // we have exhausted all reasonable efforts to retry so give up now if retry == numRetries { return admission.NewForbidden(a, err) } // get the latest namespace for the next pass in case of resource version updates time.Sleep(interval) // it's possible the namespace actually was deleted, so just forbid if this occurs namespace, err = e.client.Namespaces().Get(a.GetNamespace()) if err != nil { return admission.NewForbidden(a, err) } } return nil }
// PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range func PodLimitFunc(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error { if resourceName != "pods" { return nil } pod := obj.(*api.Pod) podCPU := int64(0) podMem := int64(0) minContainerCPU := int64(0) minContainerMem := int64(0) maxContainerCPU := int64(0) maxContainerMem := int64(0) for i := range pod.Spec.Containers { container := pod.Spec.Containers[i] containerCPU := container.Resources.Limits.Cpu().MilliValue() containerMem := container.Resources.Limits.Memory().Value() if i == 0 { minContainerCPU = containerCPU minContainerMem = containerMem maxContainerCPU = containerCPU maxContainerMem = containerMem } podCPU = podCPU + container.Resources.Limits.Cpu().MilliValue() podMem = podMem + container.Resources.Limits.Memory().Value() minContainerCPU = Min(containerCPU, minContainerCPU) minContainerMem = Min(containerMem, minContainerMem) maxContainerCPU = Max(containerCPU, maxContainerCPU) maxContainerMem = Max(containerMem, maxContainerMem) } for i := range limitRange.Spec.Limits { limit := limitRange.Spec.Limits[i] for _, minOrMax := range []string{"Min", "Max"} { var rl api.ResourceList switch minOrMax { case "Min": rl = limit.Min case "Max": rl = limit.Max } for k, v := range rl { observed := int64(0) enforced := int64(0) var err error switch k { case api.ResourceMemory: enforced = v.Value() switch limit.Type { case api.LimitTypePod: observed = podMem err = fmt.Errorf("%simum memory usage per pod is %s", minOrMax, v.String()) case api.LimitTypeContainer: observed = maxContainerMem err = fmt.Errorf("%simum memory usage per container is %s", minOrMax, v.String()) } case api.ResourceCPU: enforced = v.MilliValue() switch limit.Type { case api.LimitTypePod: observed = podCPU err = fmt.Errorf("%simum CPU usage per pod is %s, but requested %s", minOrMax, v.String(), resource.NewMilliQuantity(observed, resource.DecimalSI)) case api.LimitTypeContainer: observed = maxContainerCPU err = fmt.Errorf("%simum CPU usage per container is %s", minOrMax, v.String()) } } switch minOrMax { case "Min": if observed < enforced { return apierrors.NewForbidden(resourceName, pod.Name, err) } case "Max": if observed > enforced { return apierrors.NewForbidden(resourceName, pod.Name, err) } } } } } return nil }
func (alwaysDeny) Admit(a admission.Attributes) (err error) { return apierrors.NewForbidden(a.GetResource(), "", errors.New("Admission control is denying all modifications")) }