Feat: support impersonation for application in apiserver (#4000)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 10dc83b60a)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
github-actions[bot]
2022-05-26 16:55:09 +08:00
committed by GitHub
co-authored by Somefive
parent e4fa5a5cf1
commit eb386ce9f7
10 changed files with 364 additions and 3 deletions
@@ -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 }}
+3 -2
View File
@@ -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" {
+2
View File
@@ -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
+28
View File
@@ -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
}
@@ -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
+1
View File
@@ -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)
}
+125
View File
@@ -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...)
}
+50
View File
@@ -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
}
+74
View File
@@ -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
}
+44
View File
@@ -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},
}))
}