Skip to content

Commit

Permalink
chore: support cross cluster authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
nanjingfm committed Mar 7, 2023
1 parent 8c89d0c commit a9b7ebf
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 10 deletions.
96 changes: 95 additions & 1 deletion client/rbac_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ import (
"net/http"
"strings"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"

"github.com/katanomi/pkg/multicluster"
corev1 "k8s.io/api/core/v1"
apiserverrequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/rest"
"knative.dev/pkg/system"

"knative.dev/pkg/injection"

"github.com/emicklei/go-restful/v3"
Expand All @@ -46,9 +55,82 @@ func (p GetResourceAttributesFunc) GetResourceAttributes(ctx context.Context, re

// ResourceAttributeGetter describe an interface to get resource attributes form request
type ResourceAttributeGetter interface {
// GetResourceAttributes get resource attributes from request
GetResourceAttributes(ctx context.Context, req *restful.Request) (authv1.ResourceAttributes, error)
}

// SubjectAccessReviewClientGetter describe an interface to get client for subject access review
// It is usually used for cross-cluster authentication.
type SubjectAccessReviewClientGetter interface {
// GetClient get k8s client according to request
GetClient(ctx context.Context, req *restful.Request) (client.Client, error)
}

func NewCrossClusterSubjectReview(mClient multicluster.Interface, scheme *runtime.Scheme, restMapper meta.RESTMapper) SubjectAccessReviewClientGetter {
return &CrossClusterSubjectReview{
multiClusterClient: mClient,
scheme: scheme,
restMapper: restMapper,
ClusterParameter: "cluster",
ClusterNamespace: system.Namespace(),
}
}

type CrossClusterSubjectReview struct {
multiClusterClient multicluster.Interface
scheme *runtime.Scheme
restMapper meta.RESTMapper

ClusterParameter string
ClusterNamespace string
}

func (c *CrossClusterSubjectReview) SetClusterParameter(parameter string) {
c.ClusterParameter = parameter
}

func (c *CrossClusterSubjectReview) SetClusterNamespace(ns string) {
c.ClusterNamespace = ns
}

func (c *CrossClusterSubjectReview) getClusterParameterName(req *restful.Request) string {
if name := req.PathParameter(c.ClusterParameter); name != "" {
return name
}
return req.QueryParameter(c.ClusterParameter)
}

func (c *CrossClusterSubjectReview) GetClient(ctx context.Context, req *restful.Request) (client.Client, error) {
clusterName := c.getClusterParameterName(req)
if clusterName == "" {
return nil, nil
}
clusterRef := &corev1.ObjectReference{}
clusterRef.SetGroupVersionKind(multicluster.ClusterRegistryGVK)
clusterRef.Name = clusterName
clusterRef.Namespace = c.ClusterNamespace

config, err := c.multiClusterClient.GetConfig(ctx, clusterRef)
if err != nil {
return nil, err
}
reqCtx := req.Request.Context()
u, ok := apiserverrequest.UserFrom(reqCtx)
if !ok {
return nil, nil
}
copyConfig := rest.CopyConfig(config)
copyConfig.Impersonate.UID = u.GetUID()
copyConfig.Impersonate.Groups = u.GetGroups()
copyConfig.Impersonate.UserName = u.GetName()
copyConfig.Impersonate.Extra = u.GetExtra()
directClient, err := client.New(copyConfig, client.Options{Scheme: c.scheme, Mapper: c.restMapper})
if err != nil {
return nil, err
}
return directClient, nil
}

// DynamicSubjectReviewFilter makes a subject review and the ResourceAttribute can be dynamically obtained
func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceAttributeGetter) restful.FilterFunction {
return func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
Expand Down Expand Up @@ -79,7 +161,19 @@ func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceA
review = makeSubjectAccessReview(resourceAtt, u)
}

err = postSubjectAccessReview(reqCtx, Client(reqCtx), review)
var clt client.Client
if clientGetter, ok := resourceAttGetter.(SubjectAccessReviewClientGetter); ok {
clt, err = clientGetter.GetClient(ctx, req)
if err != nil {
log.Debugw("get custom client for authentication failed", "err", err)
kerrors.HandleError(req, resp, err)
return
}
}
if clt == nil {
clt = Client(reqCtx)
}
err = postSubjectAccessReview(reqCtx, clt, review)
if err != nil {
log.Debugw("error verifying user permissions", "err", err, "review", review.GetObject())
kerrors.HandleError(req, resp, err)
Expand Down
101 changes: 92 additions & 9 deletions client/rbac_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"

apiserverrequest "k8s.io/apiserver/pkg/endpoints/request"

"sigs.k8s.io/controller-runtime/pkg/client"

"knative.dev/pkg/injection"

"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"

"github.com/emicklei/go-restful/v3"
"github.com/golang/mock/gomock"
"github.com/katanomi/pkg/testing/mock/github.com/katanomi/pkg/multicluster"
mockfakeclient "github.com/katanomi/pkg/testing/mock/sigs.k8s.io/controller-runtime/pkg/client"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
authv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/user"
apiserverrequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/rest"
"knative.dev/pkg/injection"
"knative.dev/pkg/system"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

Expand Down Expand Up @@ -153,3 +153,86 @@ func TestGetResourceAttributesFunc_GetResourceAttributes(t *testing.T) {
got, _ := getter.GetResourceAttributes(context.Background(), &restful.Request{})
g.Expect(got.Name).Should(BeEquivalentTo("test"))
}

var _ = Describe("TestCrossClusterSubjectReview_GetClient", func() {
var (
crossReview *CrossClusterSubjectReview
req *restful.Request
clt client.Client
err error
)

BeforeEach(func() {
crossReview = &CrossClusterSubjectReview{
ClusterParameter: "cluster",
ClusterNamespace: "test",
}
req = restful.NewRequest(&http.Request{})
clt = nil
err = nil
})

JustBeforeEach(func() {
clt, err = crossReview.GetClient(context.Background(), req)
})

Context("cluster parameter is empty", func() {
It("client should be nil", func() {
Expect(clt).Should(BeNil())
Expect(err).Should(BeNil())
})
})
Context("cluster parameter is not empty", func() {
BeforeEach(func() {
request := httptest.NewRequest("GET", "http://localhost:8080?cluster=test", nil)
req = restful.NewRequest(request)
})

When("when get multi cluster client failed", func() {
BeforeEach(func() {
mockCtl := gomock.NewController(GinkgoT())
mockClient := multicluster.NewMockInterface(mockCtl)
mockClient.EXPECT().GetConfig(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("test"))
crossReview.multiClusterClient = mockClient
})

It("client should return error", func() {
Expect(clt).Should(BeNil())
Expect(err).ShouldNot(BeNil())
})
})

When("when get multi cluster client successfully", func() {
BeforeEach(func() {
mockCtl := gomock.NewController(GinkgoT())
mockClient := multicluster.NewMockInterface(mockCtl)
mockClient.EXPECT().GetConfig(gomock.Any(), gomock.Any()).Return(&rest.Config{
Username: "test",
}, nil)
crossReview.multiClusterClient = mockClient
ctx := apiserverrequest.WithUser(context.Background(), &user.DefaultInfo{})
req.Request = req.Request.WithContext(ctx)
})

It("client should return a client successfully", func() {
Expect(clt).ShouldNot(BeNil())
Expect(err).Should(BeNil())
})
})
})
})

func TestCrossClusterSubjectReview_SetClusterParameter(t *testing.T) {
g := NewGomegaWithT(t)
os.Setenv(system.NamespaceEnvKey, "test")
crossReview := NewCrossClusterSubjectReview(nil, nil, nil)
review := crossReview.(*CrossClusterSubjectReview)
g.Expect(review.ClusterNamespace).Should(Equal("test"))
g.Expect(review.ClusterParameter).Should(Equal("cluster"))

review.SetClusterNamespace("new-ns")
g.Expect(review.ClusterNamespace).Should(Equal("new-ns"))

review.SetClusterParameter("new-param")
g.Expect(review.ClusterParameter).Should(Equal("new-param"))
}

0 comments on commit a9b7ebf

Please sign in to comment.