Skip to content

Commit 5f1b9c0

Browse files
committed
Handle missing rules from escalation errors
Also sort final missing rules by namespace Signed-off-by: Tayler Geiger <[email protected]>
1 parent 69708bd commit 5f1b9c0

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

Diff for: internal/operator-controller/authorization/rbac.go

+52-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"fmt"
77
"io"
88
"maps"
9+
"reflect"
10+
"regexp"
911
"sort"
12+
"strings"
1013

1114
rbacv1 "k8s.io/api/rbac/v1"
1215
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -91,8 +94,14 @@ func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, manifestManager us
9194

9295
for _, obj := range dm.rbacObjects() {
9396
if err := ec.checkEscalation(ctx, manifestManager, obj); err != nil {
94-
// In Kubernetes 1.32.2 the specialized PrivilegeEscalationError is gone.
95-
// Instead, we simply collect the error.
97+
missingEscalationRules, namespace := parseEscalationErrorForMissingRules(err)
98+
// Check if we already have these escalation PolicyRules, if so don't append
99+
for i, rule := range missingEscalationRules {
100+
previousRule := missingRules[namespace][len(missingRules[namespace])-len(missingEscalationRules)+i]
101+
if !arePolicyRulesEqual(previousRule, rule) {
102+
missingRules[namespace] = append(missingRules[namespace], missingEscalationRules...)
103+
}
104+
}
96105
preAuthEvaluationErrors = append(preAuthEvaluationErrors, err)
97106
}
98107
}
@@ -110,6 +119,11 @@ func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, manifestManager us
110119
allMissingPolicyRules = append(allMissingPolicyRules, ScopedPolicyRules{Namespace: ns, MissingRules: sortableRules})
111120
}
112121

122+
// sort allMissingPolicyRules alphabetically by namespace
123+
sort.Slice(allMissingPolicyRules, func(i, j int) bool {
124+
return allMissingPolicyRules[i].Namespace < allMissingPolicyRules[j].Namespace
125+
})
126+
113127
if len(preAuthEvaluationErrors) > 0 {
114128
return allMissingPolicyRules, fmt.Errorf("authorization evaluation errors: %w", errors.Join(preAuthEvaluationErrors...))
115129
}
@@ -484,6 +498,42 @@ var fullAuthority = []rbacv1.PolicyRule{
484498
{Verbs: []string{"*"}, NonResourceURLs: []string{"*"}},
485499
}
486500

501+
func parseEscalationErrorForMissingRules(ecError error) ([]rbacv1.PolicyRule, string) {
502+
// Regex to capture namespace and serviceaccount
503+
userRegex := regexp.MustCompile(`system:serviceaccount:(?P<Namespace>[^:]+):(?P<ServiceAccount>[^"]+)`)
504+
// Regex to capture missing permissions
505+
permRegex := regexp.MustCompile(`{APIGroups:\[("[^"]*")\], Resources:\[("[^"]*")\], Verbs:\[("[^"]*")\]}`)
506+
507+
userMatches := userRegex.FindStringSubmatch(ecError.Error())
508+
namespace := userMatches[1]
509+
510+
// Extract permissions
511+
var permissions []rbacv1.PolicyRule
512+
permMatches := permRegex.FindAllStringSubmatch(ecError.Error(), -1)
513+
for _, match := range permMatches {
514+
permissions = append(permissions, rbacv1.PolicyRule{
515+
APIGroups: []string{strings.Trim(match[1], `"`)},
516+
Resources: []string{strings.Trim(match[2], `"`)},
517+
Verbs: []string{strings.Trim(match[3], `"`)},
518+
})
519+
}
520+
521+
return permissions, namespace
522+
}
523+
524+
func arePolicyRulesEqual(rule1 rbacv1.PolicyRule, rule2 rbacv1.PolicyRule) bool {
525+
sort.Strings(rule1.Verbs)
526+
sort.Strings(rule2.Verbs)
527+
sort.Strings(rule1.APIGroups)
528+
sort.Strings(rule2.APIGroups)
529+
sort.Strings(rule1.Resources)
530+
sort.Strings(rule2.Resources)
531+
sort.Strings(rule1.ResourceNames)
532+
sort.Strings(rule2.ResourceNames)
533+
534+
return reflect.DeepEqual(rule1, rule2)
535+
}
536+
487537
func hasAggregationRule(clusterRole *rbacv1.ClusterRole) bool {
488538
// Currently, an aggregation rule is considered present only if it has one or more selectors.
489539
// An empty slice of ClusterRoleSelectors means no selectors were provided,

Diff for: internal/operator-controller/authorization/rbac_test.go

+82-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,76 @@ subjects:
5555
- kind: ServiceAccount
5656
name: test-serviceaccount
5757
namespace: test-namespace
58+
`
59+
60+
testManifestMultiNamespace = `apiVersion: v1
61+
kind: Service
62+
metadata:
63+
name: test-service
64+
namespace: test-namespace
65+
spec:
66+
clusterIP: None
67+
---
68+
apiVersion: rbac.authorization.k8s.io/v1
69+
kind: Role
70+
metadata:
71+
name: test-extension-role
72+
namespace: test-namespace
73+
rules:
74+
- apiGroups: ["*"]
75+
resources: [serviceaccounts]
76+
verbs: [watch]
77+
- apiGroups: ["*"]
78+
resources: [certificates]
79+
verbs: [create]
80+
---
81+
apiVersion: rbac.authorization.k8s.io/v1
82+
kind: RoleBinding
83+
metadata:
84+
name: test-extension-binding
85+
namespace: test-namespace
86+
roleRef:
87+
apiGroup: rbac.authorization.k8s.io
88+
kind: Role
89+
name: test-extension-role
90+
subjects:
91+
- kind: ServiceAccount
92+
name: test-serviceaccount
93+
namespace: test-namespace
94+
---
95+
kind: Service
96+
metadata:
97+
name: test-service
98+
namespace: a-test-namespace
99+
spec:
100+
clusterIP: None
101+
---
102+
apiVersion: rbac.authorization.k8s.io/v1
103+
kind: Role
104+
metadata:
105+
name: test-extension-role
106+
namespace: a-test-namespace
107+
rules:
108+
- apiGroups: ["*"]
109+
resources: [serviceaccounts]
110+
verbs: [watch]
111+
- apiGroups: ["*"]
112+
resources: [certificates]
113+
verbs: [create]
114+
---
115+
apiVersion: rbac.authorization.k8s.io/v1
116+
kind: RoleBinding
117+
metadata:
118+
name: test-extension-binding
119+
namespace: a-test-namespace
120+
roleRef:
121+
apiGroup: rbac.authorization.k8s.io
122+
kind: Role
123+
name: test-extension-role
124+
subjects:
125+
- kind: ServiceAccount
126+
name: test-serviceaccount
127+
namespace: a-test-namespace
58128
`
59129

60130
saName = "test-serviceaccount"
@@ -158,7 +228,7 @@ func TestPreAuthorize_Success(t *testing.T) {
158228
}
159229

160230
func TestPreAuthorize_Failure(t *testing.T) {
161-
t.Run("preauthorize failes with missing rbac rules", func(t *testing.T) {
231+
t.Run("preauthorize fails with missing rbac rules", func(t *testing.T) {
162232
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
163233
fakeClient := setupFakeClient(limitedClusterRole)
164234
preAuth := NewRBACPreAuthorizer(fakeClient)
@@ -168,6 +238,17 @@ func TestPreAuthorize_Failure(t *testing.T) {
168238
})
169239
}
170240

241+
func TestPreAuthorizeMultiNamespace_Failure(t *testing.T) {
242+
t.Run("preauthorize fails with missing rbac rules in multiple namespaces", func(t *testing.T) {
243+
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
244+
fakeClient := setupFakeClient(limitedClusterRole)
245+
preAuth := NewRBACPreAuthorizer(fakeClient)
246+
missingRules, err := preAuth.PreAuthorize(context.TODO(), &testServiceAccount, strings.NewReader(testManifestMultiNamespace))
247+
require.Error(t, err)
248+
require.NotEqual(t, []ScopedPolicyRules{}, missingRules)
249+
})
250+
}
251+
171252
func TestPreAuthorize_CheckEscalation(t *testing.T) {
172253
t.Run("preauthorize succeeds with no missing rbac rules", func(t *testing.T) {
173254
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)

0 commit comments

Comments
 (0)