diff --git a/go.mod b/go.mod index 1496384b..10ca6f0c 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( k8s.io/klog v1.0.0 k8s.io/kube-aggregator v0.31.1 k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 + k8s.io/kubernetes v1.31.1 sigs.k8s.io/controller-runtime v0.19.0 ) diff --git a/go.sum b/go.sum index 4a26ecbc..56787cd5 100644 --- a/go.sum +++ b/go.sum @@ -486,6 +486,8 @@ k8s.io/kube-aggregator v0.31.1/go.mod h1:+aW4NX50uneozN+BtoCxI4g7ND922p8Wy3tWKFD k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubernetes v1.31.1 h1:1fcYJe8SAhtannpChbmnzHLwAV9Je99PrGaFtBvCxms= +k8s.io/kubernetes v1.31.1/go.mod h1:/YGPL//Fb9mdv5vukvAQ7Xon+Bqwry52bmjTdORAw+Q= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/pkg/accesscontrol/access_set.go b/pkg/accesscontrol/access_set.go index 481e4537..9c04e141 100644 --- a/pkg/accesscontrol/access_set.go +++ b/pkg/accesscontrol/access_set.go @@ -3,15 +3,19 @@ package accesscontrol import ( "sort" - "github.com/rancher/apiserver/pkg/types" - "github.com/rancher/steve/pkg/attributes" + v1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" + rbacv1 "k8s.io/kubernetes/pkg/apis/rbac/v1" + + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/steve/pkg/attributes" ) type AccessSet struct { - ID string - set map[key]resourceAccessSet + ID string + set map[key]resourceAccessSet + nonResourceSet map[nonResourceKey]struct{} } type resourceAccessSet map[Access]bool @@ -21,6 +25,11 @@ type key struct { gr schema.GroupResource } +type nonResourceKey struct { + verb string + url string +} + func (a *AccessSet) Namespaces() (result []string) { set := map[string]bool{} for k, as := range a.set { @@ -56,6 +65,17 @@ func (a *AccessSet) Merge(right *AccessSet) { m[k] = v } } + + if a.nonResourceSet == nil { + a.nonResourceSet = map[nonResourceKey]struct{}{} + } + + for k, v := range right.nonResourceSet { + _, ok := a.nonResourceSet[k] + if !ok { + a.nonResourceSet[k] = v + } + } } func (a AccessSet) Grants(verb string, gr schema.GroupResource, namespace, name string) bool { @@ -80,6 +100,26 @@ func (a AccessSet) Grants(verb string, gr schema.GroupResource, namespace, name return false } +func (a *AccessSet) GrantsNonResource(verb, url string) bool { + if a.nonResourceSet == nil { + return false + } + + if _, ok := a.nonResourceSet[nonResourceKey{url: url, verb: verb}]; ok { + rule := &v1.PolicyRule{NonResourceURLs: []string{url}, Verbs: []string{verb}} + return rbacv1.NonResourceURLMatches(rule, url) && rbacv1.VerbMatches(rule, verb) + } + + for key := range a.nonResourceSet { + rule := &v1.PolicyRule{NonResourceURLs: []string{key.url}, Verbs: []string{key.verb}} + if rbacv1.NonResourceURLMatches(rule, url) && rbacv1.VerbMatches(rule, verb) { + return true + } + } + + return false +} + func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result AccessList) { dedup := map[Access]bool{} for _, v := range []string{All, verb} { @@ -120,6 +160,25 @@ func (a *AccessSet) Add(verb string, gr schema.GroupResource, access Access) { } } +func (a *AccessSet) AddNonResourceURLs(verbs, urls []string) { + if len(verbs) == 0 || len(urls) == 0 { + return + } + + if a.nonResourceSet == nil { + a.nonResourceSet = map[nonResourceKey]struct{}{} + } + + for _, verb := range verbs { + for _, url := range urls { + a.nonResourceSet[nonResourceKey{ + verb: verb, + url: url, + }] = struct{}{} + } + } +} + type AccessListByVerb map[string]AccessList func (a AccessListByVerb) Grants(verb, namespace, name string) bool { diff --git a/pkg/accesscontrol/access_set_test.go b/pkg/accesscontrol/access_set_test.go new file mode 100644 index 00000000..98b56fdb --- /dev/null +++ b/pkg/accesscontrol/access_set_test.go @@ -0,0 +1,212 @@ +package accesscontrol + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAccessSet_AddNonResourceURLs(t *testing.T) { + testCases := []struct { + name string + verbs []string + urls []string + want []nonResourceKey + }{ + { + name: "valid case", + verbs: []string{"get", "post"}, + urls: []string{"/healthz", "/metrics"}, + want: []nonResourceKey{ + {"get", "/healthz"}, + {"get", "/metrics"}, + {"post", "/healthz"}, + {"post", "/metrics"}, + }, + }, + { + name: "url wildcard", + verbs: []string{"get"}, + urls: []string{"/metrics/*"}, + want: []nonResourceKey{ + {"get", "/metrics/*"}, + }, + }, + { + name: "verb wildcard", + verbs: []string{"*"}, + urls: []string{"/metrics"}, + want: []nonResourceKey{ + {"*", "/metrics"}, + }, + }, + { + name: "empty urls", + verbs: []string{"get", "post"}, + urls: []string{}, + want: nil, + }, + { + name: "empty verbs", + verbs: []string{}, + urls: []string{"/healthz", "/metrics"}, + want: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + accessSet := &AccessSet{} + accessSet.AddNonResourceURLs(tt.verbs, tt.urls) + + if len(tt.want) > 0 { + for _, key := range tt.want { + assert.Contains(t, accessSet.nonResourceSet, key) + } + } else { + assert.Len(t, accessSet.nonResourceSet, 0) + } + }) + } +} + +func TestAccessSet_GrantsNonResource(t *testing.T) { + testCases := []struct { + name string + verb string + url string + keys map[nonResourceKey]struct{} + expect bool + }{ + { + name: "direct match", + verb: "get", + url: "/healthz", + keys: map[nonResourceKey]struct{}{ + {verb: "get", url: "/healthz"}: {}, + }, + expect: true, + }, + { + name: "wildcard in url", + verb: "get", + url: "/api/resource", + keys: map[nonResourceKey]struct{}{ + {verb: "get", url: "/api/*"}: {}, + }, + expect: true, + }, + { + name: "wildcard in verb", + verb: "get", + url: "/healthz", + keys: map[nonResourceKey]struct{}{ + {verb: "*", url: "/healthz"}: {}, + }, + expect: true, + }, + { + name: "invalid wildcard", + verb: "get", + url: "/*", // that's invalid according to k8s rules + keys: map[nonResourceKey]struct{}{ + {verb: "get", url: "/api/*"}: {}, + }, + expect: false, + }, + { + name: "wrong verb", + verb: "post", + url: "/healthz", + keys: map[nonResourceKey]struct{}{ + {verb: "get", url: "/healthz"}: {}, + }, + expect: false, + }, + { + name: "wrong url", + verb: "post", + url: "/metrics", + keys: map[nonResourceKey]struct{}{ + {verb: "post", url: "/healthz"}: {}, + }, + expect: false, + }, + { + name: "no matching rule", + verb: "post", + url: "/healthz", + keys: map[nonResourceKey]struct{}{}, + expect: false, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + accessSet := &AccessSet{} + + for rule := range tt.keys { + accessSet.AddNonResourceURLs([]string{rule.verb}, []string{rule.url}) + } + + res := accessSet.GrantsNonResource(tt.verb, tt.url) + assert.Equal(t, tt.expect, res) + }) + } +} + +func TestAccessSet_Merge(t *testing.T) { + testCases := []struct { + name string + left *AccessSet + right *AccessSet + want *AccessSet + }{ + { + name: "merging NonResouceURLs", + left: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/healthz", verb: "get"}: {}, + }, + }, + right: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/metrics", verb: "post"}: {}, + }, + }, + want: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/healthz", verb: "get"}: {}, + {url: "/metrics", verb: "post"}: {}, + }, + }, + }, + { + name: "merging NonResouceURLs - repeated items", + left: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/healthz", verb: "get"}: {}, + {url: "/metrics", verb: "post"}: {}, + }, + }, + right: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/metrics", verb: "post"}: {}, + }, + }, + want: &AccessSet{ + nonResourceSet: map[nonResourceKey]struct{}{ + {url: "/healthz", verb: "get"}: {}, + {url: "/metrics", verb: "post"}: {}, + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + tt.left.Merge(tt.right) + assert.Equal(t, tt.want, tt.left) + }) + } +} diff --git a/pkg/accesscontrol/policy_rule_index.go b/pkg/accesscontrol/policy_rule_index.go index eed0d48e..3294f921 100644 --- a/pkg/accesscontrol/policy_rule_index.go +++ b/pkg/accesscontrol/policy_rule_index.go @@ -16,6 +16,9 @@ const ( groupKind = rbacv1.GroupKind userKind = rbacv1.UserKind svcAccountKind = rbacv1.ServiceAccountKind + + clusterRoleKind = "ClusterRole" + roleKind = "Role" ) type policyRuleIndex struct { @@ -75,6 +78,40 @@ func indexSubjects(kind string, subjects []rbacv1.Subject) []string { return result } +// addAccess appends a set of PolicyRules to a given AccessSet +func addAccess(accessSet *AccessSet, namespace string, roleRef roleRef) { + for _, rule := range roleRef.rules { + if len(rule.Resources) > 0 { + addResourceAccess(accessSet, namespace, rule) + } else if roleRef.kind == clusterRoleKind { + accessSet.AddNonResourceURLs(rule.Verbs, rule.NonResourceURLs) + } + } +} + +func addResourceAccess(accessSet *AccessSet, namespace string, rule rbacv1.PolicyRule) { + for _, group := range rule.APIGroups { + for _, resource := range rule.Resources { + names := rule.ResourceNames + if len(names) == 0 { + names = []string{All} + } + for _, resourceName := range names { + for _, verb := range rule.Verbs { + accessSet.Add(verb, + schema.GroupResource{ + Group: group, + Resource: resource, + }, Access{ + Namespace: namespace, + ResourceName: resourceName, + }) + } + } + } + } +} + func subjectIs(kind string, subject rbacv1.Subject) bool { return subject.APIGroup == rbacGroup && subject.Kind == kind } @@ -83,32 +120,6 @@ func subjectIsServiceAccount(subject rbacv1.Subject) bool { return subject.APIGroup == "" && subject.Kind == svcAccountKind && subject.Namespace != "" } -// addAccess appends a set of PolicyRules to a given AccessSet -func addAccess(accessSet *AccessSet, namespace string, rules []rbacv1.PolicyRule) { - for _, rule := range rules { - for _, group := range rule.APIGroups { - for _, resource := range rule.Resources { - names := rule.ResourceNames - if len(names) == 0 { - names = []string{All} - } - for _, resourceName := range names { - for _, verb := range rule.Verbs { - accessSet.Add(verb, - schema.GroupResource{ - Group: group, - Resource: resource, - }, Access{ - Namespace: namespace, - ResourceName: resourceName, - }) - } - } - } - } - } -} - // getRules obtain the actual Role or ClusterRole pointed at by a RoleRef, and returns PolicyRules and the resource version func (p *policyRuleIndex) getRules(namespace string, roleRef rbacv1.RoleRef) ([]rbacv1.PolicyRule, string) { switch roleRef.Kind { @@ -160,6 +171,7 @@ func (p *policyRuleIndex) getRoleRefs(subjectName string) subjectGrants { roleName: crb.RoleRef.Name, resourceVersion: resourceVersion, rules: rules, + kind: clusterRoleKind, }) } @@ -171,6 +183,7 @@ func (p *policyRuleIndex) getRoleRefs(subjectName string) subjectGrants { namespace: rb.Namespace, resourceVersion: resourceVersion, rules: rules, + kind: roleKind, }) } diff --git a/pkg/accesscontrol/user_grants.go b/pkg/accesscontrol/user_grants.go index e4f1db09..39efaab5 100644 --- a/pkg/accesscontrol/user_grants.go +++ b/pkg/accesscontrol/user_grants.go @@ -22,8 +22,8 @@ type subjectGrants struct { // roleRef contains information from a Role or ClusterRole type roleRef struct { - namespace, roleName, resourceVersion string - rules []rbacv1.PolicyRule + namespace, roleName, resourceVersion, kind string + rules []rbacv1.PolicyRule } // hash calculates a unique identifier from all the grants for a user @@ -51,11 +51,11 @@ func (b subjectGrants) toAccessSet() *AccessSet { result := new(AccessSet) for _, binding := range b.roleBindings { - addAccess(result, binding.namespace, binding.rules) + addAccess(result, binding.namespace, binding) } for _, binding := range b.clusterRoleBindings { - addAccess(result, All, binding.rules) + addAccess(result, All, binding) } return result diff --git a/pkg/ext/apiserver_authorization.go b/pkg/ext/apiserver_authorization.go index 31972f5c..2c02a072 100644 --- a/pkg/ext/apiserver_authorization.go +++ b/pkg/ext/apiserver_authorization.go @@ -22,12 +22,21 @@ func NewAccessSetAuthorizer(asl accesscontrol.AccessSetLookup) *AccessSetAuthori // Authorize implements [authorizer.Authorizer]. func (a *AccessSetAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + verb := attrs.GetVerb() + path := attrs.GetPath() + accessSet := a.asl.AccessFor(attrs.GetUser()) + if !attrs.IsResourceRequest() { - // XXX: Implement - return authorizer.DecisionDeny, "AccessSetAuthorizer does not support nonResourceURLs requests", nil + if accessSet.GrantsNonResource(verb, path) { + return authorizer.DecisionAllow, "", nil + } + + // An empty string reason will still provide enough information such as: + // + // forbidden: User "unknown-user" cannot post path /openapi/v3 + return authorizer.DecisionDeny, "", nil } - verb := attrs.GetVerb() namespace := attrs.GetNamespace() name := attrs.GetName() gr := schema.GroupResource{ @@ -35,7 +44,6 @@ func (a *AccessSetAuthorizer) Authorize(ctx context.Context, attrs authorizer.At Resource: attrs.GetResource(), } - accessSet := a.asl.AccessFor(attrs.GetUser()) if accessSet.Grants(verb, gr, namespace, name) { return authorizer.DecisionAllow, "", nil } diff --git a/pkg/ext/apiserver_authorization_test.go b/pkg/ext/apiserver_authorization_test.go index ca37b23f..dbcda54d 100644 --- a/pkg/ext/apiserver_authorization_test.go +++ b/pkg/ext/apiserver_authorization_test.go @@ -2,6 +2,7 @@ package ext import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -15,9 +16,11 @@ import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/steve/pkg/accesscontrol" + "github.com/rancher/steve/pkg/accesscontrol/fake" wrbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -311,6 +314,66 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() { }, expectedStatusCode: http.StatusForbidden, }, + { + name: "authorized access to non-resource url", + user: &user.DefaultInfo{ + Name: "openapi-v2-only", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/openapi/v2", nil) + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "unauthorized verb to non-resource url", + user: &user.DefaultInfo{ + Name: "openapi-v2-only", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodPost, "/openapi/v2", nil) + }, + expectedStatusCode: http.StatusForbidden, + }, + { + name: "unauthorized access to non-resource url (user can access only openapi/v2)", + user: &user.DefaultInfo{ + Name: "openapi-v2-only", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/openapi/v3", nil) + }, + expectedStatusCode: http.StatusForbidden, + }, + { + name: "authorized user can access both openapi v2 and v3 (v2)", + user: &user.DefaultInfo{ + Name: "openapi-v2-v3", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/openapi/v2", nil) + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "authorized user can access both openapi v2 and v3 (v3)", + user: &user.DefaultInfo{ + Name: "openapi-v2-v3", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/openapi/v3", nil) + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "authorized user can access url based in wildcard rule", + user: &user.DefaultInfo{ + Name: "openapi-v2-v3", + }, + createRequest: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/openapi/v3/apis/ext.cattle.io/v1", nil) + }, + expectedStatusCode: http.StatusOK, + }, } for _, test := range tests { @@ -342,3 +405,152 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() { }) } } + +func TestAuthorization_NonResourceURLs(t *testing.T) { + type input struct { + ctx context.Context + attrs authorizer.Attributes + } + + type expected struct { + authorized authorizer.Decision + reason string + err error + } + + sampleReadOnlyUser := &user.DefaultInfo{ + Name: "read-only-user", + } + + sampleReadOnlyAccessSet := func() *accesscontrol.AccessSet { + accessSet := &accesscontrol.AccessSet{} + accessSet.AddNonResourceURLs([]string{ + "get", + }, []string{ + "/metrics", + "/healthz", + }) + return accessSet + }() + + sampleReadWriteUser := &user.DefaultInfo{ + Name: "read-write-user", + } + + sampleReadWriteAccessSet := func() *accesscontrol.AccessSet { + accessSet := &accesscontrol.AccessSet{} + accessSet.AddNonResourceURLs([]string{ + "get", "post", + }, []string{ + "/metrics", + "/healthz", + }) + return accessSet + }() + + tests := []struct { + name string + input input + expected expected + + mockUsername *user.DefaultInfo + mockAccessSet *accesscontrol.AccessSet + }{ + { + name: "authorized read-only user to read data", + input: input{ + ctx: context.TODO(), + attrs: authorizer.AttributesRecord{ + User: sampleReadOnlyUser, + ResourceRequest: false, + Path: "/healthz", + Verb: "get", + }, + }, + expected: expected{ + authorized: authorizer.DecisionAllow, + reason: "", + err: nil, + }, + mockUsername: sampleReadOnlyUser, + mockAccessSet: sampleReadOnlyAccessSet, + }, + { + name: "unauthorized read-only user to write data", + input: input{ + ctx: context.TODO(), + attrs: authorizer.AttributesRecord{ + User: sampleReadOnlyUser, + ResourceRequest: false, + Path: "/metrics", + Verb: "post", + }, + }, + expected: expected{ + authorized: authorizer.DecisionDeny, + reason: "", + err: nil, + }, + mockUsername: sampleReadOnlyUser, + mockAccessSet: sampleReadOnlyAccessSet, + }, + { + name: "authorized read-write user to read data", + input: input{ + ctx: context.TODO(), + attrs: authorizer.AttributesRecord{ + User: sampleReadWriteUser, + ResourceRequest: false, + Path: "/metrics", + Verb: "get", + }, + }, + expected: expected{ + authorized: authorizer.DecisionAllow, + reason: "", + err: nil, + }, + mockUsername: sampleReadWriteUser, + mockAccessSet: sampleReadWriteAccessSet, + }, + { + name: "authorized read-write user to write data", + input: input{ + ctx: context.TODO(), + attrs: authorizer.AttributesRecord{ + User: sampleReadWriteUser, + ResourceRequest: false, + Path: "/metrics", + Verb: "post", + }, + }, + expected: expected{ + authorized: authorizer.DecisionAllow, + reason: "", + err: nil, + }, + mockUsername: sampleReadWriteUser, + mockAccessSet: sampleReadWriteAccessSet, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + crtl := gomock.NewController(t) + asl := fake.NewMockAccessSetLookup(crtl) + asl.EXPECT().AccessFor(tt.mockUsername).Return(tt.mockAccessSet) + + auth := NewAccessSetAuthorizer(asl) + authorized, reason, err := auth.Authorize(tt.input.ctx, tt.input.attrs) + + require.Equal(t, tt.expected.authorized, authorized) + require.Equal(t, tt.expected.reason, reason) + + if tt.expected.err != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/ext/testdata/rbac.yaml b/pkg/ext/testdata/rbac.yaml index d6dc6227..62247a00 100644 --- a/pkg/ext/testdata/rbac.yaml +++ b/pkg/ext/testdata/rbac.yaml @@ -121,3 +121,45 @@ subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: read-only-error +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openapi-v2-only-read +rules: + - nonResourceURLs: ["/openapi/v2"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openapi-read +rules: + - nonResourceURLs: ["/openapi/v2", "/openapi/v3", "/openapi/v3/*"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: openapi-v2 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: openapi-v2-only-read +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: openapi-v2-only +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: openapi-v3 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: openapi-read +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: openapi-v2-v3 diff --git a/scripts/test.sh b/scripts/test.sh index 3249dd52..14f7c8b7 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,7 +6,7 @@ if ! command -v setup-envtest; then exit 127 fi -minor=$(go list -m all | grep 'k8s.io/client-go' | cut -d ' ' -f 2 | cut -d '.' -f 2) +minor=$(go mod graph | grep ' k8s.io/client-go@' | head -n1 | cut -d@ -f2 | cut -d '.' -f 2) version="1.$minor.x" export KUBEBUILDER_ASSETS=$(setup-envtest use -p path "$version")