Files
open-cluster-management/pkg/work/webhook/v1/manifestwork_validating_test.go
Jian Zhu 4f173e7ba7 🐛 fix: Propagate UserInfo.Extra field in ManifestWork webhook SAR (#1427)
This commit fixes a security vulnerability where the ManifestWork
validating webhook was not passing the UserInfo.Extra field when
constructing SubjectAccessReview (SAR) requests. This omission could
lead to authorization bypass when external authorization policies
rely on Extra fields (e.g., OIDC claims, department attributes).

The fix adds Extra field conversion logic consistent with the
ManagedCluster webhook implementation and includes comprehensive
test coverage to verify the Extra field is properly propagated.

Fixes #1425

🤖 Assisted by Claude Code

Signed-off-by: zhujian <jiazhu@redhat.com>
2026-03-12 07:26:16 +00:00

388 lines
11 KiB
Go

package v1
import (
"context"
"fmt"
"reflect"
"testing"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/authorization/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
fakekube "k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
ocmfeature "open-cluster-management.io/api/feature"
workv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/ocm/pkg/features"
"open-cluster-management.io/ocm/pkg/work/spoke/spoketesting"
)
var manifestWorkSchema = metav1.GroupVersionResource{
Group: "work.open-cluster-management.io",
Version: "v1",
Resource: "manifestworks",
}
func TestValidateCreateUpdate(t *testing.T) {
w := ManifestWorkWebhook{}
_, err := w.ValidateUpdate(context.Background(), nil, &workv1.ManifestWork{})
if err == nil {
t.Errorf("Non work obj, Expect Error but got nil")
}
_, err = w.ValidateUpdate(context.Background(), &workv1.ManifestWork{}, nil)
if err == nil {
t.Errorf("Non work obj, Expect Error but got nil")
}
}
func TestManifestWorkExecutorValidate(t *testing.T) {
cases := []struct {
name string
request admission.Request
manifests []*unstructured.Unstructured
oldExecutor *workv1.ManifestWorkExecutor
executor *workv1.ManifestWorkExecutor
expectErr error
}{
{
name: "validate executor nil success",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Create,
UserInfo: authenticationv1.UserInfo{Username: "test1"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
expectErr: nil,
},
{
name: "validate executor nil fail",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Create,
UserInfo: authenticationv1.UserInfo{Username: "test2"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
expectErr: apierrors.NewBadRequest(
"user test2 cannot manipulate the Manifestwork with executor /klusterlet-work-sa in namespace cluster1"),
},
{
name: "validate executor not nil success",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Create,
UserInfo: authenticationv1.UserInfo{Username: "test1"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
executor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor1",
},
},
},
expectErr: nil,
},
{
name: "validate executor not nil fail",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Create,
UserInfo: authenticationv1.UserInfo{Username: "test1"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
executor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor2",
},
},
},
expectErr: apierrors.NewBadRequest(
"user test1 cannot manipulate the Manifestwork with executor ns1/executor2 in namespace cluster1"),
},
{
name: "validate executor not changed success",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Update,
UserInfo: authenticationv1.UserInfo{Username: "test1"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
executor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor2",
},
},
},
oldExecutor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor2",
},
},
},
expectErr: nil,
},
{
name: "validate executor changed fail",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Update,
UserInfo: authenticationv1.UserInfo{Username: "test1"},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
executor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor2",
},
},
},
oldExecutor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor1",
},
},
},
expectErr: apierrors.NewBadRequest(
"user test1 cannot manipulate the Manifestwork with executor ns1/executor2 in namespace cluster1"),
},
{
name: "validate executor with Extra field success",
request: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: manifestWorkSchema,
Operation: admissionv1.Create,
UserInfo: authenticationv1.UserInfo{
Username: "test-extra-user",
Extra: map[string]authenticationv1.ExtraValue{
"department": []string{"platform-team"},
"team": []string{"security"},
},
},
},
},
manifests: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "kind",
"metadata": map[string]interface{}{
"namespace": "ns1",
"name": "test",
},
},
},
},
executor: &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
Namespace: "ns1",
Name: "executor1",
},
},
},
expectErr: nil,
},
}
utilruntime.Must(features.HubMutableFeatureGate.Add(ocmfeature.DefaultHubWorkFeatureGates))
utilruntime.Must(features.HubMutableFeatureGate.Set(
fmt.Sprintf("%s=true", ocmfeature.NilExecutorValidating),
))
kubeClient := fakekube.NewSimpleClientset()
kubeClient.PrependReactor("create", "subjectaccessreviews",
func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
obj := action.(clienttesting.CreateActionImpl).Object.(*v1.SubjectAccessReview)
if obj.Spec.User == "test1" &&
reflect.DeepEqual(obj.Spec.ResourceAttributes, &v1.ResourceAttributes{
Group: "work.open-cluster-management.io",
Resource: "manifestworks",
Verb: "execute-as",
Namespace: "cluster1",
Name: "system:serviceaccount::klusterlet-work-sa",
}) {
return true, &v1.SubjectAccessReview{
Status: v1.SubjectAccessReviewStatus{
Allowed: true,
},
}, nil
}
if obj.Spec.User == "test1" &&
reflect.DeepEqual(obj.Spec.ResourceAttributes, &v1.ResourceAttributes{
Group: "work.open-cluster-management.io",
Resource: "manifestworks",
Verb: "execute-as",
Namespace: "cluster1",
Name: "system:serviceaccount:ns1:executor1",
}) {
return true, &v1.SubjectAccessReview{
Status: v1.SubjectAccessReviewStatus{
Allowed: true,
},
}, nil
}
// Handle test case with Extra field
if obj.Spec.User == "test-extra-user" &&
reflect.DeepEqual(obj.Spec.ResourceAttributes, &v1.ResourceAttributes{
Group: "work.open-cluster-management.io",
Resource: "manifestworks",
Verb: "execute-as",
Namespace: "cluster1",
Name: "system:serviceaccount:ns1:executor1",
}) {
// Verify that Extra field is properly propagated
expectedExtra := map[string]v1.ExtraValue{
"department": []string{"platform-team"},
"team": []string{"security"},
}
if !reflect.DeepEqual(obj.Spec.Extra, expectedExtra) {
return true, &v1.SubjectAccessReview{
Status: v1.SubjectAccessReviewStatus{
Allowed: false,
Reason: fmt.Sprintf("Extra field mismatch: expected %v, got %v", expectedExtra, obj.Spec.Extra),
},
}, nil
}
return true, &v1.SubjectAccessReview{
Status: v1.SubjectAccessReviewStatus{
Allowed: true,
},
}, nil
}
return true, &v1.SubjectAccessReview{
Status: v1.SubjectAccessReviewStatus{
Allowed: false,
},
}, nil
},
)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mw := ManifestWorkWebhook{
kubeClient: kubeClient,
}
ctx := admission.NewContextWithRequest(context.Background(), c.request)
newWork, _ := spoketesting.NewManifestWork(0, c.manifests...)
var oldWork *workv1.ManifestWork
if c.request.Operation == "UPDATE" {
oldWork = newWork.DeepCopy()
oldWork.Spec.Executor = c.oldExecutor
}
newWork.Spec.Executor = c.executor
err := mw.validateRequest(newWork, oldWork, ctx)
if !reflect.DeepEqual(err, c.expectErr) {
t.Errorf("case: %v, expected %v but got: %v", c.name, c.expectErr, err)
}
})
}
}