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
5 changes: 0 additions & 5 deletions pkg/builder/validating_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,6 @@ func (h *validatingWebhookHandler) Handle(ctx context.Context, req admission.Req
func (h *validatingWebhookHandler) HandleValidate(req admission.Request, ctx *pkgctx.WebhookRequestContext) admission.Response {

if _, ok := ctx.Obj.GetAnnotations()[pkgconst.SkipValidationAnnotationKey]; ok {
if !ctx.IsPrivilegedAccount {
// Only privileged users may set the skip validation annotation.
return webhook.Denied(SkipValidationDenied)
}

// The object has the skip validation annotation, so allow the object to
// bypass validation.
return webhook.Allowed(SkipValidationAllowed)
Expand Down
32 changes: 8 additions & 24 deletions pkg/builder/validating_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,10 @@ var _ = Describe("NewValidatingWebhook", func() {
pkgconst.SkipValidationAnnotationKey: "",
})
})
When("privileged user", func() {
It("should return true", func() {
req := getAdmissionRequest(obj, admissionv1.Create)
req.UserInfo.Groups = []string{"system:masters"}
assertOkay(req, pkgbuilder.SkipValidationAllowed)
})
})
When("unprivileged user", func() {
It("should return false", func() {
req := getAdmissionRequest(obj, admissionv1.Create)
assertNotOkay(req, 403, pkgbuilder.SkipValidationDenied)
})

It("should return true", func() {
req := getAdmissionRequest(obj, admissionv1.Create)
assertOkay(req, pkgbuilder.SkipValidationAllowed)
})
})
})
Expand Down Expand Up @@ -282,18 +274,10 @@ var _ = Describe("NewValidatingWebhook", func() {
pkgconst.SkipValidationAnnotationKey: "",
})
})
When("privileged user", func() {
It("should return true", func() {
req := getAdmissionRequest(obj, admissionv1.Update)
req.UserInfo.Groups = []string{"system:masters"}
assertOkay(req, pkgbuilder.SkipValidationAllowed)
})
})
When("unprivileged user", func() {
It("should return false", func() {
req := getAdmissionRequest(obj, admissionv1.Update)
assertNotOkay(req, 403, pkgbuilder.SkipValidationDenied)
})

It("should return true", func() {
req := getAdmissionRequest(obj, admissionv1.Update)
assertOkay(req, pkgbuilder.SkipValidationAllowed)
})
})
})
Expand Down
48 changes: 0 additions & 48 deletions webhooks/virtualmachine/validation/virtualmachine_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2110,52 +2110,6 @@ func (v validator) validateAvailabilityZone(
return allErrs
}

// protectedAnnotationRegex matches annotations with keys matching the pattern:
// ^.+\.protected(/.+)?$
//
// Examples that match:
// - fu.bar.protected
// - hello.world.protected/sub-key
// - vmoperator.vmware.com.protected/reconcile-priority
//
// Examples that do NOT match:
// - protected.fu.bar
// - hello.world.protected.against/sub-key
var protectedAnnotationRegex = regexp.MustCompile(`^.+\.protected(/.*)?$`)

// validateProtectedAnnotations validates that annotations matching the
// protected annotation pattern can only be modified by privileged users.
func (v validator) validateProtectedAnnotations(vm, oldVM *vmopv1.VirtualMachine) field.ErrorList {
var allErrs field.ErrorList
annotationPath := field.NewPath("metadata", "annotations")

// Collect all protected annotation keys from both old and new VMs
protectedKeys := make(map[string]struct{})

for k := range vm.Annotations {
if protectedAnnotationRegex.MatchString(k) {
protectedKeys[k] = struct{}{}
}
}

for k := range oldVM.Annotations {
if protectedAnnotationRegex.MatchString(k) {
protectedKeys[k] = struct{}{}
}
}

// Check if any protected annotations have been modified
for k := range protectedKeys {
if vm.Annotations[k] != oldVM.Annotations[k] {
allErrs = append(allErrs, field.Forbidden(
annotationPath.Key(k),
modifyAnnotationNotAllowedForNonAdmin))
}
}

return allErrs
}

func (v validator) validateAnnotation(ctx *pkgctx.WebhookRequestContext, vm, oldVM *vmopv1.VirtualMachine) field.ErrorList {
var allErrs field.ErrorList

Expand All @@ -2180,8 +2134,6 @@ func (v validator) validateAnnotation(ctx *pkgctx.WebhookRequestContext, vm, old
oldVM = &vmopv1.VirtualMachine{}
}

allErrs = append(allErrs, v.validateProtectedAnnotations(vm, oldVM)...)

if vm.Annotations[vmopv1.InstanceIDAnnotation] != oldVM.Annotations[vmopv1.InstanceIDAnnotation] {
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(vmopv1.InstanceIDAnnotation), modifyAnnotationNotAllowedForNonAdmin))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,52 +84,6 @@ type testParams struct {
skipSetControllerForPVC bool
}

type protectedAnnotationTestCase struct {
annotationKey string
oldValue string
newValue string
}

// When defining any new protected annotations, be sure to add them to this
// list so they are covered via test cases. A protected condition is one whose
// key matches the regex `^.+\.protected(/.+)?$`.
//
// Examples that match:
// - fu.bar.protected
// - hello.world.protected/sub-key
// - vmoperator.vmware.com.protected/reconcile-priority
//
// Examples that do NOT match:
// - protected.fu.bar
// - hello.world.protected.against/sub-key
var protectedAnnotationTestCases = []protectedAnnotationTestCase{
{
annotationKey: pkgconst.ReconcilePriorityAnnotationKey,
oldValue: "100",
newValue: "200",
},
{
annotationKey: pkgconst.SkipDeletePlatformResourceKey,
oldValue: "true",
newValue: "false",
},
{
annotationKey: pkgconst.ApplyPowerStateTimeAnnotation,
oldValue: time.Now().Format(time.RFC3339Nano),
newValue: time.Now().Add(time.Hour).Format(time.RFC3339Nano),
},
{
annotationKey: "hello.world.protected/condition-status",
oldValue: "red",
newValue: "green",
},
{
annotationKey: "condition.vmware.vmoperator.com.protected/hello-world",
oldValue: "True",
newValue: "False",
},
}

func bypassUpgradeCheck(ctx *context.Context, objects ...metav1.Object) {
pkgcfg.SetContext(*ctx, func(config *pkgcfg.Config) {
config.BuildVersion = fake
Expand Down Expand Up @@ -1474,56 +1428,6 @@ func unitTestsValidateCreate() {
),
)

getProtectedAnnotationTableAllowCreate := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
ctx.IsPrivilegedAccount = true
ctx.vm.Annotations[tc.annotationKey] = tc.newValue
},
expectAllowed: true,
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should allow create with "+tc.annotationKey, tc))
}

return table
}

getProtectedAnnotationTableDisallowCreate := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
ctx.vm.Annotations[tc.annotationKey] = tc.newValue
},
validate: doValidateWithMsg(
field.Forbidden(annotationPath.Key(tc.annotationKey), "modifying this annotation is not allowed for non-admin users").Error(),
),
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should disallow create with "+tc.annotationKey, tc))
}

return table
}

DescribeTable("disallow create with protected annotations by non-privileged user",
getProtectedAnnotationTableDisallowCreate()...,
)

DescribeTable("allow create with protected annotations by non-privileged user",
getProtectedAnnotationTableAllowCreate()...,
)
})

Context("Label", func() {
Expand Down Expand Up @@ -4752,117 +4656,6 @@ func unitTestsValidateUpdate() { //nolint:gocyclo
),
)

getProtectedAnnotationTableAllowUpdate := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
ctx.IsPrivilegedAccount = true
ctx.oldVM.Annotations[tc.annotationKey] = tc.oldValue
if tc.newValue != "" {
ctx.vm.Annotations[tc.annotationKey] = tc.newValue
}
},
expectAllowed: true,
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should allow update of "+tc.annotationKey, tc))
}

return table
}

getProtectedAnnotationTableAllowRemoval := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
ctx.IsPrivilegedAccount = true
ctx.oldVM.Annotations[tc.annotationKey] = tc.oldValue
delete(ctx.vm.Annotations, tc.annotationKey)
},
expectAllowed: true,
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should allow removal of "+tc.annotationKey, tc))
}

return table
}

getProtectedAnnotationTableDisallowUpdate := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
bypassUpgradeCheck(&ctx.Context, ctx.vm, ctx.oldVM)
ctx.oldVM.Annotations[tc.annotationKey] = tc.oldValue
if tc.newValue != "" {
ctx.vm.Annotations[tc.annotationKey] = tc.newValue
}
},
validate: doValidateWithMsg(
field.Forbidden(annotationPath.Key(tc.annotationKey), "modifying this annotation is not allowed for non-admin users").Error(),
),
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should disallow update of "+tc.annotationKey, tc))
}

return table
}

getProtectedAnnotationTableDisallowRemoval := func() []any {
table := []any{
func(tc protectedAnnotationTestCase) {
doTest(testParams{
setup: func(ctx *unitValidatingWebhookContext) {
bypassUpgradeCheck(&ctx.Context, ctx.vm, ctx.oldVM)
ctx.oldVM.Annotations[tc.annotationKey] = tc.oldValue
delete(ctx.vm.Annotations, tc.annotationKey)
},
validate: doValidateWithMsg(
field.Forbidden(annotationPath.Key(tc.annotationKey), "modifying this annotation is not allowed for non-admin users").Error(),
),
})
},
}

for i := range protectedAnnotationTestCases {
tc := protectedAnnotationTestCases[i]
table = append(table, Entry("should disallow removal of "+tc.annotationKey, tc))
}

return table
}

DescribeTable("disallow update of protected annotations by non-privileged user",
getProtectedAnnotationTableDisallowUpdate()...,
)

DescribeTable("disallow removal of protected annotations by non-privileged user",
getProtectedAnnotationTableDisallowRemoval()...,
)

DescribeTable("allow update of protected annotations by privileged user",
getProtectedAnnotationTableAllowUpdate()...,
)

DescribeTable("allow removal of protected annotations by privileged user",
getProtectedAnnotationTableAllowRemoval()...,
)
})

Context("Bootstrap", func() {
Expand Down