kubevela/pkg/auth/privileges.go

539 lines
17 KiB
Go

/*
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"
"fmt"
"io"
"reflect"
"strings"
"sync"
"github.com/gosuri/uitable/util/wordwrap"
velaslices "github.com/kubevela/pkg/util/slices"
"github.com/xlab/treeprint"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/strings/slices"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/utils"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
)
// 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
}
// Scope the scope of the object
func (ref authObjRef) Scope() apiextensions.ResourceScope {
if ref.Namespace == "" {
return apiextensions.ClusterScoped
}
return apiextensions.NamespaceScoped
}
// 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 := velaslices.ParMap(clusters, func(cluster string) error {
info, err := listPrivilegesInCluster(ctx, cli, cluster, identity)
if err != nil {
return err
}
m.Store(cluster, info)
return nil
})
if err := velaerrors.AggregateErrors(errs); 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 := velaslices.ParMap(infos, 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
})
if err := velaerrors.AggregateErrors(errs); 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("Scope", "")
for _, ref := range info.RoleBindingRefs {
var prefix string
if ref.Namespace != "" {
prefix = ref.Namespace + " "
}
bindingsBranch.AddMetaNode(authObjRef(ref).Scope(), fmt.Sprintf("%s(%s %s)", prefix, ref.Kind, ref.Name))
}
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()
}
// PrivilegeDescription describe the privilege to grant
type PrivilegeDescription interface {
GetCluster() string
GetRoles() []client.Object
GetRoleBinding([]rbacv1.Subject) client.Object
}
const (
// KubeVelaReaderRoleName a role that can read any resources
KubeVelaReaderRoleName = "kubevela:reader"
// KubeVelaWriterRoleName a role that can read/write any resources
KubeVelaWriterRoleName = "kubevela:writer"
// KubeVelaWriterAppRoleName a role that can read/write any application
KubeVelaWriterAppRoleName = "kubevela:writer:application"
// KubeVelaReaderAppRoleName a role that can read any application
KubeVelaReaderAppRoleName = "kubevela:reader:application"
)
// ScopedPrivilege includes all resource privileges in the destination
type ScopedPrivilege struct {
Prefix string
Cluster string
Namespace string
ReadOnly bool
}
// GetCluster the cluster of the privilege
func (p *ScopedPrivilege) GetCluster() string {
return p.Cluster
}
// GetRoles the underlying Roles/ClusterRoles for the privilege
func (p *ScopedPrivilege) GetRoles() []client.Object {
if p.ReadOnly {
return []client.Object{&rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: p.Prefix + KubeVelaReaderRoleName},
Rules: []rbacv1.PolicyRule{
{APIGroups: []string{rbacv1.APIGroupAll}, Resources: []string{rbacv1.ResourceAll}, Verbs: []string{"get", "list", "watch"}},
{NonResourceURLs: []string{rbacv1.NonResourceAll}, Verbs: []string{"get", "list", "watch"}},
},
}}
}
return []client.Object{&rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: p.Prefix + KubeVelaWriterRoleName},
Rules: []rbacv1.PolicyRule{
{APIGroups: []string{rbacv1.APIGroupAll}, Resources: []string{rbacv1.ResourceAll}, Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
{NonResourceURLs: []string{rbacv1.NonResourceAll}, Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}},
},
}}
}
// GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
func (p *ScopedPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
var binding client.Object
var roleName = KubeVelaWriterRoleName
if p.ReadOnly {
roleName = KubeVelaReaderRoleName
}
if p.Namespace == "" {
binding = &rbacv1.ClusterRoleBinding{
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
Subjects: subs,
}
} else {
binding = &rbacv1.RoleBinding{
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
Subjects: subs,
}
binding.SetNamespace(p.Namespace)
}
binding.SetName(p.Prefix + roleName + ":binding")
return binding
}
// ApplicationPrivilege includes the application privileges in the destination
type ApplicationPrivilege struct {
Prefix string
Cluster string
Namespace string
ReadOnly bool
}
// GetCluster the cluster of the privilege
func (a *ApplicationPrivilege) GetCluster() string {
return a.Cluster
}
// GetRoles the underlying Roles/ClusterRoles for the privilege
func (a *ApplicationPrivilege) GetRoles() []client.Object {
verbs := []string{"get", "list", "watch", "create", "update", "patch", "delete"}
name := a.Prefix + KubeVelaWriterAppRoleName
if a.ReadOnly {
verbs = []string{"get", "list", "watch"}
name = a.Prefix + KubeVelaReaderAppRoleName
}
return []client.Object{
&rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: name},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{"core.oam.dev"},
Resources: []string{"applications", "applications/status", "policies", "workflows", "workflowruns", "workflowruns/status"},
Verbs: verbs,
},
{
APIGroups: []string{""},
Resources: []string{"secrets", "configmaps"},
Verbs: verbs,
},
},
},
}
}
// GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
func (a *ApplicationPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
var binding client.Object
var roleName = KubeVelaWriterAppRoleName
if a.ReadOnly {
roleName = KubeVelaReaderAppRoleName
}
if a.Namespace == "" {
binding = &rbacv1.ClusterRoleBinding{
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
Subjects: subs,
}
} else {
binding = &rbacv1.RoleBinding{
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
Subjects: subs,
}
binding.SetNamespace(a.Namespace)
}
binding.SetName(a.Prefix + roleName + ":binding")
return binding
}
func mergeSubjects(src []rbacv1.Subject, merge []rbacv1.Subject) []rbacv1.Subject {
subs := append([]rbacv1.Subject{}, src...)
for _, sub := range merge {
contains := false
for _, s := range subs {
if reflect.DeepEqual(sub, s) {
contains = true
break
}
}
if !contains {
subs = append(subs, sub)
}
}
return subs
}
func removeSubjects(src []rbacv1.Subject, toRemove []rbacv1.Subject) []rbacv1.Subject {
var subs []rbacv1.Subject
for _, sub := range src {
add := true
for _, t := range toRemove {
if reflect.DeepEqual(t, sub) {
add = false
break
}
}
if add {
subs = append(subs, sub)
}
}
return subs
}
type opts struct {
replace bool
}
// WithReplace means to replace all subjects, this is only useful in Grant Privileges
func WithReplace(o *opts) {
o.replace = true
}
// GrantPrivileges grant privileges to identity
func GrantPrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer, optionFuncs ...func(*opts)) error {
var options = &opts{}
for _, fc := range optionFuncs {
fc(options)
}
subs := identity.Subjects()
if len(subs) == 0 {
return fmt.Errorf("failed to find RBAC subjects in identity")
}
for _, p := range privileges {
cluster := p.GetCluster()
_ctx := multicluster.ContextWithClusterName(ctx, cluster)
for _, role := range p.GetRoles() {
kind, key := "ClusterRole", role.GetName()
if role.GetNamespace() != "" {
kind, key = "Role", role.GetNamespace()+"/"+role.GetName()
}
res, err := utils.CreateOrUpdate(_ctx, cli, role)
if err != nil {
return fmt.Errorf("failed to create/update %s %s in %s: %w", kind, key, cluster, err)
}
if res != controllerutil.OperationResultNone {
_, _ = fmt.Fprintf(writer, "%s %s %s in %s.\n", kind, key, res, cluster)
}
}
binding := p.GetRoleBinding(subs)
kind, key := "ClusterRoleBinding", binding.GetName()
if binding.GetNamespace() != "" {
kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName()
}
switch bindingObj := binding.(type) {
case *rbacv1.RoleBinding:
obj := &rbacv1.RoleBinding{}
if err := cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
if options.replace {
bindingObj.Subjects = obj.Subjects
} else {
bindingObj.Subjects = mergeSubjects(bindingObj.Subjects, obj.Subjects)
}
}
case *rbacv1.ClusterRoleBinding:
obj := &rbacv1.ClusterRoleBinding{}
if err := cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
if options.replace {
bindingObj.Subjects = obj.Subjects
} else {
bindingObj.Subjects = mergeSubjects(bindingObj.Subjects, obj.Subjects)
}
}
}
res, err := utils.CreateOrUpdate(_ctx, cli, binding)
if err != nil {
return fmt.Errorf("failed to create/update %s %s in %s: %w", kind, key, cluster, err)
}
_, _ = fmt.Fprintf(writer, "%s %s %s in %s.\n", kind, key, res, cluster)
}
return nil
}
// RevokePrivileges revoke privileges (notice that the revoking process only deletes bond subject in the
// RoleBinding/ClusterRoleBinding, it does not ensure the identity's other related privileges are removed to
// prevent identity from accessing)
func RevokePrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer, optionFuncs ...func(*opts)) error {
var options = &opts{}
for _, fc := range optionFuncs {
fc(options)
}
subs := identity.Subjects()
if len(subs) == 0 {
return fmt.Errorf("failed to find RBAC subjects in identity")
}
for _, p := range privileges {
cluster := p.GetCluster()
_ctx := multicluster.ContextWithClusterName(ctx, cluster)
binding := p.GetRoleBinding(subs)
kind, key := "ClusterRoleBinding", binding.GetName()
if binding.GetNamespace() != "" {
kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName()
}
var err error
remove := false
var toDel client.Object
switch bindingObj := binding.(type) {
case *rbacv1.RoleBinding:
obj := &rbacv1.RoleBinding{}
if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
remove = len(bindingObj.Subjects) == 0
toDel = obj
}
case *rbacv1.ClusterRoleBinding:
obj := &rbacv1.ClusterRoleBinding{}
if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
remove = len(bindingObj.Subjects) == 0
toDel = obj
}
}
if err != nil {
if !kerrors.IsNotFound(err) {
return fmt.Errorf("failed to fetch %s %s in cluster %s: %w", kind, key, cluster, err)
}
return nil
}
if remove {
if err = cli.Delete(_ctx, toDel); err != nil {
return fmt.Errorf("failed to delete %s %s in cluster %s: %w", kind, key, cluster, err)
}
} else {
res, err := utils.CreateOrUpdate(_ctx, cli, binding)
if err != nil {
return fmt.Errorf("failed to update %s %s in cluster %s: %w", kind, key, cluster, err)
}
_, _ = fmt.Fprintf(writer, "%s %s %s in cluster %s.\n", kind, key, res, cluster)
}
}
return nil
}