49
pkg/apiserver/authorization/authorizerfactory/builtin.go
Normal file
49
pkg/apiserver/authorization/authorizerfactory/builtin.go
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 The KubeSphere Authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* /
|
||||
*/
|
||||
|
||||
package authorizerfactory
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
)
|
||||
|
||||
// alwaysAllowAuthorizer is an implementation of authorizer.Attributes
|
||||
// which always says yes to an authorization request.
|
||||
// It is useful in tests and when using kubernetes in an open manner.
|
||||
type alwaysAllowAuthorizer struct{}
|
||||
|
||||
func (alwaysAllowAuthorizer) Authorize(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
func NewAlwaysAllowAuthorizer() *alwaysAllowAuthorizer {
|
||||
return new(alwaysAllowAuthorizer)
|
||||
}
|
||||
|
||||
// alwaysDenyAuthorizer is an implementation of authorizer.Attributes
|
||||
// which always says no to an authorization request.
|
||||
// It is useful in unit tests to force an operation to be forbidden.
|
||||
type alwaysDenyAuthorizer struct{}
|
||||
|
||||
func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||
return authorizer.DecisionNoOpinion, "Everything is forbidden.", nil
|
||||
}
|
||||
|
||||
func NewAlwaysDenyAuthorizer() *alwaysDenyAuthorizer {
|
||||
return new(alwaysDenyAuthorizer)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 The KubeSphere Authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* /
|
||||
*/
|
||||
|
||||
package authorizerfactory
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAlwaysAllowAuthorizer(t *testing.T) {
|
||||
aaa := NewAlwaysAllowAuthorizer()
|
||||
if decision, _, _ := aaa.Authorize(nil); decision != authorizer.DecisionAllow {
|
||||
t.Errorf("AlwaysAllowAuthorizer.Authorize did not authorize successfully.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAlwaysDenyAuthorizer(t *testing.T) {
|
||||
ada := NewAlwaysDenyAuthorizer()
|
||||
if decision, _, _ := ada.Authorize(nil); decision == authorizer.DecisionAllow {
|
||||
t.Errorf("AlwaysDenyAuthorizer.Authorize returned nil instead of error.")
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package authorizerfactory
|
||||
import (
|
||||
"context"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
@@ -33,125 +34,140 @@ type opaAuthorizer struct {
|
||||
}
|
||||
|
||||
const (
|
||||
permissionUndefined = "permission undefined"
|
||||
defaultRegoQuery = "data.authz.allow"
|
||||
defaultRegoQuery = "data.authz.allow"
|
||||
)
|
||||
|
||||
// Make decision by request attributes
|
||||
func (o *opaAuthorizer) Authorize(attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
|
||||
// Make decisions based on the authorization policy of different levels of roles
|
||||
// Error returned when an internal error occurs
|
||||
// Reason must be returned when access is denied
|
||||
globalRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.GlobalScope, "", attr.GetUser().GetName())
|
||||
globalRole, err := o.am.GetGlobalRoleOfUser(attr.GetUser().GetName())
|
||||
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
// check global role policy rules
|
||||
// check global policy rules
|
||||
if authorized, reason, err = o.makeDecision(globalRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, nil
|
||||
}
|
||||
|
||||
// it's not in cluster resource, permission denied
|
||||
if attr.GetCluster() == "" {
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
// it's global resource, permission denied
|
||||
if attr.GetResourceScope() == iamv1alpha2.GlobalScope {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
clusterRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.ClusterScope, attr.GetCluster(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
if attr.GetResourceScope() == iamv1alpha2.WorkspaceScope {
|
||||
workspaceRole, err := o.am.GetWorkspaceRoleOfUser(attr.GetUser().GetName(), attr.GetWorkspace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
|
||||
// check workspace role policy rules
|
||||
if authorized, reason, err := o.makeDecision(workspaceRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
if attr.GetResourceScope() == iamv1alpha2.NamespaceScope {
|
||||
role, err := o.am.GetNamespaceRoleOfUser(attr.GetUser().GetName(), attr.GetNamespace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
// check namespace role policy rules
|
||||
if authorized, reason, err := o.makeDecision(role, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
clusterRole, err := o.am.GetClusterRoleOfUser(attr.GetUser().GetName(), attr.GetCluster())
|
||||
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
// check cluster role policy rules
|
||||
if authorized, reason, err := o.makeDecision(clusterRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, nil
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
// it's not in cluster resource, permission denied
|
||||
if attr.GetWorkspace() == "" && attr.GetNamespace() == "" {
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
workspaceRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.WorkspaceScope, attr.GetWorkspace(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// check workspace role policy rules
|
||||
if authorized, reason, err := o.makeDecision(workspaceRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// it's not in workspace resource, permission denied
|
||||
if attr.GetNamespace() == "" {
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
namespaceRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.NamespaceScope, attr.GetNamespace(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
// check namespace role policy rules
|
||||
if authorized, reason, err := o.makeDecision(namespaceRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// Make decision base on role
|
||||
func (o *opaAuthorizer) makeDecision(role *iamv1alpha2.Role, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
func (o *opaAuthorizer) makeDecision(role interface{}, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
|
||||
for _, ruleRef := range role.Rules {
|
||||
rule, err := o.am.GetPolicyRule(ruleRef.Name)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
regoPolicy := ""
|
||||
|
||||
// override
|
||||
if globalRole, ok := role.(*iamv1alpha2.GlobalRole); ok {
|
||||
if overrideRego, ok := globalRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
|
||||
regoPolicy = overrideRego
|
||||
}
|
||||
// Call the rego.New function to create an object that can be prepared or evaluated
|
||||
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
|
||||
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module("authz.rego", rule.Rego)).PrepareForEval(context.Background())
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("rule syntax error:%s", err)
|
||||
continue
|
||||
} else if workspaceRole, ok := role.(*iamv1alpha2.WorkspaceRole); ok {
|
||||
if overrideRego, ok := workspaceRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
|
||||
regoPolicy = overrideRego
|
||||
}
|
||||
|
||||
// The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly.
|
||||
results, err := query.Eval(context.Background(), rego.EvalInput(a))
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("rule syntax error:%s", err)
|
||||
continue
|
||||
} else if clusterRole, ok := role.(*rbacv1.ClusterRole); ok {
|
||||
if overrideRego, ok := clusterRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
|
||||
regoPolicy = overrideRego
|
||||
}
|
||||
|
||||
if len(results) > 0 && results[0].Expressions[0].Value == true {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
} else if role, ok := role.(*rbacv1.Role); ok {
|
||||
if overrideRego, ok := role.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
|
||||
regoPolicy = overrideRego
|
||||
}
|
||||
}
|
||||
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
if regoPolicy == "" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// Call the rego.New function to create an object that can be prepared or evaluated
|
||||
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
|
||||
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module("authz.rego", regoPolicy)).PrepareForEval(context.Background())
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("syntax error:%s,refer: %s+v", err, role)
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
// The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly.
|
||||
results, err := query.Eval(context.Background(), rego.EvalInput(a))
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("syntax error:%s,refer: %s+v", err, role)
|
||||
return authorizer.DecisionNoOpinion, "", err
|
||||
}
|
||||
|
||||
if len(results) > 0 && results[0].Expressions[0].Value == true {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
func NewOPAAuthorizer(am am.AccessManagementInterface) *opaAuthorizer {
|
||||
|
||||
@@ -20,209 +20,22 @@ package authorizerfactory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
fakek8s "k8s.io/client-go/kubernetes/fake"
|
||||
iamvealpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
"kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
factory "kubesphere.io/kubesphere/pkg/informers"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func prepare() (am.AccessManagementInterface, error) {
|
||||
rules := []*iamvealpha2.PolicyRule{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "always-allow",
|
||||
},
|
||||
Rego: "package authz\ndefault allow = true",
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "always-deny",
|
||||
},
|
||||
Rego: "package authz\ndefault allow = false",
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "manage-cluster1-resources",
|
||||
},
|
||||
Rego: `package authz
|
||||
default allow = false
|
||||
allow {
|
||||
resources_in_cluster1
|
||||
}
|
||||
resources_in_cluster1 {
|
||||
input.Cluster == "cluster1"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
roles := []*iamvealpha2.Role{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "always-allow",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "always-deny",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "manage-cluster1-resources",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
roleBindings := []*iamvealpha2.RoleBinding{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "global-admin",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "anonymous",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: user.Anonymous,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "tom",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ksClient := fake.NewSimpleClientset()
|
||||
informerFactory := externalversions.NewSharedInformerFactory(ksClient, 0)
|
||||
|
||||
for _, rule := range rules {
|
||||
err := informerFactory.Iam().V1alpha2().PolicyRules().Informer().GetIndexer().Add(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add rule:%s", err)
|
||||
}
|
||||
}
|
||||
for _, role := range roles {
|
||||
err := informerFactory.Iam().V1alpha2().Roles().Informer().GetIndexer().Add(role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role:%s", err)
|
||||
}
|
||||
}
|
||||
for _, roleBinding := range roleBindings {
|
||||
err := informerFactory.Iam().V1alpha2().RoleBindings().Informer().GetIndexer().Add(roleBinding)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role binding:%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
operator := am.NewAMOperator(ksClient, informerFactory)
|
||||
|
||||
return operator, nil
|
||||
}
|
||||
|
||||
func TestGlobalRole(t *testing.T) {
|
||||
|
||||
operator, err := prepare()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -244,14 +57,8 @@ func TestGlobalRole(t *testing.T) {
|
||||
Extra: nil,
|
||||
},
|
||||
Verb: "list",
|
||||
Cluster: "",
|
||||
Workspace: "",
|
||||
Namespace: "",
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Resource: "nodes",
|
||||
Subresource: "",
|
||||
Name: "",
|
||||
KubernetesRequest: true,
|
||||
ResourceRequest: true,
|
||||
Path: "/api/v1/nodes",
|
||||
@@ -268,19 +75,13 @@ func TestGlobalRole(t *testing.T) {
|
||||
Extra: nil,
|
||||
},
|
||||
Verb: "list",
|
||||
Cluster: "",
|
||||
Workspace: "",
|
||||
Namespace: "",
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Resource: "nodes",
|
||||
Subresource: "",
|
||||
Name: "",
|
||||
KubernetesRequest: true,
|
||||
ResourceRequest: true,
|
||||
Path: "/api/v1/nodes",
|
||||
},
|
||||
expectedDecision: authorizer.DecisionDeny,
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
}, {
|
||||
name: "tom can list nodes in cluster1",
|
||||
request: authorizer.AttributesRecord{
|
||||
@@ -289,13 +90,8 @@ func TestGlobalRole(t *testing.T) {
|
||||
},
|
||||
Verb: "list",
|
||||
Cluster: "cluster1",
|
||||
Workspace: "",
|
||||
Namespace: "",
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Resource: "nodes",
|
||||
Subresource: "",
|
||||
Name: "",
|
||||
KubernetesRequest: true,
|
||||
ResourceRequest: true,
|
||||
Path: "/api/v1/clusters/cluster1/nodes",
|
||||
@@ -310,18 +106,13 @@ func TestGlobalRole(t *testing.T) {
|
||||
},
|
||||
Verb: "list",
|
||||
Cluster: "cluster2",
|
||||
Workspace: "",
|
||||
Namespace: "",
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Resource: "nodes",
|
||||
Subresource: "",
|
||||
Name: "",
|
||||
KubernetesRequest: true,
|
||||
ResourceRequest: true,
|
||||
Path: "/api/v1/clusters/cluster2/nodes",
|
||||
},
|
||||
expectedDecision: authorizer.DecisionDeny,
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -335,3 +126,127 @@ func TestGlobalRole(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare() (am.AccessManagementInterface, error) {
|
||||
globalRoles := []*iamvealpha2.GlobalRole{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: "package authz\ndefault allow = true"},
|
||||
},
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: "package authz\ndefault allow = false"},
|
||||
},
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: `package authz
|
||||
default allow = false
|
||||
allow {
|
||||
resources_in_cluster1
|
||||
}
|
||||
resources_in_cluster1 {
|
||||
input.Cluster == "cluster1"
|
||||
}`},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
roleBindings := []*iamvealpha2.GlobalRoleBinding{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
Name: "global-admin",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.ResourceKindUser,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
Name: "anonymous",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.ResourceKindUser,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: user.Anonymous,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.ResourceKindGlobalRole,
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.ResourceKindUser,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "tom",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ksClient := fake.NewSimpleClientset()
|
||||
k8sClient := fakek8s.NewSimpleClientset()
|
||||
factory := factory.NewInformerFactories(k8sClient, ksClient, nil, nil)
|
||||
for _, role := range globalRoles {
|
||||
err := factory.KubeSphereSharedInformerFactory().Iam().V1alpha2().GlobalRoles().Informer().GetIndexer().Add(role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role:%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, roleBinding := range roleBindings {
|
||||
err := factory.KubeSphereSharedInformerFactory().Iam().V1alpha2().GlobalRoleBindings().Informer().GetIndexer().Add(roleBinding)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role binding:%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
operator := am.NewAMOperator(factory)
|
||||
|
||||
return operator, nil
|
||||
}
|
||||
|
||||
384
pkg/apiserver/authorization/authorizerfactory/rbac.go
Normal file
384
pkg/apiserver/authorization/authorizerfactory/rbac.go
Normal file
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 The KubeSphere Authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* /
|
||||
*/
|
||||
|
||||
package authorizerfactory
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
rbacv1helpers "kubesphere.io/kubesphere/pkg/apis/rbac/v1"
|
||||
)
|
||||
|
||||
type RBACAuthorizer struct {
|
||||
am am.AccessManagementInterface
|
||||
}
|
||||
|
||||
// authorizingVisitor short-circuits once allowed, and collects any resolution errors encountered
|
||||
type authorizingVisitor struct {
|
||||
requestAttributes authorizer.Attributes
|
||||
|
||||
allowed bool
|
||||
reason string
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
|
||||
if rule != nil && ruleAllows(v.requestAttributes, rule) {
|
||||
v.allowed = true
|
||||
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type ruleAccumulator struct {
|
||||
rules []rbacv1.PolicyRule
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
|
||||
if rule != nil {
|
||||
r.rules = append(r.rules, *rule)
|
||||
}
|
||||
if err != nil {
|
||||
r.errors = append(r.errors, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes}
|
||||
|
||||
r.visitRulesFor(requestAttributes, ruleCheckingVisitor.visit)
|
||||
|
||||
if ruleCheckingVisitor.allowed {
|
||||
return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
|
||||
}
|
||||
|
||||
// Build a detailed log of the denial.
|
||||
// Make the whole block conditional so we don't do a lot of string-building we won't use.
|
||||
if klog.V(4) {
|
||||
var operation string
|
||||
if requestAttributes.IsResourceRequest() {
|
||||
b := &bytes.Buffer{}
|
||||
b.WriteString(`"`)
|
||||
b.WriteString(requestAttributes.GetVerb())
|
||||
b.WriteString(`" resource "`)
|
||||
b.WriteString(requestAttributes.GetResource())
|
||||
if len(requestAttributes.GetAPIGroup()) > 0 {
|
||||
b.WriteString(`.`)
|
||||
b.WriteString(requestAttributes.GetAPIGroup())
|
||||
}
|
||||
if len(requestAttributes.GetSubresource()) > 0 {
|
||||
b.WriteString(`/`)
|
||||
b.WriteString(requestAttributes.GetSubresource())
|
||||
}
|
||||
b.WriteString(`"`)
|
||||
if len(requestAttributes.GetName()) > 0 {
|
||||
b.WriteString(` named "`)
|
||||
b.WriteString(requestAttributes.GetName())
|
||||
b.WriteString(`"`)
|
||||
}
|
||||
operation = b.String()
|
||||
} else {
|
||||
operation = fmt.Sprintf("%q nonResourceURL %q", requestAttributes.GetVerb(), requestAttributes.GetPath())
|
||||
}
|
||||
|
||||
var scope string
|
||||
if ns := requestAttributes.GetNamespace(); len(ns) > 0 {
|
||||
scope = fmt.Sprintf("in namespace %q", ns)
|
||||
} else if ws := requestAttributes.GetWorkspace(); len(ws) > 0 {
|
||||
scope = fmt.Sprintf("in workspace %q", ws)
|
||||
} else if cluster := requestAttributes.GetWorkspace(); len(cluster) > 0 {
|
||||
scope = fmt.Sprintf("in cluster %q", cluster)
|
||||
} else {
|
||||
scope = "global-wide"
|
||||
}
|
||||
|
||||
klog.Infof("RBAC: no rules authorize user %q with groups %q to %s %s", requestAttributes.GetUser().GetName(), requestAttributes.GetUser().GetGroups(), operation, scope)
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if len(ruleCheckingVisitor.errors) > 0 {
|
||||
reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, reason, nil
|
||||
}
|
||||
|
||||
func NewRBACAuthorizer(am am.AccessManagementInterface) *RBACAuthorizer {
|
||||
return &RBACAuthorizer{am: am}
|
||||
}
|
||||
|
||||
func ruleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool {
|
||||
if requestAttributes.IsResourceRequest() {
|
||||
combinedResource := requestAttributes.GetResource()
|
||||
if len(requestAttributes.GetSubresource()) > 0 {
|
||||
combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
|
||||
}
|
||||
|
||||
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
|
||||
rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
|
||||
rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName())
|
||||
}
|
||||
|
||||
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
|
||||
}
|
||||
|
||||
func (r *RBACAuthorizer) rulesFor(requestAttributes authorizer.Attributes) ([]rbacv1.PolicyRule, error) {
|
||||
visitor := &ruleAccumulator{}
|
||||
r.visitRulesFor(requestAttributes, visitor.visit)
|
||||
return visitor.rules, utilerrors.NewAggregate(visitor.errors)
|
||||
}
|
||||
|
||||
func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
|
||||
if globalRoleBindings, err := r.am.ListGlobalRoleBindings(""); err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
sourceDescriber := &globalRoleBindingDescriber{}
|
||||
for _, globalRoleBinding := range globalRoleBindings {
|
||||
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), globalRoleBinding.Subjects, "")
|
||||
if !applies {
|
||||
continue
|
||||
}
|
||||
rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
|
||||
if err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
sourceDescriber.binding = globalRoleBinding
|
||||
sourceDescriber.subject = &globalRoleBinding.Subjects[subjectIndex]
|
||||
for i := range rules {
|
||||
if !visitor(sourceDescriber, &rules[i], nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if requestAttributes.GetResourceScope() == iamv1alpha2.WorkspaceScope {
|
||||
if workspaceRoleBindings, err := r.am.ListWorkspaceRoleBindings("", requestAttributes.GetWorkspace()); err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
sourceDescriber := &workspaceRoleBindingDescriber{}
|
||||
for _, workspaceRoleBinding := range workspaceRoleBindings {
|
||||
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), workspaceRoleBinding.Subjects, "")
|
||||
if !applies {
|
||||
continue
|
||||
}
|
||||
rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
|
||||
if err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
sourceDescriber.binding = workspaceRoleBinding
|
||||
sourceDescriber.subject = &workspaceRoleBinding.Subjects[subjectIndex]
|
||||
for i := range rules {
|
||||
if !visitor(sourceDescriber, &rules[i], nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if requestAttributes.GetResourceScope() == iamv1alpha2.NamespaceScope {
|
||||
if roleBindings, err := r.am.ListRoleBindings("", requestAttributes.GetNamespace()); err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
sourceDescriber := &roleBindingDescriber{}
|
||||
for _, roleBinding := range roleBindings {
|
||||
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), roleBinding.Subjects, requestAttributes.GetNamespace())
|
||||
if !applies {
|
||||
continue
|
||||
}
|
||||
rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace())
|
||||
if err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
sourceDescriber.binding = roleBinding
|
||||
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
|
||||
for i := range rules {
|
||||
if !visitor(sourceDescriber, &rules[i], nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clusterRoleBindings, err := r.am.ListClusterRoleBindings(""); err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
sourceDescriber := &clusterRoleBindingDescriber{}
|
||||
for _, clusterRoleBinding := range clusterRoleBindings {
|
||||
subjectIndex, applies := appliesTo(requestAttributes.GetUser(), clusterRoleBinding.Subjects, "")
|
||||
if !applies {
|
||||
continue
|
||||
}
|
||||
rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
|
||||
if err != nil {
|
||||
if !visitor(nil, nil, err) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
sourceDescriber.binding = clusterRoleBinding
|
||||
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
|
||||
for i := range rules {
|
||||
if !visitor(sourceDescriber, &rules[i], nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// appliesTo returns whether any of the bindingSubjects applies to the specified subject,
|
||||
// and if true, the index of the first subject that applies
|
||||
func appliesTo(user user.Info, bindingSubjects []rbacv1.Subject, namespace string) (int, bool) {
|
||||
for i, bindingSubject := range bindingSubjects {
|
||||
if appliesToUser(user, bindingSubject, namespace) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func appliesToUser(user user.Info, subject rbacv1.Subject, namespace string) bool {
|
||||
switch subject.Kind {
|
||||
case rbacv1.UserKind:
|
||||
return user.GetName() == subject.Name
|
||||
|
||||
case rbacv1.GroupKind:
|
||||
return sliceutil.HasString(user.GetGroups(), subject.Name)
|
||||
|
||||
case rbacv1.ServiceAccountKind:
|
||||
// default the namespace to namespace we're working in if its available. This allows rolebindings that reference
|
||||
// SAs in th local namespace to avoid having to qualify them.
|
||||
saNamespace := namespace
|
||||
if len(subject.Namespace) > 0 {
|
||||
saNamespace = subject.Namespace
|
||||
}
|
||||
if len(saNamespace) == 0 {
|
||||
return false
|
||||
}
|
||||
// use a more efficient comparison for RBAC checking
|
||||
return serviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type globalRoleBindingDescriber struct {
|
||||
binding *iamv1alpha2.GlobalRoleBinding
|
||||
subject *rbacv1.Subject
|
||||
}
|
||||
|
||||
func (d *globalRoleBindingDescriber) String() string {
|
||||
return fmt.Sprintf("GlobalRoleBinding %q of %s %q to %s",
|
||||
d.binding.Name,
|
||||
d.binding.RoleRef.Kind,
|
||||
d.binding.RoleRef.Name,
|
||||
describeSubject(d.subject, ""),
|
||||
)
|
||||
}
|
||||
|
||||
type clusterRoleBindingDescriber struct {
|
||||
binding *rbacv1.ClusterRoleBinding
|
||||
subject *rbacv1.Subject
|
||||
}
|
||||
|
||||
func (d *clusterRoleBindingDescriber) String() string {
|
||||
return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s",
|
||||
d.binding.Name,
|
||||
d.binding.RoleRef.Kind,
|
||||
d.binding.RoleRef.Name,
|
||||
describeSubject(d.subject, ""),
|
||||
)
|
||||
}
|
||||
|
||||
type workspaceRoleBindingDescriber struct {
|
||||
binding *iamv1alpha2.WorkspaceRoleBinding
|
||||
subject *rbacv1.Subject
|
||||
}
|
||||
|
||||
func (d *workspaceRoleBindingDescriber) String() string {
|
||||
return fmt.Sprintf("GlobalRoleBinding %q of %s %q to %s",
|
||||
d.binding.Name,
|
||||
d.binding.RoleRef.Kind,
|
||||
d.binding.RoleRef.Name,
|
||||
describeSubject(d.subject, ""),
|
||||
)
|
||||
}
|
||||
|
||||
type roleBindingDescriber struct {
|
||||
binding *rbacv1.RoleBinding
|
||||
subject *rbacv1.Subject
|
||||
}
|
||||
|
||||
func (d *roleBindingDescriber) String() string {
|
||||
return fmt.Sprintf("RoleBinding %q of %s %q to %s",
|
||||
d.binding.Name+"/"+d.binding.Namespace,
|
||||
d.binding.RoleRef.Kind,
|
||||
d.binding.RoleRef.Name,
|
||||
describeSubject(d.subject, d.binding.Namespace),
|
||||
)
|
||||
}
|
||||
|
||||
func describeSubject(s *rbacv1.Subject, bindingNamespace string) string {
|
||||
switch s.Kind {
|
||||
case rbacv1.ServiceAccountKind:
|
||||
if len(s.Namespace) > 0 {
|
||||
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace)
|
||||
}
|
||||
return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace)
|
||||
default:
|
||||
return fmt.Sprintf("%s %q", s.Kind, s.Name)
|
||||
}
|
||||
}
|
||||
389
pkg/apiserver/authorization/authorizerfactory/rbac_test.go
Normal file
389
pkg/apiserver/authorization/authorizerfactory/rbac_test.go
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authorizerfactory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fakeapp "github.com/kubernetes-sigs/application/pkg/client/clientset/versioned/fake"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
fakeistio "istio.io/client-go/pkg/clientset/versioned/fake"
|
||||
fakek8s "k8s.io/client-go/kubernetes/fake"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// StaticRoles is a rule resolver that resolves from lists of role objects.
|
||||
type StaticRoles struct {
|
||||
roles []*rbacv1.Role
|
||||
roleBindings []*rbacv1.RoleBinding
|
||||
clusterRoles []*rbacv1.ClusterRole
|
||||
clusterRoleBindings []*rbacv1.ClusterRoleBinding
|
||||
}
|
||||
|
||||
func (r *StaticRoles) GetRole(namespace, name string) (*rbacv1.Role, error) {
|
||||
if len(namespace) == 0 {
|
||||
return nil, errors.New("must provide namespace when getting role")
|
||||
}
|
||||
for _, role := range r.roles {
|
||||
if role.Namespace == namespace && role.Name == name {
|
||||
return role, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
func (r *StaticRoles) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
|
||||
for _, clusterRole := range r.clusterRoles {
|
||||
if clusterRole.Name == name {
|
||||
return clusterRole, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("clusterrole not found")
|
||||
}
|
||||
|
||||
func (r *StaticRoles) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) {
|
||||
if len(namespace) == 0 {
|
||||
return nil, errors.New("must provide namespace when listing role bindings")
|
||||
}
|
||||
|
||||
roleBindingList := []*rbacv1.RoleBinding{}
|
||||
for _, roleBinding := range r.roleBindings {
|
||||
if roleBinding.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
// TODO(ericchiang): need to implement label selectors?
|
||||
roleBindingList = append(roleBindingList, roleBinding)
|
||||
}
|
||||
return roleBindingList, nil
|
||||
}
|
||||
|
||||
func (r *StaticRoles) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) {
|
||||
return r.clusterRoleBindings, nil
|
||||
}
|
||||
|
||||
// compute a hash of a policy rule so we can sort in a deterministic order
|
||||
func hashOf(p rbacv1.PolicyRule) string {
|
||||
hash := fnv.New32()
|
||||
writeStrings := func(slis ...[]string) {
|
||||
for _, sli := range slis {
|
||||
for _, s := range sli {
|
||||
io.WriteString(hash, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
writeStrings(p.Verbs, p.APIGroups, p.Resources, p.ResourceNames, p.NonResourceURLs)
|
||||
return string(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// byHash sorts a set of policy rules by a hash of its fields
|
||||
type byHash []rbacv1.PolicyRule
|
||||
|
||||
func (b byHash) Len() int { return len(b) }
|
||||
func (b byHash) Less(i, j int) bool { return hashOf(b[i]) < hashOf(b[j]) }
|
||||
func (b byHash) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
|
||||
func TestRBACAuthorizer(t *testing.T) {
|
||||
ruleReadPods := rbacv1.PolicyRule{
|
||||
Verbs: []string{"GET", "WATCH"},
|
||||
APIGroups: []string{"v1"},
|
||||
Resources: []string{"pods"},
|
||||
}
|
||||
ruleReadServices := rbacv1.PolicyRule{
|
||||
Verbs: []string{"GET", "WATCH"},
|
||||
APIGroups: []string{"v1"},
|
||||
Resources: []string{"services"},
|
||||
}
|
||||
ruleWriteNodes := rbacv1.PolicyRule{
|
||||
Verbs: []string{"PUT", "CREATE", "UPDATE"},
|
||||
APIGroups: []string{"v1"},
|
||||
Resources: []string{"nodes"},
|
||||
}
|
||||
ruleAdmin := rbacv1.PolicyRule{
|
||||
Verbs: []string{"*"},
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
}
|
||||
|
||||
staticRoles1 := StaticRoles{
|
||||
roles: []*rbacv1.Role{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "namespace1", Name: "readthings"},
|
||||
Rules: []rbacv1.PolicyRule{ruleReadPods, ruleReadServices},
|
||||
},
|
||||
},
|
||||
clusterRoles: []*rbacv1.ClusterRole{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "cluster-admin"},
|
||||
Rules: []rbacv1.PolicyRule{ruleAdmin},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "write-nodes"},
|
||||
Rules: []rbacv1.PolicyRule{ruleWriteNodes},
|
||||
},
|
||||
},
|
||||
roleBindings: []*rbacv1.RoleBinding{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "namespace1"},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "foobar"},
|
||||
{Kind: rbacv1.GroupKind, Name: "group1"},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "readthings"},
|
||||
},
|
||||
},
|
||||
clusterRoleBindings: []*rbacv1.ClusterRoleBinding{
|
||||
{
|
||||
Subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "admin"},
|
||||
{Kind: rbacv1.GroupKind, Name: "admin"},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-admin"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
StaticRoles
|
||||
|
||||
// For a given context, what are the rules that apply?
|
||||
user user.Info
|
||||
namespace string
|
||||
effectiveRules []rbacv1.PolicyRule
|
||||
}{
|
||||
{
|
||||
StaticRoles: staticRoles1,
|
||||
user: &user.DefaultInfo{Name: "foobar"},
|
||||
namespace: "namespace1",
|
||||
effectiveRules: []rbacv1.PolicyRule{ruleReadPods, ruleReadServices},
|
||||
},
|
||||
{
|
||||
StaticRoles: staticRoles1,
|
||||
user: &user.DefaultInfo{Name: "foobar"},
|
||||
namespace: "namespace2",
|
||||
effectiveRules: nil,
|
||||
},
|
||||
{
|
||||
StaticRoles: staticRoles1,
|
||||
// Same as above but without a namespace. Only cluster rules should apply.
|
||||
user: &user.DefaultInfo{Name: "foobar", Groups: []string{"admin"}},
|
||||
effectiveRules: []rbacv1.PolicyRule{ruleAdmin},
|
||||
},
|
||||
{
|
||||
StaticRoles: staticRoles1,
|
||||
user: &user.DefaultInfo{},
|
||||
effectiveRules: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
ruleResolver, err := newMockRBACAuthorizer(&tc.StaticRoles)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scope := iamv1alpha2.ClusterScope
|
||||
|
||||
if tc.namespace != "" {
|
||||
scope = iamv1alpha2.NamespaceScope
|
||||
}
|
||||
|
||||
rules, err := ruleResolver.rulesFor(authorizer.AttributesRecord{
|
||||
User: tc.user,
|
||||
Namespace: tc.namespace,
|
||||
ResourceScope: scope,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("case %d: GetEffectivePolicyRules(context)=%v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Sort for deep equals
|
||||
sort.Sort(byHash(rules))
|
||||
sort.Sort(byHash(tc.effectiveRules))
|
||||
|
||||
if diff := cmp.Diff(rules, tc.effectiveRules); diff != "" {
|
||||
t.Errorf("case %d: %s", i, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newMockRBACAuthorizer(staticRoles *StaticRoles) (*RBACAuthorizer, error) {
|
||||
|
||||
ksClient := fakeks.NewSimpleClientset()
|
||||
k8sClient := fakek8s.NewSimpleClientset()
|
||||
istioClient := fakeistio.NewSimpleClientset()
|
||||
appClient := fakeapp.NewSimpleClientset()
|
||||
fakeInformerFactory := informers.NewInformerFactories(k8sClient, ksClient, istioClient, appClient)
|
||||
|
||||
k8sInformerFactory := fakeInformerFactory.KubernetesSharedInformerFactory()
|
||||
|
||||
for _, role := range staticRoles.roles {
|
||||
err := k8sInformerFactory.Rbac().V1().Roles().Informer().GetIndexer().Add(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, roleBinding := range staticRoles.roleBindings {
|
||||
err := k8sInformerFactory.Rbac().V1().RoleBindings().Informer().GetIndexer().Add(roleBinding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, clusterRole := range staticRoles.clusterRoles {
|
||||
err := k8sInformerFactory.Rbac().V1().ClusterRoles().Informer().GetIndexer().Add(clusterRole)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, clusterRoleBinding := range staticRoles.clusterRoleBindings {
|
||||
err := k8sInformerFactory.Rbac().V1().ClusterRoleBindings().Informer().GetIndexer().Add(clusterRoleBinding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewRBACAuthorizer(am.NewAMOperator(fakeInformerFactory)), nil
|
||||
}
|
||||
|
||||
func TestAppliesTo(t *testing.T) {
|
||||
tests := []struct {
|
||||
subjects []rbacv1.Subject
|
||||
user user.Info
|
||||
namespace string
|
||||
appliesTo bool
|
||||
index int
|
||||
testCase string
|
||||
}{
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "foobar"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "foobar"},
|
||||
appliesTo: true,
|
||||
index: 0,
|
||||
testCase: "single subject that matches username",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "barfoo"},
|
||||
{Kind: rbacv1.UserKind, Name: "foobar"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "foobar"},
|
||||
appliesTo: true,
|
||||
index: 1,
|
||||
testCase: "multiple subjects, one that matches username",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "barfoo"},
|
||||
{Kind: rbacv1.UserKind, Name: "foobar"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "zimzam"},
|
||||
appliesTo: false,
|
||||
testCase: "multiple subjects, none that match username",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "barfoo"},
|
||||
{Kind: rbacv1.GroupKind, Name: "foobar"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}},
|
||||
appliesTo: true,
|
||||
index: 1,
|
||||
testCase: "multiple subjects, one that match group",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "barfoo"},
|
||||
{Kind: rbacv1.GroupKind, Name: "foobar"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "zimzam", Groups: []string{"foobar"}},
|
||||
namespace: "namespace1",
|
||||
appliesTo: true,
|
||||
index: 1,
|
||||
testCase: "multiple subjects, one that match group, should ignore namespace",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "barfoo"},
|
||||
{Kind: rbacv1.GroupKind, Name: "foobar"},
|
||||
{Kind: rbacv1.ServiceAccountKind, Namespace: "kube-system", Name: "default"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "system:serviceaccount:kube-system:default"},
|
||||
namespace: "default",
|
||||
appliesTo: true,
|
||||
index: 2,
|
||||
testCase: "multiple subjects with a service account that matches",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.UserKind, Name: "*"},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "foobar"},
|
||||
namespace: "default",
|
||||
appliesTo: false,
|
||||
testCase: "* user subject name doesn't match all users",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.GroupKind, Name: user.AllAuthenticated},
|
||||
{Kind: rbacv1.GroupKind, Name: user.AllUnauthenticated},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "foobar", Groups: []string{user.AllAuthenticated}},
|
||||
namespace: "default",
|
||||
appliesTo: true,
|
||||
index: 0,
|
||||
testCase: "binding to all authenticated and unauthenticated subjects matches authenticated user",
|
||||
},
|
||||
{
|
||||
subjects: []rbacv1.Subject{
|
||||
{Kind: rbacv1.GroupKind, Name: user.AllAuthenticated},
|
||||
{Kind: rbacv1.GroupKind, Name: user.AllUnauthenticated},
|
||||
},
|
||||
user: &user.DefaultInfo{Name: "system:anonymous", Groups: []string{user.AllUnauthenticated}},
|
||||
namespace: "default",
|
||||
appliesTo: true,
|
||||
index: 1,
|
||||
testCase: "binding to all authenticated and unauthenticated subjects matches anonymous user",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
gotIndex, got := appliesTo(tc.user, tc.subjects, tc.namespace)
|
||||
if got != tc.appliesTo {
|
||||
t.Errorf("case %q want appliesTo=%t, got appliesTo=%t", tc.testCase, tc.appliesTo, got)
|
||||
}
|
||||
if gotIndex != tc.index {
|
||||
t.Errorf("case %q want index %d, got %d", tc.testCase, tc.index, gotIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user