Skip to content

Commit 2de215e

Browse files
committed
permissions preflight: add kubernetes compatibility tests, other small fixups
Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
1 parent c06d4f2 commit 2de215e

2 files changed

Lines changed: 146 additions & 22 deletions

File tree

internal/operator-controller/authorization/rbac.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ func parseEscalationErrorForMissingRules(ecError error) (*parseResult, error) {
616616
func parseCompactRuleString(rule string) (*rbacv1.PolicyRule, error) {
617617
var fields []string
618618
if ruleText := rule[1 : len(rule)-1]; ruleText != "" {
619-
fields = mapSlice(strings.Split(rule[1:len(rule)-1], ","), func(in string) string {
619+
fields = mapSlice(strings.Split(ruleText, ","), func(in string) string {
620620
return strings.TrimSpace(in)
621621
})
622622
}

internal/operator-controller/authorization/rbac_test.go

Lines changed: 145 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package authorization
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"strings"
78
"testing"
89

@@ -12,12 +13,13 @@ import (
1213
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1415
"k8s.io/apimachinery/pkg/runtime"
15-
featuregatetesting "k8s.io/component-base/featuregate/testing"
16+
"k8s.io/apiserver/pkg/authentication/user"
17+
"k8s.io/apiserver/pkg/endpoints/request"
18+
"k8s.io/kubernetes/pkg/registry/rbac/validation"
1619
"sigs.k8s.io/controller-runtime/pkg/client"
1720
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1821

1922
ocv1 "github.com/operator-framework/operator-controller/api/v1"
20-
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
2123
)
2224

2325
var (
@@ -226,7 +228,6 @@ func setupFakeClient(role client.Object) client.Client {
226228

227229
func TestPreAuthorize_Success(t *testing.T) {
228230
t.Run("preauthorize succeeds with no missing rbac rules", func(t *testing.T) {
229-
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
230231
fakeClient := setupFakeClient(privilegedClusterRole)
231232
preAuth := NewRBACPreAuthorizer(fakeClient)
232233
missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest))
@@ -237,7 +238,6 @@ func TestPreAuthorize_Success(t *testing.T) {
237238

238239
func TestPreAuthorize_Failure(t *testing.T) {
239240
t.Run("preauthorize fails with missing rbac rules", func(t *testing.T) {
240-
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
241241
fakeClient := setupFakeClient(limitedClusterRole)
242242
preAuth := NewRBACPreAuthorizer(fakeClient)
243243
missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest))
@@ -248,7 +248,6 @@ func TestPreAuthorize_Failure(t *testing.T) {
248248

249249
func TestPreAuthorizeMultiNamespace_Failure(t *testing.T) {
250250
t.Run("preauthorize fails with missing rbac rules in multiple namespaces", func(t *testing.T) {
251-
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
252251
fakeClient := setupFakeClient(limitedClusterRole)
253252
preAuth := NewRBACPreAuthorizer(fakeClient)
254253
missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifestMultiNamespace))
@@ -259,7 +258,6 @@ func TestPreAuthorizeMultiNamespace_Failure(t *testing.T) {
259258

260259
func TestPreAuthorize_CheckEscalation(t *testing.T) {
261260
t.Run("preauthorize succeeds with no missing rbac rules", func(t *testing.T) {
262-
featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true)
263261
fakeClient := setupFakeClient(escalatingClusterRole)
264262
preAuth := NewRBACPreAuthorizer(fakeClient)
265263
missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest))
@@ -270,7 +268,7 @@ func TestPreAuthorize_CheckEscalation(t *testing.T) {
270268

271269
// TestParseEscalationErrorForMissingRules Are tests with respect to https://github.com/kubernetes/api/blob/e8d4d542f6a9a16a694bfc8e3b8cd1557eecfc9d/rbac/v1/types.go#L49-L74
272270
// Goal is: prove the regex works as planned AND that if the error messages ever change we'll learn about it with these tests
273-
func TestParseEscalationErrorForMissingRules(t *testing.T) {
271+
func TestParseEscalationErrorForMissingRules_ParsingLogic(t *testing.T) {
274272
testCases := []struct {
275273
name string
276274
inputError error
@@ -279,7 +277,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
279277
}{
280278
{
281279
name: "One Missing Resource Rule",
282-
inputError: errors.New(`user "test-user" (groups=test) is attempting to grant RBAC permissions not currently held:
280+
inputError: errors.New(`user "test-user" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
283281
{APIGroups:["apps"], Resources:["deployments"], Verbs:["get"]}`),
284282
expectedResult: &parseResult{
285283
MissingRules: []rbacv1.PolicyRule{
@@ -304,32 +302,32 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
304302
{
305303
name: "One Missing Rule with Resolution Errors",
306304
inputError: errors.New(`user "test-admin" (groups=["system:masters"]) is attempting to grant RBAC permissions not currently held:
307-
{APIGroups:["batch"], Resources:["jobs"], Verbs:["create"]}; resolution errors: role "missing-role" not found`),
305+
{APIGroups:["batch"], Resources:["jobs"], Verbs:["create"]}; resolution errors: [role "missing-role" not found]`),
308306
expectedResult: &parseResult{
309307
MissingRules: []rbacv1.PolicyRule{
310308
{APIGroups: []string{"batch"}, Resources: []string{"jobs"}, Verbs: []string{"create"}},
311309
},
312-
ResolutionErrors: errors.New(`role "missing-role" not found`),
310+
ResolutionErrors: errors.New(`[role "missing-role" not found]`),
313311
},
314312
expectError: require.NoError,
315313
},
316314
{
317315
name: "Multiple Missing Rules with Resolution Errors",
318316
inputError: errors.New(`user "another-user" (groups=[]) is attempting to grant RBAC permissions not currently held:
319317
{APIGroups:[""], Resources:["secrets"], Verbs:["get"]}
320-
{APIGroups:[""], Resources:["configmaps"], Verbs:["list"]}; resolution errors: clusterrole "missing-clusterrole" not found, role "other-missing" not found`),
318+
{APIGroups:[""], Resources:["configmaps"], Verbs:["list"]}; resolution errors: [clusterrole "missing-clusterrole" not found, role "other-missing" not found]`),
321319
expectedResult: &parseResult{
322320
MissingRules: []rbacv1.PolicyRule{
323321
{APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{"get"}},
324322
{APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"list"}},
325323
},
326-
ResolutionErrors: errors.New(`clusterrole "missing-clusterrole" not found, role "other-missing" not found`),
324+
ResolutionErrors: errors.New(`[clusterrole "missing-clusterrole" not found, role "other-missing" not found]`),
327325
},
328326
expectError: require.NoError,
329327
},
330328
{
331329
name: "Missing Rule (All Resource Fields)",
332-
inputError: errors.New(`user "resource-name-user" (groups=test) is attempting to grant RBAC permissions not currently held:
330+
inputError: errors.New(`user "resource-name-user" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
333331
{APIGroups:["extensions"], Resources:["ingresses"], ResourceNames:["my-ingress"], Verbs:["update" "patch"]}`),
334332
expectedResult: &parseResult{
335333
MissingRules: []rbacv1.PolicyRule{
@@ -340,7 +338,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
340338
},
341339
{
342340
name: "Missing Rule (No ResourceNames)",
343-
inputError: errors.New(`user "no-res-name-user" (groups=test) is attempting to grant RBAC permissions not currently held:
341+
inputError: errors.New(`user "no-res-name-user" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
344342
{APIGroups:["networking.k8s.io"], Resources:["networkpolicies"], Verbs:["watch"]}`),
345343
expectedResult: &parseResult{
346344
MissingRules: []rbacv1.PolicyRule{
@@ -351,7 +349,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
351349
},
352350
{
353351
name: "Missing Rule (NonResourceURLs only)",
354-
inputError: errors.New(`user "url-user" (groups=test) is attempting to grant RBAC permissions not currently held:
352+
inputError: errors.New(`user "url-user" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
355353
{NonResourceURLs:["/version" "/apis"], Verbs:["get"]}`),
356354
expectedResult: &parseResult{
357355
MissingRules: []rbacv1.PolicyRule{
@@ -370,7 +368,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
370368
},
371369
{
372370
name: "Empty Permissions String",
373-
inputError: errors.New(`user "empty-perms" (groups=test) is attempting to grant RBAC permissions not currently held:
371+
inputError: errors.New(`user "empty-perms" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
374372
`),
375373
expectedResult: &parseResult{},
376374
expectError: func(t require.TestingT, err error, i ...interface{}) {
@@ -379,7 +377,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
379377
},
380378
{
381379
name: "Rule with Empty Strings in lists",
382-
inputError: errors.New(`user "empty-strings" (groups=test) is attempting to grant RBAC permissions not currently held:
380+
inputError: errors.New(`user "empty-strings" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
383381
{APIGroups:["" "apps"], Resources:["" "deployments"], Verbs:["get" ""]}`),
384382
expectedResult: &parseResult{
385383
MissingRules: []rbacv1.PolicyRule{
@@ -390,7 +388,7 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
390388
},
391389
{
392390
name: "Rule with Only Empty Verb",
393-
inputError: errors.New(`user "empty-verb" (groups=test) is attempting to grant RBAC permissions not currently held:
391+
inputError: errors.New(`user "empty-verb" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
394392
{APIGroups:[""], Resources:["pods"], Verbs:[""]}`),
395393
expectedResult: &parseResult{
396394
MissingRules: []rbacv1.PolicyRule{
@@ -401,16 +399,26 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
401399
},
402400
{
403401
name: "Rule with no fields",
404-
inputError: errors.New(`user "empty-verb" (groups=test) is attempting to grant RBAC permissions not currently held:
402+
inputError: errors.New(`user "empty-verb" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
405403
{}`),
406404
expectedResult: &parseResult{
407405
MissingRules: []rbacv1.PolicyRule{{}},
408406
},
409407
expectError: require.NoError,
410408
},
409+
{
410+
name: "Rule with no colon separator",
411+
inputError: errors.New(`user "empty-verb" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
412+
{APIGroups:[""], Resources, Verbs:["get"]}
413+
`),
414+
expectedResult: &parseResult{},
415+
expectError: func(t require.TestingT, err error, i ...interface{}) {
416+
require.ErrorContains(t, err, `unexpected item "Resources": expected <Type>:[<values>...]`)
417+
},
418+
},
411419
{
412420
name: "Rule with unknown field",
413-
inputError: errors.New(`user "empty-verb" (groups=test) is attempting to grant RBAC permissions not currently held:
421+
inputError: errors.New(`user "empty-verb" (groups=["test"]) is attempting to grant RBAC permissions not currently held:
414422
{FooBar:["baz"]}
415423
{APIGroups:[""], Resources:["secrets"], Verbs:["get"]}
416424
`),
@@ -428,9 +436,125 @@ func TestParseEscalationErrorForMissingRules(t *testing.T) {
428436
for _, tc := range testCases {
429437
t.Run(tc.name, func(t *testing.T) {
430438
rules, err := parseEscalationErrorForMissingRules(tc.inputError)
431-
432439
tc.expectError(t, err)
433440
require.Equal(t, tc.expectedResult, rules)
434441
})
435442
}
436443
}
444+
445+
func TestParseEscalationErrorForMissingRules_KubernetesCompatibility(t *testing.T) {
446+
testCases := []struct {
447+
name string
448+
ruleResolver validation.AuthorizationRuleResolver
449+
wantRules []rbacv1.PolicyRule
450+
expectedErrorString string
451+
expectedResult *parseResult
452+
}{
453+
{
454+
name: "missing rules",
455+
ruleResolver: mockRulesResolver{
456+
rules: []rbacv1.PolicyRule{},
457+
err: nil,
458+
},
459+
wantRules: []rbacv1.PolicyRule{
460+
{APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{"get"}, ResourceNames: []string{"test-secret"}},
461+
{APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"get", "list", "watch"}},
462+
{APIGroups: []string{"apps"}, Resources: []string{"deployments", "replicasets"}, Verbs: []string{"create", "update", "patch", "delete"}},
463+
{NonResourceURLs: []string{"/healthz", "/livez"}, Verbs: []string{"get", "post"}},
464+
},
465+
expectedErrorString: `user "user" (groups=["a" "b"]) is attempting to grant RBAC permissions not currently held:
466+
{APIGroups:[""], Resources:["configmaps"], Verbs:["get" "list" "watch"]}
467+
{APIGroups:[""], Resources:["secrets"], ResourceNames:["test-secret"], Verbs:["get"]}
468+
{APIGroups:["apps"], Resources:["deployments"], Verbs:["create" "update" "patch" "delete"]}
469+
{APIGroups:["apps"], Resources:["replicasets"], Verbs:["create" "update" "patch" "delete"]}
470+
{NonResourceURLs:["/healthz"], Verbs:["get"]}
471+
{NonResourceURLs:["/healthz"], Verbs:["post"]}
472+
{NonResourceURLs:["/livez"], Verbs:["get"]}
473+
{NonResourceURLs:["/livez"], Verbs:["post"]}`,
474+
expectedResult: &parseResult{
475+
MissingRules: []rbacv1.PolicyRule{
476+
{APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"get", "list", "watch"}},
477+
{APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{"get"}, ResourceNames: []string{"test-secret"}},
478+
{APIGroups: []string{"apps"}, Resources: []string{"deployments"}, Verbs: []string{"create", "update", "patch", "delete"}},
479+
{APIGroups: []string{"apps"}, Resources: []string{"replicasets"}, Verbs: []string{"create", "update", "patch", "delete"}},
480+
{NonResourceURLs: []string{"/healthz"}, Verbs: []string{"get"}},
481+
{NonResourceURLs: []string{"/healthz"}, Verbs: []string{"post"}},
482+
{NonResourceURLs: []string{"/livez"}, Verbs: []string{"get"}},
483+
{NonResourceURLs: []string{"/livez"}, Verbs: []string{"post"}},
484+
},
485+
},
486+
},
487+
{
488+
name: "resolution failure",
489+
ruleResolver: mockRulesResolver{
490+
rules: []rbacv1.PolicyRule{},
491+
err: errors.New("resolution error"),
492+
},
493+
wantRules: []rbacv1.PolicyRule{
494+
{APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{"get"}, ResourceNames: []string{"test-secret"}},
495+
{APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"get", "list", "watch"}},
496+
{APIGroups: []string{"apps"}, Resources: []string{"deployments", "replicasets"}, Verbs: []string{"create", "update", "patch", "delete"}},
497+
{NonResourceURLs: []string{"/healthz", "/livez"}, Verbs: []string{"get", "post"}},
498+
},
499+
expectedErrorString: `user "user" (groups=["a" "b"]) is attempting to grant RBAC permissions not currently held:
500+
{APIGroups:[""], Resources:["configmaps"], Verbs:["get" "list" "watch"]}
501+
{APIGroups:[""], Resources:["secrets"], ResourceNames:["test-secret"], Verbs:["get"]}
502+
{APIGroups:["apps"], Resources:["deployments"], Verbs:["create" "update" "patch" "delete"]}
503+
{APIGroups:["apps"], Resources:["replicasets"], Verbs:["create" "update" "patch" "delete"]}
504+
{NonResourceURLs:["/healthz"], Verbs:["get"]}
505+
{NonResourceURLs:["/healthz"], Verbs:["post"]}
506+
{NonResourceURLs:["/livez"], Verbs:["get"]}
507+
{NonResourceURLs:["/livez"], Verbs:["post"]}; resolution errors: [resolution error]`,
508+
expectedResult: &parseResult{
509+
MissingRules: []rbacv1.PolicyRule{
510+
{APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"get", "list", "watch"}},
511+
{APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{"get"}, ResourceNames: []string{"test-secret"}},
512+
{APIGroups: []string{"apps"}, Resources: []string{"deployments"}, Verbs: []string{"create", "update", "patch", "delete"}},
513+
{APIGroups: []string{"apps"}, Resources: []string{"replicasets"}, Verbs: []string{"create", "update", "patch", "delete"}},
514+
{NonResourceURLs: []string{"/healthz"}, Verbs: []string{"get"}},
515+
{NonResourceURLs: []string{"/healthz"}, Verbs: []string{"post"}},
516+
{NonResourceURLs: []string{"/livez"}, Verbs: []string{"get"}},
517+
{NonResourceURLs: []string{"/livez"}, Verbs: []string{"post"}},
518+
},
519+
ResolutionErrors: errors.New("[resolution error]"),
520+
},
521+
},
522+
}
523+
for _, tc := range testCases {
524+
t.Run(tc.name, func(t *testing.T) {
525+
ctx := request.WithUser(request.WithNamespace(context.Background(), "namespace"), &user.DefaultInfo{
526+
Name: "user",
527+
Groups: []string{"a", "b"},
528+
})
529+
530+
// Let's actually call the upstream function that generates and returns the
531+
// error message that we are attempting to parse correctly. The hope is that
532+
// these tests will start failing if we bump to a new version of kubernetes
533+
// that causes our parsing logic to be incorrect.
534+
err := validation.ConfirmNoEscalation(ctx, tc.ruleResolver, tc.wantRules)
535+
require.Error(t, err)
536+
require.Equal(t, tc.expectedErrorString, err.Error())
537+
538+
res, err := parseEscalationErrorForMissingRules(err)
539+
require.NoError(t, err)
540+
require.Equal(t, tc.expectedResult, res)
541+
})
542+
}
543+
}
544+
545+
type mockRulesResolver struct {
546+
rules []rbacv1.PolicyRule
547+
err error
548+
}
549+
550+
func (m mockRulesResolver) GetRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) {
551+
panic("unimplemented")
552+
}
553+
554+
func (m mockRulesResolver) RulesFor(ctx context.Context, user user.Info, namespace string) ([]rbacv1.PolicyRule, error) {
555+
return m.rules, m.err
556+
}
557+
558+
func (m mockRulesResolver) VisitRulesFor(ctx context.Context, user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
559+
panic("unimplemented")
560+
}

0 commit comments

Comments
 (0)