improve IAM module

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-05-22 09:35:05 +08:00
parent 0d12529051
commit 8f93266ec0
640 changed files with 50221 additions and 18179 deletions

View File

@@ -1,175 +0,0 @@
/*
*
* 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 (
"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"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/models/iam/am"
)
type opaAuthorizer struct {
am am.AccessManagementInterface
}
const (
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.GetGlobalRoleOfUser(attr.GetUser().GetName())
if err != nil {
if errors.IsNotFound(err) {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionNoOpinion, "", err
}
// check global policy rules
if authorized, reason, err = o.makeDecision(globalRole, attr); authorized == authorizer.DecisionAllow {
return authorized, reason, nil
}
// it's global resource, permission denied
if attr.GetResourceScope() == iamv1alpha2.GlobalScope {
return authorizer.DecisionNoOpinion, "", 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
}
// 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.DecisionNoOpinion, "", err
}
return authorizer.DecisionNoOpinion, "", nil
}
// Make decision base on role
func (o *opaAuthorizer) makeDecision(role interface{}, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
regoPolicy := ""
// override
if globalRole, ok := role.(*iamv1alpha2.GlobalRole); ok {
if overrideRego, ok := globalRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if workspaceRole, ok := role.(*iamv1alpha2.WorkspaceRole); ok {
if overrideRego, ok := workspaceRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if clusterRole, ok := role.(*rbacv1.ClusterRole); ok {
if overrideRego, ok := clusterRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if role, ok := role.(*rbacv1.Role); ok {
if overrideRego, ok := role.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
}
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 {
return &opaAuthorizer{am: am}
}

View File

@@ -1,252 +0,0 @@
/*
*
* 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 (
"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"
factory "kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"testing"
)
func TestGlobalRole(t *testing.T) {
operator, err := prepare()
if err != nil {
t.Fatal(err)
}
opa := NewOPAAuthorizer(operator)
tests := []struct {
name string
request authorizer.AttributesRecord
expectedDecision authorizer.Decision
}{
{
name: "admin can list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "admin",
UID: "0",
Groups: []string{"admin"},
Extra: nil,
},
Verb: "list",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
},
expectedDecision: authorizer.DecisionAllow,
},
{
name: "anonymous can not list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: user.Anonymous,
UID: "0",
Groups: []string{"admin"},
Extra: nil,
},
Verb: "list",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
},
expectedDecision: authorizer.DecisionNoOpinion,
}, {
name: "tom can list nodes in cluster1",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "tom",
},
Verb: "list",
Cluster: "cluster1",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/clusters/cluster1/nodes",
},
expectedDecision: authorizer.DecisionAllow,
},
{
name: "tom can not list nodes in cluster2",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "tom",
},
Verb: "list",
Cluster: "cluster2",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/clusters/cluster2/nodes",
},
expectedDecision: authorizer.DecisionNoOpinion,
},
}
for _, test := range tests {
decision, _, err := opa.Authorize(test.request)
if err != nil {
t.Errorf("test failed: %s, %v", test.name, err)
}
if decision != test.expectedDecision {
t.Errorf("%s: expected decision %v, actual %+v", test.name, test.expectedDecision, decision)
}
}
}
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, 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
}

View File

@@ -20,10 +20,13 @@ package authorizerfactory
import (
"bytes"
"context"
"fmt"
"github.com/open-policy-agent/opa/rego"
"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/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
@@ -35,6 +38,11 @@ import (
rbacv1helpers "kubesphere.io/kubesphere/pkg/apis/rbac/v1"
)
const (
defaultRegoQuery = "data.authz.allow"
defaultRegoFileName = "authz.rego"
)
type RBACAuthorizer struct {
am am.AccessManagementInterface
}
@@ -48,7 +56,12 @@ type authorizingVisitor struct {
errors []error
}
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
func (v *authorizingVisitor) visit(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool {
if regoPolicy != "" && regoPolicyAllows(v.requestAttributes, regoPolicy) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if rule != nil && ruleAllows(v.requestAttributes, rule) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
@@ -65,7 +78,7 @@ type ruleAccumulator struct {
errors []error
}
func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
func (r *ruleAccumulator) visit(source fmt.Stringer, _ string, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil {
r.rules = append(r.rules, *rule)
}
@@ -155,15 +168,40 @@ func ruleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule
rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
}
func regoPolicyAllows(requestAttributes authorizer.Attributes, regoPolicy string) bool {
// 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(defaultRegoFileName, regoPolicy)).PrepareForEval(context.Background())
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
// 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(requestAttributes))
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
if len(results) > 0 && results[0].Expressions[0].Value == true {
return true
}
return false
}
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) {
func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
if globalRoleBindings, err := r.am.ListGlobalRoleBindings(""); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -173,26 +211,27 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = globalRoleBinding
sourceDescriber.subject = &globalRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
}
if requestAttributes.GetResourceScope() == iamv1alpha2.WorkspaceScope {
if requestAttributes.GetResourceScope() == request.WorkspaceScope {
if workspaceRoleBindings, err := r.am.ListWorkspaceRoleBindings("", requestAttributes.GetWorkspace()); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -202,17 +241,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = workspaceRoleBinding
sourceDescriber.subject = &workspaceRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
@@ -220,9 +260,9 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
}
}
if requestAttributes.GetResourceScope() == iamv1alpha2.NamespaceScope {
if requestAttributes.GetResourceScope() == request.NamespaceScope {
if roleBindings, err := r.am.ListRoleBindings("", requestAttributes.GetNamespace()); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -232,17 +272,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace())
regoPolicy, rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace())
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
@@ -251,7 +292,7 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
}
if clusterRoleBindings, err := r.am.ListClusterRoleBindings(""); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -261,17 +302,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}

View File

@@ -25,8 +25,8 @@ import (
"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"
"kubesphere.io/kubesphere/pkg/apiserver/request"
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
"kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/iam/am"
@@ -208,10 +208,10 @@ func TestRBACAuthorizer(t *testing.T) {
t.Fatal(err)
}
scope := iamv1alpha2.ClusterScope
scope := request.ClusterScope
if tc.namespace != "" {
scope = iamv1alpha2.NamespaceScope
scope = request.NamespaceScope
}
rules, err := ruleResolver.rulesFor(authorizer.AttributesRecord{
@@ -274,7 +274,7 @@ func newMockRBACAuthorizer(staticRoles *StaticRoles) (*RBACAuthorizer, error) {
return nil, err
}
}
return NewRBACAuthorizer(am.NewAMOperator(fakeInformerFactory)), nil
return NewRBACAuthorizer(am.NewReadOnlyOperator(fakeInformerFactory)), nil
}
func TestAppliesTo(t *testing.T) {