Skip to content

Commit cd309ed

Browse files
committed
fix: simplify checker for access to namespaces
1 parent e9ef79d commit cd309ed

File tree

6 files changed

+94
-630
lines changed

6 files changed

+94
-630
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.NewChecker(), 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.NewChecker()
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.NewChecker()
306306
clustersFetcher := new(clustersmngrfakes.FakeClusterFetcher)
307307

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

core/nsaccess/nsaccess.go

Lines changed: 35 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,12 @@ import (
66

77
authorizationv1 "k8s.io/api/authorization/v1"
88
corev1 "k8s.io/api/core/v1"
9-
rbacv1 "k8s.io/api/rbac/v1"
109
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11-
"k8s.io/apimachinery/pkg/util/sets"
1210
typedauth "k8s.io/client-go/kubernetes/typed/authorization/v1"
1311
)
1412

1513
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
1614

17-
// DefautltWegoAppRules is the minimun set of permissions a user will need to use the wego-app in a given namespace
18-
var DefautltWegoAppRules = []rbacv1.PolicyRule{
19-
{
20-
APIGroups: []string{""},
21-
Resources: []string{"pods", "secrets"},
22-
Verbs: []string{"get", "list"},
23-
},
24-
{
25-
APIGroups: []string{""},
26-
Resources: []string{"events"},
27-
Verbs: []string{"get", "list", "watch"},
28-
},
29-
{
30-
APIGroups: []string{"apps"},
31-
Resources: []string{"deployments", "replicasets"},
32-
Verbs: []string{"get", "list"},
33-
},
34-
{
35-
APIGroups: []string{"kustomize.toolkit.fluxcd.io"},
36-
Resources: []string{"kustomizations"},
37-
Verbs: []string{"get", "list"},
38-
},
39-
{
40-
APIGroups: []string{"helm.toolkit.fluxcd.io"},
41-
Resources: []string{"helmreleases"},
42-
Verbs: []string{"get", "list"},
43-
},
44-
{
45-
APIGroups: []string{"source.toolkit.fluxcd.io"},
46-
Resources: []string{"buckets", "helmcharts", "helmrepositories", "gitrepositories", "ocirepositories"},
47-
Verbs: []string{"get", "list"},
48-
},
49-
}
50-
5115
// Checker contains methods for validing user access to Kubernetes namespaces, based on a set of PolicyRules
5216
//
5317
//counterfeiter:generate . Checker
@@ -56,19 +20,25 @@ type Checker interface {
5620
FilterAccessibleNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface, namespaces []corev1.Namespace) ([]corev1.Namespace, error)
5721
}
5822

59-
type simpleChecker struct {
60-
rules []rbacv1.PolicyRule
61-
}
23+
type simpleChecker struct{}
6224

63-
func NewChecker(rules []rbacv1.PolicyRule) Checker {
64-
return simpleChecker{rules: rules}
25+
func NewChecker() Checker {
26+
return simpleChecker{}
6527
}
6628

6729
func (sc simpleChecker) FilterAccessibleNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface, namespaces []corev1.Namespace) ([]corev1.Namespace, error) {
68-
result := []corev1.Namespace{}
30+
accessToAllNamespace, err := hasAccessToAllNamespaces(ctx, auth)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
if accessToAllNamespace {
36+
return namespaces, nil
37+
}
6938

39+
var result []corev1.Namespace
7040
for _, ns := range namespaces {
71-
ok, err := userCanUseNamespace(ctx, auth, ns, sc.rules)
41+
ok, err := hasAccessToNamespace(ctx, auth, ns)
7242
if err != nil {
7343
return nil, fmt.Errorf("user namespace access: %w", err)
7444
}
@@ -81,163 +51,35 @@ func (sc simpleChecker) FilterAccessibleNamespaces(ctx context.Context, auth typ
8151
return result, nil
8252
}
8353

84-
func userCanUseNamespace(ctx context.Context, auth typedauth.AuthorizationV1Interface, ns corev1.Namespace, rules []rbacv1.PolicyRule) (bool, error) {
85-
sar := &authorizationv1.SelfSubjectRulesReview{
86-
Spec: authorizationv1.SelfSubjectRulesReviewSpec{
87-
Namespace: ns.Name,
54+
func hasAccessToNamespace(ctx context.Context, auth typedauth.AuthorizationV1Interface, ns corev1.Namespace) (bool, error) {
55+
ssar := &authorizationv1.SelfSubjectAccessReview{
56+
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
57+
ResourceAttributes: &authorizationv1.ResourceAttributes{
58+
Verb: "get",
59+
Resource: "configmaps",
60+
Namespace: ns.Name,
61+
},
8862
},
8963
}
90-
91-
authRes, err := auth.SelfSubjectRulesReviews().Create(ctx, sar, metav1.CreateOptions{})
64+
res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{})
9265
if err != nil {
9366
return false, err
9467
}
95-
96-
return hasAllRules(authRes.Status, rules), nil
68+
return res.Status.Allowed, nil
9769
}
9870

99-
var allK8sVerbs = []string{"create", "get", "list", "watch", "patch", "delete", "deletecollection"}
100-
101-
func allResources(rules []rbacv1.PolicyRule) []string {
102-
resources := sets.NewString()
103-
for _, r := range rules {
104-
resources.Insert(r.Resources...)
105-
}
106-
107-
return resources.List()
108-
}
109-
110-
func allAPIGroups(rules []rbacv1.PolicyRule) []string {
111-
apiGroups := sets.NewString()
112-
for _, r := range rules {
113-
apiGroups.Insert(r.APIGroups...)
114-
}
115-
116-
return apiGroups.List()
117-
}
118-
119-
// hasAll rules determines if a set of SubjectRulesReview rules match a minimum set of policy rules
120-
//
121-
// We need to understand the "sum" of all the rules for a role.
122-
// Convert to a hash lookup to make it easier to tell what a user can do.
123-
// Looks like { "apps": { "deployments": { get: true, list: true } } }
124-
//
125-
// We handle wildcards by expanding them out to all the types of resources/APIGroups
126-
// that are relevant to the provided rules.
127-
// This creates slightly nonsensical sets sometimes, for example given:
128-
//
129-
// PolicyRules:
130-
//
131-
// {
132-
// APIGroups: []string{""},
133-
// Resources: []string{"secrets"},
134-
// Verbs: []string{"get"},
135-
// },
136-
//
137-
// {
138-
// APIGroups: []string{"apps"},
139-
// Resources: []string{"deployment"}",
140-
// Verbs: []string{"get"},
141-
// },
142-
//
143-
// SubjectRulesReviewStatus:
144-
// ["*", "*", "get"]
145-
//
146-
// We get:
147-
//
148-
// {
149-
// "": {
150-
// "secrets": { get: true }
151-
// "deployments": { get: true }
152-
// },
153-
// "apps": {
154-
// "secrets": { get: true }
155-
// "deployments", "secrets": { get: true }
156-
// }
157-
// }
158-
//
159-
// Secrets don't exist in "apps" according to k8s,
160-
// but the "index checker" that checks this struct does not mind.
161-
func hasAllRules(status authorizationv1.SubjectRulesReviewStatus, rules []rbacv1.PolicyRule) bool {
162-
derivedAccess := map[string]map[string]map[string]bool{}
163-
164-
allResourcesInRules := allResources(rules)
165-
allAPIGroupsInRules := allAPIGroups(rules)
166-
167-
for _, statusRule := range status.ResourceRules {
168-
apiGroups := statusRule.APIGroups
169-
if containsWildcard(apiGroups) {
170-
apiGroups = allAPIGroupsInRules
171-
}
172-
173-
for _, apiGroup := range apiGroups {
174-
if _, ok := derivedAccess[apiGroup]; !ok {
175-
derivedAccess[apiGroup] = map[string]map[string]bool{}
176-
}
177-
178-
resources := statusRule.Resources
179-
if containsWildcard(resources) {
180-
resources = allResourcesInRules
181-
}
182-
183-
for _, resource := range resources {
184-
if _, ok := derivedAccess[apiGroup][resource]; !ok {
185-
derivedAccess[apiGroup][resource] = map[string]bool{}
186-
}
187-
188-
verbs := statusRule.Verbs
189-
if containsWildcard(verbs) {
190-
verbs = allK8sVerbs
191-
}
192-
193-
for _, v := range verbs {
194-
derivedAccess[apiGroup][resource][v] = true
195-
}
196-
}
197-
}
198-
}
199-
200-
hasAccess := true
201-
202-
Rules:
203-
for _, rule := range rules {
204-
for _, apiGroup := range rule.APIGroups {
205-
g, ok := derivedAccess[apiGroup]
206-
207-
if !ok {
208-
hasAccess = false
209-
break Rules
210-
}
211-
for _, resource := range rule.Resources {
212-
r, ok := g[resource]
213-
if !ok {
214-
// A resource is not present for this apiGroup.
215-
hasAccess = false
216-
break Rules
217-
}
218-
219-
for _, verb := range rule.Verbs {
220-
_, ok := r[verb]
221-
if !ok {
222-
// A verb is not present for this resource,
223-
// no need to check the rest of the verbs.
224-
hasAccess = false
225-
break Rules
226-
}
227-
}
228-
}
229-
}
71+
func hasAccessToAllNamespaces(ctx context.Context, auth typedauth.AuthorizationV1Interface) (bool, error) {
72+
ssar := &authorizationv1.SelfSubjectAccessReview{
73+
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
74+
ResourceAttributes: &authorizationv1.ResourceAttributes{
75+
Verb: "list",
76+
Resource: "namespaces",
77+
},
78+
},
23079
}
231-
232-
return hasAccess
233-
}
234-
235-
func containsWildcard(permissions []string) bool {
236-
for _, p := range permissions {
237-
if p == "*" {
238-
return true
239-
}
80+
res, err := auth.SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{})
81+
if err != nil {
82+
return false, err
24083
}
241-
242-
return false
84+
return res.Status.Allowed, nil
24385
}

0 commit comments

Comments
 (0)