From debdeb16b9718ecd55b8bb57ab7b674a07bc6c3f Mon Sep 17 00:00:00 2001 From: mozillazg Date: Fri, 24 Nov 2023 12:23:41 +0800 Subject: [PATCH] cli: add more rbac utils sub-commands * scan-user-permissions * cleanup-user-permissions --- pkg/ctl/rbac/README.zh-cn.md | 50 +++++++ .../binding.go | 67 +++++++--- .../binding_test.go | 4 +- pkg/ctl/rbac/binding/ram.go | 39 ++++++ pkg/ctl/rbac/cleanupuserpermissions/cmd.go | 116 +++++++++++++++++ pkg/ctl/rbac/root.go | 2 + pkg/ctl/rbac/scanuserpermissions/cmd.go | 122 ++++++++++++++++-- 7 files changed, 370 insertions(+), 30 deletions(-) create mode 100644 pkg/ctl/rbac/README.zh-cn.md rename pkg/ctl/rbac/{scanuserpermissions => binding}/binding.go (71%) rename pkg/ctl/rbac/{scanuserpermissions => binding}/binding_test.go (95%) create mode 100644 pkg/ctl/rbac/binding/ram.go create mode 100644 pkg/ctl/rbac/cleanupuserpermissions/cmd.go diff --git a/pkg/ctl/rbac/README.zh-cn.md b/pkg/ctl/rbac/README.zh-cn.md new file mode 100644 index 00000000..2eee0f08 --- /dev/null +++ b/pkg/ctl/rbac/README.zh-cn.md @@ -0,0 +1,50 @@ +# RBAC + + +## scan-user-permissions + +扫描指定集群中存在的 RAM 用户和角色的 RBAC bindings. + +```shell + +# 默认只输出已删除 RAM 用户和角色 +$ ack-ram-tool rbac scan-user-permissions -c <集群ID> +UID UserType UserName Binding +2432******** (deleted) RamUser ClusterRoleBinding/-/24*****-clusterrolebinding + + +# 可以通过 -A 显示所有用户和角色 +$ ack-ram-tool rbac scan-user-permissions -A -c <集群ID> +UID UserType UserName Binding +2432******** (deleted) RamUser ClusterRoleBinding/-/24*****-clusterrolebinding +2342******** RamUser foobar ClusterRoleBinding/-/23*****-clusterrolebinding + +``` + +## cleanup-user-permissions + +清理指定集群中指定的 RAM 用户和角色的 RBAC bindings. + +```shell + +# <用户ID> 可以从上面的 scan 命令的结果中获取 +$ ack-ram-tool rbac cleanup-user-permissions -c <集群ID> -u <用户ID> +Start to scan users and bindings +Will cleanup RBAC bindings as blow: +UID UserType UserName Binding +300******************** (deleted) RamRole RoleBinding/kube-system/300********************-heapster-rolebinding +300******************** (deleted) RamRole RoleBinding/arms-prom/300********************-arms-prom-rolebinding +300******************** (deleted) RamRole RoleBinding/default/300********************-default-rolebinding +300******************** (deleted) RamRole ClusterRoleBinding/-/300********************-clusterrolebinding +? Are you sure you want to cleanup these bindings? Yes +start to cleanup binding: RoleBinding/kube-system/300********************-heapster-rolebinding +finished cleanup binding: RoleBinding/kube-system/300********************-heapster-rolebinding +start to cleanup binding: RoleBinding/arms-prom/300********************-arms-prom-rolebinding +finished cleanup binding: RoleBinding/arms-prom/300********************-arms-prom-rolebinding +start to cleanup binding: RoleBinding/default/300********************-default-rolebinding +finished cleanup binding: RoleBinding/default/300********************-default-rolebinding +start to cleanup binding: ClusterRoleBinding/-/300********************-clusterrolebinding +finished cleanup binding: ClusterRoleBinding/-/300********************-clusterrolebinding +all bindings have been cleanup + +``` diff --git a/pkg/ctl/rbac/scanuserpermissions/binding.go b/pkg/ctl/rbac/binding/binding.go similarity index 71% rename from pkg/ctl/rbac/scanuserpermissions/binding.go rename to pkg/ctl/rbac/binding/binding.go index a60e1571..edd28c07 100644 --- a/pkg/ctl/rbac/scanuserpermissions/binding.go +++ b/pkg/ctl/rbac/binding/binding.go @@ -1,8 +1,9 @@ -package scanuserpermissions +package binding import ( "context" "errors" + "fmt" "regexp" "sort" "strconv" @@ -13,11 +14,11 @@ import ( "k8s.io/client-go/kubernetes" ) -type BindingKind string +type Kind string var ( - BindingKindRoleBinding BindingKind = "RoleBinding" - BindingKindClusterRoleBinding BindingKind = "ClusterRoleBinding" + KindRoleBinding Kind = "RoleBinding" + KindClusterRoleBinding Kind = "ClusterRoleBinding" ) type RawBindings struct { @@ -26,6 +27,7 @@ type RawBindings struct { } var errInvalidName = errors.New("invalid name") +var regexAliUserIdentity = regexp.MustCompile(`^(\d+)(-\d+)?$`) func (bs *RawBindings) AliUserBindings() RawBindings { filtered := RawBindings{} @@ -43,10 +45,11 @@ func (bs *RawBindings) AliUserBindings() RawBindings { } type Binding struct { - Kind BindingKind + Kind Kind Name string + Namespace string SubjectName string - AliUid int + AliUid int64 } func (bs *RawBindings) SortByUid() []Binding { @@ -54,8 +57,9 @@ func (bs *RawBindings) SortByUid() []Binding { for _, b := range bs.RoleBindings { for _, sub := range b.Subjects { bindList = append(bindList, Binding{ - Kind: BindingKindRoleBinding, + Kind: KindRoleBinding, Name: b.Name, + Namespace: b.Namespace, SubjectName: sub.Name, AliUid: 0, }) @@ -64,8 +68,9 @@ func (bs *RawBindings) SortByUid() []Binding { for _, b := range bs.ClusterRoleBindings { for _, sub := range b.Subjects { bindList = append(bindList, Binding{ - Kind: BindingKindClusterRoleBinding, + Kind: KindClusterRoleBinding, Name: b.Name, + Namespace: b.Namespace, SubjectName: sub.Name, AliUid: 0, }) @@ -86,18 +91,18 @@ func (bs *RawBindings) SortByUid() []Binding { return bindList } -func listBindings(ctx context.Context, kube kubernetes.Interface) (*RawBindings, error) { - rolebindings, err := listRoleBindings(ctx, kube) +func ListBindings(ctx context.Context, kube kubernetes.Interface) (*RawBindings, error) { + roleBindings, err := listRoleBindings(ctx, kube) if err != nil { return nil, err } - clusterroleBindings, err := listClusterRoleBindings(ctx, kube) + clusterRoleBindings, err := listClusterRoleBindings(ctx, kube) if err != nil { return nil, err } return &RawBindings{ - RoleBindings: rolebindings.Items, - ClusterRoleBindings: clusterroleBindings.Items, + RoleBindings: roleBindings.Items, + ClusterRoleBindings: clusterRoleBindings.Items, }, nil } @@ -119,7 +124,7 @@ func isAliUserClusterRoleBinding(binding rbacv1.ClusterRoleBinding) bool { return false } -func getAliUidFromSubjectName(name string) (int, error) { +func getAliUidFromSubjectName(name string) (int64, error) { matches := regexAliUserIdentity.FindAllStringSubmatch(name, -1) if len(matches) < 1 { return 0, errInvalidName @@ -132,11 +137,9 @@ func getAliUidFromSubjectName(name string) (int, error) { if err != nil { return 0, err } - return uid, nil + return int64(uid), nil } -var regexAliUserIdentity = regexp.MustCompile(`^(\d+)(-\d+)?$`) - func isAliUserSubject(subject rbacv1.Subject) bool { if subject.Kind != rbacv1.UserKind { return false @@ -205,3 +208,33 @@ func listClusterRoleBindings(ctx context.Context, kube kubernetes.Interface) (*r return allList, nil } + +func RemoveBinding(ctx context.Context, b Binding, client kubernetes.Interface) error { + switch b.Kind { + case KindRoleBinding: + return removeRoleBinding(ctx, b, client) + case KindClusterRoleBinding: + return removeClusterRoleBinding(ctx, b, client) + } + return nil +} + +func removeClusterRoleBinding(ctx context.Context, b Binding, client kubernetes.Interface) error { + err := client.RbacV1().ClusterRoleBindings().Delete(ctx, b.Name, metav1.DeleteOptions{}) + return err +} + +func removeRoleBinding(ctx context.Context, b Binding, client kubernetes.Interface) error { + err := client.RbacV1().RoleBindings(b.Namespace).Delete(ctx, b.Name, metav1.DeleteOptions{}) + return err +} + +func (b Binding) String() string { + switch b.Kind { + case KindRoleBinding: + return fmt.Sprintf("%s/%s/%s", b.Kind, b.Namespace, b.Name) + case KindClusterRoleBinding: + return fmt.Sprintf("%s/-/%s", b.Kind, b.Name) + } + return fmt.Sprintf("%v", b) +} diff --git a/pkg/ctl/rbac/scanuserpermissions/binding_test.go b/pkg/ctl/rbac/binding/binding_test.go similarity index 95% rename from pkg/ctl/rbac/scanuserpermissions/binding_test.go rename to pkg/ctl/rbac/binding/binding_test.go index 03b2607e..c723309e 100644 --- a/pkg/ctl/rbac/scanuserpermissions/binding_test.go +++ b/pkg/ctl/rbac/binding/binding_test.go @@ -1,4 +1,4 @@ -package scanuserpermissions +package binding import ( "testing" @@ -11,7 +11,7 @@ func Test_getAliUidFromSubjectName(t *testing.T) { tests := []struct { name string args args - want int + want int64 wantErr bool }{ { diff --git a/pkg/ctl/rbac/binding/ram.go b/pkg/ctl/rbac/binding/ram.go new file mode 100644 index 00000000..3fa22959 --- /dev/null +++ b/pkg/ctl/rbac/binding/ram.go @@ -0,0 +1,39 @@ +package binding + +import ( + "context" + "github.com/AliyunContainerService/ack-ram-tool/pkg/log" + "github.com/AliyunContainerService/ack-ram-tool/pkg/openapi" + "github.com/AliyunContainerService/ack-ram-tool/pkg/types" + "strconv" +) + +func ListAccounts(ctx context.Context, client openapi.RamClientInterface) (map[int64]types.Account, error) { + accounts := make(map[int64]types.Account, 0) + users, err := client.ListUsers(ctx) + if err != nil { + log.Logger.Errorf("list users failed: %s", err) + return nil, err + } + roles, err := client.ListRoles(ctx) + if err != nil { + log.Logger.Errorf("list roles failed: %s", err) + return nil, err + } + for _, u := range users { + id, _ := strconv.ParseInt(u.Id, 10, 64) + accounts[id] = types.Account{ + Type: types.AccountTypeUser, + User: u, + } + } + for _, r := range roles { + id, _ := strconv.ParseInt(r.RoleId, 10, 64) + accounts[id] = types.Account{ + Type: types.AccountTypeRole, + Role: r, + } + } + + return accounts, nil +} diff --git a/pkg/ctl/rbac/cleanupuserpermissions/cmd.go b/pkg/ctl/rbac/cleanupuserpermissions/cmd.go new file mode 100644 index 00000000..8fab651b --- /dev/null +++ b/pkg/ctl/rbac/cleanupuserpermissions/cmd.go @@ -0,0 +1,116 @@ +package cleanupuserpermissions + +import ( + "context" + "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/rbac/scanuserpermissions" + "github.com/AliyunContainerService/ack-ram-tool/pkg/log" + "github.com/briandowns/spinner" + "time" + + ctlcommon "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/common" + "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/rbac/binding" + "github.com/AliyunContainerService/ack-ram-tool/pkg/openapi" + "github.com/AliyunContainerService/ack-ram-tool/pkg/types" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" +) + +type Option struct { + userId int64 + + clusterId string + privateIpAddress bool + temporaryDuration time.Duration + //outputFormat OutputFormat + allUsers bool +} + +var opts = Option{ + temporaryDuration: time.Hour, +} + +var cmd = &cobra.Command{ + Use: "cleanup-user-permissions", + Short: "cleanup RBAC permissions for one user", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + run() + }, +} + +func run() { + ctx := context.Background() + openAPIClient := ctlcommon.GetClientOrDie() + + oneCluster(ctx, openAPIClient, opts.clusterId) +} + +func oneCluster(ctx context.Context, openAPIClient openapi.ClientInterface, clusterId string) { + kubeClient := getKubeClient(ctx, openAPIClient, clusterId) + + log.Logger.Info("Start to scan users and bindings") + spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond) + spin.Start() + defer spin.Stop() + + rawBindings, err := binding.ListBindings(ctx, kubeClient) + ctlcommon.ExitIfError(err) + accounts, err := binding.ListAccounts(ctx, openAPIClient) + ctlcommon.ExitIfError(err) + spin.Stop() + + bindings := rawBindings.SortByUid() + cleanup(ctx, bindings, accounts, kubeClient) +} + +func cleanup(ctx context.Context, bindings []binding.Binding, + accounts map[int64]types.Account, kube kubernetes.Interface) { + var newBindings []binding.Binding + for _, b := range bindings { + if b.AliUid == 0 { + continue + } + if opts.userId != 0 && b.AliUid != opts.userId { + continue + } + acc, ok := accounts[b.AliUid] + if !ok { + acc = types.NewFakeAccount(b.AliUid) + acc.MarkDeleted() + accounts[b.AliUid] = acc + } + newBindings = append(newBindings, b) + } + + log.Logger.Info("Will cleanup RBAC bindings as blow:") + scanuserpermissions.OutputBindingsTable(newBindings, accounts, false) + + ctlcommon.YesOrExit("Are you sure you want to cleanup these bindings?") + for _, b := range newBindings { + log.Logger.Infof("start to cleanup binding: %s", b.String()) + if err := binding.RemoveBinding(ctx, b, kube); err != nil { + ctlcommon.ExitIfError(err) + } + log.Logger.Infof("finished cleanup binding: %s", b.String()) + } + log.Logger.Info("all bindings have been cleanup") +} + +func getKubeClient(ctx context.Context, openAPIClient openapi.ClientInterface, clusterId string) kubernetes.Interface { + kubeconfig, err := openAPIClient.GetUserKubeConfig(ctx, clusterId, + opts.privateIpAddress, opts.temporaryDuration) + ctlcommon.ExitIfError(err) + + client, err := ctlcommon.NewKubeClient(kubeconfig.RawData) + ctlcommon.ExitIfError(err) + return client +} + +func SetupCmd(rootCmd *cobra.Command) { + rootCmd.AddCommand(cmd) + cmd.Flags().Int64VarP(&opts.userId, "user-id", "u", 0, "limit user id") + cmd.Flags().StringVarP(&opts.clusterId, "cluster-id", "c", "", "cluster id") + //cmd.Flags().BoolVarP(&opts.allUsers, "all-users", "A", false, "list all users") + ctlcommon.ExitIfError(cmd.MarkFlagRequired("cluster-id")) + ctlcommon.ExitIfError(cmd.MarkFlagRequired("user-id")) +} diff --git a/pkg/ctl/rbac/root.go b/pkg/ctl/rbac/root.go index f08103fb..de8ec059 100644 --- a/pkg/ctl/rbac/root.go +++ b/pkg/ctl/rbac/root.go @@ -1,6 +1,7 @@ package rbac import ( + "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/rbac/cleanupuserpermissions" "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/rbac/scanuserpermissions" "github.com/spf13/cobra" ) @@ -13,6 +14,7 @@ var rootCmd = &cobra.Command{ func init() { scanuserpermissions.SetupCmd(rootCmd) + cleanupuserpermissions.SetupCmd(rootCmd) } func SetupCmd(root *cobra.Command) { diff --git a/pkg/ctl/rbac/scanuserpermissions/cmd.go b/pkg/ctl/rbac/scanuserpermissions/cmd.go index 1cb218bb..59c2c046 100644 --- a/pkg/ctl/rbac/scanuserpermissions/cmd.go +++ b/pkg/ctl/rbac/scanuserpermissions/cmd.go @@ -3,27 +3,35 @@ package scanuserpermissions import ( "context" "fmt" + "github.com/AliyunContainerService/ack-ram-tool/pkg/log" + "github.com/briandowns/spinner" + "github.com/olekukonko/tablewriter" + "os" + "sort" + "strings" "time" ctlcommon "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/common" + "github.com/AliyunContainerService/ack-ram-tool/pkg/ctl/rbac/binding" "github.com/AliyunContainerService/ack-ram-tool/pkg/openapi" + "github.com/AliyunContainerService/ack-ram-tool/pkg/types" "github.com/spf13/cobra" "k8s.io/client-go/kubernetes" ) -// scan rbac permissions bound to alibaba cloud users -//type OutputFormat string - type Option struct { - userId uint64 + userId int64 clusterId string privateIpAddress bool temporaryDuration time.Duration //outputFormat OutputFormat + allUsers bool } -var opts = Option{} +var opts = Option{ + temporaryDuration: time.Hour, +} var cmd = &cobra.Command{ Use: "scan-user-permissions", @@ -37,22 +45,113 @@ var cmd = &cobra.Command{ func run() { ctx := context.Background() openAPIClient := ctlcommon.GetClientOrDie() - kubeClient := getKubeClient(ctx, openAPIClient) - rawBindings, err := listBindings(ctx, kubeClient) + oneCluster(ctx, openAPIClient, opts.clusterId) +} + +func oneCluster(ctx context.Context, openAPIClient openapi.ClientInterface, clusterId string) { + kubeClient := getKubeClient(ctx, openAPIClient, clusterId) + + log.Logger.Info("Start to scan users and bindings") + spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond) + spin.Start() + + rawBindings, err := binding.ListBindings(ctx, kubeClient) ctlcommon.ExitIfError(err) + accounts, err := binding.ListAccounts(ctx, openAPIClient) + ctlcommon.ExitIfError(err) + spin.Stop() bindings := rawBindings.SortByUid() + outputTable(bindings, accounts) +} + +func outputTable(bindings []binding.Binding, accounts map[int64]types.Account) { + var newBindings []binding.Binding for _, b := range bindings { if b.AliUid == 0 { continue } - fmt.Printf("UID: %-20d\t KIND: %s\t NAME: %s\n", b.AliUid, b.Kind, b.Name) + if opts.userId != 0 && b.AliUid != opts.userId { + continue + } + acc, ok := accounts[b.AliUid] + if !ok { + acc = types.NewFakeAccount(b.AliUid) + acc.MarkDeleted() + accounts[b.AliUid] = acc + } + if !acc.Deleted() && !opts.allUsers { + continue + } + newBindings = append(newBindings, b) + } + + OutputBindingsTable(newBindings, accounts, true) +} + +func OutputBindingsTable(bindings []binding.Binding, accounts map[int64]types.Account, + highlightDeletedUsers bool) { + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"UID", "UserType", "UserName", "Binding"}) + //table.SetAutoMergeCells(true) + table.SetAutoWrapText(true) + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.EnableBorder(false) + table.SetTablePadding(" ") + table.SetNoWhiteSpace(true) + + redColor := tablewriter.Colors{ + tablewriter.Bold, + tablewriter.FgRedColor, + } + redColors := []tablewriter.Colors{ + redColor, redColor, redColor, redColor, redColor, redColor, + } + + sort.Slice(bindings, func(i, j int) bool { + ai := accounts[bindings[i].AliUid] + bi := accounts[bindings[i].AliUid] + if ai.Deleted() { + return true + } + if bi.Deleted() { + return false + } + return strings.Compare(ai.Id(), bi.Id()) == -1 + }) + + for _, b := range bindings { + acc := accounts[b.AliUid] + + var userComment string + if acc.Deleted() { + userComment = " (deleted)" + } + + data := []string{ + fmt.Sprintf("%d%s", b.AliUid, userComment), + string(acc.Type), + acc.Name(), + b.String(), + } + if acc.Deleted() && highlightDeletedUsers { + table.Rich(data, redColors) + } else { + table.Append(data) + } } + + table.Render() } -func getKubeClient(ctx context.Context, openAPIClient *openapi.Client) kubernetes.Interface { - clusterId := opts.clusterId +func getKubeClient(ctx context.Context, openAPIClient openapi.ClientInterface, clusterId string) kubernetes.Interface { kubeconfig, err := openAPIClient.GetUserKubeConfig(ctx, clusterId, opts.privateIpAddress, opts.temporaryDuration) ctlcommon.ExitIfError(err) @@ -64,8 +163,9 @@ func getKubeClient(ctx context.Context, openAPIClient *openapi.Client) kubernete func SetupCmd(rootCmd *cobra.Command) { rootCmd.AddCommand(cmd) - cmd.Flags().Uint64VarP(&opts.userId, "user-id", "u", 0, "limit user id") + cmd.Flags().Int64Var(&opts.userId, "user-id", 0, "limit user id") cmd.Flags().StringVarP(&opts.clusterId, "cluster-id", "c", "", "cluster id") + cmd.Flags().BoolVarP(&opts.allUsers, "all-users", "A", false, "list all users") err := cmd.MarkFlagRequired("cluster-id") ctlcommon.ExitIfError(err) }