From eb386ce9f779efed580cebee4c4e55d9c9bde405 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 16:55:09 +0800 Subject: [PATCH] Feat: support impersonation for application in apiserver (#4000) Signed-off-by: Somefive (cherry picked from commit 10dc83b60a527a3c24685dbb082eee470a92241f) Co-authored-by: Somefive --- .../templates/rbac/x-definition.yaml | 23 ++++ cmd/apiserver/main.go | 5 +- pkg/apiserver/domain/service/rbac.go | 2 + pkg/apiserver/domain/service/target.go | 28 ++++ .../infrastructure/clients/kubeclient.go | 15 ++- pkg/apiserver/server.go | 1 + pkg/apiserver/utils/auth.go | 125 ++++++++++++++++++ pkg/apiserver/utils/context.go | 50 +++++++ pkg/auth/privileges.go | 74 +++++++++++ pkg/features/apiserver_features.go | 44 ++++++ 10 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 charts/vela-core/templates/rbac/x-definition.yaml create mode 100644 pkg/apiserver/utils/auth.go create mode 100644 pkg/apiserver/utils/context.go create mode 100644 pkg/features/apiserver_features.go diff --git a/charts/vela-core/templates/rbac/x-definition.yaml b/charts/vela-core/templates/rbac/x-definition.yaml new file mode 100644 index 000000000..ebf204418 --- /dev/null +++ b/charts/vela-core/templates/rbac/x-definition.yaml @@ -0,0 +1,23 @@ +{{ if .Values.authentication.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubevela:x-definition:reader +rules: + - apiGroups: [ "core.oam.dev" ] + resources: [ "componentdefinitions", "traitdefinitions", "workloaddefinitions", "workflowstepdefinitions", "policydefinitions", "definitionrevisions" ] + verbs: [ "get", "list", "watch" ] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kubevela:x-definition:reader-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubevela:x-definition:reader +subjects: + - kind: Group + name: kubevela:x-definition:reader + apiGroup: rbac.authorization.k8s.io +{{ end }} \ No newline at end of file diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index e14d306bf..0a57b937b 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -19,7 +19,6 @@ package main import ( "context" "encoding/json" - "flag" "fmt" "os" "os/signal" @@ -29,10 +28,12 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/go-openapi/spec" "github.com/google/uuid" + flag "github.com/spf13/pflag" "github.com/oam-dev/kubevela/pkg/apiserver" "github.com/oam-dev/kubevela/pkg/apiserver/config" "github.com/oam-dev/kubevela/pkg/apiserver/utils/log" + "github.com/oam-dev/kubevela/pkg/features" "github.com/oam-dev/kubevela/version" ) @@ -50,7 +51,7 @@ func main() { flag.BoolVar(&s.serverConfig.DisableStatisticCronJob, "disable-statistic-cronJob", false, "close the system statistic info calculating cronJob") flag.Float64Var(&s.serverConfig.KubeQPS, "kube-api-qps", 100, "the qps for kube clients. Low qps may lead to low throughput. High qps may give stress to api-server.") flag.IntVar(&s.serverConfig.KubeBurst, "kube-api-burst", 300, "the burst for kube clients. Recommend setting it qps*3.") - + features.APIServerMutableFeatureGate.AddFlag(flag.CommandLine) flag.Parse() if len(os.Args) > 2 && os.Args[1] == "build-swagger" { diff --git a/pkg/apiserver/domain/service/rbac.go b/pkg/apiserver/domain/service/rbac.go index cb19366a0..a3030e2e8 100644 --- a/pkg/apiserver/domain/service/rbac.go +++ b/pkg/apiserver/domain/service/rbac.go @@ -29,6 +29,7 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/domain/model" "github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1" + apiserverutils "github.com/oam-dev/kubevela/pkg/apiserver/utils" "github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode" "github.com/oam-dev/kubevela/pkg/apiserver/utils/log" "github.com/oam-dev/kubevela/pkg/utils" @@ -552,6 +553,7 @@ func (p *rbacServiceImpl) CheckPerm(resource string, actions ...string) func(req bcode.ReturnError(req, res, bcode.ErrForbidden) return } + apiserverutils.SetUsernameAndProjectInRequestContext(req, userName, projectName) chain.ProcessFilter(req, res) } return f diff --git a/pkg/apiserver/domain/service/target.go b/pkg/apiserver/domain/service/target.go index 2bbadb707..989a721cf 100644 --- a/pkg/apiserver/domain/service/target.go +++ b/pkg/apiserver/domain/service/target.go @@ -17,6 +17,7 @@ limitations under the License. package service import ( + "bytes" "context" "errors" "fmt" @@ -27,8 +28,10 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/domain/repository" "github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/utils" "github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode" "github.com/oam-dev/kubevela/pkg/apiserver/utils/log" + "github.com/oam-dev/kubevela/pkg/auth" "github.com/oam-dev/kubevela/pkg/multicluster" ) @@ -117,6 +120,9 @@ func (dt *targetServiceImpl) DeleteTarget(ctx context.Context, targetName string if err = repository.DeleteTargetNamespace(ctx, dt.K8sClient, ddt.Cluster.ClusterName, ddt.Cluster.Namespace, targetName); err != nil { return err } + if err = managePrivilegesForTarget(ctx, dt.K8sClient, ddt, true); err != nil { + return err + } if err = dt.Store.Delete(ctx, target); err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { return bcode.ErrTargetNotExist @@ -142,6 +148,9 @@ func (dt *targetServiceImpl) CreateTarget(ctx context.Context, req apisv1.Create if err := repository.CreateTargetNamespace(ctx, dt.K8sClient, req.Cluster.ClusterName, req.Cluster.Namespace, req.Name); err != nil { return nil, err } + if err := managePrivilegesForTarget(ctx, dt.K8sClient, &target, false); err != nil { + return nil, err + } err := repository.CreateTarget(ctx, dt.Store, &target) if err != nil { return nil, err @@ -227,3 +236,22 @@ func (dt *targetServiceImpl) convertFromTargetModel(ctx context.Context, target } return targetBase } + +// managePrivilegesForTarget grant or revoke privileges for target +func managePrivilegesForTarget(ctx context.Context, cli client.Client, target *model.Target, revoke bool) error { + if target.Cluster == nil { + return nil + } + p := &auth.ScopedPrivilege{Cluster: target.Cluster.ClusterName, Namespace: target.Cluster.Namespace} + identity := &auth.Identity{Groups: []string{utils.KubeVelaProjectGroupPrefix + target.Project}} + writer := &bytes.Buffer{} + f, msg := auth.GrantPrivileges, "GrantPrivileges" + if revoke { + f, msg = auth.RevokePrivileges, "RevokePrivileges" + } + if err := f(ctx, cli, []auth.PrivilegeDescription{p}, identity, writer); err != nil { + return err + } + log.Logger.Debugf("%s: %s", msg, writer.String()) + return nil +} diff --git a/pkg/apiserver/infrastructure/clients/kubeclient.go b/pkg/apiserver/infrastructure/clients/kubeclient.go index cc4a1a871..201251704 100644 --- a/pkg/apiserver/infrastructure/clients/kubeclient.go +++ b/pkg/apiserver/infrastructure/clients/kubeclient.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" apiConfig "github.com/oam-dev/kubevela/pkg/apiserver/config" + "github.com/oam-dev/kubevela/pkg/auth" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" @@ -40,6 +41,18 @@ func SetKubeClient(c client.Client) { kubeClient = c } +func setKubeConfig(conf *rest.Config) (err error) { + if conf == nil { + conf, err = config.GetConfig() + if err != nil { + return err + } + } + kubeConfig = conf + kubeConfig.Wrap(auth.NewImpersonatingRoundTripper) + return nil +} + // SetKubeConfig generate the kube config from the config of apiserver func SetKubeConfig(c apiConfig.Config) error { conf, err := config.GetConfig() @@ -49,7 +62,7 @@ func SetKubeConfig(c apiConfig.Config) error { kubeConfig = conf kubeConfig.Burst = c.KubeBurst kubeConfig.QPS = float32(c.KubeQPS) - return nil + return setKubeConfig(kubeConfig) } // GetKubeClient create and return kube runtime client diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index e7eb1f05a..528c4995f 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -105,6 +105,7 @@ func (s *restServer) buildIoCContainer() error { return fmt.Errorf("fail to provides the datastore bean to the container: %w", err) } + kubeClient = utils.NewAuthApplicationClient(kubeClient) if err := s.beanContainer.ProvideWithName("kubeClient", kubeClient); err != nil { return fmt.Errorf("fail to provides the kubeClient bean to the container: %w", err) } diff --git a/pkg/apiserver/utils/auth.go b/pkg/apiserver/utils/auth.go new file mode 100644 index 000000000..ce1323b58 --- /dev/null +++ b/pkg/apiserver/utils/auth.go @@ -0,0 +1,125 @@ +/* +Copyright 2022 The KubeVela 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 utils + +import ( + "context" + + "github.com/emicklei/go-restful/v3" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/domain/model" + "github.com/oam-dev/kubevela/pkg/features" +) + +// KubeVelaProjectGroupPrefix the prefix kubevela project +const KubeVelaProjectGroupPrefix = "kubevela:project:" + +// ContextWithUserInfo extract user from context (parse username and project) for impersonation +func ContextWithUserInfo(ctx context.Context) context.Context { + if !features.APIServerFeatureGate.Enabled(features.APIServerEnableImpersonation) { + return ctx + } + userInfo := &user.DefaultInfo{Name: user.Anonymous} + if username, ok := UsernameFrom(ctx); ok { + userInfo.Name = username + } + if project, ok := ProjectFrom(ctx); ok { + userInfo.Groups = []string{KubeVelaProjectGroupPrefix + project} + } + if userInfo.Name == model.DefaultAdminUserName && !features.APIServerFeatureGate.Enabled(features.APIServerEnableAdminImpersonation) { + return ctx + } + return request.WithUser(ctx, userInfo) +} + +// SetUsernameAndProjectInRequestContext . +func SetUsernameAndProjectInRequestContext(req *restful.Request, userName string, projectName string) { + ctx := req.Request.Context() + ctx = WithUsername(ctx, userName) + ctx = WithProject(ctx, projectName) + req.Request = req.Request.WithContext(ctx) +} + +// NewAuthApplicationClient will carry UserInfo for mutating requests related to application automatically +func NewAuthApplicationClient(cli client.Client) client.Client { + return &authAppClient{Client: cli} +} + +type authAppClient struct { + client.Client +} + +// Status . +func (c *authAppClient) Status() client.StatusWriter { + return &authAppStatusClient{StatusWriter: c.Client.Status()} +} + +// Create . +func (c *authAppClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.Client.Create(ctx, obj, opts...) +} + +// Delete . +func (c *authAppClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.Client.Delete(ctx, obj, opts...) +} + +// Update . +func (c *authAppClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.Client.Update(ctx, obj, opts...) +} + +// Patch . +func (c *authAppClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.Client.Patch(ctx, obj, patch, opts...) +} + +type authAppStatusClient struct { + client.StatusWriter +} + +// Update . +func (c *authAppStatusClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.StatusWriter.Update(ctx, obj, opts...) +} + +// Patch . +func (c *authAppStatusClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + if _, ok := obj.(*v1beta1.Application); ok { + ctx = ContextWithUserInfo(ctx) + } + return c.StatusWriter.Patch(ctx, obj, patch, opts...) +} diff --git a/pkg/apiserver/utils/context.go b/pkg/apiserver/utils/context.go new file mode 100644 index 000000000..471bbd1ce --- /dev/null +++ b/pkg/apiserver/utils/context.go @@ -0,0 +1,50 @@ +/* +Copyright 2022 The KubeVela 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 utils + +import ( + "context" +) + +type contextKey int + +const ( + projectKey contextKey = iota + usernameKey +) + +// WithProject carries project in context +func WithProject(parent context.Context, project string) context.Context { + return context.WithValue(parent, projectKey, project) +} + +// ProjectFrom extract project from context +func ProjectFrom(ctx context.Context) (string, bool) { + project, ok := ctx.Value(projectKey).(string) + return project, ok +} + +// WithUsername carries username in context +func WithUsername(parent context.Context, username string) context.Context { + return context.WithValue(parent, usernameKey, username) +} + +// UsernameFrom extract username from context +func UsernameFrom(ctx context.Context) (string, bool) { + username, ok := ctx.Value(usernameKey).(string) + return username, ok +} diff --git a/pkg/auth/privileges.go b/pkg/auth/privileges.go index e776a83db..b6d6e62d2 100644 --- a/pkg/auth/privileges.go +++ b/pkg/auth/privileges.go @@ -28,6 +28,7 @@ import ( "github.com/xlab/treeprint" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/strings/slices" @@ -325,6 +326,23 @@ func mergeSubjects(src []rbacv1.Subject, merge []rbacv1.Subject) []rbacv1.Subjec return subs } +func removeSubjects(src []rbacv1.Subject, toRemove []rbacv1.Subject) []rbacv1.Subject { + var subs []rbacv1.Subject + for _, sub := range src { + add := true + for _, t := range toRemove { + if reflect.DeepEqual(t, sub) { + add = false + break + } + } + if add { + subs = append(subs, sub) + } + } + return subs +} + // GrantPrivileges grant privileges to identity func GrantPrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer) error { subs := identity.Subjects() @@ -372,3 +390,59 @@ func GrantPrivileges(ctx context.Context, cli client.Client, privileges []Privil } return nil } + +// RevokePrivileges revoke privileges (notice that the revoking process only deletes bond subject in the +// RoleBinding/ClusterRoleBinding, it does not ensure the identity's other related privileges are removed to +// prevent identity from accessing) +func RevokePrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer) error { + subs := identity.Subjects() + if len(subs) == 0 { + return fmt.Errorf("failed to find RBAC subjects in identity") + } + for _, p := range privileges { + cluster := p.GetCluster() + _ctx := multicluster.ContextWithClusterName(ctx, cluster) + binding := p.GetRoleBinding(subs) + kind, key := "ClusterRoleBinding", binding.GetName() + if binding.GetNamespace() != "" { + kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName() + } + var err error + remove := false + var toDel client.Object + switch bindingObj := binding.(type) { + case *rbacv1.RoleBinding: + obj := &rbacv1.RoleBinding{} + if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil { + bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects) + remove = len(bindingObj.Subjects) == 0 + toDel = obj + } + case *rbacv1.ClusterRoleBinding: + obj := &rbacv1.ClusterRoleBinding{} + if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil { + bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects) + remove = len(bindingObj.Subjects) == 0 + toDel = obj + } + } + if err != nil { + if !kerrors.IsNotFound(err) { + return fmt.Errorf("failed to fetch %s %s in cluster %s: %w", kind, key, cluster, err) + } + return nil + } + if remove { + if err = cli.Delete(_ctx, toDel); err != nil { + return fmt.Errorf("failed to delete %s %s in cluster %s: %w", kind, key, cluster, err) + } + } else { + res, err := utils.CreateOrUpdate(_ctx, cli, binding) + if err != nil { + return fmt.Errorf("failed to update %s %s in cluster %s: %w", kind, key, cluster, err) + } + _, _ = fmt.Fprintf(writer, "%s %s %s in cluster %s.\n", kind, key, res, cluster) + } + } + return nil +} diff --git a/pkg/features/apiserver_features.go b/pkg/features/apiserver_features.go new file mode 100644 index 000000000..309a7c3bb --- /dev/null +++ b/pkg/features/apiserver_features.go @@ -0,0 +1,44 @@ +/* +Copyright 2022 The KubeVela 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 features + +import ( + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/component-base/featuregate" +) + +var ( + // APIServerMutableFeatureGate is a mutable version of APIServerFeatureGate + APIServerMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() + + // APIServerFeatureGate is a shared global FeatureGate for apiserver. + APIServerFeatureGate featuregate.FeatureGate = APIServerMutableFeatureGate +) + +const ( + // APIServerEnableImpersonation whether to enable impersonation for APIServer + APIServerEnableImpersonation featuregate.Feature = "EnableImpersonation" + // APIServerEnableAdminImpersonation whether to disable User admin impersonation for APIServer + APIServerEnableAdminImpersonation featuregate.Feature = "EnableAdminImpersonation" +) + +func init() { + runtime.Must(APIServerMutableFeatureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ + APIServerEnableImpersonation: {Default: false, PreRelease: featuregate.Alpha}, + APIServerEnableAdminImpersonation: {Default: false, PreRelease: featuregate.Alpha}, + })) +}