github.com/oam-dev/kubevela@v1.9.11/pkg/auth/privileges.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"reflect"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/gosuri/uitable/util/wordwrap"
    28  	velaslices "github.com/kubevela/pkg/util/slices"
    29  	"github.com/xlab/treeprint"
    30  	rbacv1 "k8s.io/api/rbac/v1"
    31  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    32  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/types"
    35  	"k8s.io/utils/strings/slices"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    38  
    39  	"github.com/oam-dev/kubevela/pkg/multicluster"
    40  	"github.com/oam-dev/kubevela/pkg/utils"
    41  	velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
    42  )
    43  
    44  // PrivilegeInfo describes one privilege in Kubernetes. Either one ClusterRole or
    45  // one Role is referenced. Related PolicyRules that describes the resource level
    46  // admissions are included. The RoleBindingRefs records where this RoleRef comes
    47  // from (from which ClusterRoleBinding or RoleBinding).
    48  type PrivilegeInfo struct {
    49  	Rules           []rbacv1.PolicyRule `json:"rules,omitempty"`
    50  	RoleRef         `json:"roleRef,omitempty"`
    51  	RoleBindingRefs []RoleBindingRef `json:"roleBindingRefs,omitempty"`
    52  }
    53  
    54  type authObjRef struct {
    55  	Kind      string `json:"kind,omitempty"`
    56  	Name      string `json:"name,omitempty"`
    57  	Namespace string `json:"namespace,omitempty"`
    58  }
    59  
    60  // FullName the namespaced name string
    61  func (ref authObjRef) FullName() string {
    62  	if ref.Namespace == "" {
    63  		return ref.Name
    64  	}
    65  	return ref.Namespace + "/" + ref.Name
    66  }
    67  
    68  // Scope the scope of the object
    69  func (ref authObjRef) Scope() apiextensions.ResourceScope {
    70  	if ref.Namespace == "" {
    71  		return apiextensions.ClusterScoped
    72  	}
    73  	return apiextensions.NamespaceScoped
    74  }
    75  
    76  // RoleRef the references to ClusterRole or Role
    77  type RoleRef authObjRef
    78  
    79  // RoleBindingRef the reference to ClusterRoleBinding or RoleBinding
    80  type RoleBindingRef authObjRef
    81  
    82  // ListPrivileges retrieve privilege information in specified clusters
    83  func ListPrivileges(ctx context.Context, cli client.Client, clusters []string, identity *Identity) (map[string][]PrivilegeInfo, error) {
    84  	var m sync.Map
    85  	errs := velaslices.ParMap(clusters, func(cluster string) error {
    86  		info, err := listPrivilegesInCluster(ctx, cli, cluster, identity)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		m.Store(cluster, info)
    91  		return nil
    92  	})
    93  	if err := velaerrors.AggregateErrors(errs); err != nil {
    94  		return nil, err
    95  	}
    96  	privilegesMap := make(map[string][]PrivilegeInfo)
    97  	m.Range(func(key, value interface{}) bool {
    98  		privilegesMap[key.(string)] = value.([]PrivilegeInfo)
    99  		return true
   100  	})
   101  	return privilegesMap, nil
   102  }
   103  
   104  func listPrivilegesInCluster(ctx context.Context, cli client.Client, cluster string, identity *Identity) ([]PrivilegeInfo, error) {
   105  	ctx = multicluster.ContextWithClusterName(ctx, cluster)
   106  	clusterRoleBindings := &rbacv1.ClusterRoleBindingList{}
   107  	roleBindings := &rbacv1.RoleBindingList{}
   108  	if err := cli.List(ctx, clusterRoleBindings); err != nil {
   109  		return nil, err
   110  	}
   111  	roleRefMap := make(map[RoleRef][]RoleBindingRef)
   112  	for _, clusterRoleBinding := range clusterRoleBindings.Items {
   113  		if identity.MatchAny(clusterRoleBinding.Subjects) {
   114  			roleRef := RoleRef{
   115  				Kind: clusterRoleBinding.RoleRef.Kind,
   116  				Name: clusterRoleBinding.RoleRef.Name,
   117  			}
   118  			roleRefMap[roleRef] = append(roleRefMap[roleRef], RoleBindingRef{
   119  				Kind: "ClusterRoleBinding",
   120  				Name: clusterRoleBinding.Name})
   121  		}
   122  	}
   123  	if err := cli.List(ctx, roleBindings); err != nil {
   124  		return nil, err
   125  	}
   126  	for _, roleBinding := range roleBindings.Items {
   127  		for i := range roleBinding.Subjects {
   128  			roleBinding.Subjects[i].Namespace = roleBinding.Namespace
   129  		}
   130  		if identity.MatchAny(roleBinding.Subjects) {
   131  			roleRef := RoleRef{
   132  				Kind: roleBinding.RoleRef.Kind,
   133  				Name: roleBinding.RoleRef.Name,
   134  			}
   135  			if roleRef.Kind == "Role" {
   136  				roleRef.Namespace = roleBinding.Namespace
   137  			}
   138  			roleRefMap[roleRef] = append(roleRefMap[roleRef], RoleBindingRef{
   139  				Kind:      "RoleBinding",
   140  				Name:      roleBinding.Name,
   141  				Namespace: roleBinding.Namespace})
   142  		}
   143  	}
   144  
   145  	var infos []PrivilegeInfo
   146  	for roleRef, roleBindingRefs := range roleRefMap {
   147  		infos = append(infos, PrivilegeInfo{RoleRef: roleRef, RoleBindingRefs: roleBindingRefs})
   148  	}
   149  	var m sync.Map
   150  	errs := velaslices.ParMap(infos, func(info PrivilegeInfo) error {
   151  		key := types.NamespacedName{Namespace: info.RoleRef.Namespace, Name: info.RoleRef.Name}
   152  		var rules []rbacv1.PolicyRule
   153  		if info.RoleRef.Kind == "Role" {
   154  			role := &rbacv1.Role{}
   155  			if err := cli.Get(ctx, key, role); err != nil {
   156  				return err
   157  			}
   158  			rules = role.Rules
   159  		} else {
   160  			clusterRole := &rbacv1.ClusterRole{}
   161  			if err := cli.Get(ctx, key, clusterRole); err != nil {
   162  				return err
   163  			}
   164  			rules = clusterRole.Rules
   165  		}
   166  		m.Store(authObjRef(info.RoleRef).FullName(), rules)
   167  		return nil
   168  	})
   169  	if err := velaerrors.AggregateErrors(errs); err != nil {
   170  		return nil, err
   171  	}
   172  	for i, info := range infos {
   173  		obj, ok := m.Load(authObjRef(info.RoleRef).FullName())
   174  		if ok {
   175  			infos[i].Rules = obj.([]rbacv1.PolicyRule)
   176  		}
   177  	}
   178  	return infos, nil
   179  }
   180  
   181  func printPolicyRule(rule rbacv1.PolicyRule, lim uint) string {
   182  	var rows []string
   183  	addRow := func(name string, values []string) {
   184  		values = slices.Filter(nil, values, func(s string) bool {
   185  			return len(s) > 0
   186  		})
   187  		if len(values) > 0 {
   188  			s := wordwrap.WrapString(strings.Join(values, ", "), lim)
   189  			for i, line := range strings.Split(s, "\n") {
   190  				prefix := []byte(name + " ")
   191  				if i > 0 {
   192  					for j := range prefix {
   193  						prefix[j] = ' '
   194  					}
   195  				}
   196  				rows = append(rows, string(prefix)+line)
   197  			}
   198  		}
   199  	}
   200  	addRow("APIGroups:      ", rule.APIGroups)
   201  	addRow("Resources:      ", rule.Resources)
   202  	addRow("ResourceNames:  ", rule.ResourceNames)
   203  	addRow("NonResourceURLs:", rule.NonResourceURLs)
   204  	addRow("Verb:           ", rule.Verbs)
   205  	return strings.Join(rows, "\n")
   206  }
   207  
   208  // PrettyPrintPrivileges print cluster privileges map in tree format
   209  func PrettyPrintPrivileges(identity *Identity, privilegesMap map[string][]PrivilegeInfo, clusters []string, lim uint) string {
   210  	tree := treeprint.New()
   211  	tree.SetValue(identity.String())
   212  	for _, cluster := range clusters {
   213  		privileges, exists := privilegesMap[cluster]
   214  		if !exists {
   215  			continue
   216  		}
   217  		root := tree.AddMetaBranch("Cluster", cluster)
   218  		for _, info := range privileges {
   219  			branch := root.AddMetaBranch(info.RoleRef.Kind, authObjRef(info.RoleRef).FullName())
   220  			bindingsBranch := branch.AddMetaBranch("Scope", "")
   221  			for _, ref := range info.RoleBindingRefs {
   222  				var prefix string
   223  				if ref.Namespace != "" {
   224  					prefix = ref.Namespace + " "
   225  				}
   226  				bindingsBranch.AddMetaNode(authObjRef(ref).Scope(), fmt.Sprintf("%s(%s %s)", prefix, ref.Kind, ref.Name))
   227  			}
   228  			rulesBranch := branch.AddMetaBranch("PolicyRules", "")
   229  			for _, rule := range info.Rules {
   230  				rulesBranch.AddNode(printPolicyRule(rule, lim))
   231  			}
   232  		}
   233  		if len(privileges) == 0 {
   234  			root.AddNode("no privilege found")
   235  		}
   236  	}
   237  	return tree.String()
   238  }
   239  
   240  // PrivilegeDescription describe the privilege to grant
   241  type PrivilegeDescription interface {
   242  	GetCluster() string
   243  	GetRoles() []client.Object
   244  	GetRoleBinding([]rbacv1.Subject) client.Object
   245  }
   246  
   247  const (
   248  	// KubeVelaReaderRoleName a role that can read any resources
   249  	KubeVelaReaderRoleName = "kubevela:reader"
   250  	// KubeVelaWriterRoleName a role that can read/write any resources
   251  	KubeVelaWriterRoleName = "kubevela:writer"
   252  	// KubeVelaWriterAppRoleName a role that can read/write any application
   253  	KubeVelaWriterAppRoleName = "kubevela:writer:application"
   254  	// KubeVelaReaderAppRoleName a role that can read any application
   255  	KubeVelaReaderAppRoleName = "kubevela:reader:application"
   256  )
   257  
   258  // ScopedPrivilege includes all resource privileges in the destination
   259  type ScopedPrivilege struct {
   260  	Prefix    string
   261  	Cluster   string
   262  	Namespace string
   263  	ReadOnly  bool
   264  }
   265  
   266  // GetCluster the cluster of the privilege
   267  func (p *ScopedPrivilege) GetCluster() string {
   268  	return p.Cluster
   269  }
   270  
   271  // GetRoles the underlying Roles/ClusterRoles for the privilege
   272  func (p *ScopedPrivilege) GetRoles() []client.Object {
   273  	if p.ReadOnly {
   274  		return []client.Object{&rbacv1.ClusterRole{
   275  			ObjectMeta: metav1.ObjectMeta{Name: p.Prefix + KubeVelaReaderRoleName},
   276  			Rules: []rbacv1.PolicyRule{
   277  				{APIGroups: []string{rbacv1.APIGroupAll}, Resources: []string{rbacv1.ResourceAll}, Verbs: []string{"get", "list", "watch"}},
   278  				{NonResourceURLs: []string{rbacv1.NonResourceAll}, Verbs: []string{"get", "list", "watch"}},
   279  			},
   280  		}}
   281  	}
   282  	return []client.Object{&rbacv1.ClusterRole{
   283  		ObjectMeta: metav1.ObjectMeta{Name: p.Prefix + KubeVelaWriterRoleName},
   284  		Rules: []rbacv1.PolicyRule{
   285  			{APIGroups: []string{rbacv1.APIGroupAll}, Resources: []string{rbacv1.ResourceAll}, Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
   286  			{NonResourceURLs: []string{rbacv1.NonResourceAll}, Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
   287  		},
   288  	}}
   289  }
   290  
   291  // GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
   292  func (p *ScopedPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
   293  	var binding client.Object
   294  	var roleName = KubeVelaWriterRoleName
   295  	if p.ReadOnly {
   296  		roleName = KubeVelaReaderRoleName
   297  	}
   298  	if p.Namespace == "" {
   299  		binding = &rbacv1.ClusterRoleBinding{
   300  			RoleRef:  rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
   301  			Subjects: subs,
   302  		}
   303  	} else {
   304  		binding = &rbacv1.RoleBinding{
   305  			RoleRef:  rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
   306  			Subjects: subs,
   307  		}
   308  		binding.SetNamespace(p.Namespace)
   309  	}
   310  	binding.SetName(p.Prefix + roleName + ":binding")
   311  	return binding
   312  }
   313  
   314  // ApplicationPrivilege includes the application privileges in the destination
   315  type ApplicationPrivilege struct {
   316  	Prefix    string
   317  	Cluster   string
   318  	Namespace string
   319  	ReadOnly  bool
   320  }
   321  
   322  // GetCluster the cluster of the privilege
   323  func (a *ApplicationPrivilege) GetCluster() string {
   324  	return a.Cluster
   325  }
   326  
   327  // GetRoles the underlying Roles/ClusterRoles for the privilege
   328  func (a *ApplicationPrivilege) GetRoles() []client.Object {
   329  	verbs := []string{"get", "list", "watch", "create", "update", "patch", "delete"}
   330  	name := a.Prefix + KubeVelaWriterAppRoleName
   331  	if a.ReadOnly {
   332  		verbs = []string{"get", "list", "watch"}
   333  		name = a.Prefix + KubeVelaReaderAppRoleName
   334  	}
   335  	return []client.Object{
   336  		&rbacv1.ClusterRole{
   337  			ObjectMeta: metav1.ObjectMeta{Name: name},
   338  			Rules: []rbacv1.PolicyRule{
   339  				{
   340  					APIGroups: []string{"core.oam.dev"},
   341  					Resources: []string{"applications", "applications/status", "policies", "workflows", "workflowruns", "workflowruns/status"},
   342  					Verbs:     verbs,
   343  				},
   344  				{
   345  					APIGroups: []string{""},
   346  					Resources: []string{"secrets", "configmaps"},
   347  					Verbs:     verbs,
   348  				},
   349  			},
   350  		},
   351  	}
   352  }
   353  
   354  // GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
   355  func (a *ApplicationPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
   356  	var binding client.Object
   357  	var roleName = KubeVelaWriterAppRoleName
   358  	if a.ReadOnly {
   359  		roleName = KubeVelaReaderAppRoleName
   360  	}
   361  	if a.Namespace == "" {
   362  		binding = &rbacv1.ClusterRoleBinding{
   363  			RoleRef:  rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
   364  			Subjects: subs,
   365  		}
   366  	} else {
   367  		binding = &rbacv1.RoleBinding{
   368  			RoleRef:  rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
   369  			Subjects: subs,
   370  		}
   371  		binding.SetNamespace(a.Namespace)
   372  	}
   373  	binding.SetName(a.Prefix + roleName + ":binding")
   374  	return binding
   375  }
   376  
   377  func mergeSubjects(src []rbacv1.Subject, merge []rbacv1.Subject) []rbacv1.Subject {
   378  	subs := append([]rbacv1.Subject{}, src...)
   379  	for _, sub := range merge {
   380  		contains := false
   381  		for _, s := range subs {
   382  			if reflect.DeepEqual(sub, s) {
   383  				contains = true
   384  				break
   385  			}
   386  		}
   387  		if !contains {
   388  			subs = append(subs, sub)
   389  		}
   390  	}
   391  	return subs
   392  }
   393  
   394  func removeSubjects(src []rbacv1.Subject, toRemove []rbacv1.Subject) []rbacv1.Subject {
   395  	var subs []rbacv1.Subject
   396  	for _, sub := range src {
   397  		add := true
   398  		for _, t := range toRemove {
   399  			if reflect.DeepEqual(t, sub) {
   400  				add = false
   401  				break
   402  			}
   403  		}
   404  		if add {
   405  			subs = append(subs, sub)
   406  		}
   407  	}
   408  	return subs
   409  }
   410  
   411  type opts struct {
   412  	replace bool
   413  }
   414  
   415  // WithReplace means to replace all subjects, this is only useful in Grant Privileges
   416  func WithReplace(o *opts) {
   417  	o.replace = true
   418  }
   419  
   420  // GrantPrivileges grant privileges to identity
   421  func GrantPrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer, optionFuncs ...func(*opts)) error {
   422  	var options = &opts{}
   423  	for _, fc := range optionFuncs {
   424  		fc(options)
   425  	}
   426  	subs := identity.Subjects()
   427  	if len(subs) == 0 {
   428  		return fmt.Errorf("failed to find RBAC subjects in identity")
   429  	}
   430  	for _, p := range privileges {
   431  		cluster := p.GetCluster()
   432  		_ctx := multicluster.ContextWithClusterName(ctx, cluster)
   433  		for _, role := range p.GetRoles() {
   434  			kind, key := "ClusterRole", role.GetName()
   435  			if role.GetNamespace() != "" {
   436  				kind, key = "Role", role.GetNamespace()+"/"+role.GetName()
   437  			}
   438  			res, err := utils.CreateOrUpdate(_ctx, cli, role)
   439  			if err != nil {
   440  				return fmt.Errorf("failed to create/update %s %s in %s: %w", kind, key, cluster, err)
   441  			}
   442  			if res != controllerutil.OperationResultNone {
   443  				_, _ = fmt.Fprintf(writer, "%s %s %s in %s.\n", kind, key, res, cluster)
   444  			}
   445  		}
   446  		binding := p.GetRoleBinding(subs)
   447  		kind, key := "ClusterRoleBinding", binding.GetName()
   448  		if binding.GetNamespace() != "" {
   449  			kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName()
   450  		}
   451  		switch bindingObj := binding.(type) {
   452  		case *rbacv1.RoleBinding:
   453  			obj := &rbacv1.RoleBinding{}
   454  			if err := cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
   455  				if options.replace {
   456  					bindingObj.Subjects = obj.Subjects
   457  				} else {
   458  					bindingObj.Subjects = mergeSubjects(bindingObj.Subjects, obj.Subjects)
   459  				}
   460  			}
   461  		case *rbacv1.ClusterRoleBinding:
   462  			obj := &rbacv1.ClusterRoleBinding{}
   463  			if err := cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
   464  				if options.replace {
   465  					bindingObj.Subjects = obj.Subjects
   466  				} else {
   467  					bindingObj.Subjects = mergeSubjects(bindingObj.Subjects, obj.Subjects)
   468  				}
   469  			}
   470  		}
   471  		res, err := utils.CreateOrUpdate(_ctx, cli, binding)
   472  		if err != nil {
   473  			return fmt.Errorf("failed to create/update %s %s in %s: %w", kind, key, cluster, err)
   474  		}
   475  		_, _ = fmt.Fprintf(writer, "%s %s %s in %s.\n", kind, key, res, cluster)
   476  	}
   477  	return nil
   478  }
   479  
   480  // RevokePrivileges revoke privileges (notice that the revoking process only deletes bond subject in the
   481  // RoleBinding/ClusterRoleBinding, it does not ensure the identity's other related privileges are removed to
   482  // prevent identity from accessing)
   483  func RevokePrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer, optionFuncs ...func(*opts)) error {
   484  	var options = &opts{}
   485  	for _, fc := range optionFuncs {
   486  		fc(options)
   487  	}
   488  	subs := identity.Subjects()
   489  	if len(subs) == 0 {
   490  		return fmt.Errorf("failed to find RBAC subjects in identity")
   491  	}
   492  	for _, p := range privileges {
   493  		cluster := p.GetCluster()
   494  		_ctx := multicluster.ContextWithClusterName(ctx, cluster)
   495  		binding := p.GetRoleBinding(subs)
   496  		kind, key := "ClusterRoleBinding", binding.GetName()
   497  		if binding.GetNamespace() != "" {
   498  			kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName()
   499  		}
   500  		var err error
   501  		remove := false
   502  		var toDel client.Object
   503  		switch bindingObj := binding.(type) {
   504  		case *rbacv1.RoleBinding:
   505  			obj := &rbacv1.RoleBinding{}
   506  			if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
   507  				bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
   508  				remove = len(bindingObj.Subjects) == 0
   509  				toDel = obj
   510  			}
   511  		case *rbacv1.ClusterRoleBinding:
   512  			obj := &rbacv1.ClusterRoleBinding{}
   513  			if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
   514  				bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
   515  				remove = len(bindingObj.Subjects) == 0
   516  				toDel = obj
   517  			}
   518  		}
   519  		if err != nil {
   520  			if !kerrors.IsNotFound(err) {
   521  				return fmt.Errorf("failed to fetch %s %s in cluster %s: %w", kind, key, cluster, err)
   522  			}
   523  			return nil
   524  		}
   525  		if remove {
   526  			if err = cli.Delete(_ctx, toDel); err != nil {
   527  				return fmt.Errorf("failed to delete %s %s in cluster %s: %w", kind, key, cluster, err)
   528  			}
   529  		} else {
   530  			res, err := utils.CreateOrUpdate(_ctx, cli, binding)
   531  			if err != nil {
   532  				return fmt.Errorf("failed to update %s %s in cluster %s: %w", kind, key, cluster, err)
   533  			}
   534  			_, _ = fmt.Fprintf(writer, "%s %s %s in cluster %s.\n", kind, key, res, cluster)
   535  		}
   536  	}
   537  	return nil
   538  }