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 }