mirror of
https://github.com/rancher/steve.git
synced 2025-04-27 19:05:09 +00:00
* adding NonResourceURLs support to access_store * added tests to AccessSet NonResourceURLs handling * change on test script suggested by @tomleb + go mod tidy * added nonresource to ext api authorization * added NonResourceURLs implementation in Authorizes + test * removed non-resource-url tests from the main test * added new tests for non-resource-urls * removed unused test data * changed nonResourceKey to point to struct{} * addressed comments from @tomleb * addressed more comments * fixing typo * check for empty accessSet
This commit is contained in:
parent
2175e090fe
commit
6ee8201c8d
1
go.mod
1
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
|
||||
)
|
||||
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -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 {
|
||||
|
212
pkg/accesscontrol/access_set_test.go
Normal file
212
pkg/accesscontrol/access_set_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
42
pkg/ext/testdata/rbac.yaml
vendored
42
pkg/ext/testdata/rbac.yaml
vendored
@ -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
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user