mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 18:09:57 +00:00
275 lines
9.1 KiB
Go
275 lines
9.1 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
operatorhelpers "github.com/openshift/library-go/pkg/operator/v1helpers"
|
|
authenticationv1 "k8s.io/api/authentication/v1"
|
|
authorizationv1 "k8s.io/api/authorization/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
v1 "open-cluster-management.io/api/cluster/v1"
|
|
clusterv1beta2 "open-cluster-management.io/api/cluster/v1beta2"
|
|
|
|
"open-cluster-management.io/ocm/pkg/registration/helpers"
|
|
)
|
|
|
|
var _ webhook.CustomValidator = &ManagedClusterWebhook{}
|
|
|
|
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
|
func (r *ManagedClusterWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
|
managedCluster, ok := obj.(*v1.ManagedCluster)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest("Request cluster obj format is not right")
|
|
}
|
|
req, err := admission.RequestFromContext(ctx)
|
|
if err != nil {
|
|
return nil, apierrors.NewBadRequest(err.Error())
|
|
}
|
|
|
|
// Validate if Spec.ManagedClusterClientConfigs is Valid HTTPS URL
|
|
err = r.validateManagedClusterObj(*managedCluster)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// the HubAcceptsClient field is changed, we need to:
|
|
// 1. check whether cluster namespace is terminating.
|
|
// 2. check the request user whether has been allowed to change the HubAcceptsClient field with
|
|
// SubjectAccessReview api.
|
|
if managedCluster.Spec.HubAcceptsClient {
|
|
if err := r.validateAcceptByClusterNamespace(managedCluster.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := r.allowUpdateAcceptField(managedCluster.Name, req.UserInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// check whether the request user has been allowed to set clusterset label
|
|
var clusterSetName string
|
|
if len(managedCluster.Labels) > 0 {
|
|
clusterSetName = managedCluster.Labels[clusterv1beta2.ClusterSetLabel]
|
|
}
|
|
|
|
return nil, r.allowSetClusterSetLabel(req.UserInfo, "", clusterSetName)
|
|
}
|
|
|
|
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
|
func (r *ManagedClusterWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (
|
|
admission.Warnings, error) {
|
|
managedCluster, ok := newObj.(*v1.ManagedCluster)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest("Request new cluster obj format is not right")
|
|
}
|
|
oldManagedCluster, ok := oldObj.(*v1.ManagedCluster)
|
|
if !ok {
|
|
return nil, apierrors.NewBadRequest("Request old cluster obj format is not right")
|
|
}
|
|
req, err := admission.RequestFromContext(ctx)
|
|
if err != nil {
|
|
return nil, apierrors.NewBadRequest(err.Error())
|
|
}
|
|
|
|
// Validate if Spec.ManagedClusterClientConfigs is Valid HTTPS URL
|
|
err = r.validateManagedClusterObj(*managedCluster)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// the HubAcceptsClient field is changed, we need to:
|
|
// 1. check whether cluster namespace is terminating.
|
|
// 2. check the request user whether has been allowed to change the HubAcceptsClient field with
|
|
// SubjectAccessReview api.
|
|
if managedCluster.Spec.HubAcceptsClient != oldManagedCluster.Spec.HubAcceptsClient {
|
|
if managedCluster.Spec.HubAcceptsClient {
|
|
if err := r.validateAcceptByClusterNamespace(managedCluster.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := r.allowUpdateAcceptField(managedCluster.Name, req.UserInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// check whether the request user has been allowed to set clusterset label
|
|
var originalClusterSetName, currentClusterSetName string
|
|
if len(oldManagedCluster.Labels) > 0 {
|
|
originalClusterSetName = oldManagedCluster.Labels[clusterv1beta2.ClusterSetLabel]
|
|
}
|
|
if len(managedCluster.Labels) > 0 {
|
|
currentClusterSetName = managedCluster.Labels[clusterv1beta2.ClusterSetLabel]
|
|
}
|
|
|
|
return nil, r.allowSetClusterSetLabel(req.UserInfo, originalClusterSetName, currentClusterSetName)
|
|
}
|
|
|
|
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
|
|
func (r *ManagedClusterWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// validateManagedClusterObj validates the fileds of ManagedCluster object
|
|
func (r *ManagedClusterWebhook) validateManagedClusterObj(cluster v1.ManagedCluster) error {
|
|
errs := []error{}
|
|
// The cluster name must be the same format of namespace name.
|
|
if errMsgs := apimachineryvalidation.ValidateNamespaceName(cluster.Name, false); len(errMsgs) > 0 {
|
|
errs = append(errs, fmt.Errorf("metadata.name format is not correct: %s", strings.Join(errMsgs, ",")))
|
|
}
|
|
// there are no spoke client configs, finish the validation process
|
|
if len(cluster.Spec.ManagedClusterClientConfigs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// validate the url in spoke client configs
|
|
for _, clientConfig := range cluster.Spec.ManagedClusterClientConfigs {
|
|
if !helpers.IsValidHTTPSURL(clientConfig.URL) {
|
|
errs = append(errs, fmt.Errorf("url %q is invalid in client configs", clientConfig.URL))
|
|
}
|
|
}
|
|
if len(errs) != 0 {
|
|
return apierrors.NewBadRequest(operatorhelpers.NewMultiLineAggregate(errs).Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// allowUpdateHubAcceptsClientField using SubjectAccessReview API to check whether a request user has been authorized to update
|
|
// HubAcceptsClient field
|
|
func (r *ManagedClusterWebhook) allowUpdateAcceptField(clusterName string, userInfo authenticationv1.UserInfo) error {
|
|
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: "register.open-cluster-management.io",
|
|
Resource: "managedclusters",
|
|
Verb: "update",
|
|
Subresource: "accept",
|
|
Name: clusterName,
|
|
},
|
|
},
|
|
}
|
|
sar, err := r.kubeClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclusters/accept"),
|
|
clusterName,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if !sar.Status.Allowed {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclusters/accept"),
|
|
clusterName,
|
|
fmt.Errorf("user %q cannot update the HubAcceptsClient field", userInfo.Username),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateClusterNamespace checks the cluster namespace, if the namespace is terminating, reject the accept request.
|
|
func (r *ManagedClusterWebhook) validateAcceptByClusterNamespace(clusterName string) error {
|
|
clusterNamespace, err := r.kubeClient.CoreV1().Namespaces().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
|
if apierrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclusters/accept"),
|
|
clusterName,
|
|
err,
|
|
)
|
|
}
|
|
if clusterNamespace.Status.Phase == corev1.NamespaceTerminating {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclusters/accept"),
|
|
clusterName,
|
|
fmt.Errorf("cluster namespace %q is terminating", clusterName),
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// allowSetClusterSetLabel checks whether a request user has been authorized to set clusterset label
|
|
func (r *ManagedClusterWebhook) allowSetClusterSetLabel(userInfo authenticationv1.UserInfo, originalClusterSet, newClusterSet string) error {
|
|
if originalClusterSet == newClusterSet {
|
|
return nil
|
|
}
|
|
|
|
if len(originalClusterSet) > 0 {
|
|
err := r.allowUpdateClusterSet(userInfo, originalClusterSet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(newClusterSet) > 0 {
|
|
err := r.allowUpdateClusterSet(userInfo, newClusterSet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// allowUpdateClusterSet checks whether a request user has been authorized to add/remove a ManagedCluster
|
|
// to/from the ManagedClusterSet
|
|
func (r *ManagedClusterWebhook) allowUpdateClusterSet(userInfo authenticationv1.UserInfo, clusterSetName string) error {
|
|
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: "cluster.open-cluster-management.io",
|
|
Resource: "managedclustersets",
|
|
Subresource: "join",
|
|
Name: clusterSetName,
|
|
Verb: "create",
|
|
},
|
|
},
|
|
}
|
|
sar, err := r.kubeClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclustersets/join"),
|
|
clusterSetName,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if !sar.Status.Allowed {
|
|
return apierrors.NewForbidden(
|
|
v1.Resource("managedclustersets/join"),
|
|
clusterSetName,
|
|
fmt.Errorf("user %q cannot add/remove a ManagedCluster to/from ManagedClusterSet %q", userInfo.Username, clusterSetName),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|