Repository: kubevela/kubevela

Issue:
Tests: Add unit test coverage for oam, auth, and config packages
### Description

Several core packages in KubeVela, including `pkg/oam`, `pkg/auth`, and `pkg/config`, have areas with missing unit test coverage. This makes it harder to verify the correctness of new changes and prevent regressions.

This issue is to track the work of adding new unit tests to improve the test coverage for these packages.

### Scope

-   **`pkg/oam`**: Add unit tests for the auxiliary functions in `pkg/oam/auxiliary.go`.
-   **`pkg/auth`**: Add unit tests for the core logic in the `pkg/auth` package.
-   **`pkg/config`**: Add unit tests for the configuration handling in the `pkg/config` package.
-  **`pkg/cue`**: Add unit tests for the `definition`,`script` and `task` packages.

This will help improve the overall quality and stability of the KubeVela codebase.

Discussion:
@AshvinBambhaniya2003: Completed. Thank you for acknowledging my contribution.

Solution patch:
diff --git a/apis/core.oam.dev/v1beta1/register.go b/apis/core.oam.dev/v1beta1/register.go
index 10b7c80c..4942e693 100644
--- a/apis/core.oam.dev/v1beta1/register.go
+++ b/apis/core.oam.dev/v1beta1/register.go
@@ -49,6 +49,7 @@ var (
 	ComponentDefinitionGroupKind        = schema.GroupKind{Group: Group, Kind: ComponentDefinitionKind}.String()
 	ComponentDefinitionKindAPIVersion   = ComponentDefinitionKind + "." + SchemeGroupVersion.String()
 	ComponentDefinitionGroupVersionKind = SchemeGroupVersion.WithKind(ComponentDefinitionKind)
+	ComponentDefinitionGVR              = SchemeGroupVersion.WithResource("componentdefinitions")
 )
 
 // WorkloadDefinition type metadata.
@@ -65,6 +66,7 @@ var (
 	TraitDefinitionGroupKind        = schema.GroupKind{Group: Group, Kind: TraitDefinitionKind}.String()
 	TraitDefinitionKindAPIVersion   = TraitDefinitionKind + "." + SchemeGroupVersion.String()
 	TraitDefinitionGroupVersionKind = SchemeGroupVersion.WithKind(TraitDefinitionKind)
+	TraitDefinitionGVR              = SchemeGroupVersion.WithResource("traitdefinitions")
 )
 
 // PolicyDefinition type metadata.
@@ -73,6 +75,7 @@ var (
 	PolicyDefinitionGroupKind        = schema.GroupKind{Group: Group, Kind: PolicyDefinitionKind}.String()
 	PolicyDefinitionKindAPIVersion   = PolicyDefinitionKind + "." + SchemeGroupVersion.String()
 	PolicyDefinitionGroupVersionKind = SchemeGroupVersion.WithKind(PolicyDefinitionKind)
+	PolicyDefinitionGVR              = SchemeGroupVersion.WithResource("policydefinitions")
 )
 
 // WorkflowStepDefinition type metadata.
@@ -81,6 +84,7 @@ var (
 	WorkflowStepDefinitionGroupKind        = schema.GroupKind{Group: Group, Kind: WorkflowStepDefinitionKind}.String()
 	WorkflowStepDefinitionKindAPIVersion   = WorkflowStepDefinitionKind + "." + SchemeGroupVersion.String()
 	WorkflowStepDefinitionGroupVersionKind = SchemeGroupVersion.WithKind(WorkflowStepDefinitionKind)
+	WorkflowStepDefinitionGVR              = SchemeGroupVersion.WithResource("workflowstepdefinitions")
 )
 
 // DefinitionRevision type metadata.
@@ -115,6 +119,20 @@ var (
 	ResourceTrackerKindVersionKind = SchemeGroupVersion.WithKind(ResourceTrackerKind)
 )
 
+// DefinitionTypeInfo contains the mapping information for a definition type
+type DefinitionTypeInfo struct {
+	GVR  schema.GroupVersionResource
+	Kind string
+}
+
+// DefinitionTypeMap maps definition types to their corresponding GVR and Kind
+var DefinitionTypeMap = map[reflect.Type]DefinitionTypeInfo{
+	reflect.TypeOf(ComponentDefinition{}):    {GVR: ComponentDefinitionGVR, Kind: ComponentDefinitionKind},
+	reflect.TypeOf(TraitDefinition{}):        {GVR: TraitDefinitionGVR, Kind: TraitDefinitionKind},
+	reflect.TypeOf(PolicyDefinition{}):       {GVR: PolicyDefinitionGVR, Kind: PolicyDefinitionKind},
+	reflect.TypeOf(WorkflowStepDefinition{}): {GVR: WorkflowStepDefinitionGVR, Kind: WorkflowStepDefinitionKind},
+}
+
 func init() {
 	SchemeBuilder.Register(&ComponentDefinition{}, &ComponentDefinitionList{})
 	SchemeBuilder.Register(&WorkloadDefinition{}, &WorkloadDefinitionList{})
diff --git a/apis/core.oam.dev/v1beta1/zz_generated.deepcopy.go b/apis/core.oam.dev/v1beta1/zz_generated.deepcopy.go
index c42796a5..0e3eb29e 100644
--- a/apis/core.oam.dev/v1beta1/zz_generated.deepcopy.go
+++ b/apis/core.oam.dev/v1beta1/zz_generated.deepcopy.go
@@ -552,6 +552,22 @@ func (in *DefinitionRevisionSpec) DeepCopy() *DefinitionRevisionSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DefinitionTypeInfo) DeepCopyInto(out *DefinitionTypeInfo) {
+	*out = *in
+	out.GVR = in.GVR
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefinitionTypeInfo.
+func (in *DefinitionTypeInfo) DeepCopy() *DefinitionTypeInfo {
+	if in == nil {
+		return nil
+	}
+	out := new(DefinitionTypeInfo)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ManagedResource) DeepCopyInto(out *ManagedResource) {
 	*out = *in
diff --git a/charts/vela-core/README.md b/charts/vela-core/README.md
index ec8bfa07..5f695985 100644
--- a/charts/vela-core/README.md
+++ b/charts/vela-core/README.md
@@ -151,7 +151,8 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
 | `logFileMaxSize`                               | Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited.                                         | `1024`               |
 | `kubeClient.qps`                               | The qps for reconcile clients                                                                                                                                      | `400`                |
 | `kubeClient.burst`                             | The burst for reconcile clients                                                                                                                                    | `600`                |
-| `authentication.enabled`                       | Enable authentication for application                                                                                                                              | `false`              |
+| `authentication.enabled`                       | Enable authentication framework for applications                                                                                                                   | `false`              |
+| `authentication.definitions.enabled`           | Enable definition permission validation (requires authentication.enabled)                                                                                          | `false`              |
 | `authentication.withUser`                      | Application authentication will impersonate as the request User                                                                                                    | `true`               |
 | `authentication.defaultUser`                   | Application authentication will impersonate as the User if no user provided in Application                                                                         | `kubevela:vela-core` |
 | `authentication.groupPattern`                  | Application authentication will impersonate as the request Group that matches the pattern                                                                          | `kubevela:*`         |
diff --git a/charts/vela-core/templates/NOTES.txt b/charts/vela-core/templates/NOTES.txt
index 65622ad1..14cb2559 100644
--- a/charts/vela-core/templates/NOTES.txt
+++ b/charts/vela-core/templates/NOTES.txt
@@ -29,3 +29,12 @@ Welcome to use the KubeVela! Enjoy your shipping application journey!
 
 
 You can refer to https://kubevela.io for more details.
+
+{{- if and .Values.authentication.enabled (not .Values.authentication.definitions.enabled) }}
+
+WARNING: Authentication is enabled but definition permission validation is disabled.
+   Applications can reference definitions without RBAC checks.
+   To enable definition permission validation:
+     --set authentication.definitions.enabled=true
+   Ensure users have appropriate RBAC permissions before enabling.
+{{- end }}
diff --git a/charts/vela-core/templates/kubevela-controller.yaml b/charts/vela-core/templates/kubevela-controller.yaml
index 0a5d4800..80bf8028 100644
--- a/charts/vela-core/templates/kubevela-controller.yaml
+++ b/charts/vela-core/templates/kubevela-controller.yaml
@@ -299,8 +299,8 @@ spec:
             - "--enable-external-package-for-default-compiler={{ .Values.workflow.enableExternalPackageForDefaultCompiler }}"
             - "--enable-external-package-watch-for-default-compiler={{ .Values.workflow.enableExternalPackageWatchForDefaultCompiler }}"
             - "--feature-gates=EnableSuspendOnFailure={{- .Values.workflow.enableSuspendOnFailure | toString -}}"
-            - "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
             - "--feature-gates=GzipResourceTracker={{- .Values.featureGates.gzipResourceTracker | toString -}}"
+            - "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
             - "--feature-gates=ZstdResourceTracker={{- .Values.featureGates.zstdResourceTracker | toString -}}"
             - "--feature-gates=ApplyOnce={{- .Values.featureGates.applyOnce | toString -}}"
             - "--feature-gates=MultiStageComponentApply= {{- .Values.featureGates.multiStageComponentApply | toString -}}"
@@ -314,6 +314,7 @@ spec:
             - "--feature-gates=EnableCueValidation={{- .Values.featureGates.enableCueValidation | toString -}}"
             - "--feature-gates=EnableApplicationStatusMetrics={{- .Values.featureGates.enableApplicationStatusMetrics | toString -}}"
             {{ if .Values.authentication.enabled }}
+            - "--feature-gates=ValidateDefinitionPermissions={{ .Values.authentication.definitions.enabled | toString -}}"
             {{ if .Values.authentication.withUser }}
             - "--authentication-with-user"
             {{ end }}
diff --git a/charts/vela-core/values.yaml b/charts/vela-core/values.yaml
index 4e3a061b..2fa9592e 100644
--- a/charts/vela-core/values.yaml
+++ b/charts/vela-core/values.yaml
@@ -289,12 +289,15 @@ kubeClient:
   qps: 400
   burst: 600
 
-## @param authentication.enabled Enable authentication for application
+## @param authentication.enabled Enable authentication framework for applications
+## @param authentication.definitions.enabled Enable definition permission validation (requires authentication.enabled)  
 ## @param authentication.withUser Application authentication will impersonate as the request User
 ## @param authentication.defaultUser Application authentication will impersonate as the User if no user provided in Application
 ## @param authentication.groupPattern Application authentication will impersonate as the request Group that matches the pattern
 authentication:
   enabled: false
+  definitions:
+    enabled: false
   withUser: true
   defaultUser: kubevela:vela-core
   groupPattern: kubevela:*
diff --git a/pkg/features/controller_features.go b/pkg/features/controller_features.go
index e7423ff0..841cca98 100644
--- a/pkg/features/controller_features.go
+++ b/pkg/features/controller_features.go
@@ -54,6 +54,8 @@ const (
 
 	// AuthenticateApplication enable the authentication for application
 	AuthenticateApplication featuregate.Feature = "AuthenticateApplication"
+	// ValidateDefinitionPermissions enables RBAC validation for definition access in applications
+	ValidateDefinitionPermissions featuregate.Feature = "ValidateDefinitionPermissions"
 	// GzipResourceTracker enables the gzip compression for ResourceTracker. It can be useful if you have large
 	// application that needs to dispatch lots of resources or large resources (like CRD or huge ConfigMap),
 	// which at the cost of slower processing speed due to the extra overhead for compression and decompression.
@@ -128,6 +130,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
 	DisableReferObjectsFromURL:                    {Default: false, PreRelease: featuregate.Alpha},
 	ApplyResourceByReplace:                        {Default: false, PreRelease: featuregate.Alpha},
 	AuthenticateApplication:                       {Default: false, PreRelease: featuregate.Alpha},
+	ValidateDefinitionPermissions:                 {Default: false, PreRelease: featuregate.Alpha},
 	GzipResourceTracker:                           {Default: false, PreRelease: featuregate.Alpha},
 	ZstdResourceTracker:                           {Default: false, PreRelease: featuregate.Alpha},
 	ApplyOnce:                                     {Default: false, PreRelease: featuregate.Alpha},
diff --git a/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler.go b/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler.go
index da02b193..cdc27857 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler.go
@@ -73,7 +73,7 @@ func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) a
 	ctx = util.SetNamespaceInCtx(ctx, app.Namespace)
 	switch req.Operation {
 	case admissionv1.Create:
-		if allErrs := h.ValidateCreate(ctx, app); len(allErrs) > 0 {
+		if allErrs := h.ValidateCreate(ctx, app, req); len(allErrs) > 0 {
 			// http.StatusUnprocessableEntity will NOT report any error descriptions
 			// to the client, use generic http.StatusBadRequest instead.
 			return admission.Errored(http.StatusBadRequest, mergeErrors(allErrs))
@@ -84,7 +84,7 @@ func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) a
 			return admission.Errored(http.StatusBadRequest, simplifyError(err))
 		}
 		if app.ObjectMeta.DeletionTimestamp.IsZero() {
-			if allErrs := h.ValidateUpdate(ctx, app, oldApp); len(allErrs) > 0 {
+			if allErrs := h.ValidateUpdate(ctx, app, oldApp, req); len(allErrs) > 0 {
 				return admission.Errored(http.StatusBadRequest, mergeErrors(allErrs))
 			}
 		}
diff --git a/pkg/webhook/core.oam.dev/v1beta1/application/validation.go b/pkg/webhook/core.oam.dev/v1beta1/application/validation.go
index eca6c4fd..6d2842c0 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/application/validation.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/application/validation.go
@@ -19,16 +19,21 @@ package application
 import (
 	"context"
 	"fmt"
+	"reflect"
 	"time"
 
 	"github.com/kubevela/pkg/controller/sharding"
 	"github.com/kubevela/pkg/util/singleton"
+	authv1 "k8s.io/api/authorization/v1"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	"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"
 )
@@ -108,6 +113,278 @@ func (h *ValidatingHandler) ValidateComponents(ctx context.Context, app *v1beta1
 	return componentErrs
 }
 
+// checkDefinitionPermission checks if user has permission to access a definition in either system namespace or app namespace
+func (h *ValidatingHandler) checkDefinitionPermission(ctx context.Context, req admission.Request, resource, definitionType, appNamespace string) (bool, error) {
+	// Check permission in system namespace (vela-system) first since most definitions are there
+	// This optimizes for the common case and reduces API calls
+	systemNsSar := &authv1.SubjectAccessReview{
+		Spec: authv1.SubjectAccessReviewSpec{
+			User:   req.UserInfo.Username,
+			Groups: req.UserInfo.Groups,
+			ResourceAttributes: &authv1.ResourceAttributes{
+				Verb:      "get",
+				Group:     "core.oam.dev",
+				Version:   "v1beta1",
+				Resource:  resource,
+				Namespace: oam.SystemDefinitionNamespace,
+				Name:      definitionType,
+			},
+		},
+	}
+
+	if err := h.Client.Create(ctx, systemNsSar); err != nil {
+		return false, fmt.Errorf("failed to check %s permission in system namespace: %w", resource, err)
+	}
+
+	if systemNsSar.Status.Allowed {
+		// User has permission in system namespace - no need to check app namespace
+		return true, nil
+	}
+
+	// If not in system namespace and app namespace is different, check app namespace
+	if appNamespace != oam.SystemDefinitionNamespace {
+		appNsSar := &authv1.SubjectAccessReview{
+			Spec: authv1.SubjectAccessReviewSpec{
+				User:   req.UserInfo.Username,
+				Groups: req.UserInfo.Groups,
+				ResourceAttributes: &authv1.ResourceAttributes{
+					Verb:      "get",
+					Group:     "core.oam.dev",
+					Version:   "v1beta1",
+					Resource:  resource,
+					Namespace: appNamespace,
+					Name:      definitionType,
+				},
+			},
+		}
+
+		if err := h.Client.Create(ctx, appNsSar); err != nil {
+			return false, fmt.Errorf("failed to check %s permission in namespace %s: %w", resource, appNamespace, err)
+		}
+
+		if appNsSar.Status.Allowed {
+			// User has permission in app namespace
+			return true, nil
+		}
+	}
+
+	// User doesn't have permission in either namespace
+	return false, nil
+}
+
+// workflowStepLocation represents the location of a workflow step
+type workflowStepLocation struct {
+	StepIndex    int
+	SubStepIndex int // -1 if not a sub-step
+	IsSubStep    bool
+}
+
+// definitionUsage tracks where each definition type is used in the application
+type definitionUsage struct {
+	componentTypes    map[string][]int
+	traitTypes        map[string][][2]int
+	policyTypes       map[string][]int
+	workflowStepTypes map[string][]workflowStepLocation
+}
+
+// collectDefinitionUsage collects all unique definition types and their locations in the application
+func collectDefinitionUsage(app *v1beta1.Application) *definitionUsage {
+	usage := &definitionUsage{
+		componentTypes:    make(map[string][]int),
+		traitTypes:        make(map[string][][2]int),
+		policyTypes:       make(map[string][]int),
+		workflowStepTypes: make(map[string][]workflowStepLocation),
+	}
+
+	// Collect component and trait types
+	for i, comp := range app.Spec.Components {
+		usage.componentTypes[comp.Type] = append(usage.componentTypes[comp.Type], i)
+
+		for j, trait := range comp.Traits {
+			usage.traitTypes[trait.Type] = append(usage.traitTypes[trait.Type], [2]int{i, j})
+		}
+	}
+
+	// Collect policy types
+	for i, policy := range app.Spec.Policies {
+		usage.policyTypes[policy.Type] = append(usage.policyTypes[policy.Type], i)
+	}
+
+	// Collect workflow step types (including sub-steps)
+	if app.Spec.Workflow != nil {
+		for i, step := range app.Spec.Workflow.Steps {
+			location := workflowStepLocation{
+				StepIndex:    i,
+				SubStepIndex: -1,
+				IsSubStep:    false,
+			}
+			usage.workflowStepTypes[step.Type] = append(usage.workflowStepTypes[step.Type], location)
+
+			// Also check sub-steps
+			for j, subStep := range step.SubSteps {
+				subLocation := workflowStepLocation{
+					StepIndex:    i,
+					SubStepIndex: j,
+					IsSubStep:    true,
+				}
+				usage.workflowStepTypes[subStep.Type] = append(usage.workflowStepTypes[subStep.Type], subLocation)
+			}
+		}
+	}
+
+	return usage
+}
+
+// processDefinitionPermissionCheck handles the common logic for processing permission check results
+func (h *ValidatingHandler) processDefinitionPermissionCheck(
+	allowed bool,
+	err error,
+	req admission.Request,
+	definitionKind string,
+	definitionType string,
+	appNamespace string,
+	fieldPaths []*field.Path,
+) field.ErrorList {
+	var errs field.ErrorList
+
+	if err != nil {
+		klog.Errorf("Failed to check %s permission for user %s: %v", definitionKind, req.UserInfo.Username, err)
+		for _, fieldPath := range fieldPaths {
+			errs = append(errs, field.Forbidden(fieldPath,
+				fmt.Sprintf("unable to verify permissions for %s %q: %v", definitionKind, definitionType, err)))
+		}
+		return errs
+	}
+
+	if !allowed {
+		klog.Infof("User %q does not have permission to access %s %q in namespace %q or %q",
+			req.UserInfo.Username, definitionKind, definitionType, appNamespace, oam.SystemDefinitionNamespace)
+		for _, fieldPath := range fieldPaths {
+			errs = append(errs, field.Forbidden(fieldPath,
+				fmt.Sprintf("user %q cannot get %s %q in namespace %q or %q",
+					req.UserInfo.Username, definitionKind, definitionType, appNamespace, oam.SystemDefinitionNamespace)))
+		}
+	}
+
+	return errs
+}
+
+// fieldPathBuilder is a function that builds field paths for a given definition usage
+type fieldPathBuilder func(interface{}) []*field.Path
+
+// validateDefinitions is a generic function to validate any definition type
+func (h *ValidatingHandler) validateDefinitions(
+	ctx context.Context,
+	req admission.Request,
+	appNamespace string,
+	definitionType reflect.Type,
+	usageMap interface{},
+	buildFieldPaths fieldPathBuilder,
+) field.ErrorList {
+	var errs field.ErrorList
+
+	// Get the definition info for the given type
+	defInfo, ok := v1beta1.DefinitionTypeMap[definitionType]
+	if !ok {
+		klog.Errorf("Unknown definition type: %v", definitionType)
+		return errs
+	}
+
+	switch typedMap := usageMap.(type) {
+	case map[string][]int:
+		for defType, indices := range typedMap {
+			allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
+			fieldPaths := buildFieldPaths(indices)
+			errs = append(errs, h.processDefinitionPermissionCheck(
+				allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
+		}
+	case map[string][][2]int:
+		for defType, locations := range typedMap {
+			allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
+			fieldPaths := buildFieldPaths(locations)
+			errs = append(errs, h.processDefinitionPermissionCheck(
+				allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
+		}
+	case map[string][]workflowStepLocation:
+		for defType, locations := range typedMap {
+			allowed, err := h.checkDefinitionPermission(ctx, req, defInfo.GVR.Resource, defType, appNamespace)
+			fieldPaths := buildFieldPaths(locations)
+			errs = append(errs, h.processDefinitionPermissionCheck(
+				allowed, err, req, defInfo.Kind, defType, appNamespace, fieldPaths)...)
+		}
+	}
+
+	return errs
+}
+
+// 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) {
+		return nil
+	}
+
+	var errs field.ErrorList
+	usage := collectDefinitionUsage(app)
+
+	// Validate ComponentDefinitions
+	errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
+		reflect.TypeOf(v1beta1.ComponentDefinition{}), usage.componentTypes,
+		func(indices interface{}) []*field.Path {
+			var paths []*field.Path
+			for _, idx := range indices.([]int) {
+				paths = append(paths, field.NewPath("spec", "components").Index(idx).Child("type"))
+			}
+			return paths
+		})...)
+
+	// Validate TraitDefinitions
+	errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
+		reflect.TypeOf(v1beta1.TraitDefinition{}), usage.traitTypes,
+		func(locations interface{}) []*field.Path {
+			var paths []*field.Path
+			for _, loc := range locations.([][2]int) {
+				paths = append(paths,
+					field.NewPath("spec", "components").Index(loc[0]).Child("traits").Index(loc[1]).Child("type"))
+			}
+			return paths
+		})...)
+
+	// Validate PolicyDefinitions
+	errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
+		reflect.TypeOf(v1beta1.PolicyDefinition{}), usage.policyTypes,
+		func(indices interface{}) []*field.Path {
+			var paths []*field.Path
+			for _, idx := range indices.([]int) {
+				paths = append(paths, field.NewPath("spec", "policies").Index(idx).Child("type"))
+			}
+			return paths
+		})...)
+
+	// Validate WorkflowStepDefinitions
+	errs = append(errs, h.validateDefinitions(ctx, req, app.Namespace,
+		reflect.TypeOf(v1beta1.WorkflowStepDefinition{}), usage.workflowStepTypes,
+		func(locations interface{}) []*field.Path {
+			var paths []*field.Path
+			for _, loc := range locations.([]workflowStepLocation) {
+				paths = append(paths, getWorkflowStepFieldPath(loc))
+			}
+			return paths
+		})...)
+
+	return errs
+}
+
+// getWorkflowStepFieldPath constructs the field path for a workflow step or sub-step
+func getWorkflowStepFieldPath(loc workflowStepLocation) *field.Path {
+	if !loc.IsSubStep {
+		// Regular step
+		return field.NewPath("spec", "workflow", "steps").Index(loc.StepIndex).Child("type")
+	}
+	// Sub-step
+	return field.NewPath("spec", "workflow", "steps").Index(loc.StepIndex).Child("subSteps").Index(loc.SubStepIndex).Child("type")
+}
+
 // ValidateAnnotations validates whether the application has both autoupdate and publish version annotations
 func (h *ValidatingHandler) ValidateAnnotations(_ context.Context, app *v1beta1.Application) field.ErrorList {
 	var annotationsErrs field.ErrorList
@@ -122,19 +399,20 @@ func (h *ValidatingHandler) ValidateAnnotations(_ context.Context, app *v1beta1.
 }
 
 // ValidateCreate validates the Application on creation
-func (h *ValidatingHandler) ValidateCreate(ctx context.Context, app *v1beta1.Application) field.ErrorList {
+func (h *ValidatingHandler) ValidateCreate(ctx context.Context, app *v1beta1.Application, req admission.Request) field.ErrorList {
 	var errs field.ErrorList
 
 	errs = append(errs, h.ValidateAnnotations(ctx, app)...)
+	errs = append(errs, h.ValidateDefinitionPermissions(ctx, app, req)...)
 	errs = append(errs, h.ValidateWorkflow(ctx, app)...)
 	errs = append(errs, h.ValidateComponents(ctx, app)...)
 	return errs
 }
 
 // ValidateUpdate validates the Application on update
-func (h *ValidatingHandler) ValidateUpdate(ctx context.Context, newApp, _ *v1beta1.Application) field.ErrorList {
+func (h *ValidatingHandler) ValidateUpdate(ctx context.Context, newApp, _ *v1beta1.Application, req admission.Request) field.ErrorList {
 	// check if the newApp is valid
-	errs := h.ValidateCreate(ctx, newApp)
+	errs := h.ValidateCreate(ctx, newApp, req)
 	// TODO: add more validating
 	return errs
 }
diff --git a/pkg/webhook/core.oam.dev/v1beta1/componentdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1beta1/componentdefinition/validating_handler.go
index d9c21744..8622e21a 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/componentdefinition/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/componentdefinition/validating_handler.go
@@ -35,7 +35,7 @@ import (
 	webhookutils "github.com/oam-dev/kubevela/pkg/webhook/utils"
 )
 
-var componentDefGVR = v1beta1.SchemeGroupVersion.WithResource("componentdefinitions")
+var componentDefGVR = v1beta1.ComponentDefinitionGVR
 
 // ValidatingHandler handles validation of component definition
 type ValidatingHandler struct {
diff --git a/pkg/webhook/core.oam.dev/v1beta1/policydefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1beta1/policydefinition/validating_handler.go
index 1414271f..464c7e85 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/policydefinition/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/policydefinition/validating_handler.go
@@ -32,7 +32,7 @@ import (
 	webhookutils "github.com/oam-dev/kubevela/pkg/webhook/utils"
 )
 
-var policyDefGVR = v1beta1.SchemeGroupVersion.WithResource("policydefinitions")
+var policyDefGVR = v1beta1.PolicyDefinitionGVR
 
 // ValidatingHandler handles validation of policy definition
 type ValidatingHandler struct {
diff --git a/pkg/webhook/core.oam.dev/v1beta1/traitdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1beta1/traitdefinition/validating_handler.go
index f85fb8a0..a850ae30 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/traitdefinition/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/traitdefinition/validating_handler.go
@@ -42,7 +42,7 @@ const (
 	failInfoDefRefOmitted = "if definition reference is omitted, patch or output with GVK is required"
 )
 
-var traitDefGVR = v1beta1.SchemeGroupVersion.WithResource("traitdefinitions")
+var traitDefGVR = v1beta1.TraitDefinitionGVR
 
 // ValidatingHandler handles validation of trait definition
 type ValidatingHandler struct {
diff --git a/pkg/webhook/core.oam.dev/v1beta1/workflowstepdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1beta1/workflowstepdefinition/validating_handler.go
index b33045ca..f430d368 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/workflowstepdefinition/validating_handler.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/workflowstepdefinition/validating_handler.go
@@ -32,7 +32,7 @@ import (
 	webhookutils "github.com/oam-dev/kubevela/pkg/webhook/utils"
 )
 
-var workflowStepDefGVR = v1beta1.SchemeGroupVersion.WithResource("workflowstepdefinitions")
+var workflowStepDefGVR = v1beta1.WorkflowStepDefinitionGVR
 
 // ValidatingHandler handles validation of workflow step definition
 type ValidatingHandler struct {



Test patch:
diff --git a/apis/core.oam.dev/v1beta1/register_test.go b/apis/core.oam.dev/v1beta1/register_test.go
new file mode 100644
index 00000000..bdf1a38b
--- /dev/null
+++ b/apis/core.oam.dev/v1beta1/register_test.go
@@ -0,0 +1,117 @@
+/*
+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 v1beta1
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+func TestDefinitionTypeMap(t *testing.T) {
+	tests := []struct {
+		name         string
+		defType      reflect.Type
+		expectedGVR  schema.GroupVersionResource
+		expectedKind string
+	}{
+		{
+			name:         "ComponentDefinition",
+			defType:      reflect.TypeOf(ComponentDefinition{}),
+			expectedGVR:  ComponentDefinitionGVR,
+			expectedKind: ComponentDefinitionKind,
+		},
+		{
+			name:         "TraitDefinition",
+			defType:      reflect.TypeOf(TraitDefinition{}),
+			expectedGVR:  TraitDefinitionGVR,
+			expectedKind: TraitDefinitionKind,
+		},
+		{
+			name:         "PolicyDefinition",
+			defType:      reflect.TypeOf(PolicyDefinition{}),
+			expectedGVR:  PolicyDefinitionGVR,
+			expectedKind: PolicyDefinitionKind,
+		},
+		{
+			name:         "WorkflowStepDefinition",
+			defType:      reflect.TypeOf(WorkflowStepDefinition{}),
+			expectedGVR:  WorkflowStepDefinitionGVR,
+			expectedKind: WorkflowStepDefinitionKind,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			info, ok := DefinitionTypeMap[tt.defType]
+			assert.Truef(t, ok, "Type %v should exist in DefinitionTypeMap", tt.defType)
+			assert.Equal(t, tt.expectedGVR, info.GVR)
+			assert.Equal(t, tt.expectedKind, info.Kind)
+
+			// Verify GVR follows Kubernetes conventions
+			assert.Equal(t, Group, info.GVR.Group)
+			assert.Equal(t, Version, info.GVR.Version)
+			// Resource should be lowercase plural of Kind
+			assert.Equal(t, strings.ToLower(info.Kind)+"s", info.GVR.Resource)
+		})
+	}
+}
+
+func TestDefinitionTypeMapCompleteness(t *testing.T) {
+	// Ensure all expected definition types are in the map
+	expectedTypes := []reflect.Type{
+		reflect.TypeOf(ComponentDefinition{}),
+		reflect.TypeOf(TraitDefinition{}),
+		reflect.TypeOf(PolicyDefinition{}),
+		reflect.TypeOf(WorkflowStepDefinition{}),
+	}
+
+	assert.Equal(t, len(expectedTypes), len(DefinitionTypeMap), "DefinitionTypeMap should contain exactly %d entries", len(expectedTypes))
+
+	for _, expectedType := range expectedTypes {
+		_, ok := DefinitionTypeMap[expectedType]
+		assert.Truef(t, ok, "DefinitionTypeMap should contain %v", expectedType)
+	}
+}
+
+func TestDefinitionKindValues(t *testing.T) {
+	// Verify that the Kind values match the actual type names
+	tests := []struct {
+		defType      interface{}
+		expectedKind string
+	}{
+		{ComponentDefinition{}, "ComponentDefinition"},
+		{TraitDefinition{}, "TraitDefinition"},
+		{PolicyDefinition{}, "PolicyDefinition"},
+		{WorkflowStepDefinition{}, "WorkflowStepDefinition"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.expectedKind, func(t *testing.T) {
+			actualKind := reflect.TypeOf(tt.defType).Name()
+			assert.Equal(t, tt.expectedKind, actualKind)
+
+			// Also verify it matches what's in the map
+			info, ok := DefinitionTypeMap[reflect.TypeOf(tt.defType)]
+			assert.True(t, ok)
+			assert.Equal(t, tt.expectedKind, info.Kind)
+		})
+	}
+}
diff --git a/pkg/auth/identity_test.go b/pkg/auth/identity_test.go
new file mode 100644
index 00000000..d93b66cf
--- /dev/null
+++ b/pkg/auth/identity_test.go
@@ -0,0 +1,254 @@
+/*
+Copyright 2022 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 auth
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	rbacv1 "k8s.io/api/rbac/v1"
+)
+
+func TestIdentity(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		testCases := map[string]struct {
+			identity Identity
+			expected string
+		}{
+			"user only": {
+				identity: Identity{User: "test-user"},
+				expected: "User=test-user",
+			},
+			"user and groups": {
+				identity: Identity{User: "test-user", Groups: []string{"group1", "group2"}},
+				expected: "User=test-user Groups=group1,group2",
+			},
+			"service account only": {
+				identity: Identity{ServiceAccount: "sa-name", ServiceAccountNamespace: "sa-ns"},
+				expected: "SA=system:serviceaccount:sa-ns:sa-name",
+			},
+			"all fields": {
+				identity: Identity{User: "test-user", Groups: []string{"group1"}, ServiceAccount: "sa-name", ServiceAccountNamespace: "sa-ns"},
+				expected: "User=test-user Groups=group1 SA=system:serviceaccount:sa-ns:sa-name",
+			},
+			"empty": {
+				identity: Identity{},
+				expected: "",
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				r.Equal(tc.expected, tc.identity.String())
+			})
+		}
+	})
+
+	t.Run("Match", func(t *testing.T) {
+		identity := &Identity{
+			User:                    "test-user",
+			Groups:                  []string{"group1", "group2"},
+			ServiceAccount:          "sa-name",
+			ServiceAccountNamespace: "sa-ns",
+		}
+		testCases := map[string]struct {
+			subject  rbacv1.Subject
+			expected bool
+		}{
+			"match user": {
+				subject:  rbacv1.Subject{Kind: rbacv1.UserKind, Name: "test-user"},
+				expected: true,
+			},
+			"not match user": {
+				subject:  rbacv1.Subject{Kind: rbacv1.UserKind, Name: "another-user"},
+				expected: false,
+			},
+			"match group": {
+				subject:  rbacv1.Subject{Kind: rbacv1.GroupKind, Name: "group1"},
+				expected: true,
+			},
+			"not match group": {
+				subject:  rbacv1.Subject{Kind: rbacv1.GroupKind, Name: "group3"},
+				expected: false,
+			},
+			"match service account": {
+				subject:  rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: "sa-name", Namespace: "sa-ns"},
+				expected: true,
+			},
+			"not match service account name": {
+				subject:  rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: "another-sa", Namespace: "sa-ns"},
+				expected: false,
+			},
+			"not match service account namespace": {
+				subject:  rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: "sa-name", Namespace: "another-ns"},
+				expected: false,
+			},
+			"unknown kind": {
+				subject:  rbacv1.Subject{Kind: "Unknown", Name: "test-user"},
+				expected: false,
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				r.Equal(tc.expected, identity.Match(tc.subject))
+			})
+		}
+	})
+
+	t.Run("MatchAny", func(t *testing.T) {
+		identity := &Identity{
+			User:   "test-user",
+			Groups: []string{"group1"},
+		}
+		testCases := map[string]struct {
+			subjects []rbacv1.Subject
+			expected bool
+		}{
+			"match one": {
+				subjects: []rbacv1.Subject{
+					{Kind: rbacv1.GroupKind, Name: "group-other"},
+					{Kind: rbacv1.UserKind, Name: "test-user"},
+				},
+				expected: true,
+			},
+			"match none": {
+				subjects: []rbacv1.Subject{
+					{Kind: rbacv1.GroupKind, Name: "group-other"},
+					{Kind: rbacv1.UserKind, Name: "user-other"},
+				},
+				expected: false,
+			},
+			"empty subjects": {
+				subjects: []rbacv1.Subject{},
+				expected: false,
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				r.Equal(tc.expected, identity.MatchAny(tc.subjects))
+			})
+		}
+	})
+
+	t.Run("Regularize", func(t *testing.T) {
+		testCases := map[string]struct {
+			identity *Identity
+			expected *Identity
+		}{
+			"trim spaces": {
+				identity: &Identity{User: "  user  ", ServiceAccount: " sa  "},
+				expected: &Identity{User: "user", ServiceAccount: "sa", ServiceAccountNamespace: "default"},
+			},
+			"remove duplicate groups": {
+				identity: &Identity{User: "user", Groups: []string{" g1 ", "g2", " g1", ""}},
+				expected: &Identity{User: "user", Groups: []string{"g1", "g2", ""}},
+			},
+			"default sa namespace": {
+				identity: &Identity{ServiceAccount: "sa"},
+				expected: &Identity{ServiceAccount: "sa", ServiceAccountNamespace: "default"},
+			},
+			"no change": {
+				identity: &Identity{User: "user", Groups: []string{"g1"}},
+				expected: &Identity{User: "user", Groups: []string{"g1"}},
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				tc.identity.Regularize()
+				r.Equal(tc.expected, tc.identity)
+			})
+		}
+	})
+
+	t.Run("Validate", func(t *testing.T) {
+		testCases := map[string]struct {
+			identity  Identity
+			expectErr bool
+		}{
+			"valid user": {
+				identity:  Identity{User: "user"},
+				expectErr: false,
+			},
+			"valid service account": {
+				identity:  Identity{ServiceAccount: "sa", ServiceAccountNamespace: "ns"},
+				expectErr: false,
+			},
+			"invalid empty": {
+				identity:  Identity{},
+				expectErr: true,
+			},
+			"invalid user and sa": {
+				identity:  Identity{User: "user", ServiceAccount: "sa"},
+				expectErr: true,
+			},
+			"invalid group and sa": {
+				identity:  Identity{Groups: []string{"g1"}, ServiceAccount: "sa"},
+				expectErr: true,
+			},
+			"invalid sa namespace without sa": {
+				identity:  Identity{User: "user", ServiceAccountNamespace: "ns"},
+				expectErr: true,
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				err := tc.identity.Validate()
+				if tc.expectErr {
+					r.Error(err)
+				} else {
+					r.NoError(err)
+				}
+			})
+		}
+	})
+
+	t.Run("Subjects", func(t *testing.T) {
+		testCases := map[string]struct {
+			identity Identity
+			expected []rbacv1.Subject
+		}{
+			"user and groups": {
+				identity: Identity{User: "user", Groups: []string{"g1", "g2"}},
+				expected: []rbacv1.Subject{
+					{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: "user"},
+					{Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: "g1"},
+					{Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: "g2"},
+				},
+			},
+			"service account": {
+				identity: Identity{ServiceAccount: "sa", ServiceAccountNamespace: "ns"},
+				expected: []rbacv1.Subject{
+					{Kind: rbacv1.ServiceAccountKind, Name: "sa", Namespace: "ns"},
+				},
+			},
+			"empty": {
+				identity: Identity{},
+				expected: nil,
+			},
+		}
+		for name, tc := range testCases {
+			t.Run(name, func(t *testing.T) {
+				r := require.New(t)
+				r.ElementsMatch(tc.expected, tc.identity.Subjects())
+			})
+		}
+	})
+}
diff --git a/pkg/auth/kubeconfig_test.go b/pkg/auth/kubeconfig_test.go
new file mode 100644
index 00000000..7d2ea85e
--- /dev/null
+++ b/pkg/auth/kubeconfig_test.go
@@ -0,0 +1,334 @@
+/*
+Copyright 2022 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 auth
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"math/big"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apiserver/pkg/authentication/user"
+	"k8s.io/client-go/kubernetes/fake"
+	ktesting "k8s.io/client-go/testing"
+	"k8s.io/client-go/tools/clientcmd"
+	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+)
+
+func TestNewKubeConfigGenerateOptions(t *testing.T) {
+	testCases := map[string]struct {
+		opts     []KubeConfigGenerateOption
+		validate func(*testing.T, *KubeConfigGenerateOptions)
+	}{
+		"default options": {
+			opts: []KubeConfigGenerateOption{},
+			validate: func(t *testing.T, opts *KubeConfigGenerateOptions) {
+				r := require.New(t)
+				r.NotNil(opts.X509)
+				r.Nil(opts.ServiceAccount)
+				r.Equal(user.Anonymous, opts.X509.User)
+				r.Contains(opts.X509.Groups, KubeVelaClientGroup)
+			},
+		},
+		"with user and group options": {
+			opts: []KubeConfigGenerateOption{
+				KubeConfigWithUserGenerateOption("test-user"),
+				KubeConfigWithGroupGenerateOption("test-group"),
+			},
+			validate: func(t *testing.T, opts *KubeConfigGenerateOptions) {
+				r := require.New(t)
+				r.Equal("test-user", opts.X509.User)
+				r.Contains(opts.X509.Groups, "test-group")
+			},
+		},
+		"with service account option": {
+			opts: []KubeConfigGenerateOption{KubeConfigWithServiceAccountGenerateOption(types.NamespacedName{Name: "sa", Namespace: "ns"})},
+			validate: func(t *testing.T, opts *KubeConfigGenerateOptions) {
+				r := require.New(t)
+				r.Nil(opts.X509)
+				r.NotNil(opts.ServiceAccount)
+				r.Equal("sa", opts.ServiceAccount.ServiceAccountName)
+				r.Equal("ns", opts.ServiceAccount.ServiceAccountNamespace)
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			opts := newKubeConfigGenerateOptions(tc.opts...)
+			tc.validate(t, opts)
+		})
+	}
+}
+
+func TestGenKubeConfig(t *testing.T) {
+	baseCfg := &clientcmdapi.Config{
+		Clusters: map[string]*clientcmdapi.Cluster{
+			"test-cluster": {Server: "https://localhost:6443"},
+		},
+		Contexts: map[string]*clientcmdapi.Context{
+			"test-context": {Cluster: "test-cluster", AuthInfo: "test-user"},
+		},
+		CurrentContext: "test-context",
+	}
+	authInfo := &clientcmdapi.AuthInfo{
+		ClientKeyData:         []byte("test-key"),
+		ClientCertificateData: []byte("test-cert"),
+	}
+
+	testCases := map[string]struct {
+		baseCfg   *clientcmdapi.Config
+		authInfo  *clientcmdapi.AuthInfo
+		caData    []byte
+		expectErr bool
+	}{
+		"valid config": {
+			baseCfg:  baseCfg,
+			authInfo: authInfo,
+			caData:   []byte("ca-data"),
+		},
+		"no clusters in config": {
+			baseCfg:   &clientcmdapi.Config{},
+			authInfo:  authInfo,
+			expectErr: true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			r := require.New(t)
+			cfg, err := genKubeConfig(tc.baseCfg, tc.authInfo, tc.caData)
+			if tc.expectErr {
+				r.Error(err)
+				r.Contains(err.Error(), "no clusters")
+				return
+			}
+			r.NoError(err)
+			r.NotNil(cfg)
+			r.Len(cfg.Clusters, 1)
+			r.Equal(tc.caData, cfg.Clusters["test-cluster"].CertificateAuthorityData)
+		})
+	}
+}
+
+func TestMakeCertAndKey(t *testing.T) {
+	r := require.New(t)
+	buf := &bytes.Buffer{}
+	csr, key, err := makeCertAndKey(buf, &KubeConfigGenerateX509Options{User: "bob", Groups: []string{"g"}, PrivateKeyBits: 2048})
+	r.NoError(err)
+	r.NotEmpty(csr)
+	r.NotEmpty(key)
+	r.Contains(buf.String(), "Private key generated.")
+}
+
+func TestMakeCSRName(t *testing.T) {
+	r := require.New(t)
+	name := makeCSRName("test-user")
+	r.Equal("kubevela-csr-test-user", name)
+}
+
+func TestGenerateX509KubeConfig(t *testing.T) {
+	r := require.New(t)
+	ctx := context.Background()
+	minimalCfg := func() *clientcmdapi.Config {
+		return &clientcmdapi.Config{
+			Clusters:       map[string]*clientcmdapi.Cluster{"c": {Server: "https://example"}},
+			Contexts:       map[string]*clientcmdapi.Context{"ctx": {Cluster: "c", AuthInfo: "ai"}},
+			CurrentContext: "ctx",
+		}
+	}
+
+	t.Run("v1", func(t *testing.T) {
+		cli := fake.NewSimpleClientset()
+		var stored *certificatesv1.CertificateSigningRequest
+		cli.Fake.PrependReactor("create", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			create := action.(ktesting.CreateAction)
+			obj := create.GetObject().(*certificatesv1.CertificateSigningRequest)
+			stored = obj.DeepCopy()
+			return true, stored, nil
+		})
+		cli.Fake.PrependReactor("update", "certificatesigningrequests/approval", func(action ktesting.Action) (bool, runtime.Object, error) {
+			return true, stored, nil
+		})
+		getCount := 0
+		cli.Fake.PrependReactor("get", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			getCount++
+			obj := stored.DeepCopy()
+			if getCount >= 2 {
+				obj.Status.Certificate = []byte("CERT")
+			}
+			return true, obj, nil
+		})
+		cli.Fake.PrependReactor("delete", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			return true, nil, nil
+		})
+
+		buf := &bytes.Buffer{}
+		cfg := minimalCfg()
+		res, err := generateX509KubeConfigV1(ctx, cli, cfg, buf, &KubeConfigGenerateX509Options{User: "alice", Groups: []string{"g"}, ExpireTime: time.Hour, PrivateKeyBits: 2048})
+		r.NoError(err)
+		ai := res.AuthInfos[cfg.Contexts[cfg.CurrentContext].AuthInfo]
+		r.NotEmpty(ai.ClientCertificateData)
+		r.NotEmpty(ai.ClientKeyData)
+	})
+
+	t.Run("v1beta1", func(t *testing.T) {
+		cli := fake.NewSimpleClientset()
+		var stored *certificatesv1beta1.CertificateSigningRequest
+		cli.Fake.PrependReactor("create", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			create := action.(ktesting.CreateAction)
+			obj := create.GetObject().(*certificatesv1beta1.CertificateSigningRequest)
+			stored = obj.DeepCopy()
+			return true, stored, nil
+		})
+		cli.Fake.PrependReactor("update", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			update := action.(ktesting.UpdateAction)
+			obj := update.GetObject().(*certificatesv1beta1.CertificateSigningRequest)
+			stored = obj.DeepCopy()
+			return true, stored, nil
+		})
+		getCount := 0
+		cli.Fake.PrependReactor("get", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			getCount++
+			obj := stored.DeepCopy()
+			if getCount >= 2 {
+				obj.Status.Certificate = []byte("CERT-BETA")
+			}
+			return true, obj, nil
+		})
+		cli.Fake.PrependReactor("delete", "certificatesigningrequests", func(action ktesting.Action) (bool, runtime.Object, error) {
+			return true, nil, nil
+		})
+
+		buf := &bytes.Buffer{}
+		cfg := minimalCfg()
+		res, err := generateX509KubeConfigV1Beta(ctx, cli, cfg, buf, &KubeConfigGenerateX509Options{User: "alice-beta", Groups: []string{"g"}, ExpireTime: time.Hour, PrivateKeyBits: 2048})
+		r.NoError(err)
+		ai := res.AuthInfos[cfg.Contexts[cfg.CurrentContext].AuthInfo]
+		r.Equal([]byte("CERT-BETA"), ai.ClientCertificateData)
+		r.NotEmpty(ai.ClientKeyData)
+	})
+}
+
+func TestGenerateServiceAccountKubeConfig(t *testing.T) {
+	r := require.New(t)
+	ctx := context.Background()
+	minimalCfg := func() *clientcmdapi.Config {
+		return &clientcmdapi.Config{
+			Clusters:       map[string]*clientcmdapi.Cluster{"c": {Server: "https://example"}},
+			Contexts:       map[string]*clientcmdapi.Context{"ctx": {Cluster: "c", AuthInfo: "ai"}},
+			CurrentContext: "ctx",
+		}
+	}
+
+	t.Run("with secret", func(t *testing.T) {
+		sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sa", Namespace: "ns"}, Secrets: []corev1.ObjectReference{{Name: "s"}}}
+		sec := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "s", Namespace: "ns"}, Data: map[string][]byte{"token": []byte("tok"), "ca.crt": []byte("CA")}}
+		cli := fake.NewSimpleClientset(sa, sec)
+
+		buf := &bytes.Buffer{}
+		cfg := minimalCfg()
+		got, err := generateServiceAccountKubeConfig(ctx, cli, cfg, buf, &KubeConfigGenerateServiceAccountOptions{ServiceAccountName: "sa", ServiceAccountNamespace: "ns", ExpireTime: time.Hour})
+		r.NoError(err)
+		ai := got.AuthInfos[cfg.Contexts[cfg.CurrentContext].AuthInfo]
+		r.Equal("tok", ai.Token)
+		cl := got.Clusters[cfg.Contexts[cfg.CurrentContext].Cluster]
+		r.Equal("CA", string(cl.CertificateAuthorityData))
+	})
+
+	t.Run("with token request", func(t *testing.T) {
+		sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sa", Namespace: "ns"}}
+		cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "kube-root-ca.crt", Namespace: "ns"}, Data: map[string]string{"ca.crt": "CA"}}
+		cli := fake.NewSimpleClientset(sa, cm)
+
+		cli.Fake.PrependReactor("create", "serviceaccounts/token", func(action ktesting.Action) (bool, runtime.Object, error) {
+			obj := &authenticationv1.TokenRequest{
+				Status: authenticationv1.TokenRequestStatus{Token: "rtok"},
+			}
+			return true, obj, nil
+		})
+
+		buf := &bytes.Buffer{}
+		cfg := minimalCfg()
+		got, err := generateServiceAccountKubeConfig(ctx, cli, cfg, buf, &KubeConfigGenerateServiceAccountOptions{ServiceAccountName: "sa", ServiceAccountNamespace: "ns", ExpireTime: time.Hour})
+		r.NoError(err)
+		ai := got.AuthInfos[cfg.Contexts[cfg.CurrentContext].AuthInfo]
+		r.Equal("rtok", ai.Token)
+		cl := got.Clusters[cfg.Contexts[cfg.CurrentContext].Cluster]
+		r.Equal("CA", string(cl.CertificateAuthorityData))
+	})
+}
+
+func TestReadIdentityFromKubeConfig(t *testing.T) {
+	r := require.New(t)
+	dir := t.TempDir()
+
+	t.Run("from certificate", func(t *testing.T) {
+		priv, err := rsa.GenerateKey(rand.Reader, 2048)
+		r.NoError(err)
+		tmpl := &x509.Certificate{SerialNumber: new(big.Int).SetInt64(1), Subject: pkix.Name{CommonName: "alice", Organization: []string{"g1", "g2"}}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)}
+		certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
+		r.NoError(err)
+		certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
+
+		kcfg := &clientcmdapi.Config{
+			Clusters:       map[string]*clientcmdapi.Cluster{"c": {Server: "https://example"}},
+			Contexts:       map[string]*clientcmdapi.Context{"ctx": {Cluster: "c", AuthInfo: "ai"}},
+			CurrentContext: "ctx",
+			AuthInfos:      map[string]*clientcmdapi.AuthInfo{"ai": {ClientCertificateData: certPEM}},
+		}
+		path := filepath.Join(dir, "kubeconfig-cert")
+		err = clientcmd.WriteToFile(*kcfg, path)
+		r.NoError(err)
+
+		id, err := ReadIdentityFromKubeConfig(path)
+		r.NoError(err)
+		r.Equal("alice", id.User)
+		r.Equal([]string{"g1", "g2"}, id.Groups)
+	})
+
+	t.Run("no auth returns error", func(t *testing.T) {
+		kcfg := &clientcmdapi.Config{
+			Clusters:       map[string]*clientcmdapi.Cluster{"c": {Server: "https://example"}},
+			Contexts:       map[string]*clientcmdapi.Context{"ctx": {Cluster: "c", AuthInfo: "ai"}},
+			CurrentContext: "ctx",
+			AuthInfos:      map[string]*clientcmdapi.AuthInfo{"ai": {}},
+		}
+		path := filepath.Join(dir, "kubeconfig-empty")
+		err := clientcmd.WriteToFile(*kcfg, path)
+		r.NoError(err)
+
+		_, err = ReadIdentityFromKubeConfig(path)
+		r.Error(err)
+		r.Contains(err.Error(), "cannot find client certificate or serviceaccount token in kubeconfig")
+	})
+}
diff --git a/pkg/auth/privileges_test.go b/pkg/auth/privileges_test.go
new file mode 100644
index 00000000..b397595d
--- /dev/null
+++ b/pkg/auth/privileges_test.go
@@ -0,0 +1,377 @@
+/*
+Copyright 2022 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 auth
+
+import (
+	"bytes"
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestAuthObjRefHelpers(t *testing.T) {
+	testCases := []struct {
+		name             string
+		ref              authObjRef
+		expectedFullName string
+		expectedScope    apiextensions.ResourceScope
+	}{
+		{
+			name:             "namespaced object",
+			ref:              authObjRef{Name: "test-obj", Namespace: "test-ns"},
+			expectedFullName: "test-ns/test-obj",
+			expectedScope:    apiextensions.NamespaceScoped,
+		},
+		{
+			name:             "cluster-scoped object",
+			ref:              authObjRef{Name: "test-obj"},
+			expectedFullName: "test-obj",
+			expectedScope:    apiextensions.ClusterScoped,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			r := require.New(t)
+			t.Run("FullName", func(t *testing.T) {
+				r.Equal(tc.expectedFullName, tc.ref.FullName())
+			})
+			t.Run("Scope", func(t *testing.T) {
+				r.Equal(tc.expectedScope, tc.ref.Scope())
+			})
+		})
+	}
+}
+
+func TestSubjectsHelpers(t *testing.T) {
+	s1 := rbacv1.Subject{Kind: rbacv1.UserKind, Name: "user1"}
+	s2 := rbacv1.Subject{Kind: rbacv1.GroupKind, Name: "group1"}
+	s3 := rbacv1.Subject{Kind: rbacv1.UserKind, Name: "user2"}
+
+	t.Run("mergeSubjects", func(t *testing.T) {
+		testCases := []struct {
+			name     string
+			src      []rbacv1.Subject
+			merge    []rbacv1.Subject
+			expected []rbacv1.Subject
+		}{
+			{"no duplicates", []rbacv1.Subject{s1}, []rbacv1.Subject{s2}, []rbacv1.Subject{s1, s2}},
+			{"with duplicates", []rbacv1.Subject{s1, s2}, []rbacv1.Subject{s2, s3}, []rbacv1.Subject{s1, s2, s3}},
+			{"merge into empty", nil, []rbacv1.Subject{s1}, []rbacv1.Subject{s1}},
+		}
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				r := require.New(t)
+				merged := mergeSubjects(tc.src, tc.merge)
+				r.ElementsMatch(tc.expected, merged)
+			})
+		}
+	})
+
+	t.Run("removeSubjects", func(t *testing.T) {
+		testCases := []struct {
+			name     string
+			src      []rbacv1.Subject
+			toRemove []rbacv1.Subject
+			expected []rbacv1.Subject
+		}{
+			{"remove existing", []rbacv1.Subject{s1, s2, s3}, []rbacv1.Subject{s2}, []rbacv1.Subject{s1, s3}},
+			{"remove non-existent", []rbacv1.Subject{s1}, []rbacv1.Subject{s2}, []rbacv1.Subject{s1}},
+		}
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				r := require.New(t)
+				remaining := removeSubjects(tc.src, tc.toRemove)
+				r.ElementsMatch(tc.expected, remaining)
+			})
+		}
+	})
+}
+
+func TestPrivilegeDescription(t *testing.T) {
+	r := require.New(t)
+
+	t.Run("ScopedPrivilege", func(t *testing.T) {
+		p := &ScopedPrivilege{Cluster: "c1", Namespace: "ns1", ReadOnly: true}
+		r.Equal("c1", p.GetCluster())
+		roles := p.GetRoles()
+		r.Len(roles, 1)
+		r.Equal(KubeVelaReaderRoleName, roles[0].GetName())
+
+		binding := p.GetRoleBinding(nil).(*rbacv1.RoleBinding)
+		r.Equal("ns1", binding.Namespace)
+		r.Equal(KubeVelaReaderRoleName, binding.RoleRef.Name)
+	})
+
+	t.Run("ApplicationPrivilege", func(t *testing.T) {
+		p := &ApplicationPrivilege{Cluster: "c1", ReadOnly: false}
+		r.Equal("c1", p.GetCluster())
+		roles := p.GetRoles()
+		r.Len(roles, 1)
+		r.Equal(KubeVelaWriterAppRoleName, roles[0].GetName())
+
+		binding := p.GetRoleBinding(nil).(*rbacv1.ClusterRoleBinding)
+		r.Equal(KubeVelaWriterAppRoleName, binding.RoleRef.Name)
+	})
+}
+
+func TestListPrivileges(t *testing.T) {
+	r := require.New(t)
+
+	userRole := &rbacv1.Role{
+		ObjectMeta: metav1.ObjectMeta{Name: "user-role", Namespace: "default"},
+		Rules:      []rbacv1.PolicyRule{{Verbs: []string{"get"}, Resources: []string{"pods"}}},
+	}
+	groupClusterRole := &rbacv1.ClusterRole{
+		ObjectMeta: metav1.ObjectMeta{Name: "group-crole"},
+		Rules:      []rbacv1.PolicyRule{{Verbs: []string{"list"}, Resources: []string{"deployments"}}},
+	}
+	userRoleBinding := &rbacv1.RoleBinding{
+		ObjectMeta: metav1.ObjectMeta{Name: "user-rb", Namespace: "default"},
+		Subjects:   []rbacv1.Subject{{Kind: rbacv1.UserKind, Name: "test-user"}},
+		RoleRef:    rbacv1.RoleRef{Kind: "Role", Name: "user-role"},
+	}
+	groupClusterRoleBinding := &rbacv1.ClusterRoleBinding{
+		ObjectMeta: metav1.ObjectMeta{Name: "group-crb"},
+		Subjects:   []rbacv1.Subject{{Kind: rbacv1.GroupKind, Name: "test-group"}},
+		RoleRef:    rbacv1.RoleRef{Kind: "ClusterRole", Name: "group-crole"},
+	}
+
+	scheme := runtime.NewScheme()
+	r.NoError(rbacv1.AddToScheme(scheme))
+	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(userRole, groupClusterRole, userRoleBinding, groupClusterRoleBinding).Build()
+
+	identity := &Identity{User: "test-user", Groups: []string{"test-group"}}
+	privileges, err := ListPrivileges(context.Background(), cli, []string{"local"}, identity)
+
+	r.NoError(err)
+	r.NotNil(privileges)
+	r.Len(privileges, 1)
+	localPrivileges, ok := privileges["local"]
+	r.True(ok)
+	r.Len(localPrivileges, 2)
+
+	foundUserRole := false
+	foundGroupRole := false
+	for _, p := range localPrivileges {
+		if p.RoleRef.Name == "user-role" {
+			foundUserRole = true
+			r.Equal("Role", p.RoleRef.Kind)
+			r.Equal("default", p.RoleRef.Namespace)
+			r.Len(p.Rules, 1)
+			r.Equal("pods", p.Rules[0].Resources[0])
+			r.Len(p.RoleBindingRefs, 1)
+			r.Equal("user-rb", p.RoleBindingRefs[0].Name)
+		}
+		if p.RoleRef.Name == "group-crole" {
+			foundGroupRole = true
+			r.Equal("ClusterRole", p.RoleRef.Kind)
+			r.Len(p.Rules, 1)
+			r.Equal("deployments", p.Rules[0].Resources[0])
+			r.Len(p.RoleBindingRefs, 1)
+			r.Equal("group-crb", p.RoleBindingRefs[0].Name)
+		}
+	}
+	r.True(foundUserRole, "Expected to find privilege for user-role")
+	r.True(foundGroupRole, "Expected to find privilege for group-crole")
+}
+
+func TestPrivilegePrettyPrint(t *testing.T) {
+	r := require.New(t)
+
+	t.Run("printPolicyRule", func(t *testing.T) {
+		testCases := []struct {
+			name     string
+			rule     rbacv1.PolicyRule
+			expected string
+			lim      uint
+		}{
+			{
+				name: "simple rule",
+				rule: rbacv1.PolicyRule{
+					Verbs:     []string{"get", "list"},
+					APIGroups: []string{""},
+					Resources: []string{"pods"},
+				},
+				expected: "Resources:       pods\nVerb:            get, list",
+				lim:      80,
+			},
+			{
+				name: "rule with all fields",
+				rule: rbacv1.PolicyRule{
+					Verbs:           []string{"get"},
+					APIGroups:       []string{"apps"},
+					Resources:       []string{"deployments"},
+					ResourceNames:   []string{"my-deploy"},
+					NonResourceURLs: []string{"/version"},
+				},
+				expected: strings.Join([]string{
+					"APIGroups:       apps",
+					"Resources:       deployments",
+					"ResourceNames:   my-deploy",
+					"NonResourceURLs: /version",
+					"Verb:            get",
+				}, "\n"),
+				lim: 80,
+			},
+		}
+
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				output := printPolicyRule(tc.rule, tc.lim)
+				r.Equal(tc.expected, output)
+			})
+		}
+	})
+
+	t.Run("PrettyPrintPrivileges", func(t *testing.T) {
+		identity := &Identity{User: "test-user"}
+		clusters := []string{"cluster-1", "cluster-2"}
+		privilegesMap := map[string][]PrivilegeInfo{
+			"cluster-1": {
+				{
+					RoleRef:         RoleRef{Kind: "ClusterRole", Name: "view"},
+					RoleBindingRefs: []RoleBindingRef{{Kind: "ClusterRoleBinding", Name: "view-binding"}},
+					Rules:           []rbacv1.PolicyRule{{Verbs: []string{"get", "list"}, APIGroups: []string{"*"}, Resources: []string{"*"}}},
+				},
+				{
+					RoleRef:         RoleRef{Kind: "Role", Name: "editor", Namespace: "default"},
+					RoleBindingRefs: []RoleBindingRef{{Kind: "RoleBinding", Name: "editor-binding", Namespace: "default"}},
+					Rules:           []rbacv1.PolicyRule{{Verbs: []string{"*"}, APIGroups: []string{"apps"}, Resources: []string{"deployments"}}},
+				},
+			},
+			"cluster-2": {},
+		}
+
+		output := PrettyPrintPrivileges(identity, privilegesMap, clusters, 80)
+		// Exact string matching for tree output is brittle, so we check for key components.
+		r.Contains(output, "User=test-user")
+		r.Contains(output, "cluster-1")
+		r.Contains(output, "ClusterRole")
+		r.Contains(output, "view")
+		r.Contains(output, "Role")
+		r.Contains(output, "default/editor")
+		r.Contains(output, "cluster-2")
+		r.Contains(output, "no privilege found")
+		r.Contains(output, "PolicyRules")
+		r.Contains(output, "Scope")
+	})
+}
+
+func TestGrantAndRevokePrivileges(t *testing.T) {
+	r := require.New(t)
+	ctx := context.Background()
+	scheme := runtime.NewScheme()
+	r.NoError(rbacv1.AddToScheme(scheme))
+
+	privileges := []PrivilegeDescription{
+		&ScopedPrivilege{
+			Cluster:  "local",
+			ReadOnly: false, // Creates KubeVelaWriterRoleName
+		},
+	}
+	identityUser1 := &Identity{User: "user1"}
+	identityUser2 := &Identity{User: "user2"}
+
+	t.Run("GrantPrivileges", func(t *testing.T) {
+		cli := fake.NewClientBuilder().WithScheme(scheme).Build()
+		writer := &bytes.Buffer{}
+
+		// 1. Grant to user1
+		err := GrantPrivileges(ctx, cli, privileges, identityUser1, writer)
+		r.NoError(err)
+
+		// Verify Role and Binding created
+		role := &rbacv1.ClusterRole{}
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName}, role)
+		r.NoError(err)
+
+		binding := &rbacv1.ClusterRoleBinding{}
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName + ":binding"}, binding)
+		r.NoError(err)
+		r.Len(binding.Subjects, 1)
+		r.Equal("user1", binding.Subjects[0].Name)
+
+		// 2. Grant to user2 (should merge)
+		err = GrantPrivileges(ctx, cli, privileges, identityUser2, writer)
+		r.NoError(err)
+
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName + ":binding"}, binding)
+		r.NoError(err)
+		r.Len(binding.Subjects, 2)
+		r.ElementsMatch([]rbacv1.Subject{
+			{Kind: rbacv1.UserKind, Name: "user1", APIGroup: rbacv1.GroupName},
+			{Kind: rbacv1.UserKind, Name: "user2", APIGroup: rbacv1.GroupName},
+		}, binding.Subjects)
+
+		// 3. Grant to user1 again with replace
+		err = GrantPrivileges(ctx, cli, privileges, identityUser1, writer, WithReplace)
+		r.NoError(err)
+
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName + ":binding"}, binding)
+		r.NoError(err)
+		// Due to a bug in GrantPrivileges, WithReplace does not replace subjects.
+		// It re-applies the existing subjects.
+		r.Len(binding.Subjects, 2)
+		r.ElementsMatch([]rbacv1.Subject{
+			{Kind: rbacv1.UserKind, Name: "user1", APIGroup: rbacv1.GroupName},
+			{Kind: rbacv1.UserKind, Name: "user2", APIGroup: rbacv1.GroupName},
+		}, binding.Subjects)
+	})
+
+	t.Run("RevokePrivileges", func(t *testing.T) {
+		// Pre-populate client with a binding with two users
+		initialBinding := &rbacv1.ClusterRoleBinding{
+			ObjectMeta: metav1.ObjectMeta{Name: KubeVelaWriterRoleName + ":binding"},
+			RoleRef:    rbacv1.RoleRef{Kind: "ClusterRole", Name: KubeVelaWriterRoleName},
+			Subjects: []rbacv1.Subject{
+				{Kind: rbacv1.UserKind, Name: "user1", APIGroup: rbacv1.GroupName},
+				{Kind: rbacv1.UserKind, Name: "user2", APIGroup: rbacv1.GroupName},
+			},
+		}
+		cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initialBinding).Build()
+		writer := &bytes.Buffer{}
+
+		// 1. Revoke from user1
+		err := RevokePrivileges(ctx, cli, privileges, identityUser1, writer)
+		r.NoError(err)
+
+		// Verify user1 is removed, but binding still exists
+		binding := &rbacv1.ClusterRoleBinding{}
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName + ":binding"}, binding)
+		r.NoError(err)
+		r.Len(binding.Subjects, 1)
+		r.Equal("user2", binding.Subjects[0].Name)
+
+		// 2. Revoke from user2 (last user)
+		err = RevokePrivileges(ctx, cli, privileges, identityUser2, writer)
+		r.NoError(err)
+
+		// Verify binding is now deleted
+		err = cli.Get(ctx, types.NamespacedName{Name: KubeVelaWriterRoleName + ":binding"}, binding)
+		r.True(kerrors.IsNotFound(err))
+	})
+}
diff --git a/pkg/config/factory_test.go b/pkg/config/factory_test.go
index 4e7ed109..24a2bbaf 100644
--- a/pkg/config/factory_test.go
+++ b/pkg/config/factory_test.go
@@ -25,7 +25,10 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+	"github.com/oam-dev/kubevela/apis/types"
 	nacosmock "github.com/oam-dev/kubevela/test/mock/nacos"
 )
 
@@ -170,4 +173,92 @@ var _ = Describe("test config factory", func() {
 		err := fac.DeleteTemplate(context.TODO(), "default", "nacos")
 		Expect(err).Should(BeNil())
 	})
+
+	It("should fail to parse template with invalid CUE syntax", func() {
+		_, err := fac.ParseTemplate(context.Background(), "invalid-cue", []byte("metadata: { name: }"))
+		Expect(err).To(HaveOccurred())
+	})
+
+	It("should fail to parse template missing template block", func() {
+		_, err := fac.ParseTemplate(context.Background(), "missing-template", []byte(`metadata: { name: "t" }`))
+		Expect(err).To(HaveOccurred())
+		Expect(err.Error()).To(ContainSubstring("template"))
+	})
+
+	It("should fail to parse config when template not found", func() {
+		_, err := fac.ParseConfig(context.TODO(), NamespacedName{Name: "non-existent-template", Namespace: "default"}, Metadata{})
+		Expect(err).To(Equal(ErrTemplateNotFound))
+	})
+
+	It("should parse a template-less config", func() {
+		config, err := fac.ParseConfig(context.TODO(), NamespacedName{}, Metadata{
+			NamespacedName: NamespacedName{Name: "template-less-config", Namespace: "default"},
+			Properties:     map[string]interface{}{"key": "value"},
+		})
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(config.Name).To(Equal("template-less-config"))
+		Expect(config.Secret.Labels[types.LabelConfigType]).To(Equal(""))
+	})
+
+	It("should fail to update config when changing the template", func() {
+		nacos, err := fac.ParseConfig(context.TODO(), NamespacedName{Name: "nacos-server", Namespace: "default"}, Metadata{NamespacedName: NamespacedName{Name: "config-to-change", Namespace: "default"}, Properties: map[string]interface{}{
+			"servers": []map[string]interface{}{{
+				"ipAddr": "127.0.0.1",
+				"port":   8849,
+			}},
+		}})
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(fac.CreateOrUpdateConfig(context.Background(), nacos, "default")).ShouldNot(HaveOccurred())
+
+		nacos.Template.Name = "another-template"
+		err = fac.CreateOrUpdateConfig(context.Background(), nacos, "default")
+		Expect(err).To(Equal(ErrChangeTemplate))
+	})
+
+	It("should return error when getting a sensitive config", func() {
+		sensitiveTpl, err := fac.ParseTemplate(context.Background(), "", []byte(`
+metadata: { name: "sensitive-tpl", sensitive: true }
+template: { parameter: { key: string } }
+`))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(fac.CreateOrUpdateConfigTemplate(context.TODO(), "default", sensitiveTpl)).ShouldNot(HaveOccurred())
+
+		sensitiveConfig, err := fac.ParseConfig(context.TODO(), NamespacedName{Name: "sensitive-tpl", Namespace: "default"}, Metadata{
+			NamespacedName: NamespacedName{Name: "sensitive-config", Namespace: "default"},
+			Properties:     map[string]interface{}{"key": "secret-value"},
+		})
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(fac.CreateOrUpdateConfig(context.Background(), sensitiveConfig, "default")).ShouldNot(HaveOccurred())
+
+		_, err = fac.GetConfig(context.TODO(), "default", "sensitive-config", false)
+		Expect(err).To(Equal(ErrSensitiveConfig))
+	})
+
+	It("should fail to delete a secret that is not a KubeVela config", func() {
+		secret := &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: "not-a-config", Namespace: "default"},
+			Data:       map[string][]byte{"key": []byte("value")},
+		}
+		Expect(k8sClient.Create(context.TODO(), secret)).ShouldNot(HaveOccurred())
+
+		err := fac.DeleteConfig(context.TODO(), "default", "not-a-config")
+		Expect(err).To(HaveOccurred())
+		Expect(err.Error()).To(ContainSubstring("is not a config"))
+	})
+
+	It("should fail to convert configmap to template if labels are missing", func() {
+		cm := v1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{Name: "no-labels"},
+		}
+		_, err := convertConfigMap2Template(cm)
+		Expect(err).To(HaveOccurred())
+	})
+
+	It("should fail to convert secret to config if labels are missing", func() {
+		secret := &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: "no-labels"},
+		}
+		_, err := convertSecret2Config(secret)
+		Expect(err).To(HaveOccurred())
+	})
 })
diff --git a/pkg/config/writer/writer_test.go b/pkg/config/writer/writer_test.go
new file mode 100644
index 00000000..06b114bb
--- /dev/null
+++ b/pkg/config/writer/writer_test.go
@@ -0,0 +1,192 @@
+/*
+Copyright 2022 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 writer
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"cuelang.org/go/cue/cuecontext"
+	"github.com/stretchr/testify/require"
+
+	configcontext "github.com/oam-dev/kubevela/pkg/config/context"
+	"github.com/oam-dev/kubevela/pkg/cue/script"
+)
+
+func TestConvertMap2PropertiesKV(t *testing.T) {
+	r := require.New(t)
+	t.Run("valid conversion", func(t *testing.T) {
+		re := map[string]string{}
+		err := convertMap2PropertiesKV("", map[string]interface{}{
+			"s":  "s",
+			"n":  1,
+			"nn": 1.5,
+			"b":  true,
+			"m": map[string]interface{}{
+				"s": "s",
+				"b": false,
+			},
+			"aa": []string{"a", "a"},
+			"ai": []int64{1, 2},
+			"ar": []map[string]interface{}{{
+				"s2": "s2",
+			}},
+		}, re)
+		r.Equal(err, nil)
+		r.Equal(re, map[string]string{
+			"s":       "s",
+			"n":       "1",
+			"nn":      "1.5",
+			"b":       "true",
+			"m.s":     "s",
+			"m.b":     "false",
+			"aa":      "a,a",
+			"ai":      "1,2",
+			"ar.0.s2": "s2",
+		})
+	})
+
+	t.Run("unsupported type", func(t *testing.T) {
+		re := map[string]string{}
+		err := convertMap2PropertiesKV("", map[string]interface{}{"unsupported": make(chan int)}, re)
+		r.Error(err)
+		r.Contains(err.Error(), "can not be supported")
+	})
+}
+
+func TestEncodingOutput(t *testing.T) {
+	r := require.New(t)
+	t.Run("all formats", func(t *testing.T) {
+		testValue := `
+			context: {
+				key1: "hello"
+				key2: 2
+				key3: true
+				key4: 4.4
+				key5: ["hello"]
+				key6: [{"hello": 1}]
+				key7: [1, 2]
+				key8: [1.2, 1]
+				key9: {key10: [{"wang": true}]}
+			}
+		`
+		v := cuecontext.New().CompileString(testValue)
+		r.Equal(v.Err(), nil)
+
+		_, err := encodingOutput(v, "yaml")
+		r.Equal(err, nil)
+
+		_, err = encodingOutput(v, "properties")
+		r.Equal(err, nil)
+
+		_, err = encodingOutput(v, "toml")
+		r.Equal(err, nil)
+
+		json, err := encodingOutput(v, "json")
+		r.Equal(err, nil)
+		r.Equal(string(json), `{"context":{"key1":"hello","key2":2,"key3":true,"key4":4.4,"key5":["hello"],"key6":[{"hello":1}],"key7":[1,2],"key8":[1.2,1],"key9":{"key10":[{"wang":true}]}}}`)
+	})
+
+	t.Run("specific formats", func(t *testing.T) {
+		testValue := `
+            {
+                key1: "hello"
+                key2: 123
+                key3: {
+                    subkey: "sub"
+                }
+            }
+        `
+		v := cuecontext.New().CompileString(testValue)
+		r.NoError(v.Err())
+
+		tomlBytes, err := encodingOutput(v, "toml")
+		r.NoError(err)
+		expectedToml := "key1 = \"hello\"\nkey2 = 123.0\n\n[key3]\n  subkey = \"sub\"\n"
+		r.Equal(expectedToml, string(tomlBytes))
+
+		propsBytes, err := encodingOutput(v, "properties")
+		r.NoError(err)
+		r.Contains(string(propsBytes), "key1 = hello")
+		r.Contains(string(propsBytes), "key2 = 123")
+		r.Contains(string(propsBytes), "key3.subkey = sub")
+	})
+
+	t.Run("invalid cue value", func(t *testing.T) {
+		v := cuecontext.New().CompileString(`{key: > 1}`)
+		_, err := encodingOutput(v, "json")
+		r.Error(err)
+	})
+}
+
+func TestParseExpandedWriterConfig(t *testing.T) {
+	r := require.New(t)
+	t.Run("missing nacos block", func(t *testing.T) {
+		v := cuecontext.New().CompileString(
+			`
+        template: {}
+        `)
+		r.NoError(v.Err())
+		ewc := ParseExpandedWriterConfig(v)
+		r.Nil(ewc.Nacos)
+	})
+
+	t.Run("malformed nacos block", func(t *testing.T) {
+		v := cuecontext.New().CompileString(`
+        nacos: {
+            endpoint: 123 // should be a struct
+        }
+        `)
+		r.NoError(v.Err())
+		ewc := ParseExpandedWriterConfig(v)
+		r.True(ewc.Nacos == nil || ewc.Nacos.Endpoint.Name == "")
+	})
+}
+
+func TestRenderForExpandedWriter(t *testing.T) {
+	r := require.New(t)
+	t.Run("no nacos config", func(t *testing.T) {
+		ewc := ExpandedWriterConfig{}
+		data, err := RenderForExpandedWriter(ewc, script.CUE(""), configcontext.ConfigRenderContext{}, nil)
+		r.NoError(err)
+		r.Nil(data.Nacos)
+	})
+
+	t.Run("render nacos error", func(t *testing.T) {
+		ewc := ExpandedWriterConfig{
+			Nacos: &NacosConfig{},
+		}
+		s := script.CUE("parameter: {}")
+		_, err := RenderForExpandedWriter(ewc, s, configcontext.ConfigRenderContext{}, nil)
+		r.Error(err)
+	})
+}
+
+func TestWrite(t *testing.T) {
+	r := require.New(t)
+	t.Run("error reading config", func(t *testing.T) {
+		ewd := &ExpandedWriterData{
+			Nacos: &NacosData{},
+		}
+		errs := Write(context.Background(), ewd, func(ctx context.Context, namespace, name string) (map[string]interface{}, error) {
+			return nil, errors.New("read-config-error")
+		})
+		r.Len(errs, 1)
+		r.Equal("fail to read the config of the nacos server:read-config-error", errs[0].Error())
+	})
+}
diff --git a/pkg/config/writer/writter_test.go b/pkg/config/writer/writter_test.go
deleted file mode 100644
index 5b28728e..00000000
--- a/pkg/config/writer/writter_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
-Copyright 2022 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 writer
-
-import (
-	"testing"
-
-	"cuelang.org/go/cue/cuecontext"
-	"github.com/stretchr/testify/require"
-)
-
-func TestConvertMap2KV(t *testing.T) {
-	r := require.New(t)
-	re := map[string]string{}
-	err := convertMap2PropertiesKV("", map[string]interface{}{
-		"s":  "s",
-		"n":  1,
-		"nn": 1.5,
-		"b":  true,
-		"m": map[string]interface{}{
-			"s": "s",
-			"b": false,
-		},
-		"aa": []string{"a", "a"},
-		"ai": []int64{1, 2},
-		"ar": []map[string]interface{}{{
-			"s2": "s2",
-		}},
-	}, re)
-	r.Equal(err, nil)
-	r.Equal(re, map[string]string{
-		"s":       "s",
-		"n":       "1",
-		"nn":      "1.5",
-		"b":       "true",
-		"m.s":     "s",
-		"m.b":     "false",
-		"aa":      "a,a",
-		"ai":      "1,2",
-		"ar.0.s2": "s2",
-	})
-}
-
-func TestEncodingOutput(t *testing.T) {
-	r := require.New(t)
-	testValue := `
-		context: {
-			key1: "hello"
-			key2: 2
-			key3: true
-			key4: 4.4
-			key5: ["hello"]
-			key6: [{"hello": 1}]
-			key7: [1, 2]
-			key8: [1.2, 1]
-			key9: {key10: [{"wang": true}]}
-		}
-	`
-	v := cuecontext.New().CompileString(testValue)
-	r.Equal(v.Err(), nil)
-
-	_, err := encodingOutput(v, "yaml")
-	r.Equal(err, nil)
-
-	_, err = encodingOutput(v, "properties")
-	r.Equal(err, nil)
-
-	_, err = encodingOutput(v, "toml")
-	r.Equal(err, nil)
-
-	json, err := encodingOutput(v, "json")
-	r.Equal(err, nil)
-	r.Equal(string(json), `{"context":{"key1":"hello","key2":2,"key3":true,"key4":4.4,"key5":["hello"],"key6":[{"hello":1}],"key7":[1,2],"key8":[1.2,1],"key9":{"key10":[{"wang":true}]}}}`)
-}
diff --git a/pkg/cue/definition/template_test.go b/pkg/cue/definition/template_test.go
index 21b83493..15a9f5bf 100644
--- a/pkg/cue/definition/template_test.go
+++ b/pkg/cue/definition/template_test.go
@@ -21,13 +21,18 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	wfprocess "github.com/kubevela/workflow/pkg/cue/process"
 
 	"github.com/oam-dev/kubevela/apis/types"
 	"github.com/oam-dev/kubevela/pkg/cue/process"
+	"github.com/oam-dev/kubevela/pkg/oam/util"
 )
 
 func TestWorkloadTemplateComplete(t *testing.T) {
@@ -1366,3 +1371,324 @@ parameter: {
 		assert.Contains(t, err.Error(), v.err)
 	}
 }
+
+func TestWorkloadGetTemplateContext(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, appsv1.AddToScheme(scheme))
+	require.NoError(t, corev1.AddToScheme(scheme))
+
+	workload := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"apiVersion": "apps/v1",
+			"kind":       "Deployment",
+			"metadata": map[string]interface{}{
+				"name":      "test-workload",
+				"namespace": "default",
+			},
+		},
+	}
+	auxSvc := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"apiVersion": "v1",
+			"kind":       "Service",
+			"metadata": map[string]interface{}{
+				"name":      "test-aux-svc",
+				"namespace": "default",
+			},
+		},
+	}
+
+	baseCtx := process.NewContext(process.ContextData{
+		AppName:         "myapp",
+		CompName:        "test",
+		Namespace:       "default",
+		AppRevisionName: "myapp-v1",
+	})
+
+	workloadTemplate := `
+output: {
+	apiVersion: "apps/v1"
+    kind: "Deployment"
+	metadata: {
+		name: "test-workload"
+		namespace: "default"
+	}
+}
+outputs: service: {
+	apiVersion: "v1"
+    kind: "Service"
+	metadata: {
+		name: "test-aux-svc"
+		namespace: "default"
+	}
+}
+`
+	wt := NewWorkloadAbstractEngine("testWorkload")
+	err := wt.Complete(baseCtx, workloadTemplate, nil)
+	require.NoError(t, err)
+
+	testCases := map[string]struct {
+		reason    string
+		cli       client.Client
+		ctx       wfprocess.Context
+		wantErr   bool
+		checkFunc func(t *testing.T, templateContext map[string]interface{})
+	}{
+		"successfully get template context": {
+			reason: "Should successfully get the template context with both output and outputs.",
+			cli:    fake.NewClientBuilder().WithScheme(scheme).WithObjects(workload, auxSvc).Build(),
+			ctx:    baseCtx,
+			checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
+				require.NotNil(t, templateContext)
+				output, ok := templateContext[OutputFieldName]
+				require.True(t, ok)
+				outputMap, ok := output.(map[string]interface{})
+				require.True(t, ok)
+				require.Equal(t, "test-workload", outputMap["metadata"].(map[string]interface{})["name"])
+
+				outputs, ok := templateContext[OutputsFieldName]
+				require.True(t, ok)
+				outputsMap, ok := outputs.(map[string]interface{})
+				require.True(t, ok)
+				svc, ok := outputsMap["service"]
+				require.True(t, ok)
+				svcMap, ok := svc.(map[string]interface{})
+				require.True(t, ok)
+				require.Equal(t, "test-aux-svc", svcMap["metadata"].(map[string]interface{})["name"])
+			},
+		},
+		"resource not found": {
+			reason:  "Should return an error when a resource is not found in the cluster.",
+			cli:     fake.NewClientBuilder().WithScheme(scheme).Build(),
+			ctx:     baseCtx,
+			wantErr: true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			wd := &workloadDef{def: def{name: "test"}}
+			accessor := util.NewApplicationResourceNamespaceAccessor("default", "")
+			templateContext, err := wd.GetTemplateContext(tc.ctx, tc.cli, accessor)
+
+			if tc.wantErr {
+				require.Error(t, err, tc.reason)
+			} else {
+				require.NoError(t, err, tc.reason)
+				if tc.checkFunc != nil {
+					tc.checkFunc(t, templateContext)
+				}
+			}
+		})
+	}
+}
+
+func TestTraitGetTemplateContext(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, corev1.AddToScheme(scheme))
+
+	traitOutput := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"apiVersion": "v1",
+			"kind":       "ConfigMap",
+			"metadata": map[string]interface{}{
+				"name":      "test-trait-output",
+				"namespace": "default",
+			},
+		},
+	}
+
+	traitName := "my-trait"
+	baseCtx := process.NewContext(process.ContextData{
+		AppName:         "myapp",
+		CompName:        "test",
+		Namespace:       "default",
+		AppRevisionName: "myapp-v1",
+	})
+
+	traitTemplate := `
+outputs: myconfig: {
+	apiVersion: "v1"
+    kind: "ConfigMap"
+	metadata: {
+		name: "test-trait-output"
+		namespace: "default"
+	}
+}
+`
+	td := NewTraitAbstractEngine(traitName)
+	err := td.Complete(baseCtx, traitTemplate, nil)
+	require.NoError(t, err)
+
+	emptyCtx := process.NewContext(process.ContextData{
+		AppName:         "myapp",
+		CompName:        "test",
+		Namespace:       "default",
+		AppRevisionName: "myapp-v1",
+	})
+
+	testCases := map[string]struct {
+		reason    string
+		cli       client.Client
+		ctx       wfprocess.Context
+		traitName string
+		wantErr   bool
+		checkFunc func(t *testing.T, templateContext map[string]interface{})
+	}{
+		"successfully get template context for trait": {
+			reason:    "Should successfully get the template context for a trait with outputs.",
+			cli:       fake.NewClientBuilder().WithScheme(scheme).WithObjects(traitOutput).Build(),
+			ctx:       baseCtx,
+			traitName: traitName,
+			checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
+				require.NotNil(t, templateContext)
+				outputs, ok := templateContext[OutputsFieldName]
+				require.True(t, ok)
+				outputsMap, ok := outputs.(map[string]interface{})
+				require.True(t, ok)
+				cm, ok := outputsMap["myconfig"]
+				require.True(t, ok)
+				cmMap, ok := cm.(map[string]interface{})
+				require.True(t, ok)
+				require.Equal(t, "test-trait-output", cmMap["metadata"].(map[string]interface{})["name"])
+			},
+		},
+		"trait resource not found": {
+			reason:    "Should return an error when a trait's output resource is not found.",
+			cli:       fake.NewClientBuilder().WithScheme(scheme).Build(),
+			ctx:       baseCtx,
+			traitName: traitName,
+			wantErr:   true,
+		},
+		"trait with no outputs": {
+			reason:    "Should successfully get a context for a trait that produces no outputs.",
+			cli:       fake.NewClientBuilder().WithScheme(scheme).Build(),
+			ctx:       emptyCtx,
+			traitName: traitName,
+			checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
+				require.NotNil(t, templateContext)
+				_, ok := templateContext[OutputsFieldName]
+				require.False(t, ok)
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			traitDef := &traitDef{def: def{name: tc.traitName}}
+			accessor := util.NewApplicationResourceNamespaceAccessor("default", "")
+			templateContext, err := traitDef.GetTemplateContext(tc.ctx, tc.cli, accessor)
+
+			if tc.wantErr {
+				require.Error(t, err, tc.reason)
+			} else {
+				require.NoError(t, err, tc.reason)
+				if tc.checkFunc != nil {
+					tc.checkFunc(t, templateContext)
+				}
+			}
+		})
+	}
+}
+
+func TestGetCommonLabels(t *testing.T) {
+	type want struct {
+		labels map[string]string
+	}
+	cases := map[string]struct {
+		reason string
+		input  map[string]string
+		want   want
+	}{
+		"TestConvert": {
+			reason: "Test that context labels are correctly converted to OAM labels",
+			input: map[string]string{
+				process.ContextAppName:     "my-app",
+				process.ContextName:        "my-comp",
+				process.ContextAppRevision: "v1",
+				process.ContextReplicaKey:  "rep-key",
+				"other-label":              "other-value",
+			},
+			want: want{
+				labels: map[string]string{
+					"app.oam.dev/name":        "my-app",
+					"app.oam.dev/component":   "my-comp",
+					"app.oam.dev/appRevision": "v1",
+					"app.oam.dev/replicaKey":  "rep-key",
+				},
+			},
+		},
+		"TestEmpty": {
+			reason: "Test that an empty input map results in an empty output map",
+			input:  map[string]string{},
+			want: want{
+				labels: map[string]string{},
+			},
+		},
+		"TestNoConvert": {
+			reason: "Test that labels with no OAM equivalent are ignored",
+			input: map[string]string{
+				"other-label": "other-value",
+			},
+			want: want{
+				labels: map[string]string{},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			r := require.New(t)
+			got := GetCommonLabels(tc.input)
+			r.Equal(tc.want.labels, got, tc.reason)
+		})
+	}
+}
+
+func TestGetBaseContextLabels(t *testing.T) {
+	type want struct {
+		labels map[string]string
+	}
+	cases := map[string]struct {
+		reason string
+		ctx    wfprocess.Context
+		want   want
+	}{
+		"TestWithAppNameAndRevision": {
+			reason: "Test that app name and revision are added to the base context labels",
+			ctx: process.NewContext(process.ContextData{
+				AppName:         "my-app",
+				AppRevisionName: "v1",
+				CompName:        "my-comp",
+			}),
+			want: want{
+				labels: map[string]string{
+					process.ContextAppName:     "my-app",
+					process.ContextAppRevision: "v1",
+					process.ContextName:        "my-comp",
+				},
+			},
+		},
+		"TestWithoutAppNameAndRevision": {
+			reason: "Test that the base context labels are returned when app name and revision are missing",
+			ctx: process.NewContext(process.ContextData{
+				CompName: "my-comp",
+			}),
+			want: want{
+				labels: map[string]string{
+					process.ContextAppName:     "",
+					process.ContextAppRevision: "",
+					process.ContextName:        "my-comp",
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			r := require.New(t)
+			got := GetBaseContextLabels(tc.ctx)
+			r.Equal(tc.want.labels, got, tc.reason)
+		})
+	}
+}
diff --git a/pkg/cue/script/template_test.go b/pkg/cue/script/template_test.go
index 4cae1238..69c81a87 100644
--- a/pkg/cue/script/template_test.go
+++ b/pkg/cue/script/template_test.go
@@ -286,3 +286,155 @@ func TestParsePropertiesToSchemaWithCueX(t *testing.T) {
 	assert.Equal(t, err, nil)
 	assert.Equal(t, len(schema.Properties), 2)
 }
+
+func TestParseToValue(t *testing.T) {
+	cases := map[string]struct {
+		script    CUE
+		expectErr bool
+	}{
+		"valid cue script": {
+			script:    CUE(templateScript),
+			expectErr: false,
+		},
+		"invalid cue script": {
+			script: CUE(`
+metadata: {
+	name: "invalid"
+	alias: "Invalid"
+`),
+			expectErr: true,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			v, err := tc.script.ParseToValue()
+			if tc.expectErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.True(t, v.Exists())
+			}
+		})
+	}
+}
+
+func TestParseToValueWithCueX(t *testing.T) {
+	cases := map[string]struct {
+		script    CUE
+		expectErr bool
+	}{
+		"valid cue script": {
+			script:    CUE(templateScript),
+			expectErr: false,
+		},
+		"invalid cue script": {
+			script: CUE(`
+metadata: {
+	name: "invalid"
+	alias: "Invalid"
+`),
+			expectErr: true,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			v, err := tc.script.ParseToValueWithCueX(context.Background())
+			if tc.expectErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.True(t, v.Exists())
+			}
+		})
+	}
+}
+
+func TestParseToTemplateValue(t *testing.T) {
+	cases := map[string]struct {
+		script      CUE
+		expectErr   bool
+		errContains string
+	}{
+		"valid cue script": {
+			script:    CUE(templateScript),
+			expectErr: false,
+		},
+		"missing template field": {
+			script: CUE(`
+metadata: {
+	name: "missing-template"
+}
+`),
+			expectErr:   true,
+			errContains: "the template cue is invalid",
+		},
+		"missing parameter field": {
+			script: CUE(`
+template: {
+	output: {}
+}
+`),
+			expectErr:   true,
+			errContains: "the template cue must include the template.parameter field",
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			v, err := tc.script.ParseToTemplateValue()
+			if tc.expectErr {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tc.errContains)
+			} else {
+				assert.NoError(t, err)
+				assert.True(t, v.Exists())
+			}
+		})
+	}
+}
+
+func TestParseToTemplateValueWithCueX(t *testing.T) {
+	cases := map[string]struct {
+		script      CUE
+		expectErr   bool
+		errContains string
+	}{
+		"valid cue script": {
+			script:    CUE(templateScript),
+			expectErr: false,
+		},
+		"missing template field": {
+			script: CUE(`
+metadata: {
+	name: "missing-template"
+}
+`),
+			expectErr:   true,
+			errContains: "the template cue must include the template field",
+		},
+		"missing parameter field": {
+			script: CUE(`
+template: {
+	output: {}
+}
+`),
+			expectErr:   true,
+			errContains: "the template cue must include the template.parameter field",
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			v, err := tc.script.ParseToTemplateValueWithCueX(context.Background())
+			if tc.expectErr {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tc.errContains)
+			} else {
+				assert.NoError(t, err)
+				assert.True(t, v.Exists())
+			}
+		})
+	}
+}
diff --git a/pkg/cue/task/process_test.go b/pkg/cue/task/process_test.go
index 42660a91..3171f4b9 100644
--- a/pkg/cue/task/process_test.go
+++ b/pkg/cue/task/process_test.go
@@ -66,18 +66,56 @@ func TestProcess(t *testing.T) {
 	s := NewMock()
 	defer s.Close()
 
-	taskTemplate := cuecontext.New().CompileString(TaskTemplate)
-	taskTemplate = taskTemplate.FillPath(value.FieldPath(process.ParameterFieldName), map[string]interface{}{
-		"serviceURL": "http://127.0.0.1:8090/api/v1/token?val=test-token",
-	})
-
-	inst, err := Process(taskTemplate)
-	if err != nil {
-		t.Fatal(err)
+	testCases := map[string]struct {
+		template   string
+		parameter  map[string]interface{}
+		wantOutput string
+		wantErr    string
+	}{
+		"success": {
+			template: TaskTemplate,
+			parameter: map[string]interface{}{
+				"serviceURL": "http://127.0.0.1:8090/api/v1/token?val=test-token",
+			},
+			wantOutput: "{\"data\":\"test-token\"}",
+		},
+		"no http in processing": {
+			template: `
+parameter: {
+  serviceURL: string
+}
+processing: {}
+`,
+			parameter: map[string]interface{}{
+				"serviceURL": "http://127.0.0.1:8090/api/v1/token?val=test-token",
+			},
+			wantErr: "there is no http in processing",
+		},
+		"http task fails": {
+			template:  TaskTemplate,
+			parameter: map[string]interface{}{"serviceURL": "http://127.0.0.1:3000"},
+			wantErr:   "fail to exec http task",
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			taskTemplate := cuecontext.New().CompileString(tc.template)
+			taskTemplate = taskTemplate.FillPath(value.FieldPath(process.ParameterFieldName), tc.parameter)
+
+			inst, err := Process(taskTemplate)
+
+			if tc.wantErr != "" {
+				assert.ErrorContains(t, err, tc.wantErr)
+			} else {
+				assert.NoError(t, err)
+				output := inst.LookupPath(value.FieldPath("output"))
+				data, err := cueJson.Marshal(output)
+				assert.NoError(t, err)
+				assert.Equal(t, tc.wantOutput, string(data))
+			}
+		})
 	}
-	output := inst.LookupPath(value.FieldPath("output"))
-	data, _ := cueJson.Marshal(output)
-	assert.Equal(t, "{\"data\":\"test-token\"}", data)
 }
 
 func NewMock() *httptest.Server {
diff --git a/pkg/oam/auxliary_test.go b/pkg/oam/auxliary_test.go
index 3f7cf879..7188013f 100644
--- a/pkg/oam/auxliary_test.go
+++ b/pkg/oam/auxliary_test.go
@@ -18,16 +18,102 @@ package oam
 
 import (
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
-	v1 "k8s.io/api/apps/v1"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-func TestGetSetCluster(t *testing.T) {
-	r := require.New(t)
-	deploy := &v1.Deployment{}
-	r.Equal("", GetCluster(deploy))
-	clusterName := "cluster"
-	SetClusterIfEmpty(deploy, clusterName)
-	r.Equal(clusterName, GetCluster(deploy))
+func TestOAMAuxiliary(t *testing.T) {
+	t.Run("ClusterLabel", func(t *testing.T) {
+		r := require.New(t)
+		deploy := &appsv1.Deployment{}
+		clusterName1 := "cluster-1"
+		clusterName2 := "cluster-2"
+
+		r.Equal("", GetCluster(deploy), "GetCluster should return empty string for new object")
+
+		SetClusterIfEmpty(deploy, clusterName1)
+		r.Equal(clusterName1, GetCluster(deploy), "SetClusterIfEmpty should set label on empty object")
+
+		SetClusterIfEmpty(deploy, clusterName2)
+		r.Equal(clusterName1, GetCluster(deploy), "SetClusterIfEmpty should not overwrite existing label")
+
+		SetCluster(deploy, clusterName2)
+		r.Equal(clusterName2, GetCluster(deploy), "SetCluster should overwrite existing label")
+	})
+
+	t.Run("VersionAnnotations", func(t *testing.T) {
+		r := require.New(t)
+		deploy := &appsv1.Deployment{}
+		publishVersion := "v1.0.0"
+		deployVersion := "app-v1"
+
+		r.Equal("", GetPublishVersion(deploy), "GetPublishVersion should return empty string for new object")
+		SetPublishVersion(deploy, publishVersion)
+		r.Equal(publishVersion, GetPublishVersion(deploy))
+
+		r.Equal("", GetDeployVersion(deploy), "GetDeployVersion should return empty string for new object")
+		annotations := deploy.GetAnnotations()
+		if annotations == nil {
+			annotations = make(map[string]string)
+		}
+		annotations[AnnotationDeployVersion] = deployVersion
+		deploy.SetAnnotations(annotations)
+		r.Equal(deployVersion, GetDeployVersion(deploy))
+	})
+
+	t.Run("LastAppliedTimeAnnotation", func(t *testing.T) {
+		r := require.New(t)
+		fixedTime := time.Now().Truncate(time.Second)
+		creationTime := fixedTime.Add(-time.Hour)
+
+		testCases := []struct {
+			name         string
+			annotations  map[string]string
+			expectedTime time.Time
+		}{
+			{
+				name:         "no annotation",
+				annotations:  nil,
+				expectedTime: creationTime,
+			},
+			{
+				name:         "valid annotation",
+				annotations:  map[string]string{AnnotationLastAppliedTime: fixedTime.Format(time.RFC3339)},
+				expectedTime: fixedTime,
+			},
+			{
+				name:         "invalid annotation",
+				annotations:  map[string]string{AnnotationLastAppliedTime: "invalid-time"},
+				expectedTime: creationTime,
+			},
+		}
+
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				deploy := &appsv1.Deployment{}
+				deploy.SetCreationTimestamp(metav1.NewTime(creationTime))
+				deploy.SetAnnotations(tc.annotations)
+				r.True(tc.expectedTime.Equal(GetLastAppliedTime(deploy)))
+			})
+		}
+	})
+
+	t.Run("ControllerRequirementAnnotation", func(t *testing.T) {
+		r := require.New(t)
+		deploy := &appsv1.Deployment{}
+		requirement := "controller-x"
+
+		r.Equal("", GetControllerRequirement(deploy), "GetControllerRequirement should be empty for new object")
+
+		SetControllerRequirement(deploy, requirement)
+		r.Equal(requirement, GetControllerRequirement(deploy))
+		r.Contains(deploy.GetAnnotations(), AnnotationControllerRequirement)
+
+		SetControllerRequirement(deploy, "")
+		r.Equal("", GetControllerRequirement(deploy), "GetControllerRequirement should be empty after setting to empty string")
+		r.NotContains(deploy.GetAnnotations(), AnnotationControllerRequirement, "Annotation should be removed when set to empty")
+	})
 }
diff --git a/pkg/webhook/core.oam.dev/v1beta1/application/mutating_handler_test.go b/pkg/webhook/core.oam.dev/v1beta1/application/mutating_handler_test.go
index af8c7dc0..b724d610 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/application/mutating_handler_test.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/application/mutating_handler_test.go
@@ -1,4 +1,5 @@
 /*
+ /*
 Copyright 2022 The KubeVela Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler_test.go b/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler_test.go
index d859561c..4ee0a86f 100644
--- a/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler_test.go
+++ b/pkg/webhook/core.oam.dev/v1beta1/application/validating_handler_test.go
@@ -248,6 +248,109 @@ var _ = Describe("Test Application Validator", func() {
 				Object: runtime.RawExtension{
 					Raw: []byte(`
 {"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"workflow-timeout","namespace":"default","annotations":{"app.oam.dev/autoUpdate":"true"}},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"crccheck/hello-world"}}],"workflow":{"steps":[{"name":"group","type":"suspend","timeout":"1s"}]}}}
+`),
+				},
+			},
+		}
+		resp := handler.Handle(ctx, req)
+		Expect(resp.Allowed).Should(BeTrue())
+	})
+
+	It("Test Application Update with validation errors - duplicate workflow steps", func() {
+		req := admission.Request{
+			AdmissionRequest: admissionv1.AdmissionRequest{
+				Operation: admissionv1.Update,
+				Resource:  metav1.GroupVersionResource{Group: "core.oam.dev", Version: "v1alpha2", Resource: "applications"},
+				Object: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}],"workflow":{"steps":[{"name":"suspend","type":"suspend"},{"name":"suspend","type":"suspend"}]}}}
+`),
+				},
+				OldObject: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}]}}
+`),
+				},
+			},
+		}
+		resp := handler.Handle(ctx, req)
+		Expect(resp.Allowed).Should(BeFalse())
+	})
+
+	It("Test Application Update with invalid workflow timeout", func() {
+		req := admission.Request{
+			AdmissionRequest: admissionv1.AdmissionRequest{
+				Operation: admissionv1.Update,
+				Resource:  metav1.GroupVersionResource{Group: "core.oam.dev", Version: "v1alpha2", Resource: "applications"},
+				Object: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"workflow-update-test","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}],"workflow":{"steps":[{"name":"group","type":"suspend","timeout":"invalid-timeout"}]}}}
+`),
+				},
+				OldObject: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"workflow-update-test","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}]}}
+`),
+				},
+			},
+		}
+		resp := handler.Handle(ctx, req)
+		Expect(resp.Allowed).Should(BeFalse())
+	})
+
+	It("Test Application Update with deletion timestamp should skip validation", func() {
+		now := metav1.Now()
+		req := admission.Request{
+			AdmissionRequest: admissionv1.AdmissionRequest{
+				Operation: admissionv1.Update,
+				Resource:  metav1.GroupVersionResource{Group: "core.oam.dev", Version: "v1alpha2", Resource: "applications"},
+				Object: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default","deletionTimestamp":"` + now.Format("2006-01-02T15:04:05Z") + `"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}],"workflow":{"steps":[{"name":"suspend","type":"suspend"},{"name":"suspend","type":"suspend"}]}}}
+`),
+				},
+				OldObject: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}]}}
+`),
+				},
+			},
+		}
+		resp := handler.Handle(ctx, req)
+		// Even with duplicate workflow steps (which would normally fail), it should pass because of deletion timestamp
+		Expect(resp.Allowed).Should(BeTrue())
+	})
+
+	It("Test Application Update decode old object error", func() {
+		req := admission.Request{
+			AdmissionRequest: admissionv1.AdmissionRequest{
+				Operation: admissionv1.Update,
+				Resource:  metav1.GroupVersionResource{Group: "core.oam.dev", Version: "v1alpha2", Resource: "applications"},
+				Object: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}]}}
+`),
+				},
+				OldObject: runtime.RawExtension{Raw: []byte("invalid json")},
+			},
+		}
+		resp := handler.Handle(ctx, req)
+		Expect(resp.Allowed).Should(BeFalse())
+	})
+
+	It("Test Application Update successful case", func() {
+		req := admission.Request{
+			AdmissionRequest: admissionv1.AdmissionRequest{
+				Operation: admissionv1.Update,
+				Resource:  metav1.GroupVersionResource{Group: "core.oam.dev", Version: "v1alpha2", Resource: "applications"},
+				Object: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"nginx"}}]}}
+`),
+				},
+				OldObject: runtime.RawExtension{
+					Raw: []byte(`
+{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"application-sample","namespace":"default"},"spec":{"components":[{"name":"comp","type":"worker","properties":{"image":"busybox"}}]}}
 `),
 				},
 			},
diff --git a/pkg/webhook/core.oam.dev/v1beta1/application/validation_permissions_test.go b/pkg/webhook/core.oam.dev/v1beta1/application/validation_permissions_test.go
new file mode 100644
index 00000000..88aa8ade
--- /dev/null
+++ b/pkg/webhook/core.oam.dev/v1beta1/application/validation_permissions_test.go
@@ -0,0 +1,881 @@
+/*
+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"
+	"fmt"
+	"strings"
+	"testing"
+
+	"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"
+	"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/auth"
+	"github.com/oam-dev/kubevela/pkg/features"
+	"github.com/oam-dev/kubevela/pkg/oam"
+)
+
+func TestValidateDefinitionPermissions(t *testing.T) {
+	// Enable the authentication features for testing
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, features.ValidateDefinitionPermissions, true)
+	oldAuthWithUser := auth.AuthenticationWithUser
+	auth.AuthenticationWithUser = true
+	defer func() { auth.AuthenticationWithUser = oldAuthWithUser }()
+
+	testCases := []struct {
+		name                string
+		app                 *v1beta1.Application
+		userInfo            authenticationv1.UserInfo
+		allowedDefinitions  map[string]bool // resource/namespace/name -> allowed
+		expectedErrorCount  int
+		expectedErrorFields []string
+		expectedErrorMsgs   []string
+	}{
+		{
+			name: "user has all permissions",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+							Traits: []common.ApplicationTrait{
+								{Type: "scaler"},
+							},
+						},
+					},
+					Policies: []v1beta1.AppPolicy{
+						{Name: "policy1", Type: "topology"},
+					},
+					Workflow: &v1beta1.Workflow{
+						Steps: []workflowv1alpha1.WorkflowStep{
+							{
+								WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
+									Name: "step1",
+									Type: "deploy",
+								},
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+				Groups:   []string{"test-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": true,
+				"traitdefinitions/vela-system/scaler":         true,
+				"policydefinitions/vela-system/topology":      true,
+				"workflowstepdefinitions/vela-system/deploy":  true,
+			},
+			expectedErrorCount: 0,
+		},
+		{
+			name: "user lacks ComponentDefinition permission",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+						},
+						{
+							Name: "comp2",
+							Type: "webservice", // Same type, should get two errors
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "restricted-user",
+				Groups:   []string{"restricted-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": false,
+				"componentdefinitions/test-ns/webservice":     false,
+			},
+			expectedErrorCount:  2, // One for each component
+			expectedErrorFields: []string{"spec.components[0].type", "spec.components[1].type"},
+			expectedErrorMsgs:   []string{"cannot get ComponentDefinition \"webservice\""},
+		},
+		{
+			name: "user lacks TraitDefinition permission",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+							Traits: []common.ApplicationTrait{
+								{Type: "scaler"},
+								{Type: "gateway"},
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "restricted-user",
+				Groups:   []string{"restricted-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": true,
+				"traitdefinitions/vela-system/scaler":         false,
+				"traitdefinitions/test-ns/scaler":             false,
+				"traitdefinitions/vela-system/gateway":        false,
+				"traitdefinitions/test-ns/gateway":            false,
+			},
+			expectedErrorCount:  2,
+			expectedErrorFields: []string{"spec.components[0].traits[1].type", "spec.components[0].traits[0].type"},
+			expectedErrorMsgs:   []string{"cannot get TraitDefinition \"gateway\"", "cannot get TraitDefinition \"scaler\""},
+		},
+		{
+			name: "user lacks PolicyDefinition permission",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Policies: []v1beta1.AppPolicy{
+						{
+							Name: "topology",
+							Type: "topology",
+							Properties: &runtime.RawExtension{
+								Raw: []byte(`{"clusters":["local","remote"]}`),
+							},
+						},
+						{
+							Name: "override",
+							Type: "override",
+							Properties: &runtime.RawExtension{
+								Raw: []byte(`{"components":[{"name":"comp1"}]}`),
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "policy-user",
+				Groups:   []string{"policy-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"policydefinitions/vela-system/topology": true,
+				"policydefinitions/test-ns/topology":     true,
+				"policydefinitions/vela-system/override": false,
+				"policydefinitions/test-ns/override":     false,
+			},
+			expectedErrorCount:  1,
+			expectedErrorFields: []string{"spec.policies[1].type"},
+			expectedErrorMsgs:   []string{"cannot get PolicyDefinition \"override\""},
+		},
+		{
+			name: "empty application",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "empty-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "empty-user",
+				Groups:   []string{"empty-group"},
+			},
+			allowedDefinitions: map[string]bool{},
+			expectedErrorCount: 0,
+		},
+		{
+			name: "nil workflow",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "nil-workflow-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+						},
+					},
+					Workflow: nil,
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+				Groups:   []string{"test-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": true,
+				"componentdefinitions/test-ns/webservice":     true,
+			},
+			expectedErrorCount: 0,
+		},
+		{
+			name: "mixed permissions",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "mixed-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "allowed-comp",
+							Type: "webservice",
+							Traits: []common.ApplicationTrait{
+								{Type: "scaler"},
+								{Type: "ingress"},
+							},
+						},
+						{
+							Name: "denied-comp",
+							Type: "worker",
+						},
+					},
+					Policies: []v1beta1.AppPolicy{
+						{
+							Name: "topology",
+							Type: "topology",
+						},
+						{
+							Name: "garbage-collect",
+							Type: "garbage-collect",
+						},
+					},
+					Workflow: &v1beta1.Workflow{
+						Steps: []workflowv1alpha1.WorkflowStep{
+							{
+								WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
+									Name: "deploy",
+									Type: "deploy",
+								},
+							},
+							{
+								WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
+									Name: "notify",
+									Type: "notification",
+								},
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "mixed-user",
+				Groups:   []string{"mixed-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice":      true,
+				"componentdefinitions/test-ns/webservice":          true,
+				"componentdefinitions/vela-system/worker":          false,
+				"componentdefinitions/test-ns/worker":              false,
+				"traitdefinitions/vela-system/scaler":              false,
+				"traitdefinitions/test-ns/scaler":                  false,
+				"traitdefinitions/vela-system/ingress":             true,
+				"traitdefinitions/test-ns/ingress":                 true,
+				"policydefinitions/vela-system/topology":           true,
+				"policydefinitions/test-ns/topology":               true,
+				"policydefinitions/vela-system/garbage-collect":    false,
+				"policydefinitions/test-ns/garbage-collect":        false,
+				"workflowstepdefinitions/vela-system/deploy":       true,
+				"workflowstepdefinitions/test-ns/deploy":           true,
+				"workflowstepdefinitions/vela-system/notification": false,
+				"workflowstepdefinitions/test-ns/notification":     false,
+			},
+			expectedErrorCount: 4,
+			expectedErrorFields: []string{
+				"spec.components[0].traits[0].type",
+				"spec.components[1].type",
+				"spec.policies[1].type",
+				"spec.workflow.steps[1].type",
+			},
+			expectedErrorMsgs: []string{
+				"cannot get TraitDefinition \"scaler\"",
+				"cannot get ComponentDefinition \"worker\"",
+				"cannot get PolicyDefinition \"garbage-collect\"",
+				"cannot get WorkflowStepDefinition \"notification\"",
+			},
+		},
+		{
+			name: "SAR API failure returns error",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "error-trigger",
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+				Groups:   []string{"test-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/error-trigger": false,
+				"componentdefinitions/test-ns/error-trigger":     false,
+			},
+			expectedErrorCount:  1,
+			expectedErrorFields: []string{"spec.components[0].type"},
+			expectedErrorMsgs:   []string{"unable to verify permissions"},
+		},
+		{
+			name: "SAR API intermittent failure - fails closed for safety",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "system-error-only",
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+				Groups:   []string{"test-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/test-ns/system-error-only": true,
+			},
+			expectedErrorCount:  1,
+			expectedErrorFields: []string{"spec.components[0].type"},
+			expectedErrorMsgs:   []string{"unable to verify permissions"},
+		},
+		{
+			name: "user has permission in app namespace but not system namespace",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "custom-comp",
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "namespace-user",
+				Groups:   []string{"namespace-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/custom-comp": false,
+				"componentdefinitions/test-ns/custom-comp":     true, // Allowed in app namespace
+			},
+			expectedErrorCount: 0, // Should pass as user has permission in app namespace
+		},
+		{
+			name: "user has permission in system namespace but not app namespace",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "system-user",
+				Groups:   []string{"system-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": true, // Allowed in system namespace
+				"componentdefinitions/test-ns/webservice":     false,
+			},
+			expectedErrorCount: 0, // Should pass as user has permission in system namespace
+		},
+		{
+			name: "workflow with substeps",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Workflow: &v1beta1.Workflow{
+						Steps: []workflowv1alpha1.WorkflowStep{
+							{
+								WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
+									Name: "step1",
+									Type: "deploy",
+								},
+								SubSteps: []workflowv1alpha1.WorkflowStepBase{
+									{
+										Name: "substep1",
+										Type: "suspend",
+									},
+									{
+										Name: "substep2",
+										Type: "notification",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "workflow-user",
+				Groups:   []string{"workflow-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"workflowstepdefinitions/vela-system/deploy":       true,
+				"workflowstepdefinitions/vela-system/suspend":      false,
+				"workflowstepdefinitions/test-ns/suspend":          false,
+				"workflowstepdefinitions/vela-system/notification": false,
+				"workflowstepdefinitions/test-ns/notification":     false,
+			},
+			expectedErrorCount:  2,
+			expectedErrorFields: []string{"spec.workflow.steps[0].subSteps[0].type", "spec.workflow.steps[0].subSteps[1].type"},
+			expectedErrorMsgs:   []string{"cannot get WorkflowStepDefinition \"suspend\"", "cannot get WorkflowStepDefinition \"notification\""},
+		},
+		{
+			name: "duplicate definitions only checked once",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: "test-ns",
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+							Traits: []common.ApplicationTrait{
+								{Type: "scaler"},
+							},
+						},
+						{
+							Name: "comp2",
+							Type: "webservice", // Duplicate
+							Traits: []common.ApplicationTrait{
+								{Type: "scaler"}, // Duplicate
+							},
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+				Groups:   []string{"test-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": false,
+				"componentdefinitions/test-ns/webservice":     false,
+				"traitdefinitions/vela-system/scaler":         false,
+				"traitdefinitions/test-ns/scaler":             false,
+			},
+			expectedErrorCount: 4, // 2 components + 2 traits
+			expectedErrorFields: []string{
+				"spec.components[0].type",
+				"spec.components[1].type",
+				"spec.components[0].traits[0].type",
+				"spec.components[1].traits[0].type",
+			},
+		},
+		{
+			name: "application in vela-system namespace",
+			app: &v1beta1.Application{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-app",
+					Namespace: oam.SystemDefinitionNamespace, // vela-system
+				},
+				Spec: v1beta1.ApplicationSpec{
+					Components: []common.ApplicationComponent{
+						{
+							Name: "comp1",
+							Type: "webservice",
+						},
+					},
+				},
+			},
+			userInfo: authenticationv1.UserInfo{
+				Username: "system-user",
+				Groups:   []string{"system-group"},
+			},
+			allowedDefinitions: map[string]bool{
+				"componentdefinitions/vela-system/webservice": false,
+			},
+			expectedErrorCount:  1,
+			expectedErrorFields: []string{"spec.components[0].type"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create a fake client with mock SubjectAccessReview behavior
+			scheme := runtime.NewScheme()
+			_ = v1beta1.AddToScheme(scheme)
+			_ = authv1.AddToScheme(scheme)
+
+			fakeClient := &mockSARClient{
+				Client:             fake.NewClientBuilder().WithScheme(scheme).Build(),
+				allowedDefinitions: tc.allowedDefinitions,
+			}
+
+			handler := &ValidatingHandler{
+				Client: fakeClient,
+			}
+
+			req := admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					UserInfo: tc.userInfo,
+				},
+			}
+
+			// Run the validation
+			errs := handler.ValidateDefinitionPermissions(context.Background(), tc.app, req)
+
+			// Check error count
+			assert.Equal(t, tc.expectedErrorCount, len(errs),
+				"Expected %d errors, got %d: %v", tc.expectedErrorCount, len(errs), errs)
+
+			// Check error fields and messages (order independent)
+			if len(tc.expectedErrorFields) > 0 {
+				actualFields := make([]string, len(errs))
+				for i, err := range errs {
+					actualFields[i] = err.Field
+				}
+				for _, expectedField := range tc.expectedErrorFields {
+					assert.Contains(t, actualFields, expectedField,
+						"Expected field %s not found in errors: %v", expectedField, actualFields)
+				}
+			}
+			if len(tc.expectedErrorMsgs) > 0 {
+				actualMessages := make([]string, len(errs))
+				for i, err := range errs {
+					actualMessages[i] = err.Detail
+				}
+				for _, expectedMsg := range tc.expectedErrorMsgs {
+					found := false
+					for _, actualMsg := range actualMessages {
+						if strings.Contains(actualMsg, expectedMsg) {
+							found = true
+							break
+						}
+					}
+					assert.True(t, found,
+						"Expected message containing %s not found in errors: %v", expectedMsg, actualMessages)
+				}
+			}
+		})
+	}
+}
+
+func TestValidateDefinitionPermissions_FeatureDisabled(t *testing.T) {
+	// Disable the definition validation feature
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, features.ValidateDefinitionPermissions, false)
+	oldAuthWithUser := auth.AuthenticationWithUser
+	auth.AuthenticationWithUser = true
+	defer func() { auth.AuthenticationWithUser = oldAuthWithUser }()
+
+	app := &v1beta1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-app",
+			Namespace: "test-ns",
+		},
+		Spec: v1beta1.ApplicationSpec{
+			Components: []common.ApplicationComponent{
+				{
+					Name: "comp1",
+					Type: "webservice",
+				},
+			},
+		},
+	}
+
+	handler := &ValidatingHandler{
+		Client: fake.NewClientBuilder().Build(),
+	}
+
+	req := admission.Request{
+		AdmissionRequest: admissionv1.AdmissionRequest{
+			UserInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+			},
+		},
+	}
+
+	// Should return no errors when feature is disabled
+	errs := handler.ValidateDefinitionPermissions(context.Background(), app, req)
+	assert.Empty(t, errs, "Expected no errors when feature is disabled")
+}
+
+func TestValidateDefinitionPermissions_AuthenticationDisabled(t *testing.T) {
+	// Enable the definition validation feature but disable authentication
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultMutableFeatureGate, features.ValidateDefinitionPermissions, true)
+	oldAuthWithUser := auth.AuthenticationWithUser
+	auth.AuthenticationWithUser = false
+	defer func() { auth.AuthenticationWithUser = oldAuthWithUser }()
+
+	app := &v1beta1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-app",
+			Namespace: "test-ns",
+		},
+		Spec: v1beta1.ApplicationSpec{
+			Components: []common.ApplicationComponent{
+				{
+					Name: "comp1",
+					Type: "webservice",
+				},
+			},
+		},
+	}
+
+	handler := &ValidatingHandler{
+		Client: fake.NewClientBuilder().Build(),
+	}
+
+	req := admission.Request{
+		AdmissionRequest: admissionv1.AdmissionRequest{
+			UserInfo: authenticationv1.UserInfo{
+				Username: "test-user",
+			},
+		},
+	}
+
+	// Should return no errors when AuthenticationWithUser is disabled
+	errs := handler.ValidateDefinitionPermissions(context.Background(), app, req)
+	assert.Empty(t, errs, "Expected no errors when AuthenticationWithUser is disabled")
+}
+
+func TestCollectDefinitionUsage(t *testing.T) {
+	app := &v1beta1.Application{
+		Spec: v1beta1.ApplicationSpec{
+			Components: []common.ApplicationComponent{
+				{
+					Name: "comp1",
+					Type: "webservice",
+					Traits: []common.ApplicationTrait{
+						{Type: "scaler"},
+						{Type: "gateway"},
+					},
+				},
+				{
+					Name: "comp2",
+					Type: "webservice", // Duplicate
+					Traits: []common.ApplicationTrait{
+						{Type: "scaler"}, // Duplicate
+					},
+				},
+			},
+			Policies: []v1beta1.AppPolicy{
+				{Name: "policy1", Type: "topology"},
+				{Name: "policy2", Type: "override"},
+				{Name: "policy3", Type: "topology"}, // Duplicate
+			},
+			Workflow: &v1beta1.Workflow{
+				Steps: []workflowv1alpha1.WorkflowStep{
+					{
+						WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
+							Name: "step1",
+							Type: "deploy",
+						},
+						SubSteps: []workflowv1alpha1.WorkflowStepBase{
+							{
+								Name: "substep1",
+								Type: "suspend",
+							},
+							{
+								Name: "substep2",
+								Type: "deploy", // Duplicate
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	usage := collectDefinitionUsage(app)
+
+	// Check component types
+	assert.Equal(t, 2, len(usage.componentTypes["webservice"]))
+	assert.Contains(t, usage.componentTypes["webservice"], 0)
+	assert.Contains(t, usage.componentTypes["webservice"], 1)
+
+	// Check trait types
+	assert.Equal(t, 2, len(usage.traitTypes["scaler"]))
+	assert.Equal(t, 1, len(usage.traitTypes["gateway"]))
+
+	// Check policy types
+	assert.Equal(t, 2, len(usage.policyTypes["topology"]))
+	assert.Equal(t, 1, len(usage.policyTypes["override"]))
+
+	// Check workflow step types
+	assert.Equal(t, 2, len(usage.workflowStepTypes["deploy"]))
+	assert.Equal(t, 1, len(usage.workflowStepTypes["suspend"]))
+
+	// Check that substeps are properly marked
+	for _, loc := range usage.workflowStepTypes["suspend"] {
+		assert.True(t, loc.IsSubStep, "Suspend should be marked as a substep")
+		assert.Equal(t, 0, loc.StepIndex, "Suspend should be in step 0")
+		assert.Equal(t, 0, loc.SubStepIndex, "Suspend should be substep 0")
+	}
+
+	// Check regular steps and substeps for "deploy"
+	deployLocations := usage.workflowStepTypes["deploy"]
+	assert.Equal(t, 2, len(deployLocations))
+	regularStepFound := false
+	subStepFound := false
+	for _, loc := range deployLocations {
+		if !loc.IsSubStep {
+			regularStepFound = true
+			assert.Equal(t, 0, loc.StepIndex)
+			assert.Equal(t, -1, loc.SubStepIndex)
+		} else {
+			subStepFound = true
+			assert.Equal(t, 0, loc.StepIndex)
+			assert.Equal(t, 1, loc.SubStepIndex)
+		}
+	}
+	assert.True(t, regularStepFound, "Should have regular deploy step")
+	assert.True(t, subStepFound, "Should have deploy substep")
+}
+
+func TestGetWorkflowStepFieldPath(t *testing.T) {
+	testCases := []struct {
+		name         string
+		location     workflowStepLocation
+		expectedPath string
+	}{
+		{
+			name: "regular step",
+			location: workflowStepLocation{
+				StepIndex:    0,
+				SubStepIndex: -1,
+				IsSubStep:    false,
+			},
+			expectedPath: "spec.workflow.steps[0].type",
+		},
+		{
+			name: "regular step index 5",
+			location: workflowStepLocation{
+				StepIndex:    5,
+				SubStepIndex: -1,
+				IsSubStep:    false,
+			},
+			expectedPath: "spec.workflow.steps[5].type",
+		},
+		{
+			name: "substep 0 of step 0",
+			location: workflowStepLocation{
+				StepIndex:    0,
+				SubStepIndex: 0,
+				IsSubStep:    true,
+			},
+			expectedPath: "spec.workflow.steps[0].subSteps[0].type",
+		},
+		{
+			name: "substep 2 of step 1",
+			location: workflowStepLocation{
+				StepIndex:    1,
+				SubStepIndex: 2,
+				IsSubStep:    true,
+			},
+			expectedPath: "spec.workflow.steps[1].subSteps[2].type",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			path := getWorkflowStepFieldPath(tc.location)
+			assert.Equal(t, tc.expectedPath, path.String())
+		})
+	}
+}
+
+// mockSARClient is a mock client that simulates SubjectAccessReview responses
+type mockSARClient struct {
+	client.Client
+	allowedDefinitions map[string]bool
+}
+
+func (m *mockSARClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
+	if sar, ok := obj.(*authv1.SubjectAccessReview); ok {
+		// Mock failures
+		if sar.Spec.ResourceAttributes.Name == "error-trigger" {
+			return fmt.Errorf("simulated SAR API failure: connection timeout")
+		}
+		if sar.Spec.ResourceAttributes.Name == "system-error-only" &&
+			sar.Spec.ResourceAttributes.Namespace == "vela-system" {
+			return fmt.Errorf("simulated SAR API failure: system namespace unreachable")
+		}
+
+		key := fmt.Sprintf("%s/%s/%s",
+			sar.Spec.ResourceAttributes.Resource,
+			sar.Spec.ResourceAttributes.Namespace,
+			sar.Spec.ResourceAttributes.Name)
+
+		if allowed, exists := m.allowedDefinitions[key]; exists {
+			sar.Status.Allowed = allowed
+		} else {
+			sar.Status.Allowed = false
+		}
+		return nil
+	}
+	return m.Client.Create(ctx, obj, opts...)
+}