mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-23 22:26:49 +00:00
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>
388 lines
11 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|