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>
123 lines
4.2 KiB
Go
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
|
|
}
|