Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions pkg/resources/core/v1/namespace/deleteNamespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package namespace

import (
"fmt"

"github.com/rancher/webhook/pkg/admission"
objectsv1 "github.com/rancher/webhook/pkg/generated/objects/core/v1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/utils/trace"
)

// deleteNamespaceAdmitter handles namespace deletion scenarios
type deleteNamespaceAdmitter struct{}

func (d deleteNamespaceAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("Namespace Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

if request.Operation != admissionv1.Delete {
return admission.ResponseAllowed(), nil
}

oldNs, _, err := objectsv1.NamespaceOldAndNewFromRequest(&request.AdmissionRequest)
if err != nil {
return nil, fmt.Errorf("failed to decode namespace from request: %w", err)
}

return admission.ResponseBadRequest(fmt.Sprintf("%q namespace my not be deleted\n", oldNs.Name)), nil
}
65 changes: 65 additions & 0 deletions pkg/resources/core/v1/namespace/deleteNamespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package namespace

import (
"testing"

"github.com/rancher/webhook/pkg/admission"
"github.com/stretchr/testify/assert"
admissionv1 "k8s.io/api/admission/v1"
)

func Test_Admit(t *testing.T) {
tests := []struct {
name string
namespaceName string
operationType admissionv1.Operation
wantAllowed bool
wantErr bool
}{
{
name: "Allow creating namespace",
namespaceName: "local",
operationType: admissionv1.Create,
wantAllowed: true,
wantErr: false,
},
{
name: "Prevent deletion of 'local' namespace",
namespaceName: "local",
operationType: admissionv1.Delete,
wantAllowed: false,
wantErr: true,
},
{
name: "Prevent deletion of 'fleet-local' namespace",
namespaceName: "fleet-local",
operationType: admissionv1.Delete,
wantAllowed: false,
wantErr: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d := deleteNamespaceAdmitter{}
request := createRequest(test.name, test.namespaceName, test.operationType)
response, err := d.Admit(request)
if test.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, test.wantAllowed, response.Allowed)
}
})
}
}

func createRequest(name, namespaceName string, operation admissionv1.Operation) *admission.Request {
return &admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: name,
Namespace: namespaceName,
Operation: operation,
},
}
}
4 changes: 4 additions & 0 deletions pkg/resources/core/v1/namespace/projectannotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ type projectNamespaceAdmitter struct {
// Admit ensures that the user has permission to change the namespace annotation for
// project membership, effectively moving a project from one namespace to another.
func (p *projectNamespaceAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
if request.Operation == admissionv1.Delete {
return admission.ResponseAllowed(), nil
}

listTrace := trace.New("Namespace Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

Expand Down
4 changes: 4 additions & 0 deletions pkg/resources/core/v1/namespace/psalabels.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type psaLabelAdmitter struct {

// Admit ensures that users have sufficient permissions to add/remove PSAs to a namespace.
func (p *psaLabelAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
if request.Operation == admissionv1.Delete {
return admission.ResponseAllowed(), nil
}

listTrace := trace.New("Namespace Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

Expand Down
3 changes: 3 additions & 0 deletions pkg/resources/core/v1/namespace/requestlimits.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ type requestLimitAdmitter struct{}

// Admit ensures that the resource requests are within the limits.
func (r *requestLimitAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
if request.Operation == admissionv1.Delete {
return admission.ResponseAllowed(), nil
}
listTrace := trace.New("Namespace Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

Expand Down
19 changes: 17 additions & 2 deletions pkg/resources/core/v1/namespace/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var projectsGVR = schema.GroupVersionResource{

// Validator validates the namespace admission request.
type Validator struct {
deleteNamespaceAdmitter deleteNamespaceAdmitter
psaAdmitter psaLabelAdmitter
projectNamespaceAdmitter projectNamespaceAdmitter
requestWithinLimitAdmitter requestLimitAdmitter
Expand All @@ -26,6 +27,7 @@ type Validator struct {
// NewValidator returns a new validator used for validation of namespace requests.
func NewValidator(sar authorizationv1.SubjectAccessReviewInterface) *Validator {
return &Validator{
deleteNamespaceAdmitter: deleteNamespaceAdmitter{},
psaAdmitter: psaLabelAdmitter{
sar: sar,
},
Expand All @@ -49,6 +51,7 @@ func (v *Validator) Operations() []admissionv1.OperationType {
return []admissionv1.OperationType{
admissionv1.Update,
admissionv1.Create,
admissionv1.Delete,
}
}

Expand Down Expand Up @@ -87,10 +90,22 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionv1.WebhookClientConf
}
kubeSystemCreateWebhook.FailurePolicy = admission.Ptr(admissionv1.Ignore)

return []admissionv1.ValidatingWebhook{*standardWebhook, *createWebhook, *kubeSystemCreateWebhook}
deleteNamespaceWebhook := admission.NewDefaultValidatingWebhook(v, clientConfig, admissionv1.ClusterScope, []admissionv1.OperationType{admissionv1.Delete})
deleteNamespaceWebhook.Name = admission.CreateWebhookName(v, "delete-namespace")
deleteNamespaceWebhook.NamespaceSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: corev1.LabelMetadataName,
Operator: metav1.LabelSelectorOpIn,
Values: []string{"fleet-local", "local"},
},
},
}

return []admissionv1.ValidatingWebhook{*deleteNamespaceWebhook, *standardWebhook, *createWebhook, *kubeSystemCreateWebhook}
}

// Admitters returns the psaAdmitter and the projectNamespaceAdmitter for namespaces.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.psaAdmitter, &v.projectNamespaceAdmitter, &v.requestWithinLimitAdmitter}
return []admission.Admitter{&v.psaAdmitter, &v.projectNamespaceAdmitter, &v.requestWithinLimitAdmitter, &v.deleteNamespaceAdmitter}
}
10 changes: 5 additions & 5 deletions pkg/resources/core/v1/namespace/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ func TestGVR(t *testing.T) {
func TestOperations(t *testing.T) {
validator := NewValidator(nil)
operations := validator.Operations()
assert.Len(t, operations, 2)
assert.Len(t, operations, 3)
assert.Contains(t, operations, v1.Update)
assert.Contains(t, operations, v1.Create)
}

func TestAdmitters(t *testing.T) {
validator := NewValidator(nil)
admitters := validator.Admitters()
assert.Len(t, admitters, 3)
assert.Len(t, admitters, 4)
hasPSAAdmitter := false
hasProjectNamespaceAdmitter := false
for i := range admitters {
Expand Down Expand Up @@ -56,7 +56,7 @@ func TestValidatingWebhook(t *testing.T) {
wantURL := "test.cattle.io/namespaces"
validator := NewValidator(nil)
webhooks := validator.ValidatingWebhook(clientConfig)
assert.Len(t, webhooks, 3)
assert.Len(t, webhooks, 4)
hasAllUpdateWebhook := false
hasCreateNonKubeSystemWebhook := false
hasCreateKubeSystemWebhook := false
Expand All @@ -71,7 +71,7 @@ func TestValidatingWebhook(t *testing.T) {
operation := operations[0]
assert.Equal(t, v1.ClusterScope, *rule.Scope)

assert.Contains(t, []v1.OperationType{v1.Create, v1.Update}, operation, "only expected webhooks for create and update")
assert.Contains(t, []v1.OperationType{v1.Create, v1.Update, v1.Delete}, operation, "only expected webhooks for create, update and delete")
if operation == v1.Update {
assert.False(t, hasAllUpdateWebhook, "had more than one webhook validating update calls, exepcted only one")
hasAllUpdateWebhook = true
Expand All @@ -81,7 +81,7 @@ func TestValidatingWebhook(t *testing.T) {
// failure policy defaults to fail, but if we specify one it needs to be fail
assert.Equal(t, v1.Fail, *webhook.FailurePolicy)
}
} else {
} else if operation == v1.Create {
assert.NotNil(t, webhook.NamespaceSelector)
matchExpressions := webhook.NamespaceSelector.MatchExpressions
assert.Len(t, matchExpressions, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
)

const (
localCluster = "local"
VersionManagementAnno = "rancher.io/imported-cluster-version-management"
VersionManagementSetting = "imported-cluster-version-management"
)
Expand Down Expand Up @@ -89,6 +90,11 @@ func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResp
return nil, fmt.Errorf("failed get old and new clusters from request: %w", err)
}

if request.Operation == admissionv1.Delete && oldCluster.Name == localCluster {
// deleting "local" cluster could corrupt the cluster Rancher is deployed in
return admission.ResponseBadRequest("local cluster may not be deleted"), nil
}

response, err := a.validateFleetPermissions(request, oldCluster, newCluster)
if err != nil {
return nil, fmt.Errorf("failed to validate fleet permissions: %w", err)
Expand Down Expand Up @@ -213,7 +219,7 @@ func (a *admitter) validatePSACT(oldCluster, newCluster *apisv3.Cluster, op admi
}
// no need to validate the PodSecurityAdmissionConfigurationTemplate on a local cluster,
// or imported cluster which represents a KEv2 cluster (GKE/EKS/AKS) or v1 Provisioning Cluster
if newCluster.Name == "local" || newCluster.Spec.RancherKubernetesEngineConfig == nil {
if newCluster.Name == localCluster || newCluster.Spec.RancherKubernetesEngineConfig == nil {
return admission.ResponseAllowed(), nil
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/resources/management.cattle.io/v3/cluster/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,16 @@ func TestAdmit(t *testing.T) {
expectAllowed: true,
expectContainWarning: true,
},
{
name: "Delete local cluster where Rancher is deployed",
operation: admissionv1.Delete,
oldCluster: v3.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "local",
},
},
expectAllowed: false,
},
}

for _, tt := range tests {
Expand Down
10 changes: 8 additions & 2 deletions pkg/resources/provisioning.cattle.io/v1/cluster/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
globalNamespace = "cattle-global-data"
systemAgentVarDirEnvVar = "CATTLE_AGENT_VAR_DIR"
failureStatus = "Failure"
localCluster = "local"
)

var (
Expand Down Expand Up @@ -97,6 +98,11 @@ func (p *provisioningAdmitter) Admit(request *admission.Request) (*admissionv1.A
return nil, err
}

if request.Operation == admissionv1.Delete && oldCluster.Name == localCluster {
// deleting "local" cluster could corrupt the cluster Rancher is deployed in
return admission.ResponseBadRequest("local cluster may not be deleted"), nil
}

response := &admissionv1.AdmissionResponse{}
if request.Operation == admissionv1.Create || request.Operation == admissionv1.Update {
if err := p.validateClusterName(request, response, cluster); err != nil || response.Result != nil {
Expand Down Expand Up @@ -416,7 +422,7 @@ func (p *provisioningAdmitter) validateMachinePoolNames(request *admission.Reque

// validatePSACT validate if the cluster and underlying secret are configured properly when PSACT is enabled or disabled
func (p *provisioningAdmitter) validatePSACT(request *admission.Request, response *admissionv1.AdmissionResponse, cluster *v1.Cluster) error {
if cluster.Name == "local" || cluster.Spec.RKEConfig == nil {
if cluster.Name == localCluster || cluster.Spec.RKEConfig == nil {
return nil
}

Expand Down Expand Up @@ -664,7 +670,7 @@ func validateACEConfig(cluster *v1.Cluster) *metav1.Status {

func isValidName(clusterName, clusterNamespace string, clusterExists bool) bool {
// A provisioning cluster with name "local" is only expected to be created in the "fleet-local" namespace.
if clusterName == "local" {
if clusterName == localCluster {
return clusterNamespace == "fleet-local"
}

Expand Down
Loading