diff --git a/go.mod b/go.mod index 17cd53bc5..61ad561e8 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/tidwall/gjson v1.9.3 github.com/wercker/stern v0.0.0-20190705090245-4fa46dd6987f github.com/wonderflow/cert-manager-api v1.0.3 + github.com/xlab/treeprint v1.1.0 go.mongodb.org/mongo-driver v1.5.1 go.uber.org/zap v1.19.1 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 @@ -259,7 +260,6 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/zclconf/go-cty v1.8.0 // indirect go.etcd.io/etcd/api/v3 v3.5.0 // indirect diff --git a/go.sum b/go.sum index 3e656ec50..b6bc3567a 100644 --- a/go.sum +++ b/go.sum @@ -1911,8 +1911,9 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= -github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= diff --git a/pkg/auth/identity.go b/pkg/auth/identity.go new file mode 100644 index 000000000..0908c2e00 --- /dev/null +++ b/pkg/auth/identity.go @@ -0,0 +1,113 @@ +/* +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 auth + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/utils/strings/slices" +) + +// Identity the kubernetes identity +type Identity struct { + User string + Groups []string + ServiceAccount string + ServiceAccountNamespace string +} + +// String . +func (identity *Identity) String() string { + var tokens []string + if identity.User != "" { + tokens = append(tokens, "User="+identity.User) + } + if len(identity.Groups) > 0 { + tokens = append(tokens, "Groups="+strings.Join(identity.Groups, ",")) + } + if identity.ServiceAccount != "" { + tokens = append(tokens, "SA="+serviceaccount.MakeUsername(identity.ServiceAccountNamespace, identity.ServiceAccount)) + } + return strings.Join(tokens, " ") +} + +// Match validate if identity matches rbac subject +func (identity *Identity) Match(subject rbacv1.Subject) bool { + switch subject.Kind { + case rbacv1.UserKind: + return subject.Name == identity.User + case rbacv1.GroupKind: + return slices.Contains(identity.Groups, subject.Name) + case rbacv1.ServiceAccountKind: + return serviceaccount.MatchesUsername(subject.Namespace, subject.Name, + serviceaccount.MakeUsername(identity.ServiceAccountNamespace, identity.ServiceAccount)) + default: + return false + } +} + +// MatchAny validate if identity matches any one of the rbac subjects +func (identity *Identity) MatchAny(subjects []rbacv1.Subject) bool { + for _, subject := range subjects { + if identity.Match(subject) { + return true + } + } + return false +} + +// Regularize clean up input info +func (identity *Identity) Regularize() { + identity.User = strings.TrimSpace(identity.User) + groupMap := map[string]struct{}{} + var groups []string + for _, group := range identity.Groups { + group = strings.TrimSpace(group) + if _, found := groupMap[group]; !found { + groupMap[group] = struct{}{} + groups = append(groups, group) + } + } + identity.Groups = groups + identity.ServiceAccount = strings.TrimSpace(identity.ServiceAccount) + if identity.ServiceAccount != "" { + if identity.ServiceAccountNamespace == "" { + identity.ServiceAccountNamespace = corev1.NamespaceDefault + } + } +} + +// Validate check if identity is valid +func (identity *Identity) Validate() error { + if identity.User == "" && identity.ServiceAccount == "" { + return fmt.Errorf("either `user` or `serviceaccount` should be set") + } + if identity.User != "" && identity.ServiceAccount != "" { + return fmt.Errorf("cannot set `user` and `serviceaccount` at the same time") + } + if len(identity.Groups) > 0 && identity.ServiceAccount != "" { + return fmt.Errorf("cannot set `group` and `serviceaccount` at the same time") + } + if identity.ServiceAccount == "" && identity.ServiceAccountNamespace != "" { + return fmt.Errorf("cannot set serviceaccount namespace when serviceaccount is not set") + } + return nil +} diff --git a/pkg/auth/kubeconfig.go b/pkg/auth/kubeconfig.go index ec7e5eae4..3f5509073 100644 --- a/pkg/auth/kubeconfig.go +++ b/pkg/auth/kubeconfig.go @@ -25,6 +25,7 @@ import ( "encoding/pem" "fmt" "io" + "io/ioutil" "time" "github.com/pkg/errors" @@ -33,10 +34,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/utils/pointer" + + "github.com/oam-dev/kubevela/pkg/utils" ) // KubeConfigGenerateOptions options for create KubeConfig @@ -92,6 +97,25 @@ func (opt KubeConfigWithServiceAccountGenerateOption) ApplyToOptions(options *Ku } } +// KubeConfigWithIdentityGenerateOption option for setting identity in KubeConfig +type KubeConfigWithIdentityGenerateOption Identity + +// ApplyToOptions . +func (opt KubeConfigWithIdentityGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) { + if opt.User != "" { + KubeConfigWithUserGenerateOption(opt.User).ApplyToOptions(options) + } + for _, group := range opt.Groups { + KubeConfigWithGroupGenerateOption(group).ApplyToOptions(options) + } + if opt.ServiceAccount != "" { + (KubeConfigWithServiceAccountGenerateOption{ + Name: opt.ServiceAccount, + Namespace: opt.ServiceAccountNamespace, + }).ApplyToOptions(options) + } +} + // KubeConfigGenerateOption option for create KubeConfig type KubeConfigGenerateOption interface { ApplyToOptions(options *KubeConfigGenerateOptions) @@ -235,3 +259,58 @@ func generateServiceAccountKubeConfig(ctx context.Context, cli kubernetes.Interf Token: string(secret.Data["token"]), }, secret.Data["ca.crt"]), nil } + +// ReadIdentityFromKubeConfig extract identity from kubeconfig +func ReadIdentityFromKubeConfig(kubeconfigPath string) (*Identity, error) { + cfg, err := clientcmd.LoadFromFile(kubeconfigPath) + if err != nil { + return nil, err + } + ctx, exists := cfg.Contexts[cfg.CurrentContext] + if !exists { + return nil, fmt.Errorf("cannot find current-context %s", cfg.CurrentContext) + } + authInfo, exists := cfg.AuthInfos[ctx.AuthInfo] + if !exists { + return nil, fmt.Errorf("cannot find auth-info %s", ctx.AuthInfo) + } + + identity := &Identity{} + token := authInfo.Token + if token == "" && authInfo.TokenFile != "" { + bs, err := ioutil.ReadFile(authInfo.TokenFile) + if err != nil { + return nil, fmt.Errorf("failed to read token file %s: %w", authInfo.TokenFile, err) + } + token = string(bs) + } + if token != "" { + sub, err := utils.GetTokenSubject(token) + if err != nil { + return nil, fmt.Errorf("failed to recognize serviceaccount: %w", err) + } + identity.ServiceAccountNamespace, identity.ServiceAccount, err = serviceaccount.SplitUsername(sub) + if err != nil { + return nil, fmt.Errorf("cannot parse serviceaccount from %s: %w", sub, err) + } + return identity, nil + } + + certData := authInfo.ClientCertificateData + if len(certData) == 0 && authInfo.ClientCertificate != "" { + certData, err = ioutil.ReadFile(authInfo.ClientCertificate) + if err != nil { + return nil, fmt.Errorf("failed to read cert file %s: %w", authInfo.ClientCertificate, err) + } + } + if len(certData) > 0 { + name, err := utils.GetCertificateSubject(certData) + if err != nil { + return nil, fmt.Errorf("failed to get subject from certificate data: %w", err) + } + identity.User = name.CommonName + identity.Groups = name.Organization + return identity, nil + } + return nil, fmt.Errorf("cannot find client certificate or serviceaccount token in kubeconfig") +} diff --git a/pkg/auth/privileges.go b/pkg/auth/privileges.go new file mode 100644 index 000000000..60883717c --- /dev/null +++ b/pkg/auth/privileges.go @@ -0,0 +1,218 @@ +/* +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 auth + +import ( + "context" + "strings" + "sync" + + "github.com/gosuri/uitable/util/wordwrap" + "github.com/xlab/treeprint" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/strings/slices" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/multicluster" + velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors" + "github.com/oam-dev/kubevela/pkg/utils/parallel" +) + +// PrivilegeInfo describes one privilege in Kubernetes. Either one ClusterRole or +// one Role is referenced. Related PolicyRules that describes the resource level +// admissions are included. The RoleBindingRefs records where this RoleRef comes +// from (from which ClusterRoleBinding or RoleBinding). +type PrivilegeInfo struct { + Rules []rbacv1.PolicyRule `json:"rules,omitempty"` + RoleRef `json:"roleRef,omitempty"` + RoleBindingRefs []RoleBindingRef `json:"roleBindingRefs,omitempty"` +} + +type authObjRef struct { + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// FullName the namespaced name string +func (ref authObjRef) FullName() string { + if ref.Namespace == "" { + return ref.Name + } + return ref.Namespace + "/" + ref.Name +} + +// RoleRef the references to ClusterRole or Role +type RoleRef authObjRef + +// RoleBindingRef the reference to ClusterRoleBinding or RoleBinding +type RoleBindingRef authObjRef + +// ListPrivileges retrieve privilege information in specified clusters +func ListPrivileges(ctx context.Context, cli client.Client, clusters []string, identity *Identity) (map[string][]PrivilegeInfo, error) { + var m sync.Map + errs := parallel.Run(func(cluster string) error { + info, err := listPrivilegesInCluster(ctx, cli, cluster, identity) + if err != nil { + return err + } + m.Store(cluster, info) + return nil + }, clusters, parallel.DefaultParallelism) + if err := velaerrors.AggregateErrors(errs.([]error)); err != nil { + return nil, err + } + privilegesMap := make(map[string][]PrivilegeInfo) + m.Range(func(key, value interface{}) bool { + privilegesMap[key.(string)] = value.([]PrivilegeInfo) + return true + }) + return privilegesMap, nil +} + +func listPrivilegesInCluster(ctx context.Context, cli client.Client, cluster string, identity *Identity) ([]PrivilegeInfo, error) { + ctx = multicluster.ContextWithClusterName(ctx, cluster) + clusterRoleBindings := &rbacv1.ClusterRoleBindingList{} + roleBindings := &rbacv1.RoleBindingList{} + if err := cli.List(ctx, clusterRoleBindings); err != nil { + return nil, err + } + roleRefMap := make(map[RoleRef][]RoleBindingRef) + for _, clusterRoleBinding := range clusterRoleBindings.Items { + if identity.MatchAny(clusterRoleBinding.Subjects) { + roleRef := RoleRef{ + Kind: clusterRoleBinding.RoleRef.Kind, + Name: clusterRoleBinding.RoleRef.Name, + } + roleRefMap[roleRef] = append(roleRefMap[roleRef], RoleBindingRef{ + Kind: "ClusterRoleBinding", + Name: clusterRoleBinding.Name}) + } + } + if err := cli.List(ctx, roleBindings); err != nil { + return nil, err + } + for _, roleBinding := range roleBindings.Items { + for i := range roleBinding.Subjects { + roleBinding.Subjects[i].Namespace = roleBinding.Namespace + } + if identity.MatchAny(roleBinding.Subjects) { + roleRef := RoleRef{ + Kind: roleBinding.RoleRef.Kind, + Name: roleBinding.RoleRef.Name, + } + if roleRef.Kind == "Role" { + roleRef.Namespace = roleBinding.Namespace + } + roleRefMap[roleRef] = append(roleRefMap[roleRef], RoleBindingRef{ + Kind: "RoleBinding", + Name: roleBinding.Name, + Namespace: roleBinding.Namespace}) + } + } + + var infos []PrivilegeInfo + for roleRef, roleBindingRefs := range roleRefMap { + infos = append(infos, PrivilegeInfo{RoleRef: roleRef, RoleBindingRefs: roleBindingRefs}) + } + var m sync.Map + errs := parallel.Run(func(info PrivilegeInfo) error { + key := types.NamespacedName{Namespace: info.RoleRef.Namespace, Name: info.RoleRef.Name} + var rules []rbacv1.PolicyRule + if info.RoleRef.Kind == "Role" { + role := &rbacv1.Role{} + if err := cli.Get(ctx, key, role); err != nil { + return err + } + rules = role.Rules + } else { + clusterRole := &rbacv1.ClusterRole{} + if err := cli.Get(ctx, key, clusterRole); err != nil { + return err + } + rules = clusterRole.Rules + } + m.Store(authObjRef(info.RoleRef).FullName(), rules) + return nil + }, infos, parallel.DefaultParallelism) + if err := velaerrors.AggregateErrors(errs.([]error)); err != nil { + return nil, err + } + for i, info := range infos { + obj, ok := m.Load(authObjRef(info.RoleRef).FullName()) + if ok { + infos[i].Rules = obj.([]rbacv1.PolicyRule) + } + } + return infos, nil +} + +func printPolicyRule(rule rbacv1.PolicyRule, lim uint) string { + var rows []string + addRow := func(name string, values []string) { + values = slices.Filter(nil, values, func(s string) bool { + return len(s) > 0 + }) + if len(values) > 0 { + s := wordwrap.WrapString(strings.Join(values, ", "), lim) + for i, line := range strings.Split(s, "\n") { + prefix := []byte(name + " ") + if i > 0 { + for j := range prefix { + prefix[j] = ' ' + } + } + rows = append(rows, string(prefix)+line) + } + } + } + addRow("APIGroups: ", rule.APIGroups) + addRow("Resources: ", rule.Resources) + addRow("ResourceNames: ", rule.ResourceNames) + addRow("NonResourceURLs:", rule.NonResourceURLs) + addRow("Verb: ", rule.Verbs) + return strings.Join(rows, "\n") +} + +// PrettyPrintPrivileges print cluster privileges map in tree format +func PrettyPrintPrivileges(identity *Identity, privilegesMap map[string][]PrivilegeInfo, clusters []string, lim uint) string { + tree := treeprint.New() + tree.SetValue(identity.String()) + for _, cluster := range clusters { + privileges, exists := privilegesMap[cluster] + if !exists { + continue + } + root := tree.AddMetaBranch("Cluster", cluster) + for _, info := range privileges { + branch := root.AddMetaBranch(info.RoleRef.Kind, authObjRef(info.RoleRef).FullName()) + bindingsBranch := branch.AddMetaBranch("Bindings", "") + for _, ref := range info.RoleBindingRefs { + bindingsBranch.AddMetaNode(ref.Kind, authObjRef(ref).FullName()) + } + rulesBranch := branch.AddMetaBranch("PolicyRules", "") + for _, rule := range info.Rules { + rulesBranch.AddNode(printPolicyRule(rule, lim)) + } + } + if len(privileges) == 0 { + root.AddNode("no privilege found") + } + } + return tree.String() +} diff --git a/pkg/cmd/completion.go b/pkg/cmd/completion.go index d1b46e479..d0bb0cea2 100644 --- a/pkg/cmd/completion.go +++ b/pkg/cmd/completion.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" + prismclusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/oam" ) @@ -79,3 +81,18 @@ func GetApplicationsForCompletion(ctx context.Context, f Factory, namespace stri } return listObjectNamesForCompletion(ctx, f, v1beta1.SchemeGroupVersion.WithKind(v1beta1.ApplicationKind), options, toComplete) } + +// GetClustersForCompletion auto-complete the cluster +func GetClustersForCompletion(ctx context.Context, f Factory, toComplete string) ([]string, cobra.ShellCompDirective) { + clusters, err := prismclusterv1alpha1.NewClusterClient(f.Client()).List(ctx) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var candidates []string + for _, obj := range clusters.Items { + if name := obj.GetName(); strings.HasPrefix(name, toComplete) { + candidates = append(candidates, name) + } + } + return candidates, cobra.ShellCompDirectiveNoFileComp +} diff --git a/pkg/cmd/factory.go b/pkg/cmd/factory.go index a647e25da..de28a9fb0 100644 --- a/pkg/cmd/factory.go +++ b/pkg/cmd/factory.go @@ -17,10 +17,15 @@ limitations under the License. package cmd import ( + "sync" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/flowcontrol" "sigs.k8s.io/controller-runtime/pkg/client" cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/utils/common" ) // Factory client factory for running command @@ -35,26 +40,62 @@ type ClientGetter func() (client.Client, error) // ConfigGetter function for getting config type ConfigGetter func() (*rest.Config, error) -type defaultFactory struct { +type delegateFactory struct { ClientGetter ConfigGetter } // Client return the client for command line use, interrupt if error encountered -func (f *defaultFactory) Client() client.Client { +func (f *delegateFactory) Client() client.Client { cli, err := f.ClientGetter() cmdutil.CheckErr(err) return cli } // Config return the kubeConfig for command line use -func (f *defaultFactory) Config() *rest.Config { +func (f *delegateFactory) Config() *rest.Config { cfg, err := f.ConfigGetter() cmdutil.CheckErr(err) return cfg } +// NewDelegateFactory create a factory based on getter function +func NewDelegateFactory(clientGetter ClientGetter, configGetter ConfigGetter) Factory { + return &delegateFactory{ClientGetter: clientGetter, ConfigGetter: configGetter} +} + +var ( + // DefaultRateLimiter default rate limiter for cmd client + DefaultRateLimiter = flowcontrol.NewTokenBucketRateLimiter(100, 200) +) + +type defaultFactory struct { + sync.Mutex + cfg *rest.Config + cli client.Client +} + +// Client return the client for command line use, interrupt if error encountered +func (f *defaultFactory) Client() client.Client { + f.Lock() + defer f.Unlock() + if f.cli == nil { + var err error + f.cli, err = client.New(f.cfg, client.Options{Scheme: common.Scheme}) + cmdutil.CheckErr(err) + } + return f.cli +} + +// Config return the kubeConfig for command line use +func (f *defaultFactory) Config() *rest.Config { + return f.cfg +} + // NewDefaultFactory create a factory based on client getter function -func NewDefaultFactory(clientGetter ClientGetter, configGetter ConfigGetter) Factory { - return &defaultFactory{ClientGetter: clientGetter, ConfigGetter: configGetter} +func NewDefaultFactory(cfg *rest.Config) Factory { + copiedCfg := *cfg + copiedCfg.RateLimiter = DefaultRateLimiter + copiedCfg.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) + return &defaultFactory{cfg: &copiedCfg} } diff --git a/pkg/utils/k8s.go b/pkg/utils/k8s.go index f13187d66..e2afd26e5 100644 --- a/pkg/utils/k8s.go +++ b/pkg/utils/k8s.go @@ -109,7 +109,7 @@ func UpdateNamespace(ctx context.Context, kubeClient client.Client, name string, return kubeClient.Update(ctx, &namespace) } -// GetServiceAccountSubjectFromConfig extract ServiceAccount subject from token +// GetServiceAccountSubjectFromConfig extract ServiceAccountName subject from token func GetServiceAccountSubjectFromConfig(cfg *rest.Config) string { sub, _ := GetTokenSubject(cfg.BearerToken) return sub diff --git a/pkg/utils/parallel/parallel.go b/pkg/utils/parallel/parallel.go index 001110aaf..e3b4d05d7 100644 --- a/pkg/utils/parallel/parallel.go +++ b/pkg/utils/parallel/parallel.go @@ -20,6 +20,11 @@ import ( "reflect" ) +const ( + // DefaultParallelism default parallelism + DefaultParallelism int = 5 +) + // ParInput input for parallel execution type ParInput interface{} diff --git a/references/cli/auth.go b/references/cli/auth.go index e4f1a2714..0b37473c0 100644 --- a/references/cli/auth.go +++ b/references/cli/auth.go @@ -18,16 +18,18 @@ package cli import ( "context" + "fmt" "strings" - "github.com/pkg/errors" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" + "golang.org/x/term" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" + prismclusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/auth" velacmd "github.com/oam-dev/kubevela/pkg/cmd" @@ -45,74 +47,27 @@ func AuthCommandGroup(f velacmd.Factory, streams util.IOStreams) *cobra.Command }, } cmd.AddCommand(NewGenKubeConfigCommand(f, streams)) + cmd.AddCommand(NewListPrivilegesCommand(f, streams)) return cmd } // GenKubeConfigOptions options for create kubeconfig type GenKubeConfigOptions struct { - User string - Groups []string - ServiceAccountName string - ServiceAccountNamespace string - + auth.Identity util.IOStreams } -func (opt *GenKubeConfigOptions) options() []auth.KubeConfigGenerateOption { - var opts []auth.KubeConfigGenerateOption - if opt.User != "" { - opts = append(opts, auth.KubeConfigWithUserGenerateOption(opt.User)) - } - for _, group := range opt.Groups { - opts = append(opts, auth.KubeConfigWithGroupGenerateOption(group)) - } - if opt.ServiceAccountName != "" { - opts = append(opts, auth.KubeConfigWithServiceAccountGenerateOption{ - Name: opt.ServiceAccountName, - Namespace: opt.ServiceAccountNamespace, - }) - } - return opts -} - // Complete . func (opt *GenKubeConfigOptions) Complete(f velacmd.Factory, cmd *cobra.Command) { - opt.User = strings.TrimSpace(opt.User) - groupMap := map[string]struct{}{} - var groups []string - for _, group := range opt.Groups { - group = strings.TrimSpace(group) - if _, found := groupMap[group]; !found { - groupMap[group] = struct{}{} - groups = append(groups, group) - } - } - opt.Groups = groups - opt.ServiceAccountName = strings.TrimSpace(opt.ServiceAccountName) - if opt.ServiceAccountName != "" { - if ns := velacmd.GetNamespace(f, cmd); ns != "" { - opt.ServiceAccountNamespace = ns - } else { - opt.ServiceAccountNamespace = corev1.NamespaceDefault - } + if opt.Identity.ServiceAccount != "" { + opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd) } + opt.Regularize() } // Validate . func (opt *GenKubeConfigOptions) Validate() error { - if opt.User == "" && opt.ServiceAccountName == "" { - return errors.Errorf("either `user` or `serviceaccount` should be set") - } - if opt.User != "" && opt.ServiceAccountName != "" { - return errors.Errorf("cannot set `user` and `serviceaccount` at the same time") - } - if opt.User == "" && len(opt.Groups) > 0 { - return errors.Errorf("cannot set groups when user is not set") - } - if opt.ServiceAccountName == "" && opt.ServiceAccountNamespace != "" { - return errors.Errorf("cannot set serviceaccount namespace when serviceaccount is not set") - } - return nil + return opt.Identity.Validate() } // Run . @@ -126,7 +81,7 @@ func (opt *GenKubeConfigOptions) Run(f velacmd.Factory) error { if err != nil { return err } - cfg, err = auth.GenerateKubeConfig(ctx, cli, cfg, opt.IOStreams.ErrOut, opt.options()...) + cfg, err = auth.GenerateKubeConfig(ctx, cli, cfg, opt.IOStreams.ErrOut, auth.KubeConfigWithIdentityGenerateOption(opt.Identity)) if err != nil { return err } @@ -193,7 +148,7 @@ func NewGenKubeConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.C } cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user of the generated kubeconfig. If set, an X509-based kubeconfig will be intended to create. It will be embedded as the Subject in the X509 certificate.") cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The groups of the generated kubeconfig. This flag only works when `--user` is set. It will be embedded as the Organization in the X509 certificate.") - cmd.Flags().StringVarP(&o.ServiceAccountName, "serviceaccount", "", o.ServiceAccountName, "The serviceaccount of the generated kubeconfig. If set, a kubeconfig will be generated based on the secret token of the serviceaccount. Cannot be set when `--user` presents.") + cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount of the generated kubeconfig. If set, a kubeconfig will be generated based on the secret token of the serviceaccount. Cannot be set when `--user` presents.") cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "serviceaccount", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if strings.TrimSpace(o.User) != "" { @@ -209,3 +164,138 @@ func NewGenKubeConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.C WithResponsiveWriter(). Build() } + +// ListPrivilegesOptions options for list privileges +type ListPrivilegesOptions struct { + auth.Identity + KubeConfig string + Clusters []string + util.IOStreams +} + +// Complete . +func (opt *ListPrivilegesOptions) Complete(f velacmd.Factory, cmd *cobra.Command) { + if opt.KubeConfig != "" { + identity, err := auth.ReadIdentityFromKubeConfig(opt.KubeConfig) + cmdutil.CheckErr(err) + opt.Identity = *identity + } + if opt.Identity.ServiceAccount != "" { + opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd) + } + if len(opt.Clusters) == 0 { + opt.Clusters = []string{types.ClusterLocalName} + } + opt.Regularize() +} + +// Validate . +func (opt *ListPrivilegesOptions) Validate(f velacmd.Factory, cmd *cobra.Command) error { + if err := opt.Identity.Validate(); err != nil { + return err + } + for _, cluster := range opt.Clusters { + if _, err := prismclusterv1alpha1.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil { + return fmt.Errorf("failed to find cluster %s: %w", cluster, err) + } + } + return nil +} + +// Run . +func (opt *ListPrivilegesOptions) Run(f velacmd.Factory) error { + ctx := context.Background() + m, err := auth.ListPrivileges(ctx, f.Client(), opt.Clusters, &opt.Identity) + if err != nil { + return err + } + width, _, err := term.GetSize(0) + if err != nil { + width = 80 + } + _, _ = opt.Out.Write([]byte(auth.PrettyPrintPrivileges(&opt.Identity, m, opt.Clusters, uint(width)-40))) + return nil +} + +var ( + listPrivilegesLong = templates.LongDesc(i18n.T(` + List privileges for user + + List privileges that user has in clusters. Use --user/--group to check the privileges + for specified user and group. They can be jointly configured to see the union of + privileges. Use --serviceaccount and -n/--namespace to see the privileges for + ServiceAccount. You can also use --kubeconfig to use the identity inside implicitly. + The privileges will be shown in tree format. + + This command supports listing privileges across multiple clusters, by using --cluster. + If not set, the control plane will be used. This feature requires cluster-gateway to be + properly setup to use. + + The privileges are collected through listing all ClusterRoleBinding and RoleBinding, + following the Kubernetes RBAC Authorization. Other authorization mechanism is not supported + now. See https://kubernetes.io/docs/reference/access-authn-authz/rbac/ for details. + + The ClusterRoleBinding and RoleBinding that matches the specified identity will be + tracked. Related ClusterRoles and Roles are retrieved and the contained PolicyRules are + demonstrated.`)) + + listPrivilegesExample = templates.Examples(i18n.T(` + # List privileges for User alice in the control plane + vela auth list-privileges --user alice + + # List privileges for Group org:dev-team in the control plane + vela auth list-privileges --group org:dev-team + + # List privileges for User bob with Groups org:dev-team and org:test-team in the control plane and managed cluster example-cluster + vela auth list-privileges --user bob --group org:dev-team --group org:test-team --cluster local --cluster example-cluster + + # List privileges for ServiceAccount example-sa in demo namespace in multiple managed clusters + vela auth list-privileges --serviceaccount example-sa -n demo --cluster cluster-1 --cluster cluster-2 + + # List privileges for identity in kubeconfig + vela auth list-privileges --kubeconfig ./example.kubeconfig --cluster local --cluster cluster-1`)) +) + +// NewListPrivilegesCommand list privileges for given identity +func NewListPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command { + o := &ListPrivilegesOptions{IOStreams: streams} + cmd := &cobra.Command{ + Use: "list-privileges", + DisableFlagsInUseLine: true, + Short: i18n.T("List privileges for user/group/serviceaccount"), + Long: listPrivilegesLong, + Example: listPrivilegesExample, + Annotations: map[string]string{ + types.TagCommandType: types.TypeCD, + }, + Args: cobra.ExactValidArgs(0), + Run: func(cmd *cobra.Command, args []string) { + o.Complete(f, cmd) + cmdutil.CheckErr(o.Validate(f, cmd)) + cmdutil.CheckErr(o.Run(f)) + }, + } + cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user to list privileges.") + cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The group to list privileges. Can be set together with --user.") + cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount to list privileges. Cannot be set with --user and --group.") + cmd.Flags().StringSliceVarP(&o.Clusters, "cluster", "c", o.Clusters, "The cluster to list privileges. If not set, the command will list privileges in the control plane.") + cmd.Flags().StringVarP(&o.KubeConfig, "kubeconfig", "", o.KubeConfig, "The kubeconfig to list privileges. If set, it will override all the other identity flags.") + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "serviceaccount", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if strings.TrimSpace(o.User) != "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + namespace := velacmd.GetNamespace(f, cmd) + return velacmd.GetServiceAccountForCompletion(cmd.Context(), f, namespace, toComplete) + })) + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "cluster", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return velacmd.GetClustersForCompletion(cmd.Context(), f, toComplete) + })) + + return velacmd.NewCommandBuilder(f, cmd). + WithNamespaceFlag(velacmd.NamespaceFlagUsageOption("The namespace of the serviceaccount. This flag only works when `--serviceaccount` is set.")). + WithStreams(streams). + WithResponsiveWriter(). + Build() +} diff --git a/references/cli/cli.go b/references/cli/cli.go index 72ac668d0..d9fde3f5a 100644 --- a/references/cli/cli.go +++ b/references/cli/cli.go @@ -26,6 +26,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/klog" + "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/oam-dev/kubevela/apis/types" velacmd "github.com/oam-dev/kubevela/pkg/cmd" @@ -70,7 +71,7 @@ func NewCommandWithIOStreams(ioStream util.IOStreams) *cobra.Command { commandArgs := common.Args{ Schema: common.Scheme, } - f := velacmd.NewDefaultFactory(commandArgs.GetClient, commandArgs.GetConfig) + f := velacmd.NewDefaultFactory(config.GetConfigOrDie()) if err := system.InitDirs(); err != nil { fmt.Println("InitDir err", err) diff --git a/references/cli/up_test.go b/references/cli/up_test.go index edba2a3e6..60c04bd83 100644 --- a/references/cli/up_test.go +++ b/references/cli/up_test.go @@ -129,7 +129,7 @@ spec: })) var buf bytes.Buffer - cmd := NewUpCommand(velacmd.NewDefaultFactory(args.GetClient, args.GetConfig), "", args, util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + cmd := NewUpCommand(velacmd.NewDelegateFactory(args.GetClient, args.GetConfig), "", args, util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) cmd.SetArgs([]string{}) cmd.SetOut(&buf) cmd.SetErr(&buf) diff --git a/test/e2e-multicluster-test/multicluster_auth_test.go b/test/e2e-multicluster-test/multicluster_auth_test.go index f61a3d593..831e581ed 100644 --- a/test/e2e-multicluster-test/multicluster_auth_test.go +++ b/test/e2e-multicluster-test/multicluster_auth_test.go @@ -37,6 +37,27 @@ var _ = Describe("Test multicluster Auth commands", func() { Expect(outputs).Should(ContainSubstring("ServiceAccount vela-system/default found.")) }) + It("Test vela list-privileges for user", func() { + outputs, err := execCommand("auth", "list-privileges", "--user", "example", "--group", "kubevela:dev-team", "--group", "kubevela:test-team") + Expect(err).Should(Succeed()) + Expect(outputs).Should(ContainSubstring("local")) + }) + + It("Test vela list-privileges for ServiceAccount", func() { + outputs, err := execCommand("auth", "list-privileges", "--serviceaccount", "node-controller", "-n", "kube-system", "--cluster", "local", "--cluster", WorkerClusterName) + Expect(err).Should(Succeed()) + Expect(outputs).Should(SatisfyAny( + ContainSubstring(WorkerClusterName), + ContainSubstring("nodes/status"), + )) + }) + + It("Test vela list-privileges for kubeconfig", func() { + outputs, err := execCommand("auth", "list-privileges", "--kubeconfig", WorkerClusterKubeConfigPath, "--cluster", "local") + Expect(err).Should(Succeed()) + Expect(outputs).Should(ContainSubstring("cluster-admin")) + }) + }) })