Fix: Unbundle the X-Definition Validation from Authentication Features (#6904)

Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
Brian Kane
2025-09-17 04:33:20 +08:00
committed by GitHub
parent 90e601a51e
commit c0e906629e
7 changed files with 485 additions and 21 deletions
@@ -33,7 +33,6 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
@@ -319,8 +318,8 @@ func (h *ValidatingHandler) validateDefinitions(
// ValidateDefinitionPermissions validates that the user has permissions to access all definition types
func (h *ValidatingHandler) ValidateDefinitionPermissions(ctx context.Context, app *v1beta1.Application, req admission.Request) field.ErrorList {
// Requires both authentication and definition validation enabled
if !auth.AuthenticationWithUser || !utilfeature.DefaultMutableFeatureGate.Enabled(features.ValidateDefinitionPermissions) {
// Check if definition validation is enabled
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.ValidateDefinitionPermissions) {
return nil
}
@@ -0,0 +1,418 @@
/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package application
import (
"context"
"testing"
"github.com/kubevela/pkg/controller/sharding"
"github.com/stretchr/testify/assert"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
authv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
func TestValidateCreate(t *testing.T) {
// Disable the definition validation feature for this test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, features.ValidateDefinitionPermissions, false)
// Enable sharding to skip component validation (since we don't have component definitions in tests)
oldSharding := sharding.EnableSharding
sharding.EnableSharding = true
defer func() { sharding.EnableSharding = oldSharding }()
scheme := runtime.NewScheme()
_ = v1beta1.AddToScheme(scheme)
_ = authv1.AddToScheme(scheme)
testCases := []struct {
name string
app *v1beta1.Application
expectedErrorCount int
expectedErrorField string
}{
{
name: "valid application",
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
},
},
expectedErrorCount: 0,
},
{
name: "application with both autoUpdate and publishVersion",
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
Annotations: map[string]string{
oam.AnnotationAutoUpdate: "true",
oam.AnnotationPublishVersion: "v1.0.0",
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
},
},
expectedErrorCount: 1,
expectedErrorField: "metadata.annotations",
},
{
name: "application with duplicate workflow step names",
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
Workflow: &v1beta1.Workflow{
Steps: []workflowv1alpha1.WorkflowStep{
{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "step1",
Type: "deploy",
},
},
{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "step1", // Duplicate name
Type: "deploy",
},
},
},
},
},
},
expectedErrorCount: 1,
expectedErrorField: "spec.workflow.steps",
},
{
name: "application with invalid timeout",
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
Workflow: &v1beta1.Workflow{
Steps: []workflowv1alpha1.WorkflowStep{
{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "step1",
Type: "deploy",
Timeout: "invalid",
},
},
},
},
},
},
expectedErrorCount: 1,
expectedErrorField: "spec.workflow.steps.timeout",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handler := &ValidatingHandler{
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
}
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
},
}
errs := handler.ValidateCreate(context.Background(), tc.app, req)
assert.Equal(t, tc.expectedErrorCount, len(errs),
"Expected %d errors, got %d: %v", tc.expectedErrorCount, len(errs), errs)
if tc.expectedErrorField != "" && len(errs) > 0 {
assert.Contains(t, errs[0].Field, tc.expectedErrorField,
"Expected error field to contain %s, got %s", tc.expectedErrorField, errs[0].Field)
}
})
}
}
func TestValidateUpdate(t *testing.T) {
// Disable the definition validation feature for this test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, features.ValidateDefinitionPermissions, false)
// Enable sharding to skip component validation (since we don't have component definitions in tests)
oldSharding := sharding.EnableSharding
sharding.EnableSharding = true
defer func() { sharding.EnableSharding = oldSharding }()
scheme := runtime.NewScheme()
_ = v1beta1.AddToScheme(scheme)
_ = authv1.AddToScheme(scheme)
handler := &ValidatingHandler{
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
}
oldApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
},
}
testCases := []struct {
name string
newApp *v1beta1.Application
expectedErrorCount int
}{
{
name: "valid update",
newApp: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
{
Name: "comp2",
Type: "worker",
},
},
},
},
expectedErrorCount: 0,
},
{
name: "update with invalid annotations",
newApp: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
Annotations: map[string]string{
oam.AnnotationAutoUpdate: "true",
oam.AnnotationPublishVersion: "v1.0.0",
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp1",
Type: "webservice",
},
},
},
},
expectedErrorCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
},
}
errs := handler.ValidateUpdate(context.Background(), tc.newApp, oldApp, req)
assert.Equal(t, tc.expectedErrorCount, len(errs),
"Expected %d errors, got %d: %v", tc.expectedErrorCount, len(errs), errs)
})
}
}
func TestValidateTimeout(t *testing.T) {
handler := &ValidatingHandler{}
testCases := []struct {
name string
timeout string
expectedError bool
}{
{
name: "valid duration - seconds",
timeout: "30s",
expectedError: false,
},
{
name: "valid duration - minutes",
timeout: "5m",
expectedError: false,
},
{
name: "valid duration - hours",
timeout: "2h",
expectedError: false,
},
{
name: "valid duration - complex",
timeout: "1h30m45s",
expectedError: false,
},
{
name: "invalid duration",
timeout: "invalid",
expectedError: true,
},
{
name: "invalid duration - missing unit",
timeout: "30",
expectedError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
errs := handler.ValidateTimeout("test-step", tc.timeout)
if tc.expectedError {
assert.NotEmpty(t, errs, "Expected validation error for timeout %s", tc.timeout)
} else {
assert.Empty(t, errs, "Expected no validation error for timeout %s", tc.timeout)
}
})
}
}
func TestValidateAnnotations(t *testing.T) {
handler := &ValidatingHandler{}
testCases := []struct {
name string
annotations map[string]string
expectedErrorCount int
}{
{
name: "no annotations",
annotations: nil,
expectedErrorCount: 0,
},
{
name: "only autoUpdate",
annotations: map[string]string{
oam.AnnotationAutoUpdate: "true",
},
expectedErrorCount: 0,
},
{
name: "only publishVersion",
annotations: map[string]string{
oam.AnnotationPublishVersion: "v1.0.0",
},
expectedErrorCount: 0,
},
{
name: "both autoUpdate and publishVersion",
annotations: map[string]string{
oam.AnnotationAutoUpdate: "true",
oam.AnnotationPublishVersion: "v1.0.0",
},
expectedErrorCount: 1,
},
{
name: "autoUpdate false with publishVersion",
annotations: map[string]string{
oam.AnnotationAutoUpdate: "false",
oam.AnnotationPublishVersion: "v1.0.0",
},
expectedErrorCount: 0,
},
{
name: "autoUpdate true with empty publishVersion",
annotations: map[string]string{
oam.AnnotationAutoUpdate: "true",
oam.AnnotationPublishVersion: "",
},
expectedErrorCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
app := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
Annotations: tc.annotations,
},
}
errs := handler.ValidateAnnotations(context.Background(), app)
assert.Equal(t, tc.expectedErrorCount, len(errs),
"Expected %d errors, got %d: %v", tc.expectedErrorCount, len(errs), errs)
})
}
}
@@ -685,8 +685,21 @@ func TestValidateDefinitionPermissions_AuthenticationDisabled(t *testing.T) {
},
}
// Use mock client that allows the definition
scheme := runtime.NewScheme()
_ = v1beta1.AddToScheme(scheme)
_ = authv1.AddToScheme(scheme)
fakeClient := &mockSARClient{
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
allowedDefinitions: map[string]bool{
"componentdefinitions/vela-system/webservice": true,
"componentdefinitions/test-ns/webservice": true,
},
}
handler := &ValidatingHandler{
Client: fake.NewClientBuilder().Build(),
Client: fakeClient,
}
req := admission.Request{
@@ -697,9 +710,10 @@ func TestValidateDefinitionPermissions_AuthenticationDisabled(t *testing.T) {
},
}
// Should return no errors when AuthenticationWithUser is disabled
// Definition validation should still work even when AuthenticationWithUser is disabled
// It validates based on the user info in the request regardless of auth flag
errs := handler.ValidateDefinitionPermissions(context.Background(), app, req)
assert.Empty(t, errs, "Expected no errors when AuthenticationWithUser is disabled")
assert.Empty(t, errs, "Expected no errors when user has permission even with AuthenticationWithUser disabled")
}
func TestCollectDefinitionUsage(t *testing.T) {