mirror of
https://github.com/rancher/steve.git
synced 2025-08-01 23:03:28 +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/klog v1.0.0
|
||||||
k8s.io/kube-aggregator v0.31.1
|
k8s.io/kube-aggregator v0.31.1
|
||||||
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
|
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
|
||||||
|
k8s.io/kubernetes v1.31.1
|
||||||
sigs.k8s.io/controller-runtime v0.19.0
|
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-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 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA=
|
||||||
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
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-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 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
@ -3,15 +3,19 @@ package accesscontrol
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
v1 "k8s.io/api/rbac/v1"
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"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 {
|
type AccessSet struct {
|
||||||
ID string
|
ID string
|
||||||
set map[key]resourceAccessSet
|
set map[key]resourceAccessSet
|
||||||
|
nonResourceSet map[nonResourceKey]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceAccessSet map[Access]bool
|
type resourceAccessSet map[Access]bool
|
||||||
@ -21,6 +25,11 @@ type key struct {
|
|||||||
gr schema.GroupResource
|
gr schema.GroupResource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nonResourceKey struct {
|
||||||
|
verb string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AccessSet) Namespaces() (result []string) {
|
func (a *AccessSet) Namespaces() (result []string) {
|
||||||
set := map[string]bool{}
|
set := map[string]bool{}
|
||||||
for k, as := range a.set {
|
for k, as := range a.set {
|
||||||
@ -56,6 +65,17 @@ func (a *AccessSet) Merge(right *AccessSet) {
|
|||||||
m[k] = v
|
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 {
|
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
|
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) {
|
func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result AccessList) {
|
||||||
dedup := map[Access]bool{}
|
dedup := map[Access]bool{}
|
||||||
for _, v := range []string{All, verb} {
|
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
|
type AccessListByVerb map[string]AccessList
|
||||||
|
|
||||||
func (a AccessListByVerb) Grants(verb, namespace, name string) bool {
|
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
|
groupKind = rbacv1.GroupKind
|
||||||
userKind = rbacv1.UserKind
|
userKind = rbacv1.UserKind
|
||||||
svcAccountKind = rbacv1.ServiceAccountKind
|
svcAccountKind = rbacv1.ServiceAccountKind
|
||||||
|
|
||||||
|
clusterRoleKind = "ClusterRole"
|
||||||
|
roleKind = "Role"
|
||||||
)
|
)
|
||||||
|
|
||||||
type policyRuleIndex struct {
|
type policyRuleIndex struct {
|
||||||
@ -75,6 +78,40 @@ func indexSubjects(kind string, subjects []rbacv1.Subject) []string {
|
|||||||
return result
|
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 {
|
func subjectIs(kind string, subject rbacv1.Subject) bool {
|
||||||
return subject.APIGroup == rbacGroup && subject.Kind == kind
|
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 != ""
|
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
|
// 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) {
|
func (p *policyRuleIndex) getRules(namespace string, roleRef rbacv1.RoleRef) ([]rbacv1.PolicyRule, string) {
|
||||||
switch roleRef.Kind {
|
switch roleRef.Kind {
|
||||||
@ -160,6 +171,7 @@ func (p *policyRuleIndex) getRoleRefs(subjectName string) subjectGrants {
|
|||||||
roleName: crb.RoleRef.Name,
|
roleName: crb.RoleRef.Name,
|
||||||
resourceVersion: resourceVersion,
|
resourceVersion: resourceVersion,
|
||||||
rules: rules,
|
rules: rules,
|
||||||
|
kind: clusterRoleKind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,6 +183,7 @@ func (p *policyRuleIndex) getRoleRefs(subjectName string) subjectGrants {
|
|||||||
namespace: rb.Namespace,
|
namespace: rb.Namespace,
|
||||||
resourceVersion: resourceVersion,
|
resourceVersion: resourceVersion,
|
||||||
rules: rules,
|
rules: rules,
|
||||||
|
kind: roleKind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ type subjectGrants struct {
|
|||||||
|
|
||||||
// roleRef contains information from a Role or ClusterRole
|
// roleRef contains information from a Role or ClusterRole
|
||||||
type roleRef struct {
|
type roleRef struct {
|
||||||
namespace, roleName, resourceVersion string
|
namespace, roleName, resourceVersion, kind string
|
||||||
rules []rbacv1.PolicyRule
|
rules []rbacv1.PolicyRule
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash calculates a unique identifier from all the grants for a user
|
// hash calculates a unique identifier from all the grants for a user
|
||||||
@ -51,11 +51,11 @@ func (b subjectGrants) toAccessSet() *AccessSet {
|
|||||||
result := new(AccessSet)
|
result := new(AccessSet)
|
||||||
|
|
||||||
for _, binding := range b.roleBindings {
|
for _, binding := range b.roleBindings {
|
||||||
addAccess(result, binding.namespace, binding.rules)
|
addAccess(result, binding.namespace, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, binding := range b.clusterRoleBindings {
|
for _, binding := range b.clusterRoleBindings {
|
||||||
addAccess(result, All, binding.rules)
|
addAccess(result, All, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -22,12 +22,21 @@ func NewAccessSetAuthorizer(asl accesscontrol.AccessSetLookup) *AccessSetAuthori
|
|||||||
|
|
||||||
// Authorize implements [authorizer.Authorizer].
|
// Authorize implements [authorizer.Authorizer].
|
||||||
func (a *AccessSetAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
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() {
|
if !attrs.IsResourceRequest() {
|
||||||
// XXX: Implement
|
if accessSet.GrantsNonResource(verb, path) {
|
||||||
return authorizer.DecisionDeny, "AccessSetAuthorizer does not support nonResourceURLs requests", nil
|
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()
|
namespace := attrs.GetNamespace()
|
||||||
name := attrs.GetName()
|
name := attrs.GetName()
|
||||||
gr := schema.GroupResource{
|
gr := schema.GroupResource{
|
||||||
@ -35,7 +44,6 @@ func (a *AccessSetAuthorizer) Authorize(ctx context.Context, attrs authorizer.At
|
|||||||
Resource: attrs.GetResource(),
|
Resource: attrs.GetResource(),
|
||||||
}
|
}
|
||||||
|
|
||||||
accessSet := a.asl.AccessFor(attrs.GetUser())
|
|
||||||
if accessSet.Grants(verb, gr, namespace, name) {
|
if accessSet.Grants(verb, gr, namespace, name) {
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package ext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -15,9 +16,11 @@ import (
|
|||||||
|
|
||||||
"github.com/rancher/lasso/pkg/controller"
|
"github.com/rancher/lasso/pkg/controller"
|
||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol/fake"
|
||||||
wrbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
wrbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -311,6 +314,66 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
|||||||
},
|
},
|
||||||
expectedStatusCode: http.StatusForbidden,
|
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 {
|
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
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
kind: User
|
kind: User
|
||||||
name: read-only-error
|
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
|
exit 127
|
||||||
fi
|
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"
|
version="1.$minor.x"
|
||||||
|
|
||||||
export KUBEBUILDER_ASSETS=$(setup-envtest use -p path "$version")
|
export KUBEBUILDER_ASSETS=$(setup-envtest use -p path "$version")
|
||||||
|
Loading…
Reference in New Issue
Block a user