mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Feat: vela auth list-privileges (#3923)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -20,6 +20,11 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultParallelism default parallelism
|
||||
DefaultParallelism int = 5
|
||||
)
|
||||
|
||||
// ParInput input for parallel execution
|
||||
type ParInput interface{}
|
||||
|
||||
|
||||
+147
-57
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user