Feat: vela auth list-privileges (#3923)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive
2022-05-18 20:57:39 +08:00
committed by GitHub
parent 1822e03505
commit 12ec62dd65
13 changed files with 653 additions and 67 deletions
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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=
+113
View File
@@ -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
}
+79
View File
@@ -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")
}
+218
View File
@@ -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()
}
+17
View File
@@ -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
}
+46 -5
View File
@@ -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}
}
+1 -1
View File
@@ -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
+5
View File
@@ -20,6 +20,11 @@ import (
"reflect"
)
const (
// DefaultParallelism default parallelism
DefaultParallelism int = 5
)
// ParInput input for parallel execution
type ParInput interface{}
+147 -57
View File
@@ -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()
}
+2 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
@@ -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"))
})
})
})