Skip to content

Commit cc2ef53

Browse files
committed
Merge remote-tracking branch 'origin/rbac-auth-k8s-replacer' into rbac-auth-k8s-replacer
2 parents fa839f2 + 5f1b9c0 commit cc2ef53

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
corev1 "k8s.io/api/core/v1"
1215
rbacv1 "k8s.io/api/rbac/v1"
@@ -89,8 +92,14 @@ func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, manifestManager us
8992

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

120+
// sort allMissingPolicyRules alphabetically by namespace
121+
sort.Slice(allMissingPolicyRules, func(i, j int) bool {
122+
return allMissingPolicyRules[i].Namespace < allMissingPolicyRules[j].Namespace
123+
})
124+
111125
if len(preAuthEvaluationErrors) > 0 {
112126
return allMissingPolicyRules, fmt.Errorf("authorization evaluation errors: %w", errors.Join(preAuthEvaluationErrors...))
113127
}
@@ -504,6 +518,42 @@ var fullAuthority = []rbacv1.PolicyRule{
504518
{Verbs: []string{"*"}, NonResourceURLs: []string{"*"}},
505519
}
506520

521+
func parseEscalationErrorForMissingRules(ecError error) ([]rbacv1.PolicyRule, string) {
522+
// Regex to capture namespace and serviceaccount
523+
userRegex := regexp.MustCompile(`system:serviceaccount:(?P<Namespace>[^:]+):(?P<ServiceAccount>[^"]+)`)
524+
// Regex to capture missing permissions
525+
permRegex := regexp.MustCompile(`{APIGroups:\[("[^"]*")\], Resources:\[("[^"]*")\], Verbs:\[("[^"]*")\]}`)
526+
527+
userMatches := userRegex.FindStringSubmatch(ecError.Error())
528+
namespace := userMatches[1]
529+
530+
// Extract permissions
531+
var permissions []rbacv1.PolicyRule
532+
permMatches := permRegex.FindAllStringSubmatch(ecError.Error(), -1)
533+
for _, match := range permMatches {
534+
permissions = append(permissions, rbacv1.PolicyRule{
535+
APIGroups: []string{strings.Trim(match[1], `"`)},
536+
Resources: []string{strings.Trim(match[2], `"`)},
537+
Verbs: []string{strings.Trim(match[3], `"`)},
538+
})
539+
}
540+
541+
return permissions, namespace
542+
}
543+
544+
func arePolicyRulesEqual(rule1 rbacv1.PolicyRule, rule2 rbacv1.PolicyRule) bool {
545+
sort.Strings(rule1.Verbs)
546+
sort.Strings(rule2.Verbs)
547+
sort.Strings(rule1.APIGroups)
548+
sort.Strings(rule2.APIGroups)
549+
sort.Strings(rule1.Resources)
550+
sort.Strings(rule2.Resources)
551+
sort.Strings(rule1.ResourceNames)
552+
sort.Strings(rule2.ResourceNames)
553+
554+
return reflect.DeepEqual(rule1, rule2)
555+
}
556+
507557
func hasAggregationRule(clusterRole *rbacv1.ClusterRole) bool {
508558
// Currently, an aggregation rule is considered present only if it has one or more selectors.
509559
// 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)