Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/v0.5] Prevent deletion of namespaces and cluster (#651) #725

Open
wants to merge 1 commit into
base: release/v0.5
Choose a base branch
from
Open
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
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,13 +18,15 @@ var projectsGVR = schema.GroupVersionResource{

// Validator validates the namespace admission request.
type Validator struct {
deleteNamespaceAdmitter deleteNamespaceAdmitter
psaAdmitter psaLabelAdmitter
projectNamespaceAdmitter projectNamespaceAdmitter
}

// 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 @@ -47,6 +49,7 @@ func (v *Validator) Operations() []admissionv1.OperationType {
return []admissionv1.OperationType{
admissionv1.Update,
admissionv1.Create,
admissionv1.Delete,
}
}

Expand Down Expand Up @@ -85,10 +88,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}
return []admission.Admitter{&v.psaAdmitter, &v.projectNamespaceAdmitter, &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, 2)
assert.Len(t, admitters, 3)
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
17 changes: 16 additions & 1 deletion pkg/resources/management.cattle.io/v3/cluster/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import (
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
)

var parsedRangeLessThan125 = semver.MustParseRange("< 1.25.0-rancher0")
const (
localCluster = "local"
VersionManagementAnno = "rancher.io/imported-cluster-version-management"
VersionManagementSetting = "imported-cluster-version-management"
)

var parsedRangeLessThan123 = semver.MustParseRange("< 1.23.0-rancher0")

// NewValidator returns a new validator for management clusters.
Expand Down Expand Up @@ -70,6 +75,16 @@ type admitter struct {

// Admit handles the webhook admission request sent to this webhook.
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
oldCluster, _, err := objectsv3.ClusterOldAndNewFromRequest(&request.AdmissionRequest)
if err != nil {
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)
if err != nil {
return nil, fmt.Errorf("failed to validate fleet permissions: %w", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ func TestAdmit(t *testing.T) {
operation: admissionv1.Delete,
expectAllowed: 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 Expand Up @@ -105,7 +115,9 @@ func TestAdmit(t *testing.T) {
assert.Equal(t, tt.expectAllowed, res.Allowed)

if !tt.expectAllowed {
assert.Equal(t, tt.expectedReason, res.Result.Reason)
if tt.expectedReason != "" {
assert.Equal(t, tt.expectedReason, res.Result.Reason)
}
}
})
}
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