diff --git a/charts/rancher-turtles/templates/rancher-turtles-exp-etcdrestore-components.yaml b/charts/rancher-turtles/templates/rancher-turtles-exp-etcdrestore-components.yaml index 0e828c1b..c860d223 100644 --- a/charts/rancher-turtles/templates/rancher-turtles-exp-etcdrestore-components.yaml +++ b/charts/rancher-turtles/templates/rancher-turtles-exp-etcdrestore-components.yaml @@ -508,6 +508,13 @@ rules: - patch - update - watch +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + - get - apiGroups: - bootstrap.cluster.x-k8s.io resources: @@ -844,4 +851,56 @@ webhooks: resources: - rke2configs sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: rancher-turtles-system/rancher-turtles-etcdsnapshotrestore-serving-cert + labels: + turtles-capi.cattle.io: etcd-restore + name: rancher-turtles-etcdsnapshotrestore-validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: rancher-turtles-etcdsnapshotrestore-webhook-service + namespace: rancher-turtles-system + path: /validate-turtles-capi-cattle-io-v1alpha1-etcdmachinesnapshot + failurePolicy: Fail + matchPolicy: Equivalent + name: etcdmachinesnapshot.kb.io + rules: + - apiGroups: + - turtles-capi.cattle.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdmachinesnapshots + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: rancher-turtles-etcdsnapshotrestore-webhook-service + namespace: rancher-turtles-system + path: /validate-turtles-capi-cattle-io-v1alpha1-etcdsnapshotrestore + failurePolicy: Fail + matchPolicy: Equivalent + name: etcdsnapshotrestore.kb.io + rules: + - apiGroups: + - turtles-capi.cattle.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdsnapshotrestores + sideEffects: None {{- end }} diff --git a/exp/etcdrestore/config/rbac/role.yaml b/exp/etcdrestore/config/rbac/role.yaml index f50ad44f..4a69a2b3 100644 --- a/exp/etcdrestore/config/rbac/role.yaml +++ b/exp/etcdrestore/config/rbac/role.yaml @@ -19,6 +19,13 @@ rules: - patch - update - watch +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + - get - apiGroups: - bootstrap.cluster.x-k8s.io resources: diff --git a/exp/etcdrestore/config/webhook/manifests.yaml b/exp/etcdrestore/config/webhook/manifests.yaml index 39303336..f88df94b 100644 --- a/exp/etcdrestore/config/webhook/manifests.yaml +++ b/exp/etcdrestore/config/webhook/manifests.yaml @@ -24,3 +24,51 @@ webhooks: resources: - rke2configs sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-turtles-capi-cattle-io-v1alpha1-etcdmachinesnapshot + failurePolicy: Fail + matchPolicy: Equivalent + name: etcdmachinesnapshot.kb.io + rules: + - apiGroups: + - turtles-capi.cattle.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdmachinesnapshots + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-turtles-capi-cattle-io-v1alpha1-etcdsnapshotrestore + failurePolicy: Fail + matchPolicy: Equivalent + name: etcdsnapshotrestore.kb.io + rules: + - apiGroups: + - turtles-capi.cattle.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - etcdsnapshotrestores + sideEffects: None diff --git a/exp/etcdrestore/main.go b/exp/etcdrestore/main.go index 4aa83a46..f073370a 100644 --- a/exp/etcdrestore/main.go +++ b/exp/etcdrestore/main.go @@ -275,4 +275,18 @@ func setupWebhooks(mgr ctrl.Manager) { setupLog.Error(err, "unable to create webhook", "webhook", "RKE2Config") os.Exit(1) } + + if err := (&expwebhooks.EtcdMachineSnapshotWebhook{ + Client: mgr.GetClient(), + }).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "EtcdMachineSnapshot") + os.Exit(1) + } + + if err := (&expwebhooks.EtcdSnapshotRestoreWebhook{ + Client: mgr.GetClient(), + }).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "EtcdSnapshotRestore") + os.Exit(1) + } } diff --git a/exp/etcdrestore/webhooks/etcdmachinesnapshot.go b/exp/etcdrestore/webhooks/etcdmachinesnapshot.go new file mode 100644 index 00000000..73bc0fe4 --- /dev/null +++ b/exp/etcdrestore/webhooks/etcdmachinesnapshot.go @@ -0,0 +1,101 @@ +/* +Copyright © 2023 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhooks + +import ( + "context" + "fmt" + + snapshotrestorev1 "github.com/rancher/turtles/exp/etcdrestore/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:webhook:path=/validate-turtles-capi-cattle-io-v1alpha1-etcdmachinesnapshot,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,sideEffects=None,groups=turtles-capi.cattle.io,resources=etcdmachinesnapshots,verbs=create;update,versions=v1alpha1,name=etcdmachinesnapshot.kb.io,admissionReviewVersions=v1 +// +kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=get;create + +// EtcdMachineSnapshotWebhook defines a webhook for EtcdMachineSnapshot. +type EtcdMachineSnapshotWebhook struct { + client.Client +} + +var _ webhook.CustomValidator = &EtcdMachineSnapshotWebhook{} + +// SetupWebhookWithManager sets up and registers the webhook with the manager. +func (r *EtcdMachineSnapshotWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&snapshotrestorev1.EtcdMachineSnapshot{}). + WithValidator(r). + Complete() +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdMachineSnapshotWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + logger := log.FromContext(ctx) + + logger.Info("Validating EtcdMachineSnapshot") + + etcdMachineSnapshot, ok := obj.(*snapshotrestorev1.EtcdMachineSnapshot) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a EtcdMachineSnapshot but got a %T", obj)) + } + + return r.validateSpec(ctx, etcdMachineSnapshot) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdMachineSnapshotWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + logger := log.FromContext(ctx) + + logger.Info("Validating EtcdMachineSnapshot") + + etcdMachineSnapshot, ok := newObj.(*snapshotrestorev1.EtcdMachineSnapshot) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a EtcdMachineSnapshot but got a %T", newObj)) + } + + return r.validateSpec(ctx, etcdMachineSnapshot) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdMachineSnapshotWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func (r *EtcdMachineSnapshotWebhook) validateSpec(ctx context.Context, etcdMachineSnapshot *snapshotrestorev1.EtcdMachineSnapshot) (admission.Warnings, error) { + var allErrs field.ErrorList + + if etcdMachineSnapshot.Spec.ClusterName == "" { + allErrs = append(allErrs, field.Required(field.NewPath("spec.clusterName"), "clusterName is required")) + } + + if len(allErrs) > 0 { + return nil, apierrors.NewInvalid(snapshotrestorev1.GroupVersion.WithKind("EtcdMachineSnapshot").GroupKind(), etcdMachineSnapshot.Name, allErrs) + } + + if err := validateRBAC(ctx, r.Client, etcdMachineSnapshot.Spec.ClusterName, etcdMachineSnapshot.Namespace); err != nil { + return nil, apierrors.NewBadRequest(fmt.Sprintf("failed to validate RBAC: %v", err)) + } + + return nil, nil +} diff --git a/exp/etcdrestore/webhooks/etcdsnapshotrestore.go b/exp/etcdrestore/webhooks/etcdsnapshotrestore.go new file mode 100644 index 00000000..6b257231 --- /dev/null +++ b/exp/etcdrestore/webhooks/etcdsnapshotrestore.go @@ -0,0 +1,92 @@ +/* +Copyright © 2023 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhooks + +import ( + "context" + "fmt" + + snapshotrestorev1 "github.com/rancher/turtles/exp/etcdrestore/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:webhook:path=/validate-turtles-capi-cattle-io-v1alpha1-etcdsnapshotrestore,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,sideEffects=None,groups=turtles-capi.cattle.io,resources=etcdsnapshotrestores,verbs=create;update,versions=v1alpha1,name=etcdsnapshotrestore.kb.io,admissionReviewVersions=v1 + +// EtcdSnapshotRestoreWebhook defines a webhook for EtcdSnapshotRestore. +type EtcdSnapshotRestoreWebhook struct { + client.Client +} + +var _ webhook.CustomValidator = &EtcdSnapshotRestoreWebhook{} + +// SetupWebhookWithManager sets up and registers the webhook with the manager. +func (r *EtcdSnapshotRestoreWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&snapshotrestorev1.EtcdSnapshotRestore{}). + WithValidator(r). + Complete() +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdSnapshotRestoreWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + logger := log.FromContext(ctx) + + logger.Info("Validating EtcdSnapshotRestore") + + etcdSnapshotRestore, ok := obj.(*snapshotrestorev1.EtcdSnapshotRestore) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a EtcdSnapshotRestore but got a %T", obj)) + } + + return r.validateSpec(ctx, etcdSnapshotRestore) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdSnapshotRestoreWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (r *EtcdSnapshotRestoreWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func (r *EtcdSnapshotRestoreWebhook) validateSpec(ctx context.Context, etcdSnapshotRestore *snapshotrestorev1.EtcdSnapshotRestore) (admission.Warnings, error) { + var allErrs field.ErrorList + + if etcdSnapshotRestore.Spec.ClusterName == "" { + allErrs = append(allErrs, field.Required(field.NewPath("spec.clusterName"), "clusterName is required")) + } + + if len(allErrs) > 0 { + return nil, apierrors.NewInvalid(snapshotrestorev1.GroupVersion.WithKind("EtcdSnapshotRestore").GroupKind(), etcdSnapshotRestore.Name, allErrs) + } + + if err := validateRBAC(ctx, r.Client, etcdSnapshotRestore.Spec.ClusterName, etcdSnapshotRestore.Namespace); err != nil { + return nil, apierrors.NewBadRequest(fmt.Sprintf("failed to validate RBAC: %v", err)) + } + + return nil, nil +} diff --git a/exp/etcdrestore/webhooks/rbac.go b/exp/etcdrestore/webhooks/rbac.go new file mode 100644 index 00000000..f7292117 --- /dev/null +++ b/exp/etcdrestore/webhooks/rbac.go @@ -0,0 +1,60 @@ +/* +Copyright © 2023 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhooks + +import ( + "context" + "fmt" + + authv1 "k8s.io/api/authorization/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func validateRBAC(ctx context.Context, cl client.Client, clusterName, clusterNamespace string) error { + admissionRequest, err := admission.RequestFromContext(ctx) + if err != nil { + return fmt.Errorf("failed to get admission request from context: %w", err) + } + + sar := authv1.SubjectAccessReview{ + Spec: authv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Verb: "*", + Group: clusterv1.GroupVersion.Group, + Version: clusterv1.GroupVersion.Version, + Resource: "clusters", + Name: clusterName, + Namespace: clusterNamespace, + }, + User: admissionRequest.UserInfo.Username, + Groups: admissionRequest.UserInfo.Groups, + UID: admissionRequest.UserInfo.UID, + }, + } + + if err := cl.Create(ctx, &sar); err != nil { + return err + } + + if !sar.Status.Allowed { + return fmt.Errorf("user is not allowed to access the cluster: %s", sar.Status.Reason) + } + + return nil +} diff --git a/exp/etcdrestore/webhooks/rbac_test.go b/exp/etcdrestore/webhooks/rbac_test.go new file mode 100644 index 00000000..3027e336 --- /dev/null +++ b/exp/etcdrestore/webhooks/rbac_test.go @@ -0,0 +1,106 @@ +/* +Copyright © 2023 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhooks + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var _ = Describe("RBAC tests", func() { + + var ( + namespace = "default" + role *rbacv1.Role + roleBinding *rbacv1.RoleBinding + ) + + BeforeEach(func() { + role = &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-role", + Namespace: namespace, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{clusterv1.GroupVersion.Group}, + Resources: []string{"clusters"}, + Verbs: []string{"*"}, + }, + }, + } + + roleBinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-role-binding", + Namespace: namespace, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: "clusteradmin", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: role.Name, + }, + } + }) + + AfterEach(func() { + testEnv.Cleanup(ctx, role, roleBinding) + }) + + It("should pass if user is allowed to access the cluster", func() { + Expect(cl.Create(ctx, role)).To(Succeed()) + Expect(cl.Create(ctx, roleBinding)).To(Succeed()) + Expect(validateRBAC(admission.NewContextWithRequest(ctx, admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + UserInfo: authenticationv1.UserInfo{ + Username: "clusteradmin", + }, + }, + }), cl, "test-cluster", namespace)).To(Succeed()) + }) + + It("should fail if user is not allowed to access the cluster", func() { + role.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{clusterv1.GroupVersion.Group}, + Resources: []string{"clusters"}, + Verbs: []string{"get"}, + }, + } + Expect(cl.Create(ctx, role)).To(Succeed()) + Expect(cl.Create(ctx, roleBinding)).To(Succeed()) + Expect(validateRBAC(admission.NewContextWithRequest(ctx, admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + UserInfo: authenticationv1.UserInfo{ + Username: "clusteradmin", + }, + }, + }), cl, "test-cluster", namespace)).ToNot(Succeed()) + }) +}) diff --git a/exp/etcdrestore/webhooks/rke2config_test.go b/exp/etcdrestore/webhooks/rke2config_test.go index 17179255..13dde340 100644 --- a/exp/etcdrestore/webhooks/rke2config_test.go +++ b/exp/etcdrestore/webhooks/rke2config_test.go @@ -47,11 +47,6 @@ var ( token []byte ) -type mockClient struct { - client.Client - listFunc func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error -} - var _ = Describe("RKE2ConfigWebhook tests", func() { BeforeEach(func() { ctx = context.Background()