From 44e89502bd5ef56419eda3f9af420ab414c85d5f Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Thu, 27 Jul 2023 09:10:40 +0200 Subject: [PATCH] Handle extension `ServiceAccounts` in `Seed{Authorizer,Restriction}` (#8264) * Update docs for feature and `Seed{Authorizer,Restriction}` * Accept extension clients in `Seed{Authorizer,Restriction}` Co-Authored-By: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> * Prevent privilege escalation by extension clients Extension clients are only allowed read access to `ServiceAccounts`, `ClusterRoleBindings`, and `CertificateSigningRequests`. Co-Authored-By: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> * Restrict extension operations for `leases` to seed namespace * Document how we want to handle future use cases for exceptions * Verify garden access in e2e tests * Address review suggestions * Address review suggestions (2) --------- Co-authored-by: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> --- .../app/app.go | 15 +- docs/deployment/gardenlet_api_access.md | 53 +- docs/extensions/garden-api-access.md | 13 +- .../seedidentity/identity.go | 80 +- .../seedidentity/identity_test.go | 135 +- .../admission/seedrestriction/handler.go | 40 +- .../admission/seedrestriction/handler_test.go | 3536 ++++++++-------- .../webhook/auth/seed/authorizer.go | 45 +- .../webhook/auth/seed/authorizer_test.go | 3540 +++++++++-------- .../eventhandler_certificatesigningrequest.go | 2 +- test/e2e/gardener/seed/garden_access.go | 41 +- 11 files changed, 4155 insertions(+), 3345 deletions(-) diff --git a/cmd/gardener-extension-provider-local/app/app.go b/cmd/gardener-extension-provider-local/app/app.go index 8c1b4cb8835..db85daa6a53 100644 --- a/cmd/gardener-extension-provider-local/app/app.go +++ b/cmd/gardener-extension-provider-local/app/app.go @@ -322,17 +322,24 @@ func NewControllerManagerCommand(ctx context.Context) *cobra.Command { // verifyGardenAccess uses the extension's access to the garden cluster to request objects related to the seed it is // running on, but doesn't do anything useful with the objects. We do this for verifying the extension's garden access // in e2e tests. If something fails in this runnable, the extension will crash loop. -func verifyGardenAccess(ctx context.Context, log logr.Logger, c client.Reader, seedName string) error { - log = log.WithName("garden-reader").WithValues("seedName", seedName) +func verifyGardenAccess(ctx context.Context, log logr.Logger, c client.Client, seedName string) error { + log = log.WithName("garden-access").WithValues("seedName", seedName) - log.Info("Getting Seed") + log.Info("Reading Seed") + // NB: reading seeds is allowed by gardener.cloud:system:read-global-resources (bound to all authenticated users) seed := &gardencorev1beta1.Seed{} if err := c.Get(ctx, client.ObjectKey{Name: seedName}, seed); err != nil { return fmt.Errorf("failed reading seed %s: %w", seedName, err) } - log.Info("Garden access successfully verified") + log.Info("Annotating Seed") + patch := client.MergeFrom(seed.DeepCopy()) + metav1.SetMetaDataAnnotation(&seed.ObjectMeta, "provider-local-e2e-test-garden-access", time.Now().UTC().Format(time.RFC3339)) + if err := c.Patch(ctx, seed, patch); err != nil { + return fmt.Errorf("failed annotating seed %s: %w", seedName, err) + } + log.Info("Garden access successfully verified") return nil } diff --git a/docs/deployment/gardenlet_api_access.md b/docs/deployment/gardenlet_api_access.md index f42b5bf1ce5..43416dc95e5 100644 --- a/docs/deployment/gardenlet_api_access.md +++ b/docs/deployment/gardenlet_api_access.md @@ -1,8 +1,8 @@ --- -title: gardenlet API Access +title: Scoped API Access for gardenlets and Extensions --- -# Scoped API Access for gardenlets +# Scoped API Access for gardenlets and Extensions By default, `gardenlet`s have administrative access in the garden cluster. They are able to execute any API request on any object independent of whether the object is related to the seed cluster the `gardenlet` is responsible for. @@ -17,6 +17,14 @@ It is a special-purpose admission plugin that specifically limits the Kubernetes 📚 You might be interested to look into the [design proposal for scoped Kubelet API access](https://github.com/kubernetes/design-proposals-archive/blob/main/node/kubelet-authorizer.md) from the Kubernetes community. It can be translated to Gardener and Gardenlets with their `Seed` and `Shoot` resources. +Historically, `gardenlet` has been the only component running in the seed cluster that has access to both the seed cluster and the garden cluster. +Starting from Gardener [`v1.74.0`](https://github.com/gardener/gardener/releases/v1.74.0), extensions running on seed clusters can also get [access to the garden cluster](../extensions/garden-api-access.md) using a token for a dedicated ServiceAccount. +Extensions using this mechanism only get permission to read global resources like `CloudProfiles` (this is granted to all authenticated users) unless the plugins described in this document are enabled. + +Generally, the plugins handle extension clients exactly like gardenlet clients with some minor [exceptions](#rule-exceptions-for-extension-clients). +Extension clients in the sense of the plugins are clients authenticated as a `ServiceAccount` with the `extension-` name prefix in a `seed-` namespace of the garden cluster. +Other `ServiceAccounts` are not considered as seed clients, not handled by the plugins, and only get the described read access to global resources. + ## Flow Diagram The following diagram shows how the two plugins are included in the request flow of a `gardenlet`. @@ -30,6 +38,8 @@ Please note that the example shows a request to an object (`Shoot`) residing in However, the `gardenlet` is also interacting with objects in API groups served by the `kube-apiserver` (e.g., `Secret`,`ConfigMap`). In this case, the consultation of the `SeedRestriction` admission plugin is performed by the `kube-apiserver` itself before it forwards the request to the `gardener-apiserver`. +## Implemented Rules + Today, the following rules are implemented: | Resource | Verbs | Path(s) | Description | @@ -61,6 +71,17 @@ Today, the following rules are implemented: ℹ️ It is allowed to delete the `Seed` resources if the corresponding `ManagedSeed` objects already have a `deletionTimestamp` (this is secure as `gardenlet`s themselves don't have permissions for deleting `ManagedSeed`s). +### Rule Exceptions for Extension Clients + +Extension clients are allowed to perform the same operations as gardenlet clients with the following exceptions: + +- Extension clients are granted the read-only subset of verbs for `CertificateSigningRequests`, `ClusterRoleBindings`, and `ServiceAccounts` (to prevent privilege escalation). +- Extension clients are granted full access to `Lease` objects but only in the seed-specific namespace. + +When the need arises, more exceptions might be added to the access rules for resources that are already handled by the plugins. +E.g., if an extension needs to populate additional shoot-specific `InternalSecrets`, according handling can be introduced. +Permissions for resources that are not handled by the plugins can be granted using additional RBAC rules (independent of the plugins). + ## `SeedAuthorizer` Authorization Webhook Enablement The `SeedAuthorizer` is implemented as a [Kubernetes authorization webhook](https://kubernetes.io/docs/reference/access-authn-authz/webhook/) and part of the [`gardener-admission-controller`](../concepts/admission-controller.md) component running in the garden cluster. @@ -118,15 +139,23 @@ As mentioned earlier, it's the authorizer's job to evaluate API requests and ret For backwards compatibility, no requests are denied at the moment, so that they are still deferred to a subsequent authorizer like RBAC. Though, this might change in the future. -First, the `SeedAuthorizer` extracts the `Seed` name from the API request. This requires a proper TLS certificate that the `gardenlet` uses to contact the API server and is automatically given if [TLS bootstrapping](../concepts/gardenlet.md#TLS-Bootstrapping) is used. -Concretely, the authorizer checks the certificate for name `gardener.cloud:system:seed:` and group `gardener.cloud:system:seeds`. -In cases where this information is missing e.g., when a custom Kubeconfig is used, the authorizer cannot make any decision. Thus, RBAC is still a considerable option to restrict the `gardenlet`'s access permission if the above explained preconditions are not given. +First, the `SeedAuthorizer` extracts the `Seed` name from the API request. +This step considers the following two cases: + +1. If the authenticated user belongs to the `gardener.cloud:system:seeds` group, it is considered a gardenlet client. + - This requires a proper TLS certificate that the `gardenlet` uses to contact the API server and is automatically given if [TLS bootstrapping](../concepts/gardenlet.md#TLS-Bootstrapping) is used. + - The authorizer extracts the seed name from the username by stripping the `gardener.cloud:system:seed:` prefix. + - In cases where this information is missing e.g., when a custom Kubeconfig is used, the authorizer cannot make any decision. Thus, RBAC is still a considerable option to restrict the `gardenlet`'s access permission if the above explained preconditions are not given. +2. If the authenticated user belongs to the `system:serviceaccounts` group, it is considered an extension client under the following conditions: + - The `ServiceAccount` must be located in a `seed-` namespace. I.e., the user has to belong to a group with the `system:serviceaccounts:seed-` prefix. The seed name is extracted from this group by stripping the prefix. + - The `ServiceAccount` must have the `extension-` prefix. I.e., the username must have the `system:serviceaccount:seed-:extension-` prefix. -With the `Seed` name at hand, the authorizer checks for an **existing path** from the resource that a request is being made for to the `Seed` belonging to the `gardenlet`. Take a look at the [Implementation Details](#implementation-details) section for more information. +With the `Seed` name at hand, the authorizer checks for an **existing path** from the resource that a request is being made for to the `Seed` belonging to the `gardenlet`/extension. +Take a look at the [Implementation Details](#implementation-details) section for more information. ### Implementation Details -Internally, the `SeedAuthorizer` uses a directed, acyclic graph data structure in order to efficiently respond to authorization requests for `gardenlet`s: +Internally, the `SeedAuthorizer` uses a directed, acyclic graph data structure in order to efficiently respond to authorization requests for `gardenlet`s/extensions: * A vertex in this graph represents a Kubernetes resource with its kind, namespace, and name (e.g., `Shoot:garden-my-project/my-shoot`). * An edge from vertex `u` to vertex `v` in this graph exists when @@ -143,10 +172,10 @@ In the above picture, the resources that are actively watched are shaded. Gardener resources are green, while Kubernetes resources are blue. It shows the dependencies between the resources and how the graph is built based on the above rules. -ℹ️ The above picture shows all resources that may be accessed by `gardenlet`s, except for the `Quota` resource which is only included for completeness. +ℹ️ The above picture shows all resources that may be accessed by `gardenlet`s/extensions, except for the `Quota` resource which is only included for completeness. -Now, when a `gardenlet` wants to access certain resources, then the `SeedAuthorizer` uses a Depth-First traversal starting from the vertex representing the resource in question, e.g., from a `Project` vertex. -If there is a path from the `Project` vertex to the vertex representing the `Seed` the `gardenlet` is responsible for. then it allows the request. +Now, when a `gardenlet`/extension wants to access certain resources, then the `SeedAuthorizer` uses a Depth-First traversal starting from the vertex representing the resource in question, e.g., from a `Project` vertex. +If there is a path from the `Project` vertex to the vertex representing the `Seed` the `gardenlet`/extension is responsible for. then it allows the request. #### Metrics @@ -230,6 +259,6 @@ Please note that it should only be activated when the `SeedAuthorizer` is active ### Admission Decisions The admission's purpose is to perform extended validation on requests which require the body of the object in question. -Additionally, it handles `CREATE` requests of `gardenlet`s (the above discussed resource dependency graph cannot be used in such cases because there won't be any vertex/edge for non-existing resources). +Additionally, it handles `CREATE` requests of `gardenlet`s/extensions (the above discussed resource dependency graph cannot be used in such cases because there won't be any vertex/edge for non-existing resources). -Gardenlets are restricted to only create new resources which are somehow related to the seed clusters they are responsible for. +Gardenlets/extensions are restricted to only create new resources which are somehow related to the seed clusters they are responsible for. diff --git a/docs/extensions/garden-api-access.md b/docs/extensions/garden-api-access.md index a0b39484b9e..8a129458265 100644 --- a/docs/extensions/garden-api-access.md +++ b/docs/extensions/garden-api-access.md @@ -6,8 +6,7 @@ title: Access to the Garden Cluster for Extensions Extensions that are installed on seed clusters via a `ControllerInstallation` can simply read the kubeconfig file specified by the `GARDEN_KUBECONFIG` environment variable to create a garden cluster client. With this, they use a short-lived token (valid for `12h`) associated with a dedicated `ServiceAccount` in the `seed-` namespace to securely access the garden cluster. - -> ⚠️ This feature is under development. The managed `ServiceAccounts` in the garden cluster don't have any API permissions as of now. They will be handled by the `SeedAuthorizer` in the future and equipped with permissions similar to the gardenlets' credentials. See [gardener/gardener#8001](https://github.com/gardener/gardener/issues/8001) for more information. +The used `ServiceAccounts` are granted permissions in the garden cluster similar to gardenlet clients. ## Background @@ -121,6 +120,16 @@ users: tokenFile: /var/run/secrets/gardener.cloud/garden/generic-kubeconfig/token ``` +## Permissions in the Garden Cluster + +Both the [`SeedAuthorizer` and the `SeedRestriction` plugin](../deployment/gardenlet_api_access.md) handle extensions clients and generally grant the same permissions in the garden cluster to them as to gardenlet clients. +With this, extensions are restricted to work with objects in the garden cluster that are related to seed they are running one just like gardenlet. +Note that if the plugins are not enabled, extension clients are only granted read access to global resources like `CloudProfiles` (this is granted to all authenticated users). +There are a few exceptions to the granted permissions as documented [here](../deployment/gardenlet_api_access.md#rule-exceptions-for-extension-clients). + +If an extension needs access to additional resources in the garden cluster (e.g., extension-specific custom resources), permissions need to be granted via the usual RBAC means. +Note that this is done outside of Gardener and might require an additional controller that manages RBAC for extension clients in the garden cluster. + ## Renewing All Garden Access Secrets Operators can trigger an automatic renewal of all garden access secrets in a given `Seed` and their requested `ServiceAccount` tokens, e.g., when rotating the garden cluster's `ServiceAccount` signing key. diff --git a/pkg/admissioncontroller/seedidentity/identity.go b/pkg/admissioncontroller/seedidentity/identity.go index 6e40d70381e..09dff39c338 100644 --- a/pkg/admissioncontroller/seedidentity/identity.go +++ b/pkg/admissioncontroller/seedidentity/identity.go @@ -19,39 +19,46 @@ import ( "strings" authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/apiserver/pkg/authentication/user" v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" "github.com/gardener/gardener/pkg/utils" + gardenerutils "github.com/gardener/gardener/pkg/utils/gardener" ) -// FromUserInfoInterface returns the seed name and a boolean indicating whether the provided user has the -// gardener.cloud:system:seeds group. -func FromUserInfoInterface(u user.Info) (string, bool) { +// UserType is used for distinguishing between clients running on a seed cluster when authenticating against the garden +// cluster. +type UserType string + +const ( + // UserTypeGardenlet is the UserType of a gardenlet client. + UserTypeGardenlet UserType = "gardenlet" + // UserTypeExtension is the UserType of a extension client. + UserTypeExtension UserType = "extension" +) + +// FromUserInfoInterface returns the seed name, a boolean indicating whether the provided user is a seed client, +// and the client's UserType. +func FromUserInfoInterface(u user.Info) (string, bool, UserType) { if u == nil { - return "", false + return "", false, "" } - userName := u.GetName() - if !strings.HasPrefix(userName, v1beta1constants.SeedUserNamePrefix) { - return "", false + if utils.ValueExists(v1beta1constants.SeedsGroup, u.GetGroups()) { + return getIdentityForSeedsGroup(u) } - if !utils.ValueExists(v1beta1constants.SeedsGroup, u.GetGroups()) { - return "", false + if utils.ValueExists(serviceaccount.AllServiceAccountsGroup, u.GetGroups()) { + return getIdentityForServiceAccountsGroup(u) } - seedName := strings.TrimPrefix(userName, v1beta1constants.SeedUserNamePrefix) - if seedName == "" { - return "", false - } - - return seedName, true + return "", false, "" } // FromAuthenticationV1UserInfo converts an authenticationv1.UserInfo structure to the user.Info interface and calls // FromUserInfoInterface to return the seed name. -func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, bool) { +func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, bool, UserType) { return FromUserInfoInterface(&user.DefaultInfo{ Name: userInfo.Username, UID: userInfo.UID, @@ -62,7 +69,7 @@ func FromAuthenticationV1UserInfo(userInfo authenticationv1.UserInfo) (string, b // FromCertificateSigningRequest converts a *x509.CertificateRequest structure to the user.Info interface and calls // FromUserInfoInterface to return the seed name. -func FromCertificateSigningRequest(csr *x509.CertificateRequest) (string, bool) { +func FromCertificateSigningRequest(csr *x509.CertificateRequest) (string, bool, UserType) { return FromUserInfoInterface(&user.DefaultInfo{ Name: csr.Subject.CommonName, Groups: csr.Subject.Organization, @@ -80,3 +87,42 @@ func convertAuthenticationV1ExtraValueToUserInfoExtra(extra map[string]authentic return ret } + +func getIdentityForSeedsGroup(u user.Info) (string, bool, UserType) { + userName := u.GetName() + + if !strings.HasPrefix(userName, v1beta1constants.SeedUserNamePrefix) { + return "", false, "" + } + + seedName := strings.TrimPrefix(userName, v1beta1constants.SeedUserNamePrefix) + if seedName == "" { + return "", false, "" + } + + return seedName, true, UserTypeGardenlet +} + +func getIdentityForServiceAccountsGroup(u user.Info) (string, bool, UserType) { + var serviceAccountNamespaceGroup string + for _, g := range u.GetGroups() { + if strings.HasPrefix(g, serviceaccount.ServiceAccountGroupPrefix) { + serviceAccountNamespaceGroup = g + break + } + } + + seedNamespace := strings.TrimPrefix(serviceAccountNamespaceGroup, serviceaccount.ServiceAccountGroupPrefix) + if !strings.HasPrefix(seedNamespace, gardenerutils.SeedNamespaceNamePrefix) { + return "", false, "" + } + + seedName := strings.TrimPrefix(seedNamespace, gardenerutils.SeedNamespaceNamePrefix) + name := strings.TrimPrefix(u.GetName(), serviceaccount.ServiceAccountUsernamePrefix+seedNamespace+serviceaccount.ServiceAccountUsernameSeparator) + + if seedName != "" && strings.HasPrefix(name, v1beta1constants.ExtensionGardenServiceAccountPrefix) { + return seedName, true, UserTypeExtension + } + + return "", false, "" +} diff --git a/pkg/admissioncontroller/seedidentity/identity_test.go b/pkg/admissioncontroller/seedidentity/identity_test.go index c254647b3fb..28cf7cb1336 100644 --- a/pkg/admissioncontroller/seedidentity/identity_test.go +++ b/pkg/admissioncontroller/seedidentity/identity_test.go @@ -27,46 +27,125 @@ import ( ) var _ = Describe("identity", func() { - DescribeTable("#FromUserInfoInterface", - func(u user.Info, expectedSeedName string, expectedIsSeedValue bool) { - seedName, isSeed := FromUserInfoInterface(u) + Describe("#FromUserInfoInterface", func() { + test := func(u user.Info, expectedSeedName string, expectedIsSeedValue bool, expectedUserType UserType) { + seedName, isSeed, userType := FromUserInfoInterface(u) Expect(seedName).To(Equal(expectedSeedName)) Expect(isSeed).To(Equal(expectedIsSeedValue)) - }, + Expect(userType).To(Equal(expectedUserType)) + } - Entry("nil", nil, "", false), - Entry("no user name prefix", &user.DefaultInfo{Name: "foo"}, "", false), - Entry("user name prefix but no groups", &user.DefaultInfo{Name: "gardener.cloud:system:seed:foo"}, "", false), - Entry("user name prefix but seed group not present", &user.DefaultInfo{Name: "gardener.cloud:system:seed:foo", Groups: []string{"bar"}}, "", false), - Entry("user name prefix and seed group", &user.DefaultInfo{Name: "gardener.cloud:system:seed:foo", Groups: []string{"gardener.cloud:system:seeds"}}, "foo", true), - ) + It("nil", func() { + test(nil, "", false, "") + }) - DescribeTable("#FromAuthenticationV1UserInfo", - func(u authenticationv1.UserInfo, expectedSeedName string, expectedIsSeedValue bool) { - seedName, isSeed := FromAuthenticationV1UserInfo(u) + It("no user name prefix", func() { + test(&user.DefaultInfo{Name: "foo"}, "", false, "") + }) + + It("user name prefix but no groups", func() { + test(&user.DefaultInfo{Name: "gardener.cloud:system:seed:foo"}, "", false, "") + }) + + It("user name prefix but seed group not present", func() { + test(&user.DefaultInfo{Name: "gardener.cloud:system:seed:foo", Groups: []string{"bar"}}, "", false, "") + }) + + It("user name prefix and seed group", func() { + test(&user.DefaultInfo{Name: "gardener.cloud:system:seed:foo", Groups: []string{"gardener.cloud:system:seeds"}}, "foo", true, UserTypeGardenlet) + }) + + It("ServiceAccount without groups", func() { + test(&user.DefaultInfo{Name: "system:serviceaccount:foo:bar"}, "", false, "") + }) + + It("ServiceAccount without namespace group", func() { + test(&user.DefaultInfo{Name: "system:serviceaccount:foo:bar", Groups: []string{"system:serviceaccounts"}}, "", false, "") + }) + + It("ServiceAccount in non-seed namespace", func() { + test(&user.DefaultInfo{Name: "system:serviceaccount:foo:bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo"}}, "", false, "") + }) + + It("Non-extension ServiceAccount in seed namespace", func() { + test(&user.DefaultInfo{Name: "system:serviceaccount:seed-foo:bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:seed-foo"}}, "", false, "") + }) + + It("Extension ServiceAccount in seed namespace", func() { + test(&user.DefaultInfo{Name: "system:serviceaccount:seed-foo:extension-bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:seed-foo"}}, "foo", true, UserTypeExtension) + }) + }) + + Describe("#FromAuthenticationV1UserInfo", func() { + test := func(u authenticationv1.UserInfo, expectedSeedName string, expectedIsSeedValue bool, expectedUserType UserType) { + seedName, isSeed, userType := FromAuthenticationV1UserInfo(u) Expect(seedName).To(Equal(expectedSeedName)) Expect(isSeed).To(Equal(expectedIsSeedValue)) - }, + Expect(userType).To(Equal(expectedUserType)) + } + + It("no user name prefix", func() { + test(authenticationv1.UserInfo{Username: "foo"}, "", false, "") + }) - Entry("no user name prefix", authenticationv1.UserInfo{Username: "foo"}, "", false), - Entry("user name prefix but no groups", authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo"}, "", false), - Entry("user name prefix but seed group not present", authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo", Groups: []string{"bar"}}, "", false), - Entry("user name prefix and seed group", authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo", Groups: []string{"gardener.cloud:system:seeds"}}, "foo", true), - ) + It("user name prefix but no groups", func() { + test(authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo"}, "", false, "") + }) - DescribeTable("#FromCertificateSigningRequest", - func(csr *x509.CertificateRequest, expectedSeedName string, expectedIsSeedValue bool) { - seedName, isSeed := FromCertificateSigningRequest(csr) + It("user name prefix but seed group not present", func() { + test(authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo", Groups: []string{"bar"}}, "", false, "") + }) + + It("user name prefix and seed group", func() { + test(authenticationv1.UserInfo{Username: "gardener.cloud:system:seed:foo", Groups: []string{"gardener.cloud:system:seeds"}}, "foo", true, UserTypeGardenlet) + }) + + It("ServiceAccount without groups", func() { + test(authenticationv1.UserInfo{Username: "system:serviceaccount:foo:bar"}, "", false, "") + }) + + It("ServiceAccount without namespace group", func() { + test(authenticationv1.UserInfo{Username: "system:serviceaccount:foo:bar", Groups: []string{"system:serviceaccounts"}}, "", false, "") + }) + + It("ServiceAccount in non-seed namespace", func() { + test(authenticationv1.UserInfo{Username: "system:serviceaccount:foo:bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo"}}, "", false, "") + }) + + It("Non-extension ServiceAccount in seed namespace", func() { + test(authenticationv1.UserInfo{Username: "system:serviceaccount:seed-foo:bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:seed-foo"}}, "", false, "") + }) + + It("Extension ServiceAccount in seed namespace", func() { + test(authenticationv1.UserInfo{Username: "system:serviceaccount:seed-foo:extension-bar", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:seed-foo"}}, "foo", true, UserTypeExtension) + }) + }) + + Describe("#FromCertificateSigningRequest", func() { + test := func(csr *x509.CertificateRequest, expectedSeedName string, expectedIsSeedValue bool, expectedUserType UserType) { + seedName, isSeed, userType := FromCertificateSigningRequest(csr) Expect(seedName).To(Equal(expectedSeedName)) Expect(isSeed).To(Equal(expectedIsSeedValue)) - }, + Expect(userType).To(Equal(expectedUserType)) + } + + It("no user name prefix", func() { + test(&x509.CertificateRequest{Subject: pkix.Name{CommonName: "foo"}}, "", false, "") + }) + + It("user name prefix but no groups", func() { + test(&x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo"}}, "", false, "") + }) + + It("user name prefix but seed group not present", func() { + test(&x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo", Organization: []string{"bar"}}}, "", false, "") + }) - Entry("no user name prefix", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "foo"}}, "", false), - Entry("user name prefix but no groups", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo"}}, "", false), - Entry("user name prefix but seed group not present", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo", Organization: []string{"bar"}}}, "", false), - Entry("user name prefix and seed group", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo", Organization: []string{"gardener.cloud:system:seeds"}}}, "foo", true), - ) + It("user name prefix and seed group", func() { + test(&x509.CertificateRequest{Subject: pkix.Name{CommonName: "gardener.cloud:system:seed:foo", Organization: []string{"gardener.cloud:system:seeds"}}}, "foo", true, UserTypeGardenlet) + }) + }) }) diff --git a/pkg/admissioncontroller/webhook/admission/seedrestriction/handler.go b/pkg/admissioncontroller/webhook/admission/seedrestriction/handler.go index c8eba34d0eb..949d2d496a6 100644 --- a/pkg/admissioncontroller/webhook/admission/seedrestriction/handler.go +++ b/pkg/admissioncontroller/webhook/admission/seedrestriction/handler.go @@ -79,7 +79,7 @@ func (h *Handler) InjectDecoder(d *admission.Decoder) error { // Handle restricts requests made by gardenlets. func (h *Handler) Handle(ctx context.Context, request admission.Request) admission.Response { - seedName, isSeed := seedidentity.FromAuthenticationV1UserInfo(request.UserInfo) + seedName, isSeed, userType := seedidentity.FromAuthenticationV1UserInfo(request.UserInfo) if !isSeed { return admissionwebhook.Allowed("") } @@ -93,19 +93,19 @@ func (h *Handler) Handle(ctx context.Context, request admission.Request) admissi case bastionResource: return h.admitBastion(seedName, request) case certificateSigningRequestResource: - return h.admitCertificateSigningRequest(seedName, request) + return h.admitCertificateSigningRequest(seedName, userType, request) case clusterRoleBindingResource: - return h.admitClusterRoleBinding(ctx, seedName, request) + return h.admitClusterRoleBinding(ctx, seedName, userType, request) case internalSecretResource: return h.admitInternalSecret(ctx, seedName, request) case leaseResource: - return h.admitLease(seedName, request) + return h.admitLease(seedName, userType, request) case secretResource: return h.admitSecret(ctx, seedName, request) case seedResource: return h.admitSeed(ctx, seedName, request) case serviceAccountResource: - return h.admitServiceAccount(ctx, seedName, request) + return h.admitServiceAccount(ctx, seedName, userType, request) case shootStateResource: return h.admitShootState(ctx, seedName, request) } @@ -211,11 +211,15 @@ func (h *Handler) admitBastion(seedName string, request admission.Request) admis return h.admit(seedName, bastion.Spec.SeedName) } -func (h *Handler) admitCertificateSigningRequest(seedName string, request admission.Request) admission.Response { +func (h *Handler) admitCertificateSigningRequest(seedName string, userType seedidentity.UserType, request admission.Request) admission.Response { if request.Operation != admissionv1.Create { return admission.Errored(http.StatusBadRequest, fmt.Errorf("unexpected operation: %q", request.Operation)) } + if userType == seedidentity.UserTypeExtension { + return admission.Errored(http.StatusForbidden, fmt.Errorf("extension client may not create CertificateSigningRequests")) + } + csr := &certificatesv1.CertificateSigningRequest{} if err := h.decoder.Decode(request, csr); err != nil { return admission.Errored(http.StatusBadRequest, err) @@ -230,15 +234,19 @@ func (h *Handler) admitCertificateSigningRequest(seedName string, request admiss return admission.Errored(http.StatusForbidden, fmt.Errorf("can only create CSRs for seed clusters: %s", reason)) } - seedNameInCSR, _ := seedidentity.FromCertificateSigningRequest(x509cr) + seedNameInCSR, _, _ := seedidentity.FromCertificateSigningRequest(x509cr) return h.admit(seedName, &seedNameInCSR) } -func (h *Handler) admitClusterRoleBinding(ctx context.Context, seedName string, request admission.Request) admission.Response { +func (h *Handler) admitClusterRoleBinding(ctx context.Context, seedName string, userType seedidentity.UserType, request admission.Request) admission.Response { if request.Operation != admissionv1.Create { return admission.Errored(http.StatusBadRequest, fmt.Errorf("unexpected operation: %q", request.Operation)) } + if userType == seedidentity.UserTypeExtension { + return admission.Errored(http.StatusForbidden, fmt.Errorf("extension client may not create ClusterRoleBindings")) + } + // Allow gardenlet to create cluster role bindings referencing service accounts which can be used to bootstrap other // gardenlets deployed as part of the ManagedSeed reconciliation. if strings.HasPrefix(request.Name, gardenletbootstraputil.ClusterRoleBindingNamePrefix) { @@ -282,11 +290,19 @@ func (h *Handler) admitInternalSecret(ctx context.Context, seedName string, requ return admission.Errored(http.StatusForbidden, fmt.Errorf("object does not belong to seed %q", seedName)) } -func (h *Handler) admitLease(seedName string, request admission.Request) admission.Response { +func (h *Handler) admitLease(seedName string, userType seedidentity.UserType, request admission.Request) admission.Response { if request.Operation != admissionv1.Create { return admission.Errored(http.StatusBadRequest, fmt.Errorf("unexpected operation: %q", request.Operation)) } + // extension clients may only work with leases in the seed namespace + if userType == seedidentity.UserTypeExtension { + if request.Namespace == gardenerutils.ComputeGardenNamespace(seedName) { + return admission.Allowed("") + } + return admission.Errored(http.StatusForbidden, fmt.Errorf("extension client can only create leases in the namespace for seed %q", seedName)) + } + // This allows the gardenlet to create a Lease for leader election (if the garden cluster is a seed as well). if request.Name == "gardenlet-leader-election" { return admission.Allowed("") @@ -434,11 +450,15 @@ func (h *Handler) admitSeed(ctx context.Context, seedName string, request admiss return response } -func (h *Handler) admitServiceAccount(ctx context.Context, seedName string, request admission.Request) admission.Response { +func (h *Handler) admitServiceAccount(ctx context.Context, seedName string, userType seedidentity.UserType, request admission.Request) admission.Response { if request.Operation != admissionv1.Create { return admission.Errored(http.StatusBadRequest, fmt.Errorf("unexpected operation: %q", request.Operation)) } + if userType == seedidentity.UserTypeExtension { + return admission.Errored(http.StatusForbidden, fmt.Errorf("extension client may not create ServiceAccounts")) + } + // Allow gardenlet to create service accounts which can be used to bootstrap other gardenlets deployed as part of // the ManagedSeed reconciliation. if strings.HasPrefix(request.Name, gardenletbootstraputil.ServiceAccountNamePrefix) { diff --git a/pkg/admissioncontroller/webhook/admission/seedrestriction/handler_test.go b/pkg/admissioncontroller/webhook/admission/seedrestriction/handler_test.go index 408566e803d..e10bb9e0641 100644 --- a/pkg/admissioncontroller/webhook/admission/seedrestriction/handler_test.go +++ b/pkg/admissioncontroller/webhook/admission/seedrestriction/handler_test.go @@ -36,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" logzap "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -50,6 +51,7 @@ import ( gardenletv1alpha1 "github.com/gardener/gardener/pkg/gardenlet/apis/config/v1alpha1" "github.com/gardener/gardener/pkg/logger" mockcache "github.com/gardener/gardener/pkg/mock/controller-runtime/cache" + gardenerutils "github.com/gardener/gardener/pkg/utils/gardener" kubernetesutils "github.com/gardener/gardener/pkg/utils/kubernetes" ) @@ -68,8 +70,10 @@ var _ = Describe("handler", func() { request admission.Request encoder runtime.Encoder - seedName string - seedUser authenticationv1.UserInfo + seedName string + seedUser authenticationv1.UserInfo + gardenletUser authenticationv1.UserInfo + extensionUser authenticationv1.UserInfo responseAllowed = admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -95,10 +99,18 @@ var _ = Describe("handler", func() { Expect(admission.InjectDecoderInto(decoder, handler)).To(BeTrue()) seedName = "seed" - seedUser = authenticationv1.UserInfo{ + gardenletUser = authenticationv1.UserInfo{ Username: fmt.Sprintf("%s%s", v1beta1constants.SeedUserNamePrefix, seedName), Groups: []string{v1beta1constants.SeedsGroup}, } + extensionUserInfo := (&serviceaccount.ServiceAccountInfo{ + Name: v1beta1constants.ExtensionGardenServiceAccountPrefix + "provider-local", + Namespace: gardenerutils.SeedNamespaceNamePrefix + seedName, + }).UserInfo() + extensionUser = authenticationv1.UserInfo{ + Username: extensionUserInfo.GetName(), + Groups: extensionUserInfo.GetGroups(), + } }) Describe("#Handle", func() { @@ -110,472 +122,226 @@ var _ = Describe("handler", func() { }) It("should have no opinion because no resource request", func() { - request.UserInfo = seedUser + request.UserInfo = gardenletUser Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) It("should have no opinion because resource is irrelevant", func() { - request.UserInfo = seedUser + request.UserInfo = gardenletUser request.Resource = metav1.GroupVersionResource{} Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) }) - Context("when requested for ShootStates", func() { - var name, namespace string - - BeforeEach(func() { - name, namespace = "foo", "bar" - - request.Name = name - request.Namespace = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "shootstates", - } - }) - - DescribeTable("should forbid because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + testCommonAccess := func() { + Context("when requested for ShootStates", func() { + var name, namespace string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should return an error because fetching the related shoot failed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) + name, namespace = "foo", "bar" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "shootstates", + } }) - DescribeTable("should forbid the request because the seed name of the related shoot does not match", - func(seedNameInShoot *string) { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: seedNameInShoot}}).DeepCopyInto(obj) - return nil - }) + DescribeTable("should forbid because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) }, - Entry("seed name is nil", nil), - Entry("seed name is different", pointer.String("some-different-seed")), + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), ) - It("should allow the request because seed name in spec matches", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) - return nil - }) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - - It("should allow the request because seed name in status matches", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Status: gardencorev1beta1.ShootStatus{SeedName: &seedName}}).DeepCopyInto(obj) - return nil + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create }) - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - }) - - Context("when requested for BackupBuckets", func() { - var name string - - BeforeEach(func() { - name = "foo" - - request.Name = name - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "backupbuckets", - } - }) - - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - - Entry("update", admissionv1.Update), - ) - - Context("when operation is create", func() { - BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should return an error because decoding the object failed", func() { - request.Object.Raw = []byte(`{]`) - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", - }, - }, - })) - }) - - DescribeTable("should forbid the request because the seed name of the related bucket does not match", - func(seedNameInBackupBucket *string) { - objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupBucket{ - Spec: gardencorev1beta1.BackupBucketSpec{ - SeedName: seedNameInBackupBucket, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + It("should return an error because fetching the related shoot failed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), }, }, })) - }, - - Entry("seed name is nil", nil), - Entry("seed name is different", pointer.String("some-different-seed")), - ) - - It("should allow the request because seed name matches", func() { - objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupBucket{ - Spec: gardencorev1beta1.BackupBucketSpec{ - SeedName: &seedName, - }, }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) + DescribeTable("should forbid the request because the seed name of the related shoot does not match", + func(seedNameInShoot *string) { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: seedNameInShoot}}).DeepCopyInto(obj) + return nil + }) - Context("when operation is delete", func() { - BeforeEach(func() { - request.Operation = admissionv1.Delete - }) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }, - It("should return an error because reading the Seed failed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) + Entry("seed name is nil", nil), + Entry("seed name is different", pointer.String("some-different-seed")), + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) - }) + It("should allow the request because seed name in spec matches", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) - It("should forbid the request because the seed UID and the bucket name does not match", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { - (&gardencorev1beta1.Seed{ObjectMeta: metav1.ObjectMeta{UID: "1234"}}).DeepCopyInto(obj) - return nil + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: "cannot delete unrelated BackupBucket", - }, - }, - })) - }) - - It("should allow the request because the seed UID and the bucket name does match", func() { - uid := "some-seed-uid" - request.Name = uid + It("should allow the request because seed name in status matches", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Status: gardencorev1beta1.ShootStatus{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { - (&gardencorev1beta1.Seed{ObjectMeta: metav1.ObjectMeta{UID: types.UID(uid)}}).DeepCopyInto(obj) - return nil + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) }) - }) - - Context("when requested for BackupEntries", func() { - var name, namespace, bucketName string - - BeforeEach(func() { - name = "foo" - namespace = "bar" - bucketName = "bucket" - - request.Name = name - request.Namespace = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "backupentries", - } - }) - - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + Context("when requested for BackupBuckets", func() { + var name string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should return an error because decoding the object failed", func() { - request.Object.Raw = []byte(`{]`) + name = "foo" - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", - }, - }, - })) + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "backupbuckets", + } }) - DescribeTable("should forbid the request because the seed name of the related entry does not match", - func(seedNameInBackupEntry, seedNameInBackupBucket *string) { - objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ - Spec: gardencorev1beta1.BackupEntrySpec{ - BucketName: bucketName, - SeedName: seedNameInBackupEntry, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - if seedNameInBackupEntry != nil && *seedNameInBackupEntry == seedName { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(bucketName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { - (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: seedNameInBackupBucket}}).DeepCopyInto(obj) - return nil - }) - } + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) }, - Entry("seed name is nil", nil, nil), - Entry("seed name is different", pointer.String("some-different-seed"), nil), - Entry("seed name is equal but bucket's seed name is nil", &seedName, nil), - Entry("seed name is equal but bucket's seed name is different", &seedName, pointer.String("some-different-seed")), + Entry("update", admissionv1.Update), ) - It("should allow the request because seed name matches for both entry and bucket", func() { - objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ - Spec: gardencorev1beta1.BackupEntrySpec{ - BucketName: bucketName, - SeedName: &seedName, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(bucketName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { - (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: &seedName}}).DeepCopyInto(obj) - return nil - }) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - - Context("when creating a source BackupEntry", func() { - const ( - shootBackupEntryName = "backupentry" - shootName = "foo" - ) - - var shoot *gardencorev1beta1.Shoot - + Context("when operation is create", func() { BeforeEach(func() { - objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-%s", v1beta1constants.BackupSourcePrefix, shootBackupEntryName), - Namespace: namespace, - OwnerReferences: []metav1.OwnerReference{ - { - Name: shootName, - Kind: "Shoot", - }, - }, - }, - Spec: gardencorev1beta1.BackupEntrySpec{ - BucketName: bucketName, - SeedName: &seedName, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - shoot = &gardencorev1beta1.Shoot{ - Status: gardencorev1beta1.ShootStatus{ - LastOperation: &gardencorev1beta1.LastOperation{ - Type: gardencorev1beta1.LastOperationTypeRestore, - State: gardencorev1beta1.LastOperationStateProcessing, - }, - }, - } + request.Operation = admissionv1.Create }) - It("should forbid the request because the shoot owning the source BackupEntry could not be found", func() { - notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, "") - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(notFoundErr) + It("should return an error because decoding the object failed", func() { + request.Object.Raw = []byte(`{]`) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: notFoundErr.Error(), + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", }, }, })) }) - DescribeTable("should forbid the request because a the shoot owning the source BackupEntry is not in restore phase", - func(lastOperation *gardencorev1beta1.LastOperation) { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.Status.LastOperation = lastOperation - shoot.DeepCopyInto(obj) - return nil + DescribeTable("should forbid the request because the seed name of the related bucket does not match", + func(seedNameInBackupBucket *string) { + objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupBucket{ + Spec: gardencorev1beta1.BackupBucketSpec{ + SeedName: seedNameInBackupBucket, + }, }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("creation of source BackupEntry is only allowed during shoot Restore operation (shoot: %s)", shootName), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), }, }, })) }, - Entry("lastOperation is nil", nil), - Entry("lastOperation is create", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeCreate}), - Entry("lastOperation is reconcile", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeReconcile}), - Entry("lastOperation is delete", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeDelete}), - Entry("lastOperation is migrate", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeMigrate}), - ) - It("should forbid the request because a BackupEntry for the shoot does not exist", func() { - notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, "") + Entry("seed name is nil", nil), + Entry("seed name is different", pointer.String("some-different-seed")), + ) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil + It("should allow the request because seed name matches", func() { + objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupBucket{ + Spec: gardencorev1beta1.BackupBucketSpec{ + SeedName: &seedName, + }, }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).Return(notFoundErr) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + + Context("when operation is delete", func() { + BeforeEach(func() { + request.Operation = admissionv1.Delete + }) + + It("should return an error because reading the Seed failed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("could not find original BackupEntry %s: %v", shootBackupEntryName, notFoundErr.Error()), + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), }, }, })) }) - It("should forbid the request because the source BackupEntry does not match the BackupEntry for the shoot", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupEntry, _ ...client.GetOption) error { - be := &gardencorev1beta1.BackupEntry{ - Spec: gardencorev1beta1.BackupEntrySpec{ - BucketName: "some-different-bucket", - SeedName: pointer.String("some-differnet-seedname"), - }, - } - be.DeepCopyInto(obj) + It("should forbid the request because the seed UID and the bucket name does not match", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { + (&gardencorev1beta1.Seed{ObjectMeta: metav1.ObjectMeta{UID: "1234"}}).DeepCopyInto(obj) return nil }) @@ -584,25 +350,18 @@ var _ = Describe("handler", func() { Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("specification of source BackupEntry must equal specification of original BackupEntry %s", shootBackupEntryName), + Message: "cannot delete unrelated BackupBucket", }, }, })) }) - It("should allow creation of source BackupEntry if a matching BackupEntry exists and shoot is in restore phase", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupEntry, _ ...client.GetOption) error { - be := &gardencorev1beta1.BackupEntry{ - Spec: gardencorev1beta1.BackupEntrySpec{ - BucketName: bucketName, - SeedName: &seedName, - }, - } - be.DeepCopyInto(obj) + It("should allow the request because the seed UID and the bucket name does match", func() { + uid := "some-seed-uid" + request.Name = uid + + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(seedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { + (&gardencorev1beta1.Seed{ObjectMeta: metav1.ObjectMeta{UID: types.UID(uid)}}).DeepCopyInto(obj) return nil }) @@ -610,246 +369,399 @@ var _ = Describe("handler", func() { }) }) }) - }) - Context("when requested for Bastions", func() { - var name string + Context("when requested for BackupEntries", func() { + var name, namespace, bucketName string - BeforeEach(func() { - name = "foo" + BeforeEach(func() { + name = "foo" + namespace = "bar" + bucketName = "bucket" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "backupentries", + } + }) - request.Name = name - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: operationsv1alpha1.SchemeGroupVersion.Group, - Resource: "bastions", - } - }) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation - DescribeTable("should have no opinion because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), + }, }, - }, - })) - }, + })) + }, - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Context("when operation is create", func() { - BeforeEach(func() { - request.Operation = admissionv1.Create - }) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create + }) - It("should return an error because decoding the object failed", func() { - request.Object.Raw = []byte(`{]`) + It("should return an error because decoding the object failed", func() { + request.Object.Raw = []byte(`{]`) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + }, }, - }, - })) - }) + })) + }) + + DescribeTable("should forbid the request because the seed name of the related entry does not match", + func(seedNameInBackupEntry, seedNameInBackupBucket *string) { + objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ + Spec: gardencorev1beta1.BackupEntrySpec{ + BucketName: bucketName, + SeedName: seedNameInBackupEntry, + }, + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + if seedNameInBackupEntry != nil && *seedNameInBackupEntry == seedName { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(bucketName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { + (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: seedNameInBackupBucket}}).DeepCopyInto(obj) + return nil + }) + } - It("should allow the request because seed name matches", func() { - objData, err := runtime.Encode(encoder, &operationsv1alpha1.Bastion{ - Spec: operationsv1alpha1.BastionSpec{ - SeedName: &seedName, + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) }, + + Entry("seed name is nil", nil, nil), + Entry("seed name is different", pointer.String("some-different-seed"), nil), + Entry("seed name is equal but bucket's seed name is nil", &seedName, nil), + Entry("seed name is equal but bucket's seed name is different", &seedName, pointer.String("some-different-seed")), + ) + + It("should allow the request because seed name matches for both entry and bucket", func() { + objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ + Spec: gardencorev1beta1.BackupEntrySpec{ + BucketName: bucketName, + SeedName: &seedName, + }, + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(bucketName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { + (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - }) + Context("when creating a source BackupEntry", func() { + const ( + shootBackupEntryName = "backupentry" + shootName = "foo" + ) - Context("when requested for Leases", func() { - var name, namespace string + var shoot *gardencorev1beta1.Shoot - BeforeEach(func() { - name, namespace = "foo", "bar" - - request.Name = name - request.Namespace = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: coordinationv1.SchemeGroupVersion.Group, - Resource: "leases", - } - }) + BeforeEach(func() { + objData, err := runtime.Encode(encoder, &gardencorev1beta1.BackupEntry{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", v1beta1constants.BackupSourcePrefix, shootBackupEntryName), + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + { + Name: shootName, + Kind: "Shoot", + }, + }, + }, + Spec: gardencorev1beta1.BackupEntrySpec{ + BucketName: bucketName, + SeedName: &seedName, + }, + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + shoot = &gardencorev1beta1.Shoot{ + Status: gardencorev1beta1.ShootStatus{ + LastOperation: &gardencorev1beta1.LastOperation{ + Type: gardencorev1beta1.LastOperationTypeRestore, + State: gardencorev1beta1.LastOperationStateProcessing, + }, + }, + } + }) - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation + It("should forbid the request because the shoot owning the source BackupEntry could not be found", func() { + notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, "") + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(notFoundErr) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: notFoundErr.Error(), + }, + }, + })) + }) + + DescribeTable("should forbid the request because a the shoot owning the source BackupEntry is not in restore phase", + func(lastOperation *gardencorev1beta1.LastOperation) { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.Status.LastOperation = lastOperation + shoot.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("creation of source BackupEntry is only allowed during shoot Restore operation (shoot: %s)", shootName), + }, + }, + })) }, - }, - })) - }, + Entry("lastOperation is nil", nil), + Entry("lastOperation is create", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeCreate}), + Entry("lastOperation is reconcile", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeReconcile}), + Entry("lastOperation is delete", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeDelete}), + Entry("lastOperation is migrate", &gardencorev1beta1.LastOperation{Type: gardencorev1beta1.LastOperationTypeMigrate}), + ) - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + It("should forbid the request because a BackupEntry for the shoot does not exist", func() { + notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, "") + + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).Return(notFoundErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("could not find original BackupEntry %s: %v", shootBackupEntryName, notFoundErr.Error()), + }, + }, + })) + }) + + It("should forbid the request because the source BackupEntry does not match the BackupEntry for the shoot", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupEntry, _ ...client.GetOption) error { + be := &gardencorev1beta1.BackupEntry{ + Spec: gardencorev1beta1.BackupEntrySpec{ + BucketName: "some-different-bucket", + SeedName: pointer.String("some-differnet-seedname"), + }, + } + be.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("specification of source BackupEntry must equal specification of original BackupEntry %s", shootBackupEntryName), + }, + }, + })) + }) + + It("should allow creation of source BackupEntry if a matching BackupEntry exists and shoot is in restore phase", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, shootBackupEntryName), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupEntry{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupEntry, _ ...client.GetOption) error { + be := &gardencorev1beta1.BackupEntry{ + Spec: gardencorev1beta1.BackupEntrySpec{ + BucketName: bucketName, + SeedName: &seedName, + }, + } + be.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + }) + }) + + Context("when requested for Bastions", func() { + var name string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create + name = "foo" + + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: operationsv1alpha1.SchemeGroupVersion.Group, + Resource: "bastions", + } }) - DescribeTable("should forbid the request because the seed name of the lease does not match", - func(seedNameInLease string) { - request.Name = seedNameInLease + DescribeTable("should have no opinion because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) }, - Entry("seed name is different", "some-different-seed"), + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), ) - It("should allow the request because lease is used for leader-election", func() { - request.Name = "gardenlet-leader-election" - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - - It("should allow the request because seed name matches", func() { - request.Name = seedName - request.Namespace = "gardener-system-seed-lease" - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - }) - - Context("when requested for Seeds", func() { - var name string - - BeforeEach(func() { - name = "foo" + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create + }) - request.Name = name - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "seeds", - } - }) + It("should return an error because decoding the object failed", func() { + request.Object.Raw = []byte(`{]`) - generateTestsForOperation := func(operation admissionv1.Operation) func() { - return func() { - BeforeEach(func() { - request.Operation = operation + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + }, + }, + })) }) It("should allow the request because seed name matches", func() { - request.Name = seedName + objData, err := runtime.Encode(encoder, &operationsv1alpha1.Bastion{ + Spec: operationsv1alpha1.BastionSpec{ + SeedName: &seedName, + }, + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) + }) + }) - Context("requiring information from managedseed", func() { - var ( - differentSeedName = "some-different-seed" - managedSeedNamespace = "garden" - shootName = "shoot" - ) + Context("when requested for Seeds", func() { + var name string + + BeforeEach(func() { + name = "foo" + + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "seeds", + } + }) + generateTestsForOperation := func(operation admissionv1.Operation) func() { + return func() { BeforeEach(func() { - request.Name = differentSeedName + request.Operation = operation }) - It("should forbid the request because seed does not belong to a managedseed", func() { - if request.Operation == admissionv1.Delete { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) - } + It("should allow the request because seed name matches", func() { + request.Name = seedName - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), - }, - }, - })) + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) - if operation == admissionv1.Delete { - It("should forbid the request because an error occurred while fetching the managedseed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) + Context("requiring information from managedseed", func() { + var ( + differentSeedName = "some-different-seed" + managedSeedNamespace = "garden" + shootName = "shoot" + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) + BeforeEach(func() { + request.Name = differentSeedName }) - It("should forbid the request because managedseed's `.metadata.deletionTimestamp` is nil", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - (&seedmanagementv1alpha1.ManagedSeed{}).DeepCopyInto(obj) - return nil - }) + It("should forbid the request because seed does not belong to a managedseed", func() { + if request.Operation == admissionv1.Delete { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) + } Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: "object can only be deleted if corresponding ManagedSeed has a deletion timestamp", + Message: fmt.Sprintf("object does not belong to seed %q", seedName), }, }, })) }) - } - if operation == admissionv1.Delete { - Context("requiring information from shoot", func() { - var deletionTimestamp *metav1.Time + if operation == admissionv1.Delete { + It("should forbid the request because an error occurred while fetching the managedseed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) - BeforeEach(func() { - deletionTimestamp = &metav1.Time{} + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) }) - It("should forbid the request because managedseed's `.spec.shoot` is nil", func() { + It("should forbid the request because managedseed's `.metadata.deletionTimestamp` is nil", func() { mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - (&seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: deletionTimestamp}, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{}, - }).DeepCopyInto(obj) + (&seedmanagementv1alpha1.ManagedSeed{}).DeepCopyInto(obj) return nil }) @@ -858,40 +770,42 @@ var _ = Describe("handler", func() { Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Message: "object can only be deleted if corresponding ManagedSeed has a deletion timestamp", }, }, })) }) + } - It("should forbid the request because reading the shoot referenced by the managedseed failed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - (&seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: managedSeedNamespace, - DeletionTimestamp: deletionTimestamp, - }, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{Name: shootName}, - }, - }).DeepCopyInto(obj) - return nil + if operation == admissionv1.Delete { + Context("requiring information from shoot", func() { + var deletionTimestamp *metav1.Time + + BeforeEach(func() { + deletionTimestamp = &metav1.Time{} }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + It("should forbid the request because managedseed's `.spec.shoot` is nil", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + (&seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: deletionTimestamp}, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{}, + }).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, }, - }, - })) - }) + })) + }) - DescribeTable("should forbid the request because the seed name of the shoot referenced by the managedseed does not match", - func(seedNameInShoot *string) { + It("should forbid the request because reading the shoot referenced by the managedseed failed", func() { mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { (&seedmanagementv1alpha1.ManagedSeed{ ObjectMeta: metav1.ObjectMeta{ @@ -904,322 +818,125 @@ var _ = Describe("handler", func() { }).DeepCopyInto(obj) return nil }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: seedNameInShoot}}).DeepCopyInto(obj) - return nil - }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), }, }, })) - }, - - Entry("seed name is nil", nil), - Entry("seed name is different", pointer.String("some-different-seed")), - ) - - It("should allow the request because the seed name of the shoot referenced by the managedseed matches", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - (&seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: managedSeedNamespace, - DeletionTimestamp: deletionTimestamp, - }, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{Name: shootName}, - }, - }).DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) - return nil }) - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - } - }) - } - } + DescribeTable("should forbid the request because the seed name of the shoot referenced by the managedseed does not match", + func(seedNameInShoot *string) { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + (&seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: managedSeedNamespace, + DeletionTimestamp: deletionTimestamp, + }, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{Name: shootName}, + }, + }).DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: seedNameInShoot}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }, - Context("when operation is create", generateTestsForOperation(admissionv1.Create)) - Context("when operation is update", generateTestsForOperation(admissionv1.Update)) - Context("when operation is delete", generateTestsForOperation(admissionv1.Delete)) - }) + Entry("seed name is nil", nil), + Entry("seed name is different", pointer.String("some-different-seed")), + ) - Context("when requested for CertificateSigningRequests", func() { - var name string + It("should allow the request because the seed name of the shoot referenced by the managedseed matches", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, differentSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + (&seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: managedSeedNamespace, + DeletionTimestamp: deletionTimestamp, + }, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{Name: shootName}, + }, + }).DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) - BeforeEach(func() { - name = "foo" - - request.Name = name - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: certificatesv1.SchemeGroupVersion.Group, - Version: "v1", - Resource: "certificatesigningrequests", + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + } + }) + } } - }) - - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) - - Context("when operation is create", func() { - BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should return an error because decoding the object failed", func() { - request.Object.Raw = []byte(`{]`) - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", - }, - }, - })) - }) - - It("should forbid the request because the CSR is not a valid seed-related CSR", func() { - objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ - TypeMeta: metav1.TypeMeta{ - APIVersion: certificatesv1.SchemeGroupVersion.String(), - Kind: "CertificateSigningRequest", - }, - Spec: certificatesv1.CertificateSigningRequestSpec{ - Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- -MIIClzCCAX8CAQAwUjEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl -ZWRzMSowKAYDVQQDEyFnYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpteXNlZWQw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNgJWhogJrCSzAhKKmHkJ -FuooKAbxpWRGDOe5DiB8jPdgCoRCkZYnF7D9x9cDzliljA9IeBad3P3E9oegtSV/ -sXFJYqb+lRuhJQ5oo2eBC6WRg+Oxglp+n7o7xt0bO7JHS977mqNrqsJ1d1FnJHTB -MPHPxqoqkgIbdW4t219ckSA20aWzC3PU7I7+Z9OD+YfuuYgzkWG541XyBBKVSD2w -Ix2yGu6zrslqZ1eVBZ4IoxpWrQNmLSMFQVnABThyEUi0U1eVtW0vPNwSnBf0mufX -Z0PpqAIPVjr64Z4s3HHml2GSu64iOxaG5wwb9qIPcdyFaQCep/sFh7kq1KjNI1Ql -AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAb+meLvm7dgHpzhu0XQ39w41FgpTv -S7p78ABFwzDNcP1NwfrEUft0T/rUwPiMlN9zve2rRicaZX5Z7Bol/newejsu8H5z -OdotvtKjE7zBCMzwnXZwO/0pA0cuUFcAy50DPcr35gdGjGlzV9ogO+HPKPTieS3n -TRVg+MWlcLqCjALr9Y4N39DOzf4/SJts8AZJJ+lyyxnY3XIPXx7SdADwNWC8BX0U -OK8CwMwN3iiBQ4redVeMK7LU1unV899q/PWB+NXFcKVr+Grm/Kom5VxuhXSzcHEp -yO57qEcJqG1cB7iSchFuCSTuDBbZlN0fXgn4YjiWZyb4l3BDp3rm4iJImA== ------END CERTIFICATE REQUEST-----`), - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: "can only create CSRs for seed clusters: key usages are not set to [key encipherment digital signature client auth]", - }, - }, - })) - }) - It("should forbid the request because the seed name of the csr does not match", func() { - objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ - TypeMeta: metav1.TypeMeta{ - APIVersion: certificatesv1.SchemeGroupVersion.String(), - Kind: "CertificateSigningRequest", - }, - Spec: certificatesv1.CertificateSigningRequestSpec{ - Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- -MIIClzCCAX8CAQAwUjEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl -ZWRzMSowKAYDVQQDEyFnYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpteXNlZWQw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNgJWhogJrCSzAhKKmHkJ -FuooKAbxpWRGDOe5DiB8jPdgCoRCkZYnF7D9x9cDzliljA9IeBad3P3E9oegtSV/ -sXFJYqb+lRuhJQ5oo2eBC6WRg+Oxglp+n7o7xt0bO7JHS977mqNrqsJ1d1FnJHTB -MPHPxqoqkgIbdW4t219ckSA20aWzC3PU7I7+Z9OD+YfuuYgzkWG541XyBBKVSD2w -Ix2yGu6zrslqZ1eVBZ4IoxpWrQNmLSMFQVnABThyEUi0U1eVtW0vPNwSnBf0mufX -Z0PpqAIPVjr64Z4s3HHml2GSu64iOxaG5wwb9qIPcdyFaQCep/sFh7kq1KjNI1Ql -AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAb+meLvm7dgHpzhu0XQ39w41FgpTv -S7p78ABFwzDNcP1NwfrEUft0T/rUwPiMlN9zve2rRicaZX5Z7Bol/newejsu8H5z -OdotvtKjE7zBCMzwnXZwO/0pA0cuUFcAy50DPcr35gdGjGlzV9ogO+HPKPTieS3n -TRVg+MWlcLqCjALr9Y4N39DOzf4/SJts8AZJJ+lyyxnY3XIPXx7SdADwNWC8BX0U -OK8CwMwN3iiBQ4redVeMK7LU1unV899q/PWB+NXFcKVr+Grm/Kom5VxuhXSzcHEp -yO57qEcJqG1cB7iSchFuCSTuDBbZlN0fXgn4YjiWZyb4l3BDp3rm4iJImA== ------END CERTIFICATE REQUEST-----`), - Usages: []certificatesv1.KeyUsage{ - certificatesv1.UsageKeyEncipherment, - certificatesv1.UsageDigitalSignature, - certificatesv1.UsageClientAuth, - }, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), - }, - }, - })) - }) - - It("should allow the request because seed name matches", func() { - objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ - TypeMeta: metav1.TypeMeta{ - APIVersion: certificatesv1.SchemeGroupVersion.String(), - Kind: "CertificateSigningRequest", - }, - Spec: certificatesv1.CertificateSigningRequestSpec{ - Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- -MIIClTCCAX0CAQAwUDEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl -ZWRzMSgwJgYDVQQDEx9nYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpzZWVkMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsDqibMtE5PXULTT12u0TYW1U -EI2f2MFImNPdEdmyTO8kjy61JzBQxUz6NLLmZWks7dnhZOrhfXqJjVzLWi7gAAIH -hkoxnu8spKTV53l6eY5RrivVsNFRuPF763bKd6JvsF1p9QD9y8uk6bY4NbLAjgMJ -MH64Sj398AnvLlIL+8XIFKtT/SjvOp99oGkKxWHBvokcz9MLUJc/2/JcOdsZ62ue -ZAsqimh0F085+BoG2YtLa4kLNAAiNsijgJ5QCXc7/F8uqkj4uy436LGgGmDfcQxC -9W2snEqriv1dsjF5R/kjh+UbTd+ZdHoAaNaiE7lfZcwe/ap6SNeZaszcDoR//wID -AQABoAAwDQYJKoZIhvcNAQELBQADggEBAKGWWWDHGHdUkOvE1L+tR/v3sDvLfmO7 -jWtF/Sq7kRCrr6xEHLKmVA4wRovpzOML0ntrDCu3npKAWqN+U56L1ZeZSsxyOhvN -dXjk2wPg0+IXPscd33hq0wGZRtBc5MHNWwYLv3ERKnHNbPE2ifkYy6FQ/h/2Kx55 -tHu5PlIwWS6CP+03s3/gjbHX7VL+V3RF5BIHDWcp9QfjN0zEx0R2WVXKIbhC8RTR -BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC -2L4LgmHdCmMFOtPkykwLK6wV1YW7Ce8AxU3j+q4kgZQ+51HJDQDdB74= ------END CERTIFICATE REQUEST-----`), - Usages: []certificatesv1.KeyUsage{ - certificatesv1.UsageKeyEncipherment, - certificatesv1.UsageDigitalSignature, - certificatesv1.UsageClientAuth, - }, - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - }) - - Context("when requested for Secrets", func() { - var name, namespace string - - BeforeEach(func() { - name, namespace = "foo", "bar" - - request.Name = name - request.Namespace = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: corev1.SchemeGroupVersion.Group, - Resource: "secrets", - } + Context("when operation is create", generateTestsForOperation(admissionv1.Create)) + Context("when operation is update", generateTestsForOperation(admissionv1.Update)) + Context("when operation is delete", generateTestsForOperation(admissionv1.Delete)) }) - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + Context("when requested for Secrets", func() { + var name, namespace string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should forbid the request because it's no expected secret", func() { - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})) - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), - }, - }, - })) + name, namespace = "foo", "bar" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Resource: "secrets", + } }) - Context("backupbucket secret", func() { - BeforeEach(func() { - request.Name = "generated-bucket-" + name - }) - - It("should return an error because the related backupbucket was not found", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf(" %q not found", name), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) + }, - It("should return an error because the related backupbucket could not be read", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).Return(fakeErr) + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create }) - It("should forbid because the related backupbucket does not belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { - (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) - return nil - }) + It("should forbid the request because it's no expected secret", func() { + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -1232,24 +949,13 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC })) }) - It("should allow because the related backupbucket does belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { - (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: &seedName}}).DeepCopyInto(obj) - return nil - }) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - - Context("shoot-related project secret", func() { - testSuite := func(suffix string) { + Context("backupbucket secret", func() { BeforeEach(func() { - request.Name = name + suffix + request.Name = "generated-bucket-" + name }) - It("should return an error because the related shoot was not found", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) + It("should return an error because the related backupbucket was not found", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -1262,8 +968,8 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC })) }) - It("should return an error because the related shoot could not be read", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + It("should return an error because the related backupbucket could not be read", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).Return(fakeErr) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -1276,9 +982,9 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC })) }) - It("should forbid because the related shoot does not belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) + It("should forbid because the related backupbucket does not belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { + (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) return nil }) @@ -1293,455 +999,967 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC })) }) - It("should allow because the related shoot does belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) + It("should allow because the related backupbucket does belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(name), gomock.AssignableToTypeOf(&gardencorev1beta1.BackupBucket{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.BackupBucket, _ ...client.GetOption) error { + (&gardencorev1beta1.BackupBucket{Spec: gardencorev1beta1.BackupBucketSpec{SeedName: &seedName}}).DeepCopyInto(obj) return nil }) Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) - } + }) - Describe("kubeconfig suffix", func() { testSuite(".kubeconfig") }) - Describe("ca-cluster suffix", func() { testSuite(".ca-cluster") }) - Describe("ssh-keypair suffix", func() { testSuite(".ssh-keypair") }) - Describe("ssh-keypair.old suffix", func() { testSuite(".ssh-keypair.old") }) - Describe("monitoring suffix", func() { testSuite(".monitoring") }) - }) + Context("shoot-related project secret", func() { + testSuite := func(suffix string) { + BeforeEach(func() { + request.Name = name + suffix + }) - Context("bootstrap token secret for managed seed", func() { - var ( - secret *corev1.Secret - managedSeed *seedmanagementv1alpha1.ManagedSeed - shoot *gardencorev1beta1.Shoot + It("should return an error because the related shoot was not found", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) - managedSeedNamespace = "ms1ns" - managedSeedName = "ms1name" - shootName = "ms1shoot" - ) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf(" %q not found", name), + }, + }, + })) + }) - BeforeEach(func() { - secret = &corev1.Secret{ - Type: corev1.SecretTypeBootstrapToken, - Data: map[string][]byte{ - "usage-bootstrap-authentication": []byte("true"), - "usage-bootstrap-signing": []byte("true"), - "description": []byte("A bootstrap token for the Gardenlet for managed seed " + managedSeedNamespace + "/" + managedSeedName + "."), - }, - } - managedSeed = &seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{ - Name: managedSeedName, - Namespace: managedSeedNamespace, - }, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{ - Name: shootName, - }, - }, - } - shoot = &gardencorev1beta1.Shoot{ - Spec: gardencorev1beta1.ShootSpec{ - SeedName: &seedName, - }, + It("should return an error because the related shoot could not be read", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should forbid because the related shoot does not belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) + + It("should allow because the related shoot does belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) } - request.Name = "bootstrap-token-123456" - request.Namespace = "kube-system" + Describe("kubeconfig suffix", func() { testSuite(".kubeconfig") }) + Describe("ca-cluster suffix", func() { testSuite(".ca-cluster") }) + Describe("ssh-keypair suffix", func() { testSuite(".ssh-keypair") }) + Describe("ssh-keypair.old suffix", func() { testSuite(".ssh-keypair.old") }) + Describe("monitoring suffix", func() { testSuite(".monitoring") }) + }) + + Context("bootstrap token secret for managed seed", func() { + var ( + secret *corev1.Secret + managedSeed *seedmanagementv1alpha1.ManagedSeed + shoot *gardencorev1beta1.Shoot + + managedSeedNamespace = "ms1ns" + managedSeedName = "ms1name" + shootName = "ms1shoot" + ) + + BeforeEach(func() { + secret = &corev1.Secret{ + Type: corev1.SecretTypeBootstrapToken, + Data: map[string][]byte{ + "usage-bootstrap-authentication": []byte("true"), + "usage-bootstrap-signing": []byte("true"), + "description": []byte("A bootstrap token for the Gardenlet for managed seed " + managedSeedNamespace + "/" + managedSeedName + "."), + }, + } + managedSeed = &seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedSeedName, + Namespace: managedSeedNamespace, + }, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{ + Name: shootName, + }, + }, + } + shoot = &gardencorev1beta1.Shoot{ + Spec: gardencorev1beta1.ShootSpec{ + SeedName: &seedName, + }, + } + + request.Name = "bootstrap-token-123456" + request.Namespace = "kube-system" + + objData, err := runtime.Encode(encoder, secret) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + }) + + It("should return an error if decoding the secret fails", func() { + request.Object.Raw = []byte(`{]`) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + }, + }, + })) + }) + + It("should return an error if the secret type is unexpected", func() { + secret.Type = corev1.SecretTypeOpaque + objData, err := runtime.Encode(encoder, secret) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusUnprocessableEntity), + Message: fmt.Sprintf("unexpected secret type: %q", secret.Type), + }, + }, + })) + }) + + It("should return an error if the usage-bootstrap-authentication field is unexpected", func() { + secret.Data["usage-bootstrap-authentication"] = []byte("false") + objData, err := runtime.Encode(encoder, secret) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusUnprocessableEntity), + Message: "\"usage-bootstrap-authentication\" must be set to 'true'", + }, + }, + })) + }) + + It("should return an error if the usage-bootstrap-signing field is unexpected", func() { + secret.Data["usage-bootstrap-signing"] = []byte("false") + objData, err := runtime.Encode(encoder, secret) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusUnprocessableEntity), + Message: "\"usage-bootstrap-signing\" must be set to 'true'", + }, + }, + })) + }) + + It("should return an error if the auth-extra-groups field is unexpected", func() { + secret.Data["auth-extra-groups"] = []byte("foo") + objData, err := runtime.Encode(encoder, secret) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusUnprocessableEntity), + Message: "\"auth-extra-groups\" must not be set", + }, + }, + })) + }) + + It("should forbid if the managedseed does not exist", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: " \"\" not found", + }, + }, + })) + }) + + It("should return an error if reading the managedseed fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should return an error if reading the shoot fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should return an error if the shoot does not belong to the gardenlet's seed", func() { + shoot.Spec.SeedName = pointer.String("some-other-seed") + + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) + + It("should return an error if reading the seed fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should forbid if the seed does exist already", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "managed seed " + managedSeedNamespace + "/" + managedSeedName + " is already bootstrapped", + }, + }, + })) + }) + + It("should allow if the seed does not yet exist", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + + It("should allow if the seed does exist but client cert is expired", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { + (&gardencorev1beta1.Seed{Status: gardencorev1beta1.SeedStatus{ClientCertificateExpirationTimestamp: &metav1.Time{Time: time.Now().Add(-time.Hour)}}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + + It("should allow if the seed does exist but the managedseed is annotated with the renew-kubeconfig annotation", func() { + managedSeed.Annotations = map[string]string{v1beta1constants.GardenerOperation: v1beta1constants.GardenerOperationRenewKubeconfig} + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + + Context("managed seed secret", func() { + var ( + managedSeed1Namespace string + shoot1, shoot2 *gardencorev1beta1.Shoot + seedConfig1, seedConfig2 *gardenletv1alpha1.SeedConfig + managedSeeds []seedmanagementv1alpha1.ManagedSeed + ) + + BeforeEach(func() { + managedSeed1Namespace = "ns1" + shoot1 = &gardencorev1beta1.Shoot{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: managedSeed1Namespace, + Name: "shoot1", + }, + Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-other-seed-name")}, + } + shoot2 = &gardencorev1beta1.Shoot{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: managedSeed1Namespace, + Name: "shoot2", + }, + Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}, + } + seedConfig1 = &gardenletv1alpha1.SeedConfig{ + SeedTemplate: gardencorev1beta1.SeedTemplate{}, + } + seedConfig2 = &gardenletv1alpha1.SeedConfig{ + SeedTemplate: gardencorev1beta1.SeedTemplate{}, + } + managedSeeds = []seedmanagementv1alpha1.ManagedSeed{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: managedSeed1Namespace}, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{Name: shoot1.Name}, + Gardenlet: &seedmanagementv1alpha1.Gardenlet{ + Config: runtime.RawExtension{ + Object: &gardenletv1alpha1.GardenletConfiguration{ + SeedConfig: seedConfig1, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Namespace: managedSeed1Namespace}, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{Name: shoot2.Name}, + Gardenlet: &seedmanagementv1alpha1.Gardenlet{ + Config: runtime.RawExtension{ + Object: &gardenletv1alpha1.GardenletConfiguration{ + SeedConfig: seedConfig2, + }, + }, + }, + }, + }, + } + }) + + It("should return an error because listing managed seeds failed", func() { + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should return an error because reading a shoot failed", func() { + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) + + It("should return an error because extracting the seed template failed", func() { + managedSeeds[1].Spec.Gardenlet = nil + + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot1.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot2.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: "no gardenlet specified in managedseed: \"\"", + }, + }, + })) + }) + + It("should forbid because the secret is referenced in a managedseed's gardenlet config but belongs to another seed", func() { + var ( + secretName = "secret-foo" + secretNamespace = "secret-bar" + ) + + request.Namespace = secretNamespace + request.Name = secretName + seedConfig1.Spec.SecretRef = &corev1.SecretReference{ + Name: secretName, + Namespace: secretNamespace, + } + + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot1.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot2.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) + + It("should forbid because the secret is referenced in a managedseed's gardenlet config but belongs to another seed", func() { + var ( + secretName = "secret-bar" + secretNamespace = "secret-foo" + ) + + request.Namespace = secretNamespace + request.Name = secretName + seedConfig1.Spec.Backup = &gardencorev1beta1.SeedBackup{ + SecretRef: corev1.SecretReference{ + Name: secretName, + Namespace: secretNamespace, + }, + } + + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot1.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot2.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) + + It("should allow because the secret is referenced in a managedseed's gardenlet config", func() { + var ( + secretName = "secret-foo" + secretNamespace = "secret-bar" + ) + + request.Namespace = secretNamespace + request.Name = secretName + seedConfig2.Spec.SecretRef = &corev1.SecretReference{ + Name: secretName, + Namespace: secretNamespace, + } + + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot1.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot2.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + + It("should allow because the secret is referenced in a managedseed's gardenlet config", func() { + var ( + secretName = "secret-bar" + secretNamespace = "secret-foo" + ) + + request.Namespace = secretNamespace + request.Name = secretName + seedConfig2.Spec.Backup = &gardencorev1beta1.SeedBackup{ + SecretRef: corev1.SecretReference{ + Name: secretName, + Namespace: secretNamespace, + }, + } + + mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { + (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot1.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot2.DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + }) + }) + + Context("when requested for InternalSecrets", func() { + var name, namespace string - objData, err := runtime.Encode(encoder, secret) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - }) + BeforeEach(func() { + name, namespace = "foo", "bar" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "internalsecrets", + } + }) - It("should return an error if decoding the secret fails", func() { - request.Object.Raw = []byte(`{]`) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) + }, - It("should return an error if the secret type is unexpected", func() { - secret.Type = corev1.SecretTypeOpaque - objData, err := runtime.Encode(encoder, secret) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusUnprocessableEntity), - Message: fmt.Sprintf("unexpected secret type: %q", secret.Type), - }, - }, - })) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create }) - It("should return an error if the usage-bootstrap-authentication field is unexpected", func() { - secret.Data["usage-bootstrap-authentication"] = []byte("false") - objData, err := runtime.Encode(encoder, secret) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData - + It("should forbid the request because it's no expected internal secret", func() { Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusUnprocessableEntity), - Message: "\"usage-bootstrap-authentication\" must be set to 'true'", + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), }, }, })) }) - It("should return an error if the usage-bootstrap-signing field is unexpected", func() { - secret.Data["usage-bootstrap-signing"] = []byte("false") - objData, err := runtime.Encode(encoder, secret) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + Context("shoot-related project secret", func() { + testSuite := func(suffix string) { + BeforeEach(func() { + request.Name = name + suffix + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusUnprocessableEntity), - Message: "\"usage-bootstrap-signing\" must be set to 'true'", - }, - }, - })) - }) + It("should return an error because the related shoot was not found", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) - It("should return an error if the auth-extra-groups field is unexpected", func() { - secret.Data["auth-extra-groups"] = []byte("foo") - objData, err := runtime.Encode(encoder, secret) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf(" %q not found", name), + }, + }, + })) + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusUnprocessableEntity), - Message: "\"auth-extra-groups\" must not be set", - }, - }, - })) - }) + It("should return an error because the related shoot could not be read", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) - It("should forbid if the managedseed does not exist", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) + It("should forbid because the related shoot does not belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) + return nil + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: " \"\" not found", - }, - }, - })) - }) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) - It("should return an error if reading the managedseed fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) + It("should allow because the related shoot does belong to gardenlet's seed", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) + return nil + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + } + + Describe("ca-client suffix", func() { testSuite(".ca-client") }) }) + }) + }) + } - It("should return an error if reading the shoot fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + Context("gardenlet client", func() { + BeforeEach(func() { + seedUser = gardenletUser + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) - }) + testCommonAccess() - It("should return an error if the shoot does not belong to the gardenlet's seed", func() { - shoot.Spec.SeedName = pointer.String("some-other-seed") + Context("when requested for CertificateSigningRequests", func() { + var name string - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) + BeforeEach(func() { + name = "foo" + + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: certificatesv1.SchemeGroupVersion.Group, + Version: "v1", + Resource: "certificatesigningrequests", + } + }) + + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) + }, + + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) + + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create }) - It("should return an error if reading the seed fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) + It("should return an error because decoding the object failed", func() { + request.Object.Raw = []byte(`{]`) Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", }, }, })) }) - It("should forbid if the seed does exist already", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil + It("should forbid the request because the CSR is not a valid seed-related CSR", func() { + objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: certificatesv1.SchemeGroupVersion.String(), + Kind: "CertificateSigningRequest", + }, + Spec: certificatesv1.CertificateSigningRequestSpec{ + Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- +MIIClzCCAX8CAQAwUjEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl +ZWRzMSowKAYDVQQDEyFnYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpteXNlZWQw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNgJWhogJrCSzAhKKmHkJ +FuooKAbxpWRGDOe5DiB8jPdgCoRCkZYnF7D9x9cDzliljA9IeBad3P3E9oegtSV/ +sXFJYqb+lRuhJQ5oo2eBC6WRg+Oxglp+n7o7xt0bO7JHS977mqNrqsJ1d1FnJHTB +MPHPxqoqkgIbdW4t219ckSA20aWzC3PU7I7+Z9OD+YfuuYgzkWG541XyBBKVSD2w +Ix2yGu6zrslqZ1eVBZ4IoxpWrQNmLSMFQVnABThyEUi0U1eVtW0vPNwSnBf0mufX +Z0PpqAIPVjr64Z4s3HHml2GSu64iOxaG5wwb9qIPcdyFaQCep/sFh7kq1KjNI1Ql +AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAb+meLvm7dgHpzhu0XQ39w41FgpTv +S7p78ABFwzDNcP1NwfrEUft0T/rUwPiMlN9zve2rRicaZX5Z7Bol/newejsu8H5z +OdotvtKjE7zBCMzwnXZwO/0pA0cuUFcAy50DPcr35gdGjGlzV9ogO+HPKPTieS3n +TRVg+MWlcLqCjALr9Y4N39DOzf4/SJts8AZJJ+lyyxnY3XIPXx7SdADwNWC8BX0U +OK8CwMwN3iiBQ4redVeMK7LU1unV899q/PWB+NXFcKVr+Grm/Kom5VxuhXSzcHEp +yO57qEcJqG1cB7iSchFuCSTuDBbZlN0fXgn4YjiWZyb4l3BDp3rm4iJImA== +-----END CERTIFICATE REQUEST-----`), + }, }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: "managed seed " + managedSeedNamespace + "/" + managedSeedName + " is already bootstrapped", + Code: int32(http.StatusForbidden), + Message: "can only create CSRs for seed clusters: key usages are not set to [key encipherment digital signature client auth]", }, }, })) }) - It("should allow if the seed does not yet exist", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - - It("should allow if the seed does exist but client cert is expired", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { - (&gardencorev1beta1.Seed{Status: gardencorev1beta1.SeedStatus{ClientCertificateExpirationTimestamp: &metav1.Time{Time: time.Now().Add(-time.Hour)}}}).DeepCopyInto(obj) - return nil - }) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - - It("should allow if the seed does exist but the managedseed is annotated with the renew-kubeconfig annotation", func() { - managedSeed.Annotations = map[string]string{v1beta1constants.GardenerOperation: v1beta1constants.GardenerOperationRenewKubeconfig} - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - - Context("managed seed secret", func() { - var ( - managedSeed1Namespace string - shoot1, shoot2 *gardencorev1beta1.Shoot - seedConfig1, seedConfig2 *gardenletv1alpha1.SeedConfig - managedSeeds []seedmanagementv1alpha1.ManagedSeed - ) - - BeforeEach(func() { - managedSeed1Namespace = "ns1" - shoot1 = &gardencorev1beta1.Shoot{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: managedSeed1Namespace, - Name: "shoot1", - }, - Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-other-seed-name")}, - } - shoot2 = &gardencorev1beta1.Shoot{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: managedSeed1Namespace, - Name: "shoot2", - }, - Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}, - } - seedConfig1 = &gardenletv1alpha1.SeedConfig{ - SeedTemplate: gardencorev1beta1.SeedTemplate{}, - } - seedConfig2 = &gardenletv1alpha1.SeedConfig{ - SeedTemplate: gardencorev1beta1.SeedTemplate{}, - } - managedSeeds = []seedmanagementv1alpha1.ManagedSeed{ - { - ObjectMeta: metav1.ObjectMeta{Namespace: managedSeed1Namespace}, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{Name: shoot1.Name}, - Gardenlet: &seedmanagementv1alpha1.Gardenlet{ - Config: runtime.RawExtension{ - Object: &gardenletv1alpha1.GardenletConfiguration{ - SeedConfig: seedConfig1, - }, - }, - }, - }, + It("should forbid the request because the seed name of the csr does not match", func() { + objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: certificatesv1.SchemeGroupVersion.String(), + Kind: "CertificateSigningRequest", }, - { - ObjectMeta: metav1.ObjectMeta{Namespace: managedSeed1Namespace}, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{Name: shoot2.Name}, - Gardenlet: &seedmanagementv1alpha1.Gardenlet{ - Config: runtime.RawExtension{ - Object: &gardenletv1alpha1.GardenletConfiguration{ - SeedConfig: seedConfig2, - }, - }, - }, + Spec: certificatesv1.CertificateSigningRequestSpec{ + Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- +MIIClzCCAX8CAQAwUjEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl +ZWRzMSowKAYDVQQDEyFnYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpteXNlZWQw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNgJWhogJrCSzAhKKmHkJ +FuooKAbxpWRGDOe5DiB8jPdgCoRCkZYnF7D9x9cDzliljA9IeBad3P3E9oegtSV/ +sXFJYqb+lRuhJQ5oo2eBC6WRg+Oxglp+n7o7xt0bO7JHS977mqNrqsJ1d1FnJHTB +MPHPxqoqkgIbdW4t219ckSA20aWzC3PU7I7+Z9OD+YfuuYgzkWG541XyBBKVSD2w +Ix2yGu6zrslqZ1eVBZ4IoxpWrQNmLSMFQVnABThyEUi0U1eVtW0vPNwSnBf0mufX +Z0PpqAIPVjr64Z4s3HHml2GSu64iOxaG5wwb9qIPcdyFaQCep/sFh7kq1KjNI1Ql +AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAb+meLvm7dgHpzhu0XQ39w41FgpTv +S7p78ABFwzDNcP1NwfrEUft0T/rUwPiMlN9zve2rRicaZX5Z7Bol/newejsu8H5z +OdotvtKjE7zBCMzwnXZwO/0pA0cuUFcAy50DPcr35gdGjGlzV9ogO+HPKPTieS3n +TRVg+MWlcLqCjALr9Y4N39DOzf4/SJts8AZJJ+lyyxnY3XIPXx7SdADwNWC8BX0U +OK8CwMwN3iiBQ4redVeMK7LU1unV899q/PWB+NXFcKVr+Grm/Kom5VxuhXSzcHEp +yO57qEcJqG1cB7iSchFuCSTuDBbZlN0fXgn4YjiWZyb4l3BDp3rm4iJImA== +-----END CERTIFICATE REQUEST-----`), + Usages: []certificatesv1.KeyUsage{ + certificatesv1.UsageKeyEncipherment, + certificatesv1.UsageDigitalSignature, + certificatesv1.UsageClientAuth, }, }, - } - }) - - It("should return an error because listing managed seeds failed", func() { - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).Return(fakeErr) + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), }, }, })) }) - It("should return an error because reading a shoot failed", func() { - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + It("should allow the request because seed name matches", func() { + objData, err := runtime.Encode(encoder, &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: certificatesv1.SchemeGroupVersion.String(), + Kind: "CertificateSigningRequest", + }, + Spec: certificatesv1.CertificateSigningRequestSpec{ + Request: []byte(`-----BEGIN CERTIFICATE REQUEST----- +MIIClTCCAX0CAQAwUDEkMCIGA1UEChMbZ2FyZGVuZXIuY2xvdWQ6c3lzdGVtOnNl +ZWRzMSgwJgYDVQQDEx9nYXJkZW5lci5jbG91ZDpzeXN0ZW06c2VlZDpzZWVkMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsDqibMtE5PXULTT12u0TYW1U +EI2f2MFImNPdEdmyTO8kjy61JzBQxUz6NLLmZWks7dnhZOrhfXqJjVzLWi7gAAIH +hkoxnu8spKTV53l6eY5RrivVsNFRuPF763bKd6JvsF1p9QD9y8uk6bY4NbLAjgMJ +MH64Sj398AnvLlIL+8XIFKtT/SjvOp99oGkKxWHBvokcz9MLUJc/2/JcOdsZ62ue +ZAsqimh0F085+BoG2YtLa4kLNAAiNsijgJ5QCXc7/F8uqkj4uy436LGgGmDfcQxC +9W2snEqriv1dsjF5R/kjh+UbTd+ZdHoAaNaiE7lfZcwe/ap6SNeZaszcDoR//wID +AQABoAAwDQYJKoZIhvcNAQELBQADggEBAKGWWWDHGHdUkOvE1L+tR/v3sDvLfmO7 +jWtF/Sq7kRCrr6xEHLKmVA4wRovpzOML0ntrDCu3npKAWqN+U56L1ZeZSsxyOhvN +dXjk2wPg0+IXPscd33hq0wGZRtBc5MHNWwYLv3ERKnHNbPE2ifkYy6FQ/h/2Kx55 +tHu5PlIwWS6CP+03s3/gjbHX7VL+V3RF5BIHDWcp9QfjN0zEx0R2WVXKIbhC8RTR +BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC +2L4LgmHdCmMFOtPkykwLK6wV1YW7Ce8AxU3j+q4kgZQ+51HJDQDdB74= +-----END CERTIFICATE REQUEST-----`), + Usages: []certificatesv1.KeyUsage{ + certificatesv1.UsageKeyEncipherment, + certificatesv1.UsageDigitalSignature, + certificatesv1.UsageClientAuth, }, }, - })) + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) }) + }) + }) - It("should return an error because extracting the seed template failed", func() { - managedSeeds[1].Spec.Gardenlet = nil + Context("when requested for ClusterRoleBindings", func() { + var name string - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot1.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot2.DeepCopyInto(obj) - return nil - }) + BeforeEach(func() { + name = "foo" + + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: rbacv1.SchemeGroupVersion.Group, + Resource: "clusterrolebindings", + } + }) + + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: "no gardenlet specified in managedseed: \"\"", + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) - - It("should forbid because the secret is referenced in a managedseed's gardenlet config but belongs to another seed", func() { - var ( - secretName = "secret-foo" - secretNamespace = "secret-bar" - ) + }, - request.Namespace = secretNamespace - request.Name = secretName - seedConfig1.Spec.SecretRef = &corev1.SecretReference{ - Name: secretName, - Namespace: secretNamespace, - } + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot1.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot2.DeepCopyInto(obj) - return nil - }) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create + }) + It("should forbid the request because name pattern does not match", func() { Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, @@ -1753,198 +1971,274 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC })) }) - It("should forbid because the secret is referenced in a managedseed's gardenlet config but belongs to another seed", func() { + Context("name pattern matches", func() { var ( - secretName = "secret-bar" - secretNamespace = "secret-foo" + managedSeed *seedmanagementv1alpha1.ManagedSeed + shoot *gardencorev1beta1.Shoot + + managedSeedNamespace = "ms1ns" + managedSeedName = "ms1name" + shootName = "ms1shoot" ) - request.Namespace = secretNamespace - request.Name = secretName - seedConfig1.Spec.Backup = &gardencorev1beta1.SeedBackup{ - SecretRef: corev1.SecretReference{ - Name: secretName, - Namespace: secretNamespace, - }, - } + BeforeEach(func() { + managedSeed = &seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedSeedName, + Namespace: managedSeedNamespace, + }, + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{ + Name: shootName, + }, + }, + } + shoot = &gardencorev1beta1.Shoot{ + Spec: gardencorev1beta1.ShootSpec{ + SeedName: &seedName, + }, + } - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot1.DeepCopyInto(obj) - return nil + request.Name = "gardener.cloud:system:seed-bootstrapper:" + managedSeedNamespace + ":" + managedSeedName }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot2.DeepCopyInto(obj) - return nil + + It("should forbid if decoding the object fails", func() { + request.Object.Raw = []byte(`{]`) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + }, + }, + })) }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + It("should forbid if the role ref doesn't match expectations", func() { + objData, err := runtime.Encode(encoder, &rbacv1.ClusterRoleBinding{ + RoleRef: rbacv1.RoleRef{ + Name: "cluster-admin", }, - }, - })) - }) + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData - It("should allow because the secret is referenced in a managedseed's gardenlet config", func() { - var ( - secretName = "secret-foo" - secretNamespace = "secret-bar" - ) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: "can only bindings referring to the bootstrapper role", + }, + }, + })) + }) - request.Namespace = secretNamespace - request.Name = secretName - seedConfig2.Spec.SecretRef = &corev1.SecretReference{ - Name: secretName, - Namespace: secretNamespace, - } + Context("when role ref is expected", func() { + BeforeEach(func() { + objData, err := runtime.Encode(encoder, &rbacv1.ClusterRoleBinding{ + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "gardener.cloud:system:seed-bootstrapper", + }, + }) + Expect(err).NotTo(HaveOccurred()) + request.Object.Raw = objData + }) - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot1.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot2.DeepCopyInto(obj) - return nil - }) + It("should forbid if the managedseed does not exist", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: " \"\" not found", + }, + }, + })) + }) - It("should allow because the secret is referenced in a managedseed's gardenlet config", func() { - var ( - secretName = "secret-bar" - secretNamespace = "secret-foo" - ) + It("should return an error if reading the managedseed fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) - request.Namespace = secretNamespace - request.Name = secretName - seedConfig2.Spec.Backup = &gardencorev1beta1.SeedBackup{ - SecretRef: corev1.SecretReference{ - Name: secretName, - Namespace: secretNamespace, - }, - } + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) - mockCache.EXPECT().List(ctx, gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeedList{})).DoAndReturn(func(ctx context.Context, list *seedmanagementv1alpha1.ManagedSeedList, opts ...client.ListOption) error { - (&seedmanagementv1alpha1.ManagedSeedList{Items: managedSeeds}).DeepCopyInto(list) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot1.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot1.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeed1Namespace, shoot2.Name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot2.DeepCopyInto(obj) - return nil - }) + It("should return an error if reading the shoot fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - }) - }) - }) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) - Context("when requested for InternalSecrets", func() { - var name, namespace string + It("should return an error if the shoot does not belong to the gardenlet's seed", func() { + shoot.Spec.SeedName = pointer.String("some-other-seed") - BeforeEach(func() { - name, namespace = "foo", "bar" - - request.Name = name - request.Namespace = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "internalsecrets", - } - }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("object does not belong to seed %q", seedName), + }, + }, + })) + }) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, + It("should return an error if reading the seed fails", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusInternalServerError), + Message: fakeErr.Error(), + }, + }, + })) + }) - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + It("should forbid if the seed does exist already", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) - Context("when operation is create", func() { - BeforeEach(func() { - request.Operation = admissionv1.Create - }) + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: "managed seed " + managedSeedNamespace + "/" + managedSeedName + " is already bootstrapped", + }, + }, + })) + }) - It("should forbid the request because it's no expected internal secret", func() { - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), - }, - }, - })) - }) + It("should allow if the seed does not yet exist", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) - Context("shoot-related project secret", func() { - testSuite := func(suffix string) { - BeforeEach(func() { - request.Name = name + suffix + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + + It("should allow if the seed does exist but client cert is expired", func() { + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { + managedSeed.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { + shoot.DeepCopyInto(obj) + return nil + }) + mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { + (&gardencorev1beta1.Seed{Status: gardencorev1beta1.SeedStatus{ClientCertificateExpirationTimestamp: &metav1.Time{Time: time.Now().Add(-time.Hour)}}}).DeepCopyInto(obj) + return nil + }) + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) }) + }) + }) + }) - It("should return an error because the related shoot was not found", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(apierrors.NewNotFound(schema.GroupResource{}, name)) + Context("when requested for Leases", func() { + var name, namespace string - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf(" %q not found", name), - }, + BeforeEach(func() { + name, namespace = "foo", "bar" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: coordinationv1.SchemeGroupVersion.Group, + Resource: "leases", + } + }) + + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation + + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, - })) - }) + }, + })) + }, - It("should return an error because the related shoot could not be read", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, - }, - })) - }) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create + }) - It("should forbid because the related shoot does not belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: pointer.String("some-different-seed")}}).DeepCopyInto(obj) - return nil - }) + DescribeTable("should forbid the request because the seed name of the lease does not match", + func(seedNameInLease string) { + request.Name = seedNameInLease Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -1955,149 +2249,112 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC }, }, })) - }) - - It("should allow because the related shoot does belong to gardenlet's seed", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(namespace, name), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - (&gardencorev1beta1.Shoot{Spec: gardencorev1beta1.ShootSpec{SeedName: &seedName}}).DeepCopyInto(obj) - return nil - }) + }, - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) - } + Entry("seed name is different", "some-different-seed"), + ) - Describe("ca-client suffix", func() { testSuite(".ca-client") }) - }) - }) - }) + It("should allow the request because lease is used for leader-election", func() { + request.Name = "gardenlet-leader-election" - Context("when requested for ClusterRoleBindings", func() { - var name string + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) - BeforeEach(func() { - name = "foo" + It("should allow the request because seed name matches", func() { + request.Name = seedName + request.Namespace = "gardener-system-seed-lease" - request.Name = name - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: rbacv1.SchemeGroupVersion.Group, - Resource: "clusterrolebindings", - } + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) }) - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, - - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + Context("when requested for ServiceAccounts", func() { + var name, namespace string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create - }) - - It("should forbid the request because name pattern does not match", func() { - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), - }, - }, - })) + name, namespace = "foo", "bar" + + request.Name = name + request.Name = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Resource: "serviceaccounts", + } }) - Context("name pattern matches", func() { - var ( - managedSeed *seedmanagementv1alpha1.ManagedSeed - shoot *gardencorev1beta1.Shoot - - managedSeedNamespace = "ms1ns" - managedSeedName = "ms1name" - shootName = "ms1shoot" - ) - - BeforeEach(func() { - managedSeed = &seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{ - Name: managedSeedName, - Namespace: managedSeedNamespace, - }, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{ - Name: shootName, - }, - }, - } - shoot = &gardencorev1beta1.Shoot{ - Spec: gardencorev1beta1.ShootSpec{ - SeedName: &seedName, - }, - } - - request.Name = "gardener.cloud:system:seed-bootstrapper:" + managedSeedNamespace + ":" + managedSeedName - }) - - It("should forbid if decoding the object fails", func() { - request.Object.Raw = []byte(`{]`) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusBadRequest), - Message: "couldn't get version/kind; json parse error: invalid character ']' looking for beginning of object key string", + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) + }, + + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) + + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create }) - It("should forbid if the role ref doesn't match expectations", func() { - objData, err := runtime.Encode(encoder, &rbacv1.ClusterRoleBinding{ - RoleRef: rbacv1.RoleRef{ - Name: "cluster-admin", - }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + It("should allow the request because namespace is seed namespace", func() { + request.Namespace = "seed-" + seedName + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + It("should forbid the request because name pattern does not match", func() { Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: "can only bindings referring to the bootstrapper role", + Message: fmt.Sprintf("object does not belong to seed %q", seedName), }, }, })) }) - Context("when role ref is expected", func() { + Context("name pattern matches", func() { + var ( + managedSeed *seedmanagementv1alpha1.ManagedSeed + shoot *gardencorev1beta1.Shoot + + managedSeedNamespace = "ms1ns" + managedSeedName = "ms1name" + shootName = "ms1shoot" + ) + BeforeEach(func() { - objData, err := runtime.Encode(encoder, &rbacv1.ClusterRoleBinding{ - RoleRef: rbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: "gardener.cloud:system:seed-bootstrapper", + managedSeed = &seedmanagementv1alpha1.ManagedSeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedSeedName, + Namespace: managedSeedNamespace, }, - }) - Expect(err).NotTo(HaveOccurred()) - request.Object.Raw = objData + Spec: seedmanagementv1alpha1.ManagedSeedSpec{ + Shoot: &seedmanagementv1alpha1.Shoot{ + Name: shootName, + }, + }, + } + shoot = &gardencorev1beta1.Shoot{ + Spec: gardencorev1beta1.ShootSpec{ + SeedName: &seedName, + }, + } + + request.Name = "gardenlet-bootstrap-" + managedSeedName + request.Namespace = managedSeedNamespace }) It("should forbid if the managedseed does not exist", func() { @@ -2248,237 +2505,230 @@ BkEao/FEz4eQuV5atSD0S78+aF4BriEtWKKjXECTCxMuqcA24vGOgHIrEbKd7zSC }) }) - Context("when requested for ServiceAccounts", func() { - var name, namespace string - + Context("extension client", func() { BeforeEach(func() { - name, namespace = "foo", "bar" - - request.Name = name - request.Name = namespace - request.UserInfo = seedUser - request.Resource = metav1.GroupVersionResource{ - Group: corev1.SchemeGroupVersion.Group, - Resource: "serviceaccounts", - } + seedUser = extensionUser }) - DescribeTable("should not allow the request because no allowed verb", - func(operation admissionv1.Operation) { - request.Operation = operation - - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusBadRequest), - Message: fmt.Sprintf("unexpected operation: %q", operation), - }, - }, - })) - }, + testCommonAccess() - Entry("update", admissionv1.Update), - Entry("delete", admissionv1.Delete), - ) + Context("when requested for CertificateSigningRequests", func() { + var name string - Context("when operation is create", func() { BeforeEach(func() { - request.Operation = admissionv1.Create + name = "foo" + + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: certificatesv1.SchemeGroupVersion.Group, + Version: "v1", + Resource: "certificatesigningrequests", + } }) - It("should allow the request because namespace is seed namespace", func() { - request.Namespace = "seed-" + seedName - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation - It("should forbid the request because name pattern does not match", func() { + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), + }, + }, + })) + }, + + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) + + It("should not allow create request", func() { + request.Operation = admissionv1.Create Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Message: "extension client may not create CertificateSigningRequests", }, }, })) }) + }) - Context("name pattern matches", func() { - var ( - managedSeed *seedmanagementv1alpha1.ManagedSeed - shoot *gardencorev1beta1.Shoot - - managedSeedNamespace = "ms1ns" - managedSeedName = "ms1name" - shootName = "ms1shoot" - ) + Context("when requested for ClusterRoleBindings", func() { + var name string - BeforeEach(func() { - managedSeed = &seedmanagementv1alpha1.ManagedSeed{ - ObjectMeta: metav1.ObjectMeta{ - Name: managedSeedName, - Namespace: managedSeedNamespace, - }, - Spec: seedmanagementv1alpha1.ManagedSeedSpec{ - Shoot: &seedmanagementv1alpha1.Shoot{ - Name: shootName, - }, - }, - } - shoot = &gardencorev1beta1.Shoot{ - Spec: gardencorev1beta1.ShootSpec{ - SeedName: &seedName, - }, - } + BeforeEach(func() { + name = "foo" - request.Name = "gardenlet-bootstrap-" + managedSeedName - request.Namespace = managedSeedNamespace - }) + request.Name = name + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: rbacv1.SchemeGroupVersion.Group, + Resource: "clusterrolebindings", + } + }) - It("should forbid if the managedseed does not exist", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Message: " \"\" not found", + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) + }, - It("should return an error if reading the managedseed fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).Return(fakeErr) + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), - }, + It("should not allow create request", func() { + request.Operation = admissionv1.Create + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: "extension client may not create ClusterRoleBindings", }, - })) - }) + }, + })) + }) + }) - It("should return an error if reading the shoot fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).Return(fakeErr) + Context("when requested for Leases", func() { + var name, namespace string + + BeforeEach(func() { + name, namespace = "foo", "bar" + + request.Name = name + request.Namespace = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: coordinationv1.SchemeGroupVersion.Group, + Resource: "leases", + } + }) + + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + Code: int32(http.StatusBadRequest), + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) + }, - It("should return an error if the shoot does not belong to the gardenlet's seed", func() { - shoot.Spec.SeedName = pointer.String("some-other-seed") + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) + Context("when operation is create", func() { + BeforeEach(func() { + request.Operation = admissionv1.Create + }) + + It("should forbid the request because lease is reserved for gardenlet leader-election", func() { + request.Name = "gardenlet-leader-election" Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusForbidden), - Message: fmt.Sprintf("object does not belong to seed %q", seedName), + Message: fmt.Sprintf("extension client can only create leases in the namespace for seed %q", seedName), }, }, })) }) - It("should return an error if reading the seed fails", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(fakeErr) + It("should forbid the request because lease is reserved for gardenlet seed lease", func() { + request.Name = seedName + request.Namespace = "gardener-system-seed-lease" Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Code: int32(http.StatusInternalServerError), - Message: fakeErr.Error(), + Code: int32(http.StatusForbidden), + Message: fmt.Sprintf("extension client can only create leases in the namespace for seed %q", seedName), }, }, })) }) - It("should forbid if the seed does exist already", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})) + It("should allow the request because lease is in seed namespace", func() { + request.Name = seedName + request.Namespace = "seed-" + seedName + + Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) + }) + }) + }) + + Context("when requested for ServiceAccounts", func() { + var name, namespace string + + BeforeEach(func() { + name, namespace = "foo", "bar" + + request.Name = name + request.Name = namespace + request.UserInfo = seedUser + request.Resource = metav1.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Resource: "serviceaccounts", + } + }) + + DescribeTable("should not allow the request because no allowed verb", + func(operation admissionv1.Operation) { + request.Operation = operation Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: int32(http.StatusBadRequest), - Message: "managed seed " + managedSeedNamespace + "/" + managedSeedName + " is already bootstrapped", + Message: fmt.Sprintf("unexpected operation: %q", operation), }, }, })) - }) - - It("should allow if the seed does not yet exist", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).Return(apierrors.NewNotFound(schema.GroupResource{}, "")) - - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) + }, - It("should allow if the seed does exist but client cert is expired", func() { - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, managedSeedName), gomock.AssignableToTypeOf(&seedmanagementv1alpha1.ManagedSeed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *seedmanagementv1alpha1.ManagedSeed, _ ...client.GetOption) error { - managedSeed.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedNamespace, shootName), gomock.AssignableToTypeOf(&gardencorev1beta1.Shoot{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Shoot, _ ...client.GetOption) error { - shoot.DeepCopyInto(obj) - return nil - }) - mockCache.EXPECT().Get(ctx, kubernetesutils.Key(managedSeedName), gomock.AssignableToTypeOf(&gardencorev1beta1.Seed{})).DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *gardencorev1beta1.Seed, _ ...client.GetOption) error { - (&gardencorev1beta1.Seed{Status: gardencorev1beta1.SeedStatus{ClientCertificateExpirationTimestamp: &metav1.Time{Time: time.Now().Add(-time.Hour)}}}).DeepCopyInto(obj) - return nil - }) + Entry("update", admissionv1.Update), + Entry("delete", admissionv1.Delete), + ) - Expect(handler.Handle(ctx, request)).To(Equal(responseAllowed)) - }) + It("should not allow create request", func() { + request.Operation = admissionv1.Create + Expect(handler.Handle(ctx, request)).To(Equal(admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Code: int32(http.StatusForbidden), + Message: "extension client may not create ServiceAccounts", + }, + }, + })) }) }) }) diff --git a/pkg/admissioncontroller/webhook/auth/seed/authorizer.go b/pkg/admissioncontroller/webhook/auth/seed/authorizer.go index 345980370bd..eeac1e18fac 100644 --- a/pkg/admissioncontroller/webhook/auth/seed/authorizer.go +++ b/pkg/admissioncontroller/webhook/auth/seed/authorizer.go @@ -90,12 +90,12 @@ var ( // With `DecisionNoOpinion`, RBAC will be respected in the authorization chain afterwards. func (a *authorizer) Authorize(_ context.Context, attrs auth.Attributes) (auth.Decision, string, error) { - seedName, isSeed := seedidentity.FromUserInfoInterface(attrs.GetUser()) + seedName, isSeed, userType := seedidentity.FromUserInfoInterface(attrs.GetUser()) if !isSeed { return auth.DecisionNoOpinion, "", nil } - requestLog := a.logger.WithValues("seedName", seedName, "attributes", fmt.Sprintf("%#v", attrs)) + requestLog := a.logger.WithValues("seedName", seedName, "attributes", fmt.Sprintf("%#v", attrs), "userType", userType) if attrs.IsResourceRequest() { requestResource := schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()} @@ -119,6 +119,10 @@ func (a *authorizer) Authorize(_ context.Context, attrs auth.Attributes) (auth.D []string{"status"}, ) case certificateSigningRequestResource: + if userType == seedidentity.UserTypeExtension { + return a.authorizeRead(requestLog, seedName, graph.VertexTypeCertificateSigningRequest, attrs) + } + return a.authorize(requestLog, seedName, graph.VertexTypeCertificateSigningRequest, attrs, []string{"get", "list", "watch"}, []string{"create"}, @@ -127,6 +131,16 @@ func (a *authorizer) Authorize(_ context.Context, attrs auth.Attributes) (auth.D case cloudProfileResource: return a.authorizeRead(requestLog, seedName, graph.VertexTypeCloudProfile, attrs) case clusterRoleBindingResource: + if userType == seedidentity.UserTypeExtension { + // We don't use authorizeRead here, as it would also grant list and watch permissions, which gardenlet doesn't + // have. We want to grant the read-only subset of gardenlet's permissions. + return a.authorize(requestLog, seedName, graph.VertexTypeClusterRoleBinding, attrs, + []string{"get"}, + nil, + nil, + ) + } + return a.authorizeClusterRoleBinding(requestLog, seedName, attrs) case configMapResource: return a.authorizeRead(requestLog, seedName, graph.VertexTypeConfigMap, attrs) @@ -155,7 +169,7 @@ func (a *authorizer) Authorize(_ context.Context, attrs auth.Attributes) (auth.D nil, ) case leaseResource: - return a.authorizeLease(requestLog, seedName, attrs) + return a.authorizeLease(requestLog, seedName, userType, attrs) case managedSeedResource: return a.authorize(requestLog, seedName, graph.VertexTypeManagedSeed, attrs, []string{"update", "patch"}, @@ -177,6 +191,16 @@ func (a *authorizer) Authorize(_ context.Context, attrs auth.Attributes) (auth.D []string{"status"}, ) case serviceAccountResource: + if userType == seedidentity.UserTypeExtension { + // We don't use authorizeRead here, as it would also grant list and watch permissions, which gardenlet doesn't + // have. We want to grant the read-only subset of gardenlet's permissions. + return a.authorize(requestLog, seedName, graph.VertexTypeServiceAccount, attrs, + []string{"get"}, + nil, + nil, + ) + } + return a.authorizeServiceAccount(requestLog, seedName, attrs) case shootResource: return a.authorize(requestLog, seedName, graph.VertexTypeShoot, attrs, @@ -236,7 +260,20 @@ func (a *authorizer) authorizeEvent(log logr.Logger, attrs auth.Attributes) (aut return auth.DecisionAllow, "", nil } -func (a *authorizer) authorizeLease(log logr.Logger, seedName string, attrs auth.Attributes) (auth.Decision, string, error) { +func (a *authorizer) authorizeLease(log logr.Logger, seedName string, userType seedidentity.UserType, attrs auth.Attributes) (auth.Decision, string, error) { + // extension clients may only work with leases in the seed namespace + if userType == seedidentity.UserTypeExtension { + if attrs.GetNamespace() == gardenerutils.ComputeGardenNamespace(seedName) { + if ok, reason := a.checkVerb(log, attrs, "create", "get", "list", "watch", "update", "patch", "delete", "deletecollection"); !ok { + return auth.DecisionNoOpinion, reason, nil + } + + return auth.DecisionAllow, "", nil + } + + return auth.DecisionNoOpinion, "lease object is not in seed namespace", nil + } + // This is needed if the seed cluster is a garden cluster at the same time. if attrs.GetName() == "gardenlet-leader-election" && utils.ValueExists(attrs.GetVerb(), []string{"create", "get", "list", "watch", "update"}) { diff --git a/pkg/admissioncontroller/webhook/auth/seed/authorizer_test.go b/pkg/admissioncontroller/webhook/auth/seed/authorizer_test.go index cd9040fe5b7..991b2f12d94 100644 --- a/pkg/admissioncontroller/webhook/auth/seed/authorizer_test.go +++ b/pkg/admissioncontroller/webhook/auth/seed/authorizer_test.go @@ -28,6 +28,7 @@ import ( eventsv1 "k8s.io/api/events/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/apiserver/pkg/authentication/user" auth "k8s.io/apiserver/pkg/authorization/authorizer" logzap "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -41,6 +42,7 @@ import ( seedmanagementv1alpha1 "github.com/gardener/gardener/pkg/apis/seedmanagement/v1alpha1" gardenletbootstraputil "github.com/gardener/gardener/pkg/gardenlet/bootstrap/util" "github.com/gardener/gardener/pkg/logger" + gardenerutils "github.com/gardener/gardener/pkg/utils/gardener" ) var _ = Describe("Seed", func() { @@ -52,8 +54,10 @@ var _ = Describe("Seed", func() { graph *mockgraph.MockInterface authorizer auth.Authorizer - seedName string - seedUser user.Info + seedName string + seedUser user.Info + gardenletUser user.Info + extensionUser user.Info ) BeforeEach(func() { @@ -65,10 +69,14 @@ var _ = Describe("Seed", func() { authorizer = NewAuthorizer(log, graph) seedName = "seed" - seedUser = &user.DefaultInfo{ + gardenletUser = &user.DefaultInfo{ Name: fmt.Sprintf("%s%s", v1beta1constants.SeedUserNamePrefix, seedName), Groups: []string{v1beta1constants.SeedsGroup}, } + extensionUser = (&serviceaccount.ServiceAccountInfo{ + Name: v1beta1constants.ExtensionGardenServiceAccountPrefix + "provider-local", + Namespace: gardenerutils.SeedNamespaceNamePrefix + seedName, + }).UserInfo() }) AfterEach(func() { @@ -93,7 +101,7 @@ var _ = Describe("Seed", func() { It("should have no opinion because no resource request", func() { attrs := auth.AttributesRecord{ - User: seedUser, + User: gardenletUser, APIGroup: "", Resource: "", } @@ -107,7 +115,7 @@ var _ = Describe("Seed", func() { It("should have no opinion because resource is irrelevant", func() { attrs := auth.AttributesRecord{ - User: seedUser, + User: gardenletUser, APIGroup: "", Resource: "", ResourceRequest: true, @@ -121,2071 +129,2389 @@ var _ = Describe("Seed", func() { }) }) - Context("when requested for CloudProfiles", func() { - var ( - cloudProfileName string - attrs *auth.AttributesRecord - ) - - BeforeEach(func() { - cloudProfileName = "fooCloud" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: cloudProfileName, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "cloudprofiles", - ResourceRequest: true, - Verb: "get", - } - }) - - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb - - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCloudProfile, "", cloudProfileName, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) - - DescribeTable("should have no opinion because no allowed verb", func(verb string) { - attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, - Entry("create", "create"), - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCloudProfile, "", cloudProfileName, graphpkg.VertexTypeSeed, "", seedName).Return(false) - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) - - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) - - Context("when requested for ConfigMaps", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) - - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: corev1.SchemeGroupVersion.Group, - Resource: "configmaps", - ResourceRequest: true, - Verb: "get", - } - }) - - It("should allow because cluster-identity is retrieved", func() { - attrs.Name = "cluster-identity" - attrs.Namespace = "kube-system" + testCommonAccess := func() { + Context("when requested for CloudProfiles", func() { + var ( + cloudProfileName string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + cloudProfileName = "fooCloud" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: cloudProfileName, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "cloudprofiles", + ResourceRequest: true, + Verb: "get", + } + }) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, attrs.Namespace, attrs.Name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCloudProfile, "", cloudProfileName, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - DescribeTable("should return correct result if path exists", - func(verb string) { + DescribeTable("should have no opinion because no allowed verb", func(verb string) { attrs.Verb = verb - - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) }, + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) - - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCloudProfile, "", cloudProfileName, graphpkg.VertexTypeSeed, "", seedName).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, - - Entry("create", "create"), - Entry("update", "update"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) - - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) - - Context("when requested for SecretBindings", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) - - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "secretbindings", - ResourceRequest: true, - Verb: "get", - } - }) - - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + Expect(reason).To(ContainSubstring("no relationship found")) + }) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecretBinding, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for ConfigMaps", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "configmaps", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("create", "create"), - Entry("update", "update"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + It("should allow because cluster-identity is retrieved", func() { + attrs.Name = "cluster-identity" + attrs.Namespace = "kube-system" - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecretBinding, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, attrs.Namespace, attrs.Name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - Context("when requested for ShootStates", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "shootstates", - ResourceRequest: true, - Verb: "get", - } - }) - - It("should allow because verb is create", func() { - attrs.Verb = "create" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("create", "create"), + Entry("update", "update"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - It("should allow when verb is delete and resource does not exist", func() { - attrs.Verb = "delete" + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeConfigMap, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - graph.EXPECT().HasVertex(graphpkg.VertexTypeShootState, namespace, name).Return(false) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }) - if verb == "delete" { - graph.EXPECT().HasVertex(graphpkg.VertexTypeShootState, namespace, name).Return(true).Times(2) - } + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShootState, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShootState, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("patch", "patch"), - Entry("update", "update"), - Entry("delete", "delete"), - ) - - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch delete list watch]")) - }, + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for SecretBindings", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "secretbindings", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("deletecollection", "deletecollection"), - ) + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecretBinding, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) - - Context("when requested for Namespaces", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: corev1.SchemeGroupVersion.Group, - Resource: "namespaces", - ResourceRequest: true, - Verb: "get", - } - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + }, - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + Entry("create", "create"), + Entry("update", "update"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeNamespace, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecretBinding, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }) - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, - - Entry("create", "create"), - Entry("update", "update"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeNamespace, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) - - It("should have no opinion because no resources requested", func() { - attrs.Subresource = "status" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Context("when requested for Projects", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for ShootStates", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "shootstates", + ResourceRequest: true, + Verb: "get", + } + }) - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "projects", - ResourceRequest: true, - Verb: "get", - } - }) + It("should allow because verb is create", func() { + attrs.Verb = "create" - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeProject, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should allow when verb is delete and resource does not exist", func() { + attrs.Verb = "delete" + graph.EXPECT().HasVertex(graphpkg.VertexTypeShootState, namespace, name).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) - }, + }) + + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb + + if verb == "delete" { + graph.EXPECT().HasVertex(graphpkg.VertexTypeShootState, namespace, name).Return(true).Times(2) + } + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShootState, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShootState, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("patch", "patch"), + Entry("update", "update"), + Entry("delete", "delete"), + ) + + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb + + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch delete list watch]")) + }, - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - Entry("create", "create"), - Entry("update", "update"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeProject, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err := authorizer.Authorize(ctx, attrs) - decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for Namespaces", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "namespaces", + ResourceRequest: true, + Verb: "get", + } + }) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - It("should have no opinion because no resources requested", func() { - attrs.Subresource = "status" + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeNamespace, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Context("when requested for BackupBuckets", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + }, - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "backupbuckets", - ResourceRequest: true, - Verb: "list", - } - }) + Entry("create", "create"), + Entry("update", "update"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeNamespace, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("create", "create"), - ) - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb - - decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch delete]")) - }, - - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) - - It("should allow when verb is delete and resource does not exist", func() { - attrs.Verb = "delete" - - graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupBucket, "", name).Return(false) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) - - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + Expect(reason).To(ContainSubstring("no relationship found")) + }) - if verb == "delete" { - graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupBucket, "", name).Return(true).Times(2) - } + It("should have no opinion because no resources requested", func() { + attrs.Subresource = "status" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupBucket, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupBucket, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - Entry("delete", "delete", ""), - ) - }) - - Context("when requested for BackupEntries", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) - - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "backupentries", - ResourceRequest: true, - Verb: "list", - } - }) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for Projects", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "projects", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("create", "create"), - ) + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeProject, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch delete]")) + decision, reason, err := authorizer.Authorize(ctx, attrs) - }, + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Entry("deletecollection", "deletecollection"), - ) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - It("should allow when verb is delete and resource does not exist", func() { - attrs.Verb = "delete" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + }, - graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupEntry, namespace, name).Return(false) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("create", "create"), + Entry("update", "update"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeProject, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupEntry, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupEntry, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - ) - }) - - Context("when requested for ExposureClasses", func() { - var ( - exposureClassName string - attrs *auth.AttributesRecord - ) + }) - BeforeEach(func() { - exposureClassName = "fooExposureClass" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: exposureClassName, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "exposureclasses", - ResourceRequest: true, - Verb: "get", - } - }) - - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb - - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeExposureClass, "", exposureClassName, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should have no opinion because no resources requested", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - }, - - Entry("create", "create"), - Entry("update", "update"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeExposureClass, "", exposureClassName, graphpkg.VertexTypeSeed, "", seedName).Return(false) - - decision, reason, err := authorizer.Authorize(ctx, attrs) - - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for BackupBuckets", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "backupbuckets", + ResourceRequest: true, + Verb: "list", + } + }) - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("create", "create"), + ) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch delete]")) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + Entry("deletecollection", "deletecollection"), + ) - Context("when requested for Bastions", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: operationsv1alpha1.SchemeGroupVersion.Group, - Resource: "bastions", - ResourceRequest: true, - Verb: "list", - } - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) - DescribeTable("should allow with consulting the graph because verb is get, list, watch, create", - func(verb string) { - attrs.Verb = verb + It("should allow when verb is delete and resource does not exist", func() { + attrs.Verb = "delete" + graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupBucket, "", name).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) - }, + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + if verb == "delete" { + graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupBucket, "", name).Return(true).Times(2) + } + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupBucket, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupBucket, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("create", "create"), - ) + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + Entry("delete", "delete", ""), + ) + }) + + Context("when requested for BackupEntries", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "backupentries", + ResourceRequest: true, + Verb: "list", + } + }) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch]")) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - }, + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("create", "create"), + ) - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch delete]")) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) + }, - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + Entry("deletecollection", "deletecollection"), + ) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBastion, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBastion, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - ) - }) - - Context("when requested for ManagedSeeds", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) - - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: seedmanagementv1alpha1.SchemeGroupVersion.Group, - Resource: "managedseeds", - ResourceRequest: true, - Verb: "list", - } - }) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) - DescribeTable("should allow without consulting the graph because verb is get, list, or watch", - func(verb string) { - attrs.Verb = verb + It("should allow when verb is delete and resource does not exist", func() { + attrs.Verb = "delete" + graph.EXPECT().HasVertex(graphpkg.VertexTypeBackupEntry, namespace, name).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) - }, + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupEntry, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBackupEntry, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, + + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + ) + }) + + Context("when requested for ExposureClasses", func() { + var ( + exposureClassName string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + exposureClassName = "fooExposureClass" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: exposureClassName, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "exposureclasses", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeExposureClass, "", exposureClassName, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) + decision, reason, err := authorizer.Authorize(ctx, attrs) - }, + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Entry("create", "create"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + }, + + Entry("create", "create"), + Entry("update", "update"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeExposureClass, "", exposureClassName, graphpkg.VertexTypeSeed, "", seedName).Return(false) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeManagedSeed, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeManagedSeed, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) Expect(reason).To(ContainSubstring("no relationship found")) - }, + }) - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - ) - }) + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" - Context("when requested for ControllerInstallations", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "controllerinstallations", - ResourceRequest: true, - Verb: "list", - } - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - DescribeTable("should allow without consulting the graph because verb is get, list, or watch", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for Bastions", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: operationsv1alpha1.SchemeGroupVersion.Group, + Resource: "bastions", + ResourceRequest: true, + Verb: "list", + } + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + DescribeTable("should allow with consulting the graph because verb is get, list, watch, create", + func(verb string) { + attrs.Verb = verb - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("create", "create"), + ) - }, + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Entry("create", "create"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch]")) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerInstallation, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerInstallation, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBastion, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeBastion, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - ) - }) + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + ) + }) + + Context("when requested for ManagedSeeds", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: seedmanagementv1alpha1.SchemeGroupVersion.Group, + Resource: "managedseeds", + ResourceRequest: true, + Verb: "list", + } + }) - Context("when requested for corev1.Events", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + DescribeTable("should allow without consulting the graph because verb is get, list, or watch", + func(verb string) { + attrs.Verb = verb - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: corev1.SchemeGroupVersion.Group, - Resource: "events", - ResourceRequest: true, - Verb: "create", - } - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - DescribeTable("should allow without consulting the graph because verb is create", - func(verb string) { - attrs.Verb = verb + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Entry("create", "create"), - Entry("patch", "patch"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + }, + + Entry("create", "create"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create patch]")) - - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeManagedSeed, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeManagedSeed, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + ) + }) + + Context("when requested for ControllerInstallations", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "controllerinstallations", + ResourceRequest: true, + Verb: "list", + } + }) - decision, reason, err := authorizer.Authorize(ctx, attrs) + DescribeTable("should allow without consulting the graph because verb is get, list, or watch", + func(verb string) { + attrs.Verb = verb - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Context("when requested for events.k8s.io/v1.Events", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: eventsv1.SchemeGroupVersion.Group, - Resource: "events", - ResourceRequest: true, - Verb: "create", - } - }) + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb - DescribeTable("should allow without consulting the graph because verb is create", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + }, - Entry("create", "create"), - Entry("patch", "patch"), - ) + Entry("create", "create"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create patch]")) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerInstallation, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerInstallation, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - }, + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + ) + }) + + Context("when requested for corev1.Events", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "events", + ResourceRequest: true, + Verb: "create", + } + }) + + DescribeTable("should allow without consulting the graph because verb is create", + func(verb string) { + attrs.Verb = verb - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("update", "update"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + Entry("create", "create"), + Entry("patch", "patch"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create patch]")) - Context("when requested for Leases", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + }, - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: coordinationv1.SchemeGroupVersion.Group, - Resource: "leases", - ResourceRequest: true, - Verb: "get", - } - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should allow without consulting the graph because verb is create", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + }) + + Context("when requested for events.k8s.io/v1.Events", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: eventsv1.SchemeGroupVersion.Group, + Resource: "events", + ResourceRequest: true, + Verb: "create", + } + }) - Entry("create", "create"), - ) + DescribeTable("should allow without consulting the graph because verb is create", + func(verb string) { + attrs.Verb = verb - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeLease, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + Entry("create", "create"), + Entry("patch", "patch"), + ) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeLease, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("update", "update"), - Entry("patch", "patch"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create patch]")) - DescribeTable("should have no opinion because verb is not allowed", - func(verb string) { - attrs.Verb = verb + }, + + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch list watch]")) - - }, - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + }) + + Context("when requested for Shoots", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "shoots", + ResourceRequest: true, + Verb: "list", + } + }) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + DescribeTable("should allow without consulting the graph because verb is get, list, or watch", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Context("when requested for Shoots", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "shoots", - ResourceRequest: true, - Verb: "list", - } - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - DescribeTable("should allow without consulting the graph because verb is get, list, or watch", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + }, - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Entry("create", "create"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch update patch]")) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShoot, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShoot, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - }, + Entry("patch w/o subresource", "patch", ""), + Entry("patch w/ subresource", "patch", "status"), + Entry("update w/o subresource", "update", ""), + Entry("update w/ subresource", "update", "status"), + ) + }) + + Context("when requested for Seeds", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "seeds", + ResourceRequest: true, + Verb: "list", + } + }) - Entry("create", "create"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", + func(verb string) { + attrs.Verb = verb - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + ) - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + It("should have no opinion because no allowed verb", func() { + attrs.Verb = "deletecollection" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShoot, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create update patch delete get list watch]")) + }) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeShoot, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" + + decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) + }) + }) + + Context("when requested for ControllerRegistrations", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "controllerregistrations", + ResourceRequest: true, + Verb: "list", + } + }) - Entry("patch w/o subresource", "patch", ""), - Entry("patch w/ subresource", "patch", "status"), - Entry("update w/o subresource", "update", ""), - Entry("update w/ subresource", "update", "status"), - ) - }) + DescribeTable("should allow without consulting the graph because verb is get, list, or watch", + func(verb string) { + attrs.Verb = verb - Context("when requested for Seeds", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "seeds", - ResourceRequest: true, - Verb: "list", - } - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - DescribeTable("should allow without consulting the graph because verb is get, list, watch, create", - func(verb string) { - attrs.Verb = verb + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("create", "create"), - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - ) + }, - It("should have no opinion because no allowed verb", func() { - attrs.Verb = "deletecollection" + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create update patch delete get list watch]")) - }) + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + }) + + Context("when requested for ControllerDeployments", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "controllerdeployments", + ResourceRequest: true, + Verb: "get", + } + }) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [status]")) - }) - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Context("when requested for ControllerRegistrations", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "controllerregistrations", - ResourceRequest: true, - Verb: "list", - } - }) + }, - DescribeTable("should allow without consulting the graph because verb is get, list, or watch", - func(verb string) { - attrs.Verb = verb + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerDeployment, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerDeployment, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, + + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) + }) + + Context("when requested for Secrets", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "secrets", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + DescribeTable("should allow without consulting the graph because verb is get, list, or watch in the seed's namespace", + func(verb string) { + attrs.Namespace = "seed-" + seedName + attrs.Verb = verb - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, + + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) + + It("should allow to delete the gardenlet's bootstrap tokens without consulting the graph", func() { + attrs.Verb = "delete" + attrs.Namespace = "kube-system" + attrs.Name = "bootstrap-token-" + gardenletbootstraputil.TokenID(metav1.ObjectMeta{Name: seedName, Namespace: v1beta1constants.GardenNamespace}) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - }, + DescribeTable("should allow without consulting the graph because verb is create", + func(verb string) { + attrs.Verb = verb - Entry("create", "create"), - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + Entry("create", "create"), + ) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Context("when requested for ControllerDeployments", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update delete]")) - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "controllerdeployments", - ResourceRequest: true, - Verb: "get", - } - }) + }, - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + Entry("list", "list"), + Entry("watch", "watch"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - - }, + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - Entry("create", "create"), - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" - - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) - - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + It("should allow when verb is delete and resource does not exist", func() { + attrs.Verb = "delete" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerDeployment, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + graph.EXPECT().HasVertex(graphpkg.VertexTypeSecret, namespace, name).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) + }) + + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb + + if verb == "delete" { + graph.EXPECT().HasVertex(graphpkg.VertexTypeSecret, namespace, name).Return(true).Times(2) + } + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeControllerDeployment, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) - }) + Entry("get", "get"), + Entry("patch", "patch"), + Entry("update", "update"), + Entry("delete", "delete"), + ) + }) + + Context("when requested for InternalSecrets", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, + Resource: "internalsecrets", + ResourceRequest: true, + Verb: "get", + } + }) - Context("when requested for CertificateSigningRequests", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + It("should allow because verb is create", func() { + attrs.Verb = "create" - BeforeEach(func() { - name = "foo" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: certificatesv1.SchemeGroupVersion.Group, - Resource: "certificatesigningrequests", - ResourceRequest: true, - Verb: "get", - } - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - DescribeTable("should allow without consulting the graph because verb is create", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + It("should allow when verb is delete and resource does not exist", func() { + attrs.Verb = "delete" + graph.EXPECT().HasVertex(graphpkg.VertexTypeInternalSecret, namespace, name).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) - }, + }) + + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb + + if verb == "delete" { + graph.EXPECT().HasVertex(graphpkg.VertexTypeInternalSecret, namespace, name).Return(true).Times(2) + } + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeInternalSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeInternalSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, + + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("patch", "patch"), + Entry("update", "update"), + Entry("delete", "delete"), + ) + + DescribeTable("should have no opinion because no allowed verb", + func(verb string) { + attrs.Verb = verb + + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch delete list watch]")) + }, - Entry("create", "create", ""), - Entry("create with subresource", "create", "seedclient"), - ) + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should return correct result if path exists", - func(verb, subresource string) { - attrs.Verb = verb - attrs.Subresource = subresource + It("should have no opinion because request is for a subresource", func() { + attrs.Subresource = "status" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("get", "get", ""), - Entry("list", "list", ""), - Entry("watch", "watch", ""), - Entry("get with subresource", "get", "seedclient"), - ) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch]")) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + } - }, + Context("gardenlet client", func() { + BeforeEach(func() { + seedUser = gardenletUser + }) + + testCommonAccess() + + Context("when requested for CertificateSigningRequests", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: certificatesv1.SchemeGroupVersion.Group, + Resource: "certificatesigningrequests", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + DescribeTable("should allow without consulting the graph because verb is create", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [seedclient]")) - }) - }) + Entry("create", "create", ""), + Entry("create with subresource", "create", "seedclient"), + ) + + DescribeTable("should return correct result if path exists", + func(verb, subresource string) { + attrs.Verb = verb + attrs.Subresource = subresource + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - Context("when requested for Secrets", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + Entry("get", "get", ""), + Entry("list", "list", ""), + Entry("watch", "watch", ""), + Entry("get with subresource", "get", "seedclient"), + ) - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: corev1.SchemeGroupVersion.Group, - Resource: "secrets", - ResourceRequest: true, - Verb: "get", - } - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - DescribeTable("should allow without consulting the graph because verb is get, list, or watch in the seed's namespace", - func(verb string) { - attrs.Namespace = "seed-" + seedName - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch]")) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + }, - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - ) + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - It("should allow to delete the gardenlet's bootstrap tokens without consulting the graph", func() { - attrs.Verb = "delete" - attrs.Namespace = "kube-system" - attrs.Name = "bootstrap-token-" + gardenletbootstraputil.TokenID(metav1.ObjectMeta{Name: seedName, Namespace: v1beta1constants.GardenNamespace}) + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: [seedclient]")) + }) + }) + + Context("when requested for ClusterRoleBindings", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "fooClusterRoleBinding" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: rbacv1.SchemeGroupVersion.Group, + Resource: "clusterrolebindings", + ResourceRequest: true, + Verb: "get", + } + }) - DescribeTable("should allow without consulting the graph because verb is create", - func(verb string) { - attrs.Verb = verb + It("should allow to delete the gardenlet's bootstrap cluster role binding without consulting the graph", func() { + attrs.Verb = "delete" + attrs.Name = "gardener.cloud:system:seed-bootstrapper:garden:" + seedName decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) - }, + }) - Entry("create", "create"), - ) + It("should allow because path to seed exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + decision, reason, err := authorizer.Authorize(ctx, attrs) + + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) + + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update delete]")) + Expect(reason).To(ContainSubstring("no relationship found")) + }) - }, + It("should allow without consulting the graph because verb is create", func() { + attrs.Verb = "create" - Entry("list", "list"), - Entry("watch", "watch"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - It("should allow when verb is delete and resource does not exist", func() { - attrs.Verb = "delete" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update]")) - graph.EXPECT().HasVertex(graphpkg.VertexTypeSecret, namespace, name).Return(false) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + }, - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + Entry("list", "list"), + Entry("watch", "watch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - if verb == "delete" { - graph.EXPECT().HasVertex(graphpkg.VertexTypeSecret, namespace, name).Return(true).Times(2) - } + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" + + decision, reason, err := authorizer.Authorize(ctx, attrs) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }, + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for Leases", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: coordinationv1.SchemeGroupVersion.Group, + Resource: "leases", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("get", "get"), - Entry("patch", "patch"), - Entry("update", "update"), - Entry("delete", "delete"), - ) - }) + DescribeTable("should allow without consulting the graph because verb is create", + func(verb string) { + attrs.Verb = verb - Context("when requested for InternalSecrets", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: gardencorev1beta1.SchemeGroupVersion.Group, - Resource: "internalsecrets", - ResourceRequest: true, - Verb: "get", - } - }) + Entry("create", "create"), + ) - It("should allow because verb is create", func() { - attrs.Verb = "create" + DescribeTable("should return correct result if path exists", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeLease, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) - It("should allow when verb is delete and resource does not exist", func() { - attrs.Verb = "delete" + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeLease, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - graph.EXPECT().HasVertex(graphpkg.VertexTypeInternalSecret, namespace, name).Return(false) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("update", "update"), + Entry("patch", "patch"), + ) - DescribeTable("should return correct result if path exists", - func(verb string) { - attrs.Verb = verb + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb + + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch list watch]")) + + }, + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - if verb == "delete" { - graph.EXPECT().HasVertex(graphpkg.VertexTypeInternalSecret, namespace, name).Return(true).Times(2) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + }) + + Context("when requested for ServiceAccounts", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "serviceaccounts", + ResourceRequest: true, + Verb: "get", } + }) + + It("should allow to delete the gardenlet's bootstrap service account without consulting the graph", func() { + attrs.Verb = "delete" + attrs.Namespace = "garden" + attrs.Name = "gardenlet-bootstrap-" + seedName + + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) + + It("should allow because path to seed exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeInternalSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionAllow)) Expect(reason).To(BeEmpty()) + }) + + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + + decision, reason, err := authorizer.Authorize(ctx, attrs) - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeInternalSecret, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - decision, reason, err = authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) Expect(reason).To(ContainSubstring("no relationship found")) - }, - - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("patch", "patch"), - Entry("update", "update"), - Entry("delete", "delete"), - ) + }) - DescribeTable("should have no opinion because no allowed verb", - func(verb string) { - attrs.Verb = verb + It("should allow without consulting the graph because verb is create", func() { + attrs.Verb = "create" decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get update patch delete list watch]")) - }, + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - Entry("deletecollection", "deletecollection"), - ) + DescribeTable("should allow without consulting the graph because object is in the seed's namespace", + func(verb string) { + attrs.Namespace = "seed-" + seedName + attrs.Verb = verb - It("should have no opinion because request is for a subresource", func() { - attrs.Subresource = "status" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + It("should allow token subresource without consulting the graph because object is in the seed's namespace", func() { + attrs.Namespace = "seed-" + seedName + attrs.Verb = "create" + attrs.Subresource = "token" - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - decision, reason, err := authorizer.Authorize(ctx, attrs) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update]")) - Context("when requested for ClusterRoleBindings", func() { - var ( - name string - attrs *auth.AttributesRecord - ) + }, - BeforeEach(func() { - name = "fooClusterRoleBinding" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - APIGroup: rbacv1.SchemeGroupVersion.Group, - Resource: "clusterrolebindings", - ResourceRequest: true, - Verb: "get", - } - }) + Entry("list", "list"), + Entry("watch", "watch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - It("should allow to delete the gardenlet's bootstrap cluster role binding without consulting the graph", func() { - attrs.Verb = "delete" - attrs.Name = "gardener.cloud:system:seed-bootstrapper:garden:" + seedName + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - It("should allow because path to seed exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) }) + }) - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + Context("extension client", func() { + BeforeEach(func() { + seedUser = extensionUser + }) + + testCommonAccess() + + Context("when requested for CertificateSigningRequests", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "foo" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: certificatesv1.SchemeGroupVersion.Group, + Resource: "certificatesigningrequests", + ResourceRequest: true, + Verb: "get", + } + }) + + DescribeTable("should allow read access if path exists", + func(verb string) { + attrs.Verb = verb + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeCertificateSigningRequest, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + decision, reason, err = authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + ) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - It("should allow without consulting the graph because verb is create", func() { - attrs.Verb = "create" + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get list watch]")) - decision, reason, err := authorizer.Authorize(ctx, attrs) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("create", "create"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "seedclient" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update]")) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) + }) + + Context("when requested for ClusterRoleBindings", func() { + var ( + name string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name = "fooClusterRoleBinding" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + APIGroup: rbacv1.SchemeGroupVersion.Group, + Resource: "clusterrolebindings", + ResourceRequest: true, + Verb: "get", + } + }) - }, + It("should allow because path to seed exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - Entry("list", "list"), - Entry("watch", "watch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeClusterRoleBinding, "", name, graphpkg.VertexTypeSeed, "", seedName).Return(false) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + decision, reason, err := authorizer.Authorize(ctx, attrs) - decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("no relationship found")) + }) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) - }) - }) + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Context("when requested for ServiceAccounts", func() { - var ( - name, namespace string - attrs *auth.AttributesRecord - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get]")) + }, - BeforeEach(func() { - name, namespace = "foo", "bar" - attrs = &auth.AttributesRecord{ - User: seedUser, - Name: name, - Namespace: namespace, - APIGroup: corev1.SchemeGroupVersion.Group, - Resource: "serviceaccounts", - ResourceRequest: true, - Verb: "get", - } - }) + Entry("create", "create"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("patch", "patch"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - It("should allow to delete the gardenlet's bootstrap service account without consulting the graph", func() { - attrs.Verb = "delete" - attrs.Namespace = "garden" - attrs.Name = "gardenlet-bootstrap-" + seedName + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "foo" - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - It("should allow because path to seed exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) + }) + + Context("when requested for Leases", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "seed-"+seedName + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: coordinationv1.SchemeGroupVersion.Group, + Resource: "leases", + ResourceRequest: true, + Verb: "get", + } + }) - It("should have no opinion because path to seed does not exists", func() { - graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) + XDescribeTable("should allow without consulting the graph because verb is create", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("no relationship found")) - }) + Entry("create", "create"), + ) - It("should allow without consulting the graph because verb is create", func() { - attrs.Verb = "create" + DescribeTable("should allow because lease is in seed namespace", + func(verb string) { + attrs.Verb = verb - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }, - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Entry("create", "create"), + Entry("get", "get"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("update", "update"), + Entry("patch", "patch"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) + + DescribeTable("should have no opinion because verb is not allowed", + func(verb string) { + attrs.Verb = verb + + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get list watch update patch delete deletecollection]")) + }, + Entry("foo", "foo"), + ) - DescribeTable("should allow without consulting the graph because object is in the seed's namespace", - func(verb string) { - attrs.Namespace = "seed-" + seedName - attrs.Verb = verb + It("should have no opinion because lease is not in seed namespace", func() { + attrs.Verb = "create" + attrs.Name = seedName + attrs.Namespace = "gardener-system-seed-lease" decision, reason, err := authorizer.Authorize(ctx, attrs) Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }, + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("lease object is not in seed namespace")) + }) + }) + + Context("when requested for ServiceAccounts", func() { + var ( + name, namespace string + attrs *auth.AttributesRecord + ) + + BeforeEach(func() { + name, namespace = "foo", "bar" + attrs = &auth.AttributesRecord{ + User: seedUser, + Name: name, + Namespace: namespace, + APIGroup: corev1.SchemeGroupVersion.Group, + Resource: "serviceaccounts", + ResourceRequest: true, + Verb: "get", + } + }) - Entry("get", "get"), - Entry("list", "list"), - Entry("watch", "watch"), - Entry("create", "create"), - Entry("update", "update"), - Entry("patch", "patch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) - - It("should allow token subresource without consulting the graph because object is in the seed's namespace", func() { - attrs.Namespace = "seed-" + seedName - attrs.Verb = "create" - attrs.Subresource = "token" + It("should allow because path to seed exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(true) - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionAllow)) - Expect(reason).To(BeEmpty()) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionAllow)) + Expect(reason).To(BeEmpty()) + }) - DescribeTable("should deny because verb is not allowed", - func(verb string) { - attrs.Verb = verb + It("should have no opinion because path to seed does not exists", func() { + graph.EXPECT().HasPathFrom(graphpkg.VertexTypeServiceAccount, namespace, name, graphpkg.VertexTypeSeed, "", seedName).Return(false) decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [create get patch update]")) + Expect(reason).To(ContainSubstring("no relationship found")) + }) - }, + DescribeTable("should deny because verb is not allowed", + func(verb string) { + attrs.Verb = verb - Entry("list", "list"), - Entry("watch", "watch"), - Entry("delete", "delete"), - Entry("deletecollection", "deletecollection"), - ) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following verbs are allowed for this resource type: [get]")) - It("should have no opinion because no allowed subresource", func() { - attrs.Subresource = "foo" + }, - decision, reason, err := authorizer.Authorize(ctx, attrs) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) - }) + Entry("create", "create"), + Entry("list", "list"), + Entry("watch", "watch"), + Entry("patch", "patch"), + Entry("update", "update"), + Entry("delete", "delete"), + Entry("deletecollection", "deletecollection"), + ) - It("should have no opinion because no resource name is given", func() { - attrs.Name = "" + It("should have no opinion because no allowed subresource", func() { + attrs.Subresource = "token" - decision, reason, err := authorizer.Authorize(ctx, attrs) + decision, reason, err := authorizer.Authorize(ctx, attrs) + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("only the following subresources are allowed for this resource type: []")) + }) - Expect(err).NotTo(HaveOccurred()) - Expect(decision).To(Equal(auth.DecisionNoOpinion)) - Expect(reason).To(ContainSubstring("No Object name found")) + It("should have no opinion because no resource name is given", func() { + attrs.Name = "" + + decision, reason, err := authorizer.Authorize(ctx, attrs) + + Expect(err).NotTo(HaveOccurred()) + Expect(decision).To(Equal(auth.DecisionNoOpinion)) + Expect(reason).To(ContainSubstring("No Object name found")) + }) }) }) }) diff --git a/pkg/admissioncontroller/webhook/auth/seed/graph/eventhandler_certificatesigningrequest.go b/pkg/admissioncontroller/webhook/auth/seed/graph/eventhandler_certificatesigningrequest.go index 99bf575f671..fd74951ed22 100644 --- a/pkg/admissioncontroller/webhook/auth/seed/graph/eventhandler_certificatesigningrequest.go +++ b/pkg/admissioncontroller/webhook/auth/seed/graph/eventhandler_certificatesigningrequest.go @@ -65,7 +65,7 @@ func (g *graph) handleCertificateSigningRequestCreate(name string, request []byt if ok, _ := gardenerutils.IsSeedClientCert(x509cr, usages); !ok { return } - seedName, _ := seedidentity.FromCertificateSigningRequest(x509cr) + seedName, _, _ := seedidentity.FromCertificateSigningRequest(x509cr) var ( certificateSigningRequestVertex = g.getOrCreateVertex(VertexTypeCertificateSigningRequest, "", name) diff --git a/test/e2e/gardener/seed/garden_access.go b/test/e2e/gardener/seed/garden_access.go index 8a28db48468..a8c43150d08 100644 --- a/test/e2e/gardener/seed/garden_access.go +++ b/test/e2e/gardener/seed/garden_access.go @@ -15,6 +15,8 @@ package seed import ( + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -37,6 +39,8 @@ var _ = Describe("Seed Tests", Label("Seed", "default"), func() { seed *gardencorev1beta1.Seed seedNamespace string gardenAccessName string + + accessSecret *corev1.Secret ) BeforeEach(func() { @@ -48,11 +52,8 @@ var _ = Describe("Seed Tests", Label("Seed", "default"), func() { seedNamespace = gardenerutils.ComputeGardenNamespace(seed.Name) gardenAccessName = "test-" + utils.ComputeSHA256Hex([]byte(uuid.NewUUID()))[:8] - }) - It("should request tokens for garden access secrets", func() { - By("Create garden access secret") - accessSecret := &corev1.Secret{ + accessSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: gardenAccessName, Namespace: v1beta1constants.GardenNamespace, @@ -65,6 +66,10 @@ var _ = Describe("Seed Tests", Label("Seed", "default"), func() { }, }, } + }) + + It("should request tokens for garden access secrets", func() { + By("Create garden access secret") Expect(testClient.Create(ctx, accessSecret)).To(Succeed()) log.Info("Created garden access secret for test", "secret", client.ObjectKeyFromObject(accessSecret)) @@ -98,19 +103,6 @@ var _ = Describe("Seed Tests", Label("Seed", "default"), func() { It("should renew all garden access secrets when triggered by annotation", func() { By("Create garden access secret") - accessSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: gardenAccessName, - Namespace: v1beta1constants.GardenNamespace, - Labels: map[string]string{ - resourcesv1alpha1.ResourceManagerPurpose: resourcesv1alpha1.LabelPurposeTokenRequest, - resourcesv1alpha1.ResourceManagerClass: resourcesv1alpha1.ResourceManagerClassGarden, - }, - Annotations: map[string]string{ - resourcesv1alpha1.ServiceAccountName: gardenAccessName, - }, - }, - } Expect(testClient.Create(ctx, accessSecret)).To(Succeed()) log.Info("Created garden access secret for test", "secret", client.ObjectKeyFromObject(accessSecret)) @@ -147,6 +139,21 @@ var _ = Describe("Seed Tests", Label("Seed", "default"), func() { g.Expect(accessSecret.Annotations).To(HaveKeyWithValue(resourcesv1alpha1.ServiceAccountTokenRenewTimestamp, Not(Equal(accessSecretBefore.Annotations[resourcesv1alpha1.ServiceAccountTokenRenewTimestamp])))) }).Should(Succeed()) }) + + Describe("usage in provider-local", func() { + It("should be allowed via seed authorizer to annotate its own seed", func() { + const testAnnotation = "provider-local-e2e-test-garden-access" + + Eventually(func(g Gomega) { + g.Expect(testClient.Get(ctx, client.ObjectKeyFromObject(seed), seed)).To(Succeed()) + + g.Expect(seed.Annotations).To(HaveKey(testAnnotation)) + g.Expect(time.Parse(time.RFC3339, seed.Annotations[testAnnotation])). + Should(BeTemporally(">", seed.CreationTimestamp.UTC()), + "Timestamp in %s annotation on seed %s should be after creationTimestamp of seed", testAnnotation, seed.Name) + }).Should(Succeed()) + }) + }) }) })