Files
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

123 lines
4.2 KiB
Go

package v1
import (
"context"
"fmt"
"reflect"
authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"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/webhook/common"
)
var _ admission.Validator[*workv1.ManifestWork] = &ManifestWorkWebhook{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ManifestWorkWebhook) ValidateCreate(ctx context.Context, work *workv1.ManifestWork) (admission.Warnings, error) {
return nil, r.validateRequest(work, nil, ctx)
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ManifestWorkWebhook) ValidateUpdate(ctx context.Context, oldWork, newWork *workv1.ManifestWork) (
admission.Warnings, error) {
return nil, r.validateRequest(newWork, oldWork, ctx)
}
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *ManifestWorkWebhook) ValidateDelete(_ context.Context, _ *workv1.ManifestWork) (admission.Warnings, error) {
return nil, nil
}
func (r *ManifestWorkWebhook) validateRequest(newWork, oldWork *workv1.ManifestWork, ctx context.Context) error {
if newWork == nil {
return fmt.Errorf("newWork is nil")
}
if len(newWork.Spec.Workload.Manifests) == 0 {
return apierrors.NewBadRequest("manifests should not be empty")
}
if err := common.ManifestValidator.ValidateManifests(newWork.Spec.Workload.Manifests); err != nil {
return apierrors.NewBadRequest(err.Error())
}
req, err := admission.RequestFromContext(ctx)
if err != nil {
return apierrors.NewBadRequest(err.Error())
}
// do not need to check the executor when it is not changed
if oldWork != nil && reflect.DeepEqual(oldWork.Spec.Executor, newWork.Spec.Executor) {
return nil
}
return validateExecutor(r.kubeClient, newWork, req.UserInfo)
}
func validateExecutor(kubeClient kubernetes.Interface, work *workv1.ManifestWork, userInfo authenticationv1.UserInfo) error {
executor := work.Spec.Executor
if !features.HubMutableFeatureGate.Enabled(ocmfeature.NilExecutorValidating) {
if executor == nil {
return nil
}
}
if work.Spec.Executor == nil {
executor = &workv1.ManifestWorkExecutor{
Subject: workv1.ManifestWorkExecutorSubject{
Type: workv1.ExecutorSubjectTypeServiceAccount,
ServiceAccount: &workv1.ManifestWorkSubjectServiceAccount{
// give the default value "system:serviceaccount::klusterlet-work-sa"
Namespace: "",
Name: "klusterlet-work-sa", // the default sa of the work agent
},
},
}
}
if executor.Subject.Type == workv1.ExecutorSubjectTypeServiceAccount && executor.Subject.ServiceAccount == nil {
return apierrors.NewBadRequest("executor service account can not be nil")
}
extra := make(map[string]authorizationv1.ExtraValue)
for k, v := range userInfo.Extra {
extra[k] = authorizationv1.ExtraValue(v)
}
sar := &authorizationv1.SubjectAccessReview{
Spec: authorizationv1.SubjectAccessReviewSpec{
User: userInfo.Username,
UID: userInfo.UID,
Groups: userInfo.Groups,
Extra: extra,
ResourceAttributes: &authorizationv1.ResourceAttributes{
Group: "work.open-cluster-management.io",
Resource: "manifestworks",
Verb: "execute-as",
Namespace: work.Namespace,
Name: fmt.Sprintf("system:serviceaccount:%s:%s",
executor.Subject.ServiceAccount.Namespace, executor.Subject.ServiceAccount.Name),
},
},
}
sar, err := kubeClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
if err != nil {
return apierrors.NewBadRequest(err.Error())
}
if !sar.Status.Allowed {
return apierrors.NewBadRequest(fmt.Sprintf("user %s cannot manipulate the Manifestwork with executor %s/%s in namespace %s",
userInfo.Username, executor.Subject.ServiceAccount.Namespace, executor.Subject.ServiceAccount.Name, work.Namespace))
}
return nil
}