diff --git a/cmd/gitops-server/cmd/cmd.go b/cmd/gitops-server/cmd/cmd.go index 8db3928100..aedad82981 100644 --- a/cmd/gitops-server/cmd/cmd.go +++ b/cmd/gitops-server/cmd/cmd.go @@ -244,7 +244,7 @@ func runCmd(cmd *cobra.Command, args []string) error { fetcher := fetcher.NewSingleClusterFetcher(cl) - clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{fetcher}, nsaccess.NewChecker(nsaccess.DefautltWegoAppRules), log) + clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{fetcher}, nsaccess.NewChecker(), log) clustersManager.Start(ctx) healthChecker := health.NewHealthChecker() diff --git a/core/clustersmngr/factory_test.go b/core/clustersmngr/factory_test.go index 7362bcd0a2..9ec1b988f3 100644 --- a/core/clustersmngr/factory_test.go +++ b/core/clustersmngr/factory_test.go @@ -268,7 +268,7 @@ func TestUpdateUserNamespacesFailsToConnect(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - nsChecker := nsaccess.NewChecker(nil) + nsChecker := nsaccess.NewChecker() clustersFetcher := new(clustersmngrfakes.FakeClusterFetcher) clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{clustersFetcher}, nsChecker, logger) @@ -302,7 +302,7 @@ func TestGetClusters(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - nsChecker := nsaccess.NewChecker(nil) + nsChecker := nsaccess.NewChecker() clustersFetcher := new(clustersmngrfakes.FakeClusterFetcher) clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{clustersFetcher}, nsChecker, logger) diff --git a/core/nsaccess/nsaccess.go b/core/nsaccess/nsaccess.go index cd7ad2298f..9bed129a50 100644 --- a/core/nsaccess/nsaccess.go +++ b/core/nsaccess/nsaccess.go @@ -6,48 +6,12 @@ import ( authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" typedauth "k8s.io/client-go/kubernetes/typed/authorization/v1" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate -// DefautltWegoAppRules is the minimun set of permissions a user will need to use the wego-app in a given namespace -var DefautltWegoAppRules = []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"pods", "secrets"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"events"}, - Verbs: []string{"get", "list", "watch"}, - }, - { - APIGroups: []string{"apps"}, - Resources: []string{"deployments", "replicasets"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{"kustomize.toolkit.fluxcd.io"}, - Resources: []string{"kustomizations"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{"helm.toolkit.fluxcd.io"}, - Resources: []string{"helmreleases"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{"source.toolkit.fluxcd.io"}, - Resources: []string{"buckets", "helmcharts", "helmrepositories", "gitrepositories", "ocirepositories"}, - Verbs: []string{"get", "list"}, - }, -} - // Checker contains methods for validing user access to Kubernetes namespaces, based on a set of PolicyRules // //counterfeiter:generate . Checker @@ -56,19 +20,25 @@ type Checker interface { FilterAccessibleNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface, namespaces []corev1.Namespace) ([]corev1.Namespace, error) } -type simpleChecker struct { - rules []rbacv1.PolicyRule -} +type simpleChecker struct{} -func NewChecker(rules []rbacv1.PolicyRule) Checker { - return simpleChecker{rules: rules} +func NewChecker() Checker { + return simpleChecker{} } func (sc simpleChecker) FilterAccessibleNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface, namespaces []corev1.Namespace) ([]corev1.Namespace, error) { - result := []corev1.Namespace{} + accessToAllNamespace, err := hasAccessToAllNamespaces(ctx, auth) + if err != nil { + return nil, err + } + + if accessToAllNamespace { + return namespaces, nil + } + var result []corev1.Namespace for _, ns := range namespaces { - ok, err := userCanUseNamespace(ctx, auth, ns, sc.rules) + ok, err := hasAccessToNamespace(ctx, auth, ns) if err != nil { return nil, fmt.Errorf("user namespace access: %w", err) } @@ -81,163 +51,35 @@ func (sc simpleChecker) FilterAccessibleNamespaces(ctx context.Context, auth typ return result, nil } -func userCanUseNamespace(ctx context.Context, auth typedauth.AuthorizationV1Interface, ns corev1.Namespace, rules []rbacv1.PolicyRule) (bool, error) { - sar := &authorizationv1.SelfSubjectRulesReview{ - Spec: authorizationv1.SelfSubjectRulesReviewSpec{ - Namespace: ns.Name, +func hasAccessToNamespace(ctx context.Context, auth typedauth.AuthorizationV1Interface, ns corev1.Namespace) (bool, error) { + ssar := &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Verb: "get", + Resource: "configmaps", + Namespace: ns.Name, + }, }, } - - authRes, err := auth.SelfSubjectRulesReviews().Create(ctx, sar, metav1.CreateOptions{}) + res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{}) if err != nil { return false, err } - - return hasAllRules(authRes.Status, rules), nil + return res.Status.Allowed, nil } -var allK8sVerbs = []string{"create", "get", "list", "watch", "patch", "delete", "deletecollection"} - -func allResources(rules []rbacv1.PolicyRule) []string { - resources := sets.NewString() - for _, r := range rules { - resources.Insert(r.Resources...) - } - - return resources.List() -} - -func allAPIGroups(rules []rbacv1.PolicyRule) []string { - apiGroups := sets.NewString() - for _, r := range rules { - apiGroups.Insert(r.APIGroups...) - } - - return apiGroups.List() -} - -// hasAll rules determines if a set of SubjectRulesReview rules match a minimum set of policy rules -// -// We need to understand the "sum" of all the rules for a role. -// Convert to a hash lookup to make it easier to tell what a user can do. -// Looks like { "apps": { "deployments": { get: true, list: true } } } -// -// We handle wildcards by expanding them out to all the types of resources/APIGroups -// that are relevant to the provided rules. -// This creates slightly nonsensical sets sometimes, for example given: -// -// PolicyRules: -// -// { -// APIGroups: []string{""}, -// Resources: []string{"secrets"}, -// Verbs: []string{"get"}, -// }, -// -// { -// APIGroups: []string{"apps"}, -// Resources: []string{"deployment"}", -// Verbs: []string{"get"}, -// }, -// -// SubjectRulesReviewStatus: -// ["*", "*", "get"] -// -// We get: -// -// { -// "": { -// "secrets": { get: true } -// "deployments": { get: true } -// }, -// "apps": { -// "secrets": { get: true } -// "deployments", "secrets": { get: true } -// } -// } -// -// Secrets don't exist in "apps" according to k8s, -// but the "index checker" that checks this struct does not mind. -func hasAllRules(status authorizationv1.SubjectRulesReviewStatus, rules []rbacv1.PolicyRule) bool { - derivedAccess := map[string]map[string]map[string]bool{} - - allResourcesInRules := allResources(rules) - allAPIGroupsInRules := allAPIGroups(rules) - - for _, statusRule := range status.ResourceRules { - apiGroups := statusRule.APIGroups - if containsWildcard(apiGroups) { - apiGroups = allAPIGroupsInRules - } - - for _, apiGroup := range apiGroups { - if _, ok := derivedAccess[apiGroup]; !ok { - derivedAccess[apiGroup] = map[string]map[string]bool{} - } - - resources := statusRule.Resources - if containsWildcard(resources) { - resources = allResourcesInRules - } - - for _, resource := range resources { - if _, ok := derivedAccess[apiGroup][resource]; !ok { - derivedAccess[apiGroup][resource] = map[string]bool{} - } - - verbs := statusRule.Verbs - if containsWildcard(verbs) { - verbs = allK8sVerbs - } - - for _, v := range verbs { - derivedAccess[apiGroup][resource][v] = true - } - } - } - } - - hasAccess := true - -Rules: - for _, rule := range rules { - for _, apiGroup := range rule.APIGroups { - g, ok := derivedAccess[apiGroup] - - if !ok { - hasAccess = false - break Rules - } - for _, resource := range rule.Resources { - r, ok := g[resource] - if !ok { - // A resource is not present for this apiGroup. - hasAccess = false - break Rules - } - - for _, verb := range rule.Verbs { - _, ok := r[verb] - if !ok { - // A verb is not present for this resource, - // no need to check the rest of the verbs. - hasAccess = false - break Rules - } - } - } - } +func hasAccessToAllNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface) (bool, error) { + ssar := &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Verb: "list", + Resource: "namespaces", + }, + }, } - - return hasAccess -} - -func containsWildcard(permissions []string) bool { - for _, p := range permissions { - if p == "*" { - return true - } + res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{}) + if err != nil { + return false, err } - - return false + return res.Status.Allowed, nil } diff --git a/core/nsaccess/nsaccess_test.go b/core/nsaccess/nsaccess_test.go index 5286c382da..a82872ca1d 100644 --- a/core/nsaccess/nsaccess_test.go +++ b/core/nsaccess/nsaccess_test.go @@ -2,495 +2,117 @@ package nsaccess import ( "context" - "fmt" "testing" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - typedauth "k8s.io/client-go/kubernetes/typed/authorization/v1" - "k8s.io/client-go/rest" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/kubectl/pkg/scheme" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - - "github.com/weaveworks/weave-gitops/core/clustersmngr/cluster" - "github.com/weaveworks/weave-gitops/pkg/kube" - "github.com/weaveworks/weave-gitops/pkg/server/auth" ) -var userName = "test-user" - -func TestFilterAccessibleNamespaces(t *testing.T) { +func Test_simplerChecker_FilterAccessibleNamespaces(t *testing.T) { g := NewGomegaWithT(t) ctx := context.Background() testEnv := &envtest.Environment{} - testEnv.ControlPlane.GetAPIServer().Configure().Append("--authorization-mode=RBAC") testCfg, err := testEnv.Start() g.Expect(err).NotTo(HaveOccurred()) - defer func() { + t.Cleanup(func() { err := testEnv.Stop() if err != nil { t.Error(err) } - }() - - scheme, err := kube.CreateScheme() - g.Expect(err).To(BeNil()) + }) adminClient, err := client.New(testCfg, client.Options{ - Scheme: scheme, + Scheme: scheme.Scheme, }) g.Expect(err).NotTo(HaveOccurred()) - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - t.Run("returns a namespace that the user has access to", func(t *testing.T) { - accessibleNS := &corev1.Namespace{} - accessibleNS.Name = "accessible-ns" - g.Expect(adminClient.Create(ctx, accessibleNS)).To(Succeed()) - defer removeNs(t, adminClient, accessibleNS) - - inaccessibleNS := &corev1.Namespace{} - inaccessibleNS.Name = "nope" - g.Expect(adminClient.Create(ctx, inaccessibleNS)).To(Succeed()) - defer removeNs(t, adminClient, inaccessibleNS) - - roleName := types.NamespacedName{Namespace: accessibleNS.Name, Name: "test-role"} - rules := []rbacv1.PolicyRule{ - { - APIGroups: []string{"mygroup"}, - Resources: []string{"coolresource"}, - Verbs: []string{"get", "list"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, rules) - - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - checker := NewChecker(rules) - - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) - if err != nil { - t.Error(err) - } - - if len(filtered) != 1 { - t.Errorf("expected filtered length to be 1, received %v", len(filtered)) - } - - ok := false - for _, ns := range filtered { - if ns.Name == inaccessibleNS.Name { - t.Error("inaccessible NS should not have appeared") - } - - if ns.Name == accessibleNS.Name { - ok = true - } - } - - if ok == false { - t.Error("expected the accessible namespace to exist in the list of filtered namespaces") - } + // The aggregated cluster role controller is not running in the simplified testEnv control plane. + // Prepare the default admin user-facing role for the following tests. + cr := &rbacv1.ClusterRole{} + g.Expect(adminClient.Get(ctx, client.ObjectKey{Name: "admin"}, cr)).To(Succeed()) + g.Expect(cr.Rules).To(BeEmpty()) + cr.Rules = append(cr.Rules, rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, }) - t.Run("filters out namespaces that do not have the right resources", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - roleName := makeRole(ns) - - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - // Don't allow "pods" in the role - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "pods", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } + g.Expect(adminClient.Update(ctx, cr)).To(Succeed()) + t.Run("cluster-admin has access to all namespaces", func(t *testing.T) { list := &corev1.NamespaceList{} g.Expect(adminClient.List(ctx, list)).To(Succeed()) + g.Expect(list.Items).To(Not(BeEmpty())) - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + cs, err := kubernetes.NewForConfig(testCfg) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(filtered).To(HaveLen(0)) - }) - t.Run("filters out namespaces that do not have the right verbs", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - roleName := makeRole(ns) - - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "pods", "events", "namespaces"}, - // Don't allow listing "pods" in the role - Verbs: []string{"get"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "pods", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } - - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + res, err := NewChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items) g.Expect(err).NotTo(HaveOccurred()) - - g.Expect(filtered).To(HaveLen(0)) + g.Expect(res).To(ContainElements(list.Items)) }) - t.Run("filters out namespaces that do not have the right resources (multiple required rules)", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - roleName := makeRole(ns) - - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } + t.Run("standard user has access to no namespaces", func(t *testing.T) { list := &corev1.NamespaceList{} g.Expect(adminClient.List(ctx, list)).To(Succeed()) + g.Expect(list.Items).To(Not(BeEmpty())) - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + user, err := testEnv.AddUser(envtest.User{Name: "no-access-user"}, nil) g.Expect(err).NotTo(HaveOccurred()) - - g.Expect(filtered).To(HaveLen(0)) - }) - t.Run("filters out namespaces that do not have the right verbs (multiple required rules)", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - roleName := makeRole(ns) - - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "pods", "events", "namespaces"}, - Verbs: []string{"get"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "pods", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get"}, - }, - } - - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + cs, err := kubernetes.NewForConfig(user.Config()) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(filtered).To(HaveLen(0)) - }) - t.Run("works when api groups are defined in multiple roles", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - roleName := makeRole(ns) - - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get", "list"}, - }, - } - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "pods", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } - - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + res, err := NewChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items) g.Expect(err).NotTo(HaveOccurred()) - - g.Expect(filtered).To(HaveLen(1)) + g.Expect(res).To(BeEmpty()) }) - t.Run("works when api groups are defined in multiple roles (multiple required rules)", func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - roleName := makeRole(ns) + t.Run("namespace owner has access to namespace", func(t *testing.T) { + username := "test-user" - roleRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get", "list"}, + ns := &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + GenerateName: "test-ns-", }, } + g.Expect(adminClient.Create(ctx, ns)).To(Succeed()) - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, + rb := &rbacv1.RoleBinding{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ns-admins", + Namespace: ns.Name, }, - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get", "list"}, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "admin", }, + Subjects: []rbacv1.Subject{{ + Kind: "User", + Name: username, + }}, } + g.Expect(adminClient.Create(ctx, rb)).To(Succeed()) list := &corev1.NamespaceList{} g.Expect(adminClient.List(ctx, list)).To(Succeed()) + g.Expect(list.Items).To(ContainElement(HaveField("Name", ns.Name))) - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) + user, err := testEnv.AddUser(envtest.User{Name: username}, nil) + g.Expect(err).NotTo(HaveOccurred()) + cs, err := kubernetes.NewForConfig(user.Config()) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(filtered).To(HaveLen(2)) - }) - t.Run("works when a user has * permissions on api/group/verb", func(t *testing.T) { - testCases := map[string][]rbacv1.PolicyRule{ - "_ _ *": { - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"*"}, - }, - }, - "_ * _": { - { - APIGroups: []string{""}, - Resources: []string{"*"}, - Verbs: []string{"get", "list"}, - }, - }, - "* _ _": { - { - APIGroups: []string{"*"}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - }, - "_ * *": { - { - APIGroups: []string{""}, - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - }, - "* * *": { - { - APIGroups: []string{"*"}, - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - }, - } - - for name, roleRules := range testCases { - t.Run(name, func(t *testing.T) { - g := NewGomegaWithT(t) - ns := newNamespace(context.Background(), adminClient, NewGomegaWithT(t)) - defer removeNs(t, adminClient, ns) - - userName = userName + "-" + rand.String(5) - - roleName := makeRole(ns) - - userCfg := newRestConfigWithRole(t, testCfg, roleName, roleRules) - - requiredRules := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets", "events", "namespaces"}, - Verbs: []string{"get", "list"}, - }, - } - - list := &corev1.NamespaceList{} - g.Expect(adminClient.List(ctx, list)).To(Succeed()) - - checker := NewChecker(requiredRules) - filtered, err := checker.FilterAccessibleNamespaces(ctx, userCfg, list.Items) - g.Expect(err).NotTo(HaveOccurred()) - - g.Expect(filtered).To(HaveLen(1)) - }) - } - }) -} - -func newNamespace(ctx context.Context, k client.Client, g *GomegaWithT) *corev1.Namespace { - ns := &corev1.Namespace{} - ns.Name = "kube-test-" + rand.String(5) - - g.Expect(k.Create(ctx, ns)).To(Succeed()) - - return ns -} - -func makeRole(ns *corev1.Namespace) types.NamespacedName { - return types.NamespacedName{Namespace: ns.Name, Name: fmt.Sprintf("test-role-%v", rand.String(5))} -} - -func createRole(t *testing.T, cl client.Client, key types.NamespacedName, rules []rbacv1.PolicyRule) { - t.Helper() - role := &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{Name: "test-role", Namespace: key.Namespace}, - Rules: rules, - } - if err := cl.Create(context.TODO(), role); err != nil { - t.Fatalf("failed to write role: %s", err) - } - - binding := &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-role-binding", Namespace: key.Namespace, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "User", - Name: userName, - APIGroup: "rbac.authorization.k8s.io", - }, - }, - RoleRef: rbacv1.RoleRef{ - Kind: "Role", - Name: role.Name, - APIGroup: "rbac.authorization.k8s.io", - }, - } - - if err := cl.Create(context.TODO(), binding); err != nil { - t.Fatalf("failed to write role-binding: %s", err) - } -} - -func newRestConfigWithRole(t *testing.T, testCfg *rest.Config, roleName types.NamespacedName, rules []rbacv1.PolicyRule) typedauth.AuthorizationV1Interface { - t.Helper() - - scheme, err := kube.CreateScheme() - if err != nil { - t.Fatal(err) - } - - adminClient, err := client.New(testCfg, client.Options{ - Scheme: scheme, + res, err := NewChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).To(ConsistOf(HaveField("Name", ns.Name))) }) - if err != nil { - t.Fatal(err) - } - - cluster, err := cluster.NewSingleCluster("test", testCfg, scheme, kube.UserPrefixes{}) - if err != nil { - t.Fatal(err) - } - - createRole(t, adminClient, roleName, rules) - - userCfg := *testCfg - - userCfg.Impersonate = rest.ImpersonationConfig{ - UserName: userName, - } - - userClient, err := cluster.GetUserClientset(&auth.UserPrincipal{ID: userName}) - if err != nil { - t.Fatal(err) - } - - return userClient.AuthorizationV1() -} - -func removeNs(t *testing.T, k client.Client, ns *corev1.Namespace) { - t.Helper() - - if err := k.Delete(context.Background(), ns); err != nil { - t.Error(err) - } } diff --git a/core/server/server.go b/core/server/server.go index 5da1ad70e1..85181aee85 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -62,7 +62,7 @@ func NewCoreConfig(log logr.Logger, cfg *rest.Config, clusterName string, cluste log: log.WithName("core-server"), RestCfg: cfg, clusterName: clusterName, - NSAccess: nsaccess.NewChecker(nsaccess.DefautltWegoAppRules), + NSAccess: nsaccess.NewChecker(), ClustersManager: clustersManager, PrimaryKinds: kinds, HealthChecker: healthChecker, diff --git a/pkg/services/crd/suite_test.go b/pkg/services/crd/suite_test.go index 0513ef2d74..c329c938ae 100644 --- a/pkg/services/crd/suite_test.go +++ b/pkg/services/crd/suite_test.go @@ -68,7 +68,7 @@ func createClient(k8sEnv *testutils.K8sTestEnv) (clustersmngr.Client, clustersmn clustersManager := clustersmngr.NewClustersManager( []clustersmngr.ClusterFetcher{fetcher}, - nsaccess.NewChecker(nsaccess.DefautltWegoAppRules), + nsaccess.NewChecker(), log, )