Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-03-20 02:32:49 +08:00
parent 9769357005
commit 1f26e62105
18 changed files with 359 additions and 183 deletions

View File

@@ -187,7 +187,7 @@ func (s *APIServer) buildHandlerChain() {
excludedPaths := []string{"/oauth/authorize", "/oauth/token"}
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewAMOperator(s.KubernetesClient.Kubernetes(), s.InformerFactory.KubernetesSharedInformerFactory())))
authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator(cache.NewSimpleCache())))
handler = filters.WithAuthorization(handler, authorizer)
handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch)
handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{})

View File

@@ -48,9 +48,6 @@ type Attributes interface {
// The namespace of the object, if a request is for a REST object.
GetNamespace() string
// The devops project of the object, if a request is for a REST object.
GetDevopsProject() string
// The kind of object, if a request is for a REST object.
GetResource() string
@@ -106,7 +103,6 @@ type AttributesRecord struct {
Cluster string
Workspace string
Namespace string
DevopsProject string
APIGroup string
APIVersion string
Resource string
@@ -141,10 +137,6 @@ func (a AttributesRecord) GetNamespace() string {
return a.Namespace
}
func (a AttributesRecord) GetDevopsProject() string {
return a.DevopsProject
}
func (a AttributesRecord) GetResource() string {
return a.Resource
}

View File

@@ -22,79 +22,68 @@ import (
"context"
"github.com/open-policy-agent/opa/rego"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
am2 "kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/models/iam/am"
)
type opaAuthorizer struct {
am am2.AccessManagementInterface
am am.AccessManagementInterface
}
func (o *opaAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
func (o *opaAuthorizer) Authorize(attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
platformRole, err := o.am.GetPlatformRole(a.GetUser().GetName())
platformRole, err := o.am.GetPlatformRole(attr.GetUser().GetName())
if err != nil {
return authorizer.DecisionDeny, "", err
}
// check platform role policy rules
if a, r, e := o.roleAuthorize(platformRole, a); a == authorizer.DecisionAllow {
if a, r, e := makeDecision(platformRole, attr); a == authorizer.DecisionAllow {
return a, r, e
}
// it's not in cluster resource, permission denied
// TODO declare implicit cluster info in request Info
if a.GetCluster() == "" {
if attr.GetCluster() == "" {
return authorizer.DecisionDeny, "permission undefined", nil
}
clusterRole, err := o.am.GetClusterRole(a.GetCluster(), a.GetUser().GetName())
clusterRole, err := o.am.GetClusterRole(attr.GetCluster(), attr.GetUser().GetName())
if err != nil {
return authorizer.DecisionDeny, "", err
}
// check cluster role policy rules
if a, r, e := o.roleAuthorize(clusterRole, a); a == authorizer.DecisionAllow {
if a, r, e := makeDecision(clusterRole, attr); a == authorizer.DecisionAllow {
return a, r, e
}
// it's not in cluster resource, permission denied
if a.GetWorkspace() == "" && a.GetNamespace() == "" && a.GetDevopsProject() == "" {
if attr.GetWorkspace() == "" && attr.GetNamespace() == "" {
return authorizer.DecisionDeny, "permission undefined", nil
}
workspaceRole, err := o.am.GetWorkspaceRole(a.GetWorkspace(), a.GetUser().GetName())
workspaceRole, err := o.am.GetWorkspaceRole(attr.GetWorkspace(), attr.GetUser().GetName())
if err != nil {
return authorizer.DecisionDeny, "", err
}
// check workspace role policy rules
if a, r, e := o.roleAuthorize(workspaceRole, a); a == authorizer.DecisionAllow {
if a, r, e := makeDecision(workspaceRole, attr); a == authorizer.DecisionAllow {
return a, r, e
}
// it's not in workspace resource, permission denied
if a.GetNamespace() == "" && a.GetDevopsProject() == "" {
if attr.GetNamespace() == "" {
return authorizer.DecisionDeny, "permission undefined", nil
}
if a.GetNamespace() != "" {
namespaceRole, err := o.am.GetNamespaceRole(a.GetNamespace(), a.GetUser().GetName())
if attr.GetNamespace() != "" {
namespaceRole, err := o.am.GetNamespaceRole(attr.GetNamespace(), attr.GetUser().GetName())
if err != nil {
return authorizer.DecisionDeny, "", err
}
// check namespace role policy rules
if a, r, e := o.roleAuthorize(namespaceRole, a); a == authorizer.DecisionAllow {
return a, r, e
}
}
if a.GetDevopsProject() != "" {
devOpsRole, err := o.am.GetDevOpsRole(a.GetNamespace(), a.GetUser().GetName())
if err != nil {
return authorizer.DecisionDeny, "", err
}
// check devops role policy rules
if a, r, e := o.roleAuthorize(devOpsRole, a); a == authorizer.DecisionAllow {
if a, r, e := makeDecision(namespaceRole, attr); a == authorizer.DecisionAllow {
return a, r, e
}
}
@@ -102,14 +91,18 @@ func (o *opaAuthorizer) Authorize(a authorizer.Attributes) (authorized authorize
return authorizer.DecisionDeny, "", nil
}
func (o *opaAuthorizer) roleAuthorize(role am2.Role, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
// Make decision base on role
func makeDecision(role am.Role, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
// 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("data.authz.allow"), rego.Module("authz.rego", role.GetRego())).PrepareForEval(context.Background())
if err != nil {
return authorizer.DecisionDeny, "", 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 {
@@ -123,6 +116,6 @@ func (o *opaAuthorizer) roleAuthorize(role am2.Role, a authorizer.Attributes) (a
return authorizer.DecisionDeny, "permission undefined", nil
}
func NewOPAAuthorizer(am am2.AccessManagementInterface) *opaAuthorizer {
func NewOPAAuthorizer(am am.AccessManagementInterface) *opaAuthorizer {
return &opaAuthorizer{am: am}
}

View File

@@ -19,66 +19,79 @@
package authorizerfactory
import (
"context"
"github.com/open-policy-agent/opa/rego"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"testing"
)
func TestPlatformRole(t *testing.T) {
module := `package platform.authz
default allow = false
opa := NewOPAAuthorizer(am.NewFakeAMOperator(cache.NewSimpleCache()))
allow {
input.User.name == "admin"
}
allow {
is_admin
}
is_admin {
input.User.Groups[_] == "admin"
}
`
query, err := rego.New(rego.Query("data.authz.allow"), rego.Module("authz.rego", module)).PrepareForEval(context.Background())
if err != nil {
t.Fatal(err)
}
input := authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "admin",
UID: "0",
Groups: []string{"admin"},
Extra: nil,
tests := []struct {
name string
request authorizer.AttributesRecord
expectedDecision authorizer.Decision
}{
{
name: "list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "admin",
UID: "0",
Groups: []string{"admin"},
Extra: nil,
},
Verb: "list",
Cluster: "",
Workspace: "",
Namespace: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Subresource: "",
Name: "",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
},
expectedDecision: authorizer.DecisionAllow,
},
{
name: "list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: user.Anonymous,
UID: "0",
Groups: []string{"admin"},
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,
},
Verb: "list",
Cluster: "",
Workspace: "",
Namespace: "",
DevopsProject: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Subresource: "",
Name: "",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
}
results, err := query.Eval(context.Background(), rego.EvalInput(input))
if err != nil {
t.Log(err)
}
if len(results) > 0 && results[0].Expressions[0].Value == true {
t.Log("allowed")
} else {
t.Log("deny")
for _, test := range tests {
decision, _, err := opa.Authorize(test.request)
if err != nil {
t.Error(err)
}
if decision != test.expectedDecision {
t.Errorf("%s: expected decision %v, actual %+v", test.name, test.expectedDecision, decision)
}
}
}

View File

@@ -64,7 +64,7 @@ func newTestConfig() *Config {
GroupSearchBase: "ou=Groups,dc=example,dc=org",
},
RedisOptions: &cache.Options{
Host: "localhost:6379",
Host: "localhost",
Port: 6379,
Password: "P@88w0rd",
DB: 0,

View File

@@ -68,7 +68,6 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error)
attribs.Resource = requestInfo.Resource
attribs.Subresource = requestInfo.Subresource
attribs.Namespace = requestInfo.Namespace
attribs.DevopsProject = requestInfo.DevopsProject
attribs.Name = requestInfo.Name
return &attribs, nil

View File

@@ -3,7 +3,11 @@ package request
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/validation/path"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog"
"net/http"
"strings"
@@ -19,6 +23,13 @@ type RequestInfoResolver interface {
// master's Mux.
var specialVerbs = sets.NewString("proxy", "watch")
// specialVerbsNoSubresources contains root verbs which do not allow subresources
var specialVerbsNoSubresources = sets.NewString("proxy")
// namespaceSubresources contains subresources of namespace
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
var namespaceSubresources = sets.NewString("status", "finalize")
var kubernetesAPIPrefixes = sets.NewString("api", "apis")
// RequestInfo holds information parsed from the http.Request,
@@ -34,9 +45,6 @@ type RequestInfo struct {
// Cluster of requested resource, this is empty in single-cluster environment
Cluster string
// Devops project of requested resource, this may be empty
DevopsProject string
}
type RequestInfoFactory struct {
@@ -101,16 +109,9 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
currentParts = currentParts[1:]
if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
if len(currentParts) < 2 {
return &requestInfo, nil
}
if currentParts[0] == "clusters" {
requestInfo.Cluster = currentParts[1]
currentParts = currentParts[2:]
}
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
if len(currentParts) < 3 {
// return a non-resource request
return &requestInfo, nil
}
@@ -122,6 +123,18 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
requestInfo.APIVersion = currentParts[0]
currentParts = currentParts[1:]
if currentParts[0] == "clusters" {
requestInfo.Cluster = currentParts[1]
currentParts = currentParts[2:]
} else if len(currentParts) > 0 {
requestInfo.Cluster = "host-cluster"
}
if currentParts[0] == "workspaces" {
requestInfo.Workspace = currentParts[1]
currentParts = currentParts[2:]
}
if specialVerbs.Has(currentParts[0]) {
if len(currentParts) < 2 {
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url: %v", req.URL)
@@ -146,6 +159,73 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
}
}
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
if currentParts[0] == "namespaces" {
if len(currentParts) > 1 {
requestInfo.Namespace = currentParts[1]
// if there is another step after the namespace name and it is not a known namespace subresource
// move currentParts to include it as a resource in its own right
if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
currentParts = currentParts[2:]
}
}
} else {
requestInfo.Namespace = metav1.NamespaceNone
}
// parsing successful, so we now know the proper value for .Parts
requestInfo.Parts = currentParts
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
switch {
case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
requestInfo.Subresource = requestInfo.Parts[2]
fallthrough
case len(requestInfo.Parts) >= 2:
requestInfo.Name = requestInfo.Parts[1]
fallthrough
case len(requestInfo.Parts) >= 1:
requestInfo.Resource = requestInfo.Parts[0]
}
// if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
opts := metainternalversion.ListOptions{}
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil {
// An error in parsing request will result in default to "list" and not setting "name" field.
klog.Errorf("Couldn't parse request %#v: %v", req.URL.Query(), err)
// Reset opts to not rely on partial results from parsing.
// However, if watch is set, let's report it.
opts = metainternalversion.ListOptions{}
if values := req.URL.Query()["watch"]; len(values) > 0 {
switch strings.ToLower(values[0]) {
case "false", "0":
default:
opts.Watch = true
}
}
}
if opts.Watch {
requestInfo.Verb = "watch"
} else {
requestInfo.Verb = "list"
}
if opts.FieldSelector != nil {
if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok {
if len(path.IsValidPathSegmentName(name)) == 0 {
requestInfo.Name = name
}
}
}
}
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
requestInfo.Verb = "deletecollection"
}
return &requestInfo, nil
}

View File

@@ -43,6 +43,8 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
expectedResource string
expectedIsResourceRequest bool
expectedCluster string
expectedWorkspace string
exceptedNamespace string
}{
{
name: "login",
@@ -54,15 +56,88 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
expectedIsResourceRequest: false,
expectedCluster: "",
},
{
name: "list cluster roles",
url: "/apis/rbac.authorization.k8s.io/v1/clusters/cluster1/clusterroles",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "clusterroles",
expectedIsResourceRequest: true,
expectedCluster: "cluster1",
},
{
name: "list cluster nodes",
url: "/api/v1/clusters/cluster1/nodes",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "nodes",
expectedIsResourceRequest: true,
expectedCluster: "cluster1",
},
{
name: "list cluster nodes",
url: "/api/v1/clusters/cluster1/nodes",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "nodes",
expectedIsResourceRequest: true,
expectedCluster: "cluster1",
},
{
name: "list cluster nodes",
url: "/api/v1/nodes",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "nodes",
expectedIsResourceRequest: true,
expectedCluster: "host-cluster",
},
{
name: "list roles",
url: "/apis/rbac.authorization.k8s.io/v1/clusters/cluster1/namespaces/namespace1/roles",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "roles",
expectedIsResourceRequest: true,
exceptedNamespace: "namespace1",
expectedCluster: "cluster1",
},
{
name: "list roles",
url: "/apis/rbac.authorization.k8s.io/v1/namespaces/namespace1/roles",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "roles",
expectedIsResourceRequest: true,
expectedCluster: "host-cluster",
},
{
name: "list namespaces",
url: "/kapis/resources.kubesphere.io/v1alpha2/namespaces",
url: "/kapis/resources.kubesphere.io/v1alpha3/workspaces/workspace1/namespaces",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "namespaces",
expectedIsResourceRequest: true,
expectedCluster: "",
expectedWorkspace: "workspace1",
expectedCluster: "host-cluster",
},
{
name: "list namespaces",
url: "/kapis/resources.kubesphere.io/v1alpha3/clusters/cluster1/workspaces/workspace1/namespaces",
method: http.MethodGet,
expectedErr: nil,
expectedVerb: "list",
expectedResource: "namespaces",
expectedIsResourceRequest: true,
expectedWorkspace: "workspace1",
expectedCluster: "cluster1",
},
}
@@ -80,15 +155,24 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
t.Errorf("%s: expected error %v, actual %v", test.name, test.expectedErr, err)
}
} else {
if test.expectedVerb != requestInfo.Verb {
if test.expectedVerb != "" && test.expectedVerb != requestInfo.Verb {
t.Errorf("%s: expected verb %v, actual %+v", test.name, test.expectedVerb, requestInfo.Verb)
}
if test.expectedResource != requestInfo.Resource {
if test.expectedResource != "" && test.expectedResource != requestInfo.Resource {
t.Errorf("%s: expected resource %v, actual %+v", test.name, test.expectedResource, requestInfo.Resource)
}
if test.expectedIsResourceRequest != requestInfo.IsResourceRequest {
t.Errorf("%s: expected is resource request %v, actual %+v", test.name, test.expectedIsResourceRequest, requestInfo.IsResourceRequest)
}
if test.expectedCluster != "" && test.expectedCluster != requestInfo.Cluster {
t.Errorf("%s: expected cluster %v, actual %+v", test.name, test.expectedCluster, requestInfo.Cluster)
}
if test.expectedWorkspace != "" && test.expectedWorkspace != requestInfo.Workspace {
t.Errorf("%s: expected workspace %v, actual %+v", test.name, test.expectedWorkspace, requestInfo.Workspace)
}
if test.exceptedNamespace != "" && test.exceptedNamespace != requestInfo.Namespace {
t.Errorf("%s: expected namespace %v, actual %+v", test.name, test.exceptedNamespace, requestInfo.Namespace)
}
}
}
}