Skip to content

Commit 1a70fb3

Browse files
committed
fix: simplify checker for access to namespaces
1 parent bf9b879 commit 1a70fb3

File tree

6 files changed

+198
-5
lines changed

6 files changed

+198
-5
lines changed

cmd/gitops-server/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func runCmd(cmd *cobra.Command, args []string) error {
244244

245245
fetcher := fetcher.NewSingleClusterFetcher(cl)
246246

247-
clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{fetcher}, nsaccess.NewChecker(nsaccess.DefautltWegoAppRules), log)
247+
clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{fetcher}, nsaccess.NewSimplerChecker(), log)
248248
clustersManager.Start(ctx)
249249

250250
healthChecker := health.NewHealthChecker()

core/clustersmngr/factory_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func TestUpdateUserNamespacesFailsToConnect(t *testing.T) {
268268
ctx, cancel := context.WithCancel(context.Background())
269269
defer cancel()
270270

271-
nsChecker := nsaccess.NewChecker(nil)
271+
nsChecker := nsaccess.NewSimplerChecker()
272272
clustersFetcher := new(clustersmngrfakes.FakeClusterFetcher)
273273

274274
clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{clustersFetcher}, nsChecker, logger)
@@ -302,7 +302,7 @@ func TestGetClusters(t *testing.T) {
302302
ctx, cancel := context.WithCancel(context.Background())
303303
defer cancel()
304304

305-
nsChecker := nsaccess.NewChecker(nil)
305+
nsChecker := nsaccess.NewSimplerChecker()
306306
clustersFetcher := new(clustersmngrfakes.FakeClusterFetcher)
307307

308308
clustersManager := clustersmngr.NewClustersManager([]clustersmngr.ClusterFetcher{clustersFetcher}, nsChecker, logger)

core/nsaccess/simpler_nsaccess.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package nsaccess
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
authv1 "k8s.io/api/authorization/v1"
8+
corev1 "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
typedauth "k8s.io/client-go/kubernetes/typed/authorization/v1"
11+
)
12+
13+
type simplerChecker struct{}
14+
15+
func NewSimplerChecker() Checker {
16+
return simplerChecker{}
17+
}
18+
19+
func (sc simplerChecker) FilterAccessibleNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface, namespaces []corev1.Namespace) ([]corev1.Namespace, error) {
20+
accessToAllNamespace, err := hasAccessToAllNamespaces(ctx, auth)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
if accessToAllNamespace {
26+
return namespaces, nil
27+
}
28+
29+
var result []corev1.Namespace
30+
for _, ns := range namespaces {
31+
ok, err := hasAccessToNamespace(ctx, auth, ns)
32+
if err != nil {
33+
return nil, fmt.Errorf("user namespace access: %w", err)
34+
}
35+
36+
if ok {
37+
result = append(result, ns)
38+
}
39+
}
40+
41+
return result, nil
42+
}
43+
44+
func hasAccessToNamespace(ctx context.Context, auth typedauth.AuthorizationV1Interface, ns corev1.Namespace) (bool, error) {
45+
ssar := &authv1.SelfSubjectAccessReview{
46+
Spec: authv1.SelfSubjectAccessReviewSpec{
47+
ResourceAttributes: &authv1.ResourceAttributes{
48+
Verb: "get",
49+
Resource: "configmaps",
50+
Namespace: ns.Name,
51+
},
52+
},
53+
}
54+
res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{})
55+
if err != nil {
56+
return false, err
57+
}
58+
return res.Status.Allowed, nil
59+
}
60+
61+
func hasAccessToAllNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface) (bool, error) {
62+
ssar := &authv1.SelfSubjectAccessReview{
63+
Spec: authv1.SelfSubjectAccessReviewSpec{
64+
ResourceAttributes: &authv1.ResourceAttributes{
65+
Verb: "list",
66+
Resource: "namespaces",
67+
},
68+
},
69+
}
70+
res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{})
71+
if err != nil {
72+
return false, err
73+
}
74+
return res.Status.Allowed, nil
75+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package nsaccess
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
. "github.com/onsi/gomega"
8+
corev1 "k8s.io/api/core/v1"
9+
rbacv1 "k8s.io/api/rbac/v1"
10+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/client-go/kubernetes"
12+
"k8s.io/kubectl/pkg/scheme"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/envtest"
15+
)
16+
17+
func Test_simplerChecker_FilterAccessibleNamespaces(t *testing.T) {
18+
g := NewGomegaWithT(t)
19+
ctx := context.Background()
20+
21+
testEnv := &envtest.Environment{}
22+
23+
testCfg, err := testEnv.Start()
24+
g.Expect(err).NotTo(HaveOccurred())
25+
26+
t.Cleanup(func() {
27+
err := testEnv.Stop()
28+
if err != nil {
29+
t.Error(err)
30+
}
31+
})
32+
33+
adminClient, err := client.New(testCfg, client.Options{
34+
Scheme: scheme.Scheme,
35+
})
36+
g.Expect(err).NotTo(HaveOccurred())
37+
38+
// The aggregated cluster role controller is not running in the simplified testEnv control plane.
39+
// Prepare the default admin user-facing role for the following tests.
40+
cr := &rbacv1.ClusterRole{}
41+
g.Expect(adminClient.Get(ctx, client.ObjectKey{Name: "admin"}, cr)).To(Succeed())
42+
g.Expect(cr.Rules).To(BeEmpty())
43+
cr.Rules = append(cr.Rules, rbacv1.PolicyRule{
44+
APIGroups: []string{""},
45+
Resources: []string{"configmaps"},
46+
Verbs: []string{"get", "list", "watch"},
47+
})
48+
g.Expect(adminClient.Update(ctx, cr)).To(Succeed())
49+
50+
t.Run("cluster-admin has access to all namespaces", func(t *testing.T) {
51+
list := &corev1.NamespaceList{}
52+
g.Expect(adminClient.List(ctx, list)).To(Succeed())
53+
g.Expect(list.Items).To(Not(BeEmpty()))
54+
55+
cs, err := kubernetes.NewForConfig(testCfg)
56+
g.Expect(err).NotTo(HaveOccurred())
57+
58+
res, err := NewSimplerChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items)
59+
g.Expect(err).NotTo(HaveOccurred())
60+
g.Expect(res).To(ContainElements(list.Items))
61+
})
62+
63+
t.Run("standard user has access to no namespaces", func(t *testing.T) {
64+
list := &corev1.NamespaceList{}
65+
g.Expect(adminClient.List(ctx, list)).To(Succeed())
66+
g.Expect(list.Items).To(Not(BeEmpty()))
67+
68+
user, err := testEnv.AddUser(envtest.User{Name: "no-access-user"}, nil)
69+
g.Expect(err).NotTo(HaveOccurred())
70+
cs, err := kubernetes.NewForConfig(user.Config())
71+
g.Expect(err).NotTo(HaveOccurred())
72+
73+
res, err := NewSimplerChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items)
74+
g.Expect(err).NotTo(HaveOccurred())
75+
g.Expect(res).To(BeEmpty())
76+
})
77+
78+
t.Run("namespace owner has access to namespace", func(t *testing.T) {
79+
username := "test-user"
80+
81+
ns := &corev1.Namespace{
82+
ObjectMeta: v1.ObjectMeta{
83+
GenerateName: "test-ns-",
84+
},
85+
}
86+
g.Expect(adminClient.Create(ctx, ns)).To(Succeed())
87+
88+
rb := &rbacv1.RoleBinding{
89+
ObjectMeta: v1.ObjectMeta{
90+
Name: "test-ns-admins",
91+
Namespace: ns.Name,
92+
},
93+
RoleRef: rbacv1.RoleRef{
94+
APIGroup: "rbac.authorization.k8s.io",
95+
Kind: "ClusterRole",
96+
Name: "admin",
97+
},
98+
Subjects: []rbacv1.Subject{{
99+
Kind: "User",
100+
Name: username,
101+
}},
102+
}
103+
g.Expect(adminClient.Create(ctx, rb)).To(Succeed())
104+
105+
list := &corev1.NamespaceList{}
106+
g.Expect(adminClient.List(ctx, list)).To(Succeed())
107+
g.Expect(list.Items).To(ContainElement(HaveField("Name", ns.Name)))
108+
109+
user, err := testEnv.AddUser(envtest.User{Name: username}, nil)
110+
g.Expect(err).NotTo(HaveOccurred())
111+
cs, err := kubernetes.NewForConfig(user.Config())
112+
g.Expect(err).NotTo(HaveOccurred())
113+
114+
res, err := NewSimplerChecker().FilterAccessibleNamespaces(ctx, cs.AuthorizationV1(), list.Items)
115+
g.Expect(err).NotTo(HaveOccurred())
116+
g.Expect(res).To(ConsistOf(HaveField("Name", ns.Name)))
117+
})
118+
}

core/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func NewCoreConfig(log logr.Logger, cfg *rest.Config, clusterName string, cluste
6262
log: log.WithName("core-server"),
6363
RestCfg: cfg,
6464
clusterName: clusterName,
65-
NSAccess: nsaccess.NewChecker(nsaccess.DefautltWegoAppRules),
65+
NSAccess: nsaccess.NewSimplerChecker(),
6666
ClustersManager: clustersManager,
6767
PrimaryKinds: kinds,
6868
HealthChecker: healthChecker,

pkg/services/crd/suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func createClient(k8sEnv *testutils.K8sTestEnv) (clustersmngr.Client, clustersmn
6868

6969
clustersManager := clustersmngr.NewClustersManager(
7070
[]clustersmngr.ClusterFetcher{fetcher},
71-
nsaccess.NewChecker(nsaccess.DefautltWegoAppRules),
71+
nsaccess.NewSimplerChecker(),
7272
log,
7373
)
7474

0 commit comments

Comments
 (0)