From 1f26e62105b7c97f2b632ef03c9f3739e6f2f027 Mon Sep 17 00:00:00 2001 From: hongming Date: Fri, 20 Mar 2020 02:32:49 +0800 Subject: [PATCH] update Signed-off-by: hongming --- Makefile | 6 +- build/ks-apigateway/Dockerfile | 20 ---- build/ks-iam/Dockerfile | 18 --- cmd/ks-apiserver/app/server.go | 2 + pkg/apiserver/apiserver.go | 2 +- .../authorization/authorizer/interfaces.go | 8 -- .../authorization/authorizerfactory/opa.go | 49 ++++---- .../authorizerfactory/opa_test.go | 113 ++++++++++-------- pkg/apiserver/config/config_test.go | 2 +- pkg/apiserver/filters/authorization.go | 1 - pkg/apiserver/request/requestinfo.go | 104 ++++++++++++++-- pkg/apiserver/request/requestinfo_test.go | 92 +++++++++++++- pkg/kapis/tenant/v1alpha2/handler.go | 6 +- pkg/models/iam/am/am.go | 26 +--- pkg/models/iam/am/fake_operator.go | 73 +++++++++++ pkg/models/tenant/namespaces.go | 8 +- pkg/models/tenant/tenant.go | 6 +- pkg/models/tenant/workspaces.go | 6 +- 18 files changed, 359 insertions(+), 183 deletions(-) delete mode 100644 build/ks-apigateway/Dockerfile delete mode 100644 build/ks-iam/Dockerfile create mode 100644 pkg/models/iam/am/fake_operator.go diff --git a/Makefile b/Makefile index 9c03307db..685d5f9c8 100644 --- a/Makefile +++ b/Makefile @@ -39,16 +39,12 @@ define ALL_HELP_INFO # debugging tools like delve. endef .PHONY: all -all: test hypersphere ks-apiserver ks-apigateway controller-manager +all: test hypersphere ks-apiserver controller-manager # Build ks-apiserver binary ks-apiserver: fmt vet hack/gobuild.sh cmd/ks-apiserver -# Build ks-apigateway binary -ks-apigateway: fmt vet - hack/gobuild.sh cmd/ks-apigateway - # Build controller-manager binary controller-manager: fmt vet hack/gobuild.sh cmd/controller-manager diff --git a/build/ks-apigateway/Dockerfile b/build/ks-apigateway/Dockerfile deleted file mode 100644 index 61700244d..000000000 --- a/build/ks-apigateway/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2018 The KubeSphere Authors. All rights reserved. -# Use of this source code is governed by a Apache license -# that can be found in the LICENSE file. - -# Copyright 2018 The KubeSphere Authors. All rights reserved. -# Use of this source code is governed by a Apache license -# that can be found in the LICENSE file. - -FROM golang:1.12 as ks-apigateway-builder - -COPY / /go/src/kubesphere.io/kubesphere -WORKDIR /go/src/kubesphere.io/kubesphere -RUN CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 GOFLAGS=-mod=vendor go build -i -ldflags '-w -s' -o ks-apigateway cmd/ks-apigateway/apiserver.go && \ - go run tools/cmd/doc-gen/main.go --output=install/swagger-ui/api.json - -FROM alpine:3.9 -RUN apk add --update ca-certificates && update-ca-certificates -COPY --from=ks-apigateway-builder /go/src/kubesphere.io/kubesphere/ks-apigateway /usr/local/bin/ -COPY --from=ks-apigateway-builder /go/src/kubesphere.io/kubesphere/install/swagger-ui /var/static/swagger-ui -CMD ["sh"] diff --git a/build/ks-iam/Dockerfile b/build/ks-iam/Dockerfile deleted file mode 100644 index 3e65a47e8..000000000 --- a/build/ks-iam/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2018 The KubeSphere Authors. All rights reserved. -# Use of this source code is governed by a Apache license -# that can be found in the LICENSE file. - -# Copyright 2018 The KubeSphere Authors. All rights reserved. -# Use of this source code is governed by a Apache license -# that can be found in the LICENSE file. -FROM golang:1.12 as ks-iam-builder - -COPY / /go/src/kubesphere.io/kubesphere - -WORKDIR /go/src/kubesphere.io/kubesphere -RUN CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 GOFLAGS=-mod=vendor go build -i -ldflags '-w -s' -o ks-iam cmd/ks-iam/apiserver.go - -FROM alpine:3.9 -RUN apk add --update ca-certificates && update-ca-certificates -COPY --from=ks-iam-builder /go/src/kubesphere.io/kubesphere/ks-iam /usr/local/bin/ -CMD ["sh"] diff --git a/cmd/ks-apiserver/app/server.go b/cmd/ks-apiserver/app/server.go index 3375ced9f..78caa399e 100644 --- a/cmd/ks-apiserver/app/server.go +++ b/cmd/ks-apiserver/app/server.go @@ -46,6 +46,8 @@ func NewAPIServerCommand() *cobra.Command { S3Options: conf.S3Options, OpenPitrixOptions: conf.OpenPitrixOptions, LoggingOptions: conf.LoggingOptions, + LdapOptions: conf.LdapOptions, + CacheOptions: conf.RedisOptions, AuthenticateOptions: conf.AuthenticateOptions, } } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 6e04a7d68..335352bb4 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -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{}) diff --git a/pkg/apiserver/authorization/authorizer/interfaces.go b/pkg/apiserver/authorization/authorizer/interfaces.go index 261c320b2..3c8771328 100644 --- a/pkg/apiserver/authorization/authorizer/interfaces.go +++ b/pkg/apiserver/authorization/authorizer/interfaces.go @@ -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 } diff --git a/pkg/apiserver/authorization/authorizerfactory/opa.go b/pkg/apiserver/authorization/authorizerfactory/opa.go index b845ff2f0..7b1d9ab69 100644 --- a/pkg/apiserver/authorization/authorizerfactory/opa.go +++ b/pkg/apiserver/authorization/authorizerfactory/opa.go @@ -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} } diff --git a/pkg/apiserver/authorization/authorizerfactory/opa_test.go b/pkg/apiserver/authorization/authorizerfactory/opa_test.go index c4d9c08f6..f435e7f7e 100644 --- a/pkg/apiserver/authorization/authorizerfactory/opa_test.go +++ b/pkg/apiserver/authorization/authorizerfactory/opa_test.go @@ -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) + } } } diff --git a/pkg/apiserver/config/config_test.go b/pkg/apiserver/config/config_test.go index cea9d7b77..4410940e4 100644 --- a/pkg/apiserver/config/config_test.go +++ b/pkg/apiserver/config/config_test.go @@ -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, diff --git a/pkg/apiserver/filters/authorization.go b/pkg/apiserver/filters/authorization.go index 2388df5ce..583a94145 100644 --- a/pkg/apiserver/filters/authorization.go +++ b/pkg/apiserver/filters/authorization.go @@ -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 diff --git a/pkg/apiserver/request/requestinfo.go b/pkg/apiserver/request/requestinfo.go index 002ca32cf..8f0c53533 100644 --- a/pkg/apiserver/request/requestinfo.go +++ b/pkg/apiserver/request/requestinfo.go @@ -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 } diff --git a/pkg/apiserver/request/requestinfo_test.go b/pkg/apiserver/request/requestinfo_test.go index 41fda5397..3742e180c 100644 --- a/pkg/apiserver/request/requestinfo_test.go +++ b/pkg/apiserver/request/requestinfo_test.go @@ -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) + } } } } diff --git a/pkg/kapis/tenant/v1alpha2/handler.go b/pkg/kapis/tenant/v1alpha2/handler.go index 6f67dcc36..ec4385e6b 100644 --- a/pkg/kapis/tenant/v1alpha2/handler.go +++ b/pkg/kapis/tenant/v1alpha2/handler.go @@ -8,7 +8,7 @@ import ( "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/informers" - am2 "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/monitoring" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2" "kubesphere.io/kubesphere/pkg/models/tenant" @@ -21,14 +21,14 @@ import ( type tenantHandler struct { tenant tenant.Interface - am am2.AccessManagementInterface + am am.AccessManagementInterface } func newTenantHandler(k8sClient k8s.Client, factory informers.InformerFactory, db *mysql.Database) *tenantHandler { return &tenantHandler{ tenant: tenant.New(k8sClient.Kubernetes(), factory.KubernetesSharedInformerFactory(), factory.KubeSphereSharedInformerFactory(), db), - am: am2.NewAMOperator(k8sClient.Kubernetes(), factory.KubernetesSharedInformerFactory()), + am: am.NewAMOperator(k8sClient.Kubernetes(), factory.KubernetesSharedInformerFactory()), } } diff --git a/pkg/models/iam/am/am.go b/pkg/models/iam/am/am.go index 1cfdc818d..4212954e9 100644 --- a/pkg/models/iam/am/am.go +++ b/pkg/models/iam/am/am.go @@ -18,7 +18,6 @@ package am import ( - rbacv1 "k8s.io/api/rbac/v1" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2" @@ -38,7 +37,6 @@ type AccessManagementInterface interface { GetClusterRole(cluster, username string) (Role, error) GetWorkspaceRole(workspace, username string) (Role, error) GetNamespaceRole(namespace, username string) (Role, error) - GetDevOpsRole(project, username string) (Role, error) } type Role interface { @@ -52,26 +50,6 @@ type amOperator struct { kubeClient kubernetes.Interface } -func (am *amOperator) ListClusterRoleBindings(clusterRole string) ([]*rbacv1.ClusterRoleBinding, error) { - panic("implement me") -} - -func (am *amOperator) GetRoles(namespace, username string) ([]*rbacv1.Role, error) { - panic("implement me") -} - -func (am *amOperator) GetClusterPolicyRules(username string) ([]rbacv1.PolicyRule, error) { - panic("implement me") -} - -func (am *amOperator) GetPolicyRules(namespace, username string) ([]rbacv1.PolicyRule, error) { - panic("implement me") -} - -func (am *amOperator) GetWorkspaceRole(workspace, username string) (Role, error) { - panic("implement me") -} - func NewAMOperator(kubeClient kubernetes.Interface, informers informers.SharedInformerFactory) AccessManagementInterface { resourceGetter := resource.ResourceGetter{} resourceGetter.Add(v1alpha2.Role, role.NewRoleSearcher(informers)) @@ -91,6 +69,10 @@ func (am *amOperator) GetClusterRole(cluster, username string) (Role, error) { panic("implement me") } +func (am *amOperator) GetWorkspaceRole(workspace, username string) (Role, error) { + panic("implement me") +} + func (am *amOperator) GetNamespaceRole(namespace, username string) (Role, error) { panic("implement me") } diff --git a/pkg/models/iam/am/fake_operator.go b/pkg/models/iam/am/fake_operator.go new file mode 100644 index 000000000..8297e5654 --- /dev/null +++ b/pkg/models/iam/am/fake_operator.go @@ -0,0 +1,73 @@ +/* + * + * 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 am + +import ( + "k8s.io/apiserver/pkg/authentication/user" + "kubesphere.io/kubesphere/pkg/simple/client/cache" +) + +type fakeRole struct { + Name string + Rego string +} +type fakeOperator struct { + cache cache.Interface +} + +func newFakeRole(username string) Role { + if username == user.Anonymous { + return &fakeRole{ + Name: "anonymous", + Rego: "package authz\ndefault allow = false", + } + } + return &fakeRole{ + Name: "admin", + Rego: "package authz\ndefault allow = true", + } +} + +func (f fakeOperator) GetPlatformRole(username string) (Role, error) { + return newFakeRole(username), nil +} + +func (f fakeOperator) GetClusterRole(cluster, username string) (Role, error) { + return newFakeRole(username), nil +} + +func (f fakeOperator) GetWorkspaceRole(workspace, username string) (Role, error) { + return newFakeRole(username), nil +} + +func (f fakeOperator) GetNamespaceRole(namespace, username string) (Role, error) { + return newFakeRole(username), nil +} + +func (f fakeRole) GetName() string { + return f.Name +} + +func (f fakeRole) GetRego() string { + return f.Rego +} + +func NewFakeAMOperator(cache cache.Interface) AccessManagementInterface { + return &fakeOperator{cache: cache} +} diff --git a/pkg/models/tenant/namespaces.go b/pkg/models/tenant/namespaces.go index 577dd0d19..b8ea11a5c 100644 --- a/pkg/models/tenant/namespaces.go +++ b/pkg/models/tenant/namespaces.go @@ -20,9 +20,9 @@ package tenant import ( "k8s.io/api/core/v1" k8sinformers "k8s.io/client-go/informers" - kubernetes "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes" "kubesphere.io/kubesphere/pkg/constants" - am2 "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2" "kubesphere.io/kubesphere/pkg/server/params" "kubesphere.io/kubesphere/pkg/utils/sliceutil" @@ -37,7 +37,7 @@ type NamespaceInterface interface { type namespaceSearcher struct { k8s kubernetes.Interface informers k8sinformers.SharedInformerFactory - am am2.AccessManagementInterface + am am.AccessManagementInterface } func (s *namespaceSearcher) CreateNamespace(workspace string, namespace *v1.Namespace, username string) (*v1.Namespace, error) { @@ -53,7 +53,7 @@ func (s *namespaceSearcher) CreateNamespace(workspace string, namespace *v1.Name return s.k8s.CoreV1().Namespaces().Create(namespace) } -func newNamespaceOperator(k8s kubernetes.Interface, informers k8sinformers.SharedInformerFactory, am am2.AccessManagementInterface) NamespaceInterface { +func newNamespaceOperator(k8s kubernetes.Interface, informers k8sinformers.SharedInformerFactory, am am.AccessManagementInterface) NamespaceInterface { return &namespaceSearcher{k8s: k8s, informers: informers, am: am} } diff --git a/pkg/models/tenant/tenant.go b/pkg/models/tenant/tenant.go index e138019ef..dd3ace488 100644 --- a/pkg/models/tenant/tenant.go +++ b/pkg/models/tenant/tenant.go @@ -24,7 +24,7 @@ import ( "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" "kubesphere.io/kubesphere/pkg/models" - am2 "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/server/params" "kubesphere.io/kubesphere/pkg/simple/client/mysql" ) @@ -43,7 +43,7 @@ type Interface interface { type tenantOperator struct { workspaces WorkspaceInterface namespaces NamespaceInterface - am am2.AccessManagementInterface + am am.AccessManagementInterface devops DevOpsProjectOperator } @@ -68,7 +68,7 @@ func (t *tenantOperator) DeleteNamespace(workspace, namespace string) error { } func New(client kubernetes.Interface, informers k8sinformers.SharedInformerFactory, ksinformers ksinformers.SharedInformerFactory, db *mysql.Database) Interface { - amOperator := am2.NewAMOperator(client, informers) + amOperator := am.NewAMOperator(client, informers) return &tenantOperator{ workspaces: newWorkspaceOperator(client, informers, ksinformers, amOperator, db), namespaces: newNamespaceOperator(client, informers, amOperator), diff --git a/pkg/models/tenant/workspaces.go b/pkg/models/tenant/workspaces.go index ec2ac4561..f56103177 100644 --- a/pkg/models/tenant/workspaces.go +++ b/pkg/models/tenant/workspaces.go @@ -27,7 +27,7 @@ import ( "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/db" "kubesphere.io/kubesphere/pkg/models/devops" - am2 "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2" "kubesphere.io/kubesphere/pkg/server/params" "kubesphere.io/kubesphere/pkg/simple/client/mysql" @@ -64,14 +64,14 @@ type workspaceOperator struct { client kubernetes.Interface informers informers.SharedInformerFactory ksInformers externalversions.SharedInformerFactory - am am2.AccessManagementInterface + am am.AccessManagementInterface // TODO: use db interface instead of mysql client // we can refactor this after rewrite devops using crd db *mysql.Database } -func newWorkspaceOperator(client kubernetes.Interface, informers informers.SharedInformerFactory, ksinformers externalversions.SharedInformerFactory, am am2.AccessManagementInterface, db *mysql.Database) WorkspaceInterface { +func newWorkspaceOperator(client kubernetes.Interface, informers informers.SharedInformerFactory, ksinformers externalversions.SharedInformerFactory, am am.AccessManagementInterface, db *mysql.Database) WorkspaceInterface { return &workspaceOperator{ client: client, informers: informers,