Skip to content
Merged
34 changes: 34 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@ When a scale operation is performed on a MachineDeployment, the webhook synchron

# core/v1

## LimitRange

### Validation Checks

The checks apply only to user-initiated requests.
Rancher-initiated requests bypass the validation.

#### On delete

A rancher-managed limit range cannot be deleted by users.

#### On create and update

A rancher-managed limit range cannot be created or modified by users.

Neither is it possible to promote an unmanaged resource to rancher-managed.

## Namespace

### Validation Checks
Expand Down Expand Up @@ -134,6 +151,23 @@ The following labels are considered relevant for PSA enforcement:

Validation ensures that the limits for cpu/memory must not be less than the requests for cpu/memory.

## ResourceQuota

### Validation Checks

The checks apply only to user-initiated requests.
Rancher-initiated requests bypass the validation.

#### On delete

A rancher-managed resource quota cannot be deleted by users.

#### On create and update

A rancher-managed resource quota cannot be created or modified by users.

Neither is it possible to promote an unmanaged resource to rancher-managed.

## Secret

### Validation Checks
Expand Down
4 changes: 3 additions & 1 deletion pkg/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ func main() {
"core": {
Types: []interface{}{
&unstructured.Unstructured{},
&corev1.Secret{},
&corev1.LimitRange{},
&corev1.Namespace{},
&corev1.ResourceQuota{},
&corev1.Secret{},
},
},
"autoscaling": {
Expand Down
124 changes: 115 additions & 9 deletions pkg/generated/objects/core/v1/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ func UnstructuredFromRequest(request *admissionv1.AdmissionRequest) (*unstructur
return object, nil
}

// SecretOldAndNewFromRequest gets the old and new Secret objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for Secret.
// Similarly, if the request is a Create operation, then the old object is the zero value for Secret.
func SecretOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.Secret, *v1.Secret, error) {
// LimitRangeOldAndNewFromRequest gets the old and new LimitRange objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for LimitRange.
// Similarly, if the request is a Create operation, then the old object is the zero value for LimitRange.
func LimitRangeOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.LimitRange, *v1.LimitRange, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v1.Secret{}
oldObject := &v1.Secret{}
object := &v1.LimitRange{}
oldObject := &v1.LimitRange{}

if request.Operation != admissionv1.Delete {
err := json.Unmarshal(request.Object.Raw, object)
Expand All @@ -92,15 +92,15 @@ func SecretOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.Secr
return oldObject, object, nil
}

// SecretFromRequest returns a Secret object from the webhook request.
// LimitRangeFromRequest returns a LimitRange object from the webhook request.
// If the operation is a Delete operation, then the old object is returned.
// Otherwise, the new object is returned.
func SecretFromRequest(request *admissionv1.AdmissionRequest) (*v1.Secret, error) {
func LimitRangeFromRequest(request *admissionv1.AdmissionRequest) (*v1.LimitRange, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v1.Secret{}
object := &v1.LimitRange{}
raw := request.Object.Raw

if request.Operation == admissionv1.Delete {
Expand Down Expand Up @@ -167,3 +167,109 @@ func NamespaceFromRequest(request *admissionv1.AdmissionRequest) (*v1.Namespace,

return object, nil
}

// ResourceQuotaOldAndNewFromRequest gets the old and new ResourceQuota objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for ResourceQuota.
// Similarly, if the request is a Create operation, then the old object is the zero value for ResourceQuota.
func ResourceQuotaOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.ResourceQuota, *v1.ResourceQuota, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v1.ResourceQuota{}
oldObject := &v1.ResourceQuota{}

if request.Operation != admissionv1.Delete {
err := json.Unmarshal(request.Object.Raw, object)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}
}

if request.Operation == admissionv1.Create {
return oldObject, object, nil
}

err := json.Unmarshal(request.OldObject.Raw, oldObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err)
}

return oldObject, object, nil
}

// ResourceQuotaFromRequest returns a ResourceQuota object from the webhook request.
// If the operation is a Delete operation, then the old object is returned.
// Otherwise, the new object is returned.
func ResourceQuotaFromRequest(request *admissionv1.AdmissionRequest) (*v1.ResourceQuota, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v1.ResourceQuota{}
raw := request.Object.Raw

if request.Operation == admissionv1.Delete {
raw = request.OldObject.Raw
}

err := json.Unmarshal(raw, object)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}

return object, nil
}

// SecretOldAndNewFromRequest gets the old and new Secret objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for Secret.
// Similarly, if the request is a Create operation, then the old object is the zero value for Secret.
func SecretOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v1.Secret, *v1.Secret, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v1.Secret{}
oldObject := &v1.Secret{}

if request.Operation != admissionv1.Delete {
err := json.Unmarshal(request.Object.Raw, object)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}
}

if request.Operation == admissionv1.Create {
return oldObject, object, nil
}

err := json.Unmarshal(request.OldObject.Raw, oldObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err)
}

return oldObject, object, nil
}

// SecretFromRequest returns a Secret object from the webhook request.
// If the operation is a Delete operation, then the old object is returned.
// Otherwise, the new object is returned.
func SecretFromRequest(request *admissionv1.AdmissionRequest) (*v1.Secret, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v1.Secret{}
raw := request.Object.Raw

if request.Operation == admissionv1.Delete {
raw = request.OldObject.Raw
}

err := json.Unmarshal(raw, object)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal request object: %w", err)
}

return object, nil
}
14 changes: 14 additions & 0 deletions pkg/resources/core/v1/limitrange/LimitRange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Validation Checks

The checks apply only to user-initiated requests.
Rancher-initiated requests bypass the validation.

### On delete

A rancher-managed limit range cannot be deleted by users.

### On create and update

A rancher-managed limit range cannot be created or modified by users.

Neither is it possible to promote an unmanaged resource to rancher-managed.
152 changes: 152 additions & 0 deletions pkg/resources/core/v1/limitrange/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Package limitrange holds the Admitters and Validator for webhook
// validation of requests modifying LimitRange objects. It rejects all attempts
// by users to create, modify, or delete the rancher-managed resource.
package limitrange

import (
"fmt"

"github.com/rancher/webhook/pkg/admission"
objectsv1 "github.com/rancher/webhook/pkg/generated/objects/core/v1"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/trace"
)

// kubernetesNamespaceController is the system user who is allowed to all limit
// ranges, even the rancher-managed one. because it does so only as part of
// deleting the entire namespace.
const kubernetesNamespaceController = "system:serviceaccount:kube-system:namespace-controller"

// defaultLimitRangeLabel is the label that Rancher sets on the namespace
// LimitRange it manages. LimitRanges carrying this label cannot be created,
// modified, or deleted by regular users. This includes indirect creation by
// adding the marker label to an unmanaged user resource. Note, yes, the
// limitrange resource use the same label as resource quota resources.
const defaultLimitRangeLabel = "resourcequota.management.cattle.io/default-resource-quota"

// limitRangeGVR is the GroupVersionResource for core LimitRange objects.
var limitRangeGVR = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "limitranges",
}

// Validator implements admission.ValidatingAdmissionWebhook.
type Validator struct {
admitter admitter
}

// NewValidator returns a LimitRange validator.
func NewValidator() *Validator {
return &Validator{
admitter: admitter{},
}
}

// GVR returns the GroupVersionResource that this webhook handles.
func (v *Validator) GVR() schema.GroupVersionResource {
return limitRangeGVR
}

// Operations returns the list of operations handled by this validator.
func (v *Validator) Operations() []admissionregistrationv1.OperationType {
return []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Update,
admissionregistrationv1.Delete,
}
}

// ValidatingWebhook returns the ValidatingWebhook configuration for this validator.
func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.WebhookClientConfig) []admissionregistrationv1.ValidatingWebhook {
wh := admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.NamespacedScope, v.Operations())
wh.Rules[0].Rule.Resources = []string{limitRangeGVR.Resource, limitRangeGVR.Resource + "/status"}
return []admissionregistrationv1.ValidatingWebhook{*wh}
}

// Admitters returns the list of admitters used by this validator.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}

type admitter struct {
}

// Admit validates the admission request. Note that this validator receives only
// user-made requests. Rancher requests are marked with the webhook bypass and do
// not reach this function
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("limitrange Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

oldRq, newRq, err := objectsv1.LimitRangeOldAndNewFromRequest(&request.AdmissionRequest)
if err != nil {
return nil, fmt.Errorf("failed to decode LimitRange from request: %w", err)
}
switch request.Operation {
case admissionv1.Create:
if hasMarkerLabel(newRq) {
// Reject the user's attempt to create a rancher-managed
// quota resource.
return admission.ResponseBadRequest(
"users are forbidden from creating resources managed by Rancher. Remove the marker label",
), nil
}
case admissionv1.Update:
if request.SubResource == "status" {
oldHasMarkerLabel := hasMarkerLabel(oldRq)
newHasMarkerLabel := hasMarkerLabel(newRq)
if oldHasMarkerLabel && !newHasMarkerLabel {
// Reject the user's attempt to demote a Rancher-managed resource.
return admission.ResponseBadRequest(
"users are forbidden from changing resources managed by Rancher",
), nil
}
if !oldHasMarkerLabel && newHasMarkerLabel {
// Reject the user's attempt to promote an unmanaged resource to Rancher managed.
return admission.ResponseBadRequest(
"users are forbidden from promoting resources to Rancher management. Remove the marker label",
), nil
}
return admission.ResponseAllowed(), nil
}
if hasMarkerLabel(oldRq) {
// Reject the user's attempt to update the
// rancher-managed quota resource
return admission.ResponseBadRequest(
"users are forbidden from changing resources managed by Rancher",
), nil
}
if hasMarkerLabel(newRq) {
// Reject the user's attempt to promote an unmanaged resource to Rancher managed
return admission.ResponseBadRequest(
"users are forbidden from promoting resources to Rancher management. Remove the marker label",
), nil
}
case admissionv1.Delete:
if request.UserInfo.Username == kubernetesNamespaceController {
// The kubernetes controller is allowed to delete the rancher-managed resource.
// This happens as part of deleting the containing namespace.
return admission.ResponseAllowed(), nil
}
if hasMarkerLabel(oldRq) {
// Reject the user's attempt to delete the
// rancher-managed quota resource
return admission.ResponseBadRequest(
"users are forbidden from deleting resources managed by Rancher",
), nil
}
default:
return nil, admission.ErrUnsupportedOperation
}

return admission.ResponseAllowed(), nil
}

// hasMarkerLabel reports whether rq is a Rancher-managed namespace LimitRange resource.
func hasMarkerLabel(rq *corev1.LimitRange) bool {
return rq.Labels[defaultLimitRangeLabel] == "true"
}
Loading