mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Feat: vela auth grant-privileges (#3943)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
@@ -147,4 +147,7 @@ subjects:
|
||||
- kind: Group
|
||||
name: cluster-gateway-accessor
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: Group
|
||||
name: kubevela:client
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{ end }}
|
||||
@@ -111,3 +111,18 @@ func (identity *Identity) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subjects return rbac subjects
|
||||
func (identity *Identity) Subjects() []rbacv1.Subject {
|
||||
var subs []rbacv1.Subject
|
||||
if identity.User != "" {
|
||||
subs = append(subs, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: identity.User})
|
||||
}
|
||||
for _, group := range identity.Groups {
|
||||
subs = append(subs, rbacv1.Subject{Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group})
|
||||
}
|
||||
if identity.ServiceAccount != "" {
|
||||
subs = append(subs, rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: identity.ServiceAccount, Namespace: identity.ServiceAccountNamespace})
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
@@ -18,17 +18,23 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gosuri/uitable/util/wordwrap"
|
||||
"github.com/xlab/treeprint"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
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"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/parallel"
|
||||
)
|
||||
@@ -216,3 +222,137 @@ func PrettyPrintPrivileges(identity *Identity, privilegesMap map[string][]Privil
|
||||
}
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
if p.Namespace == "" {
|
||||
binding = &rbacv1.ClusterRoleBinding{
|
||||
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: KubeVelaReaderRoleName},
|
||||
Subjects: subs,
|
||||
}
|
||||
} else {
|
||||
binding = &rbacv1.RoleBinding{
|
||||
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: KubeVelaWriterRoleName},
|
||||
Subjects: subs,
|
||||
}
|
||||
binding.SetNamespace(p.Namespace)
|
||||
}
|
||||
if p.ReadOnly {
|
||||
binding.SetName(p.Prefix + KubeVelaReaderRoleName + ":binding")
|
||||
} else {
|
||||
binding.SetName(p.Prefix + KubeVelaWriterRoleName + ":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
|
||||
}
|
||||
|
||||
// GrantPrivileges grant privileges to identity
|
||||
func GrantPrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer) error {
|
||||
subs := identity.Subjects()
|
||||
if len(subs) == 0 {
|
||||
return fmt.Errorf("failed to find RBAC subjects in identity")
|
||||
}
|
||||
for _, p := range privileges {
|
||||
_ctx := multicluster.ContextWithClusterName(ctx, p.GetCluster())
|
||||
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: %w", kind, key, err)
|
||||
}
|
||||
if res != controllerutil.OperationResultNone {
|
||||
_, _ = fmt.Fprintf(writer, "%s %s %s.\n", kind, key, res)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
bindingObj.Subjects = mergeSubjects(bindingObj.Subjects, obj.Subjects)
|
||||
}
|
||||
case *rbacv1.ClusterRoleBinding:
|
||||
obj := &rbacv1.ClusterRoleBinding{}
|
||||
if err := cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
|
||||
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: %w", kind, key, err)
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "%s %s %s.\n", kind, key, res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ import (
|
||||
|
||||
// GetTokenSubject extract the subject field from the jwt token
|
||||
func GetTokenSubject(token string) (string, error) {
|
||||
claims := jwt.MapClaims{}
|
||||
if _, err := jwt.ParseWithClaims(token, claims, nil); err != nil {
|
||||
return "", err
|
||||
claims, sub := jwt.MapClaims{}, ""
|
||||
_, err := jwt.ParseWithClaims(token, claims, nil)
|
||||
if len(claims) > 0 {
|
||||
sub, _ = claims["sub"].(string)
|
||||
}
|
||||
sub, _ := claims["sub"].(string)
|
||||
return sub, nil
|
||||
return sub, err
|
||||
}
|
||||
|
||||
// GetCertificateSubject extract Subject from Certificate
|
||||
|
||||
@@ -18,15 +18,18 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
velaerr "github.com/oam-dev/kubevela/pkg/utils/errors"
|
||||
@@ -128,12 +131,45 @@ func GetCertificateCommonNameAndOrganizationsFromConfig(cfg *rest.Config) (strin
|
||||
return name.CommonName, name.Organization
|
||||
}
|
||||
|
||||
// GetUserInfoFromConfig extract UserInfo from KubeConfig
|
||||
func GetUserInfoFromConfig(cfg *rest.Config) *authv1.UserInfo {
|
||||
if sub := GetServiceAccountSubjectFromConfig(cfg); sub != "" {
|
||||
return &authv1.UserInfo{Username: sub}
|
||||
}
|
||||
if cn, orgs := GetCertificateCommonNameAndOrganizationsFromConfig(cfg); cn != "" {
|
||||
return &authv1.UserInfo{Username: cn, Groups: orgs}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoSetSelfImpersonationInConfig set impersonate username and group to the identity in the original rest config
|
||||
func AutoSetSelfImpersonationInConfig(cfg *rest.Config) {
|
||||
if sub := GetServiceAccountSubjectFromConfig(cfg); sub != "" {
|
||||
cfg.Impersonate.UserName = sub
|
||||
} else if cn, orgs := GetCertificateCommonNameAndOrganizationsFromConfig(cfg); cn != "" {
|
||||
cfg.Impersonate.UserName = cn
|
||||
cfg.Impersonate.Groups = append(cfg.Impersonate.Groups, orgs...)
|
||||
if userInfo := GetUserInfoFromConfig(cfg); userInfo != nil {
|
||||
cfg.Impersonate.UserName = userInfo.Username
|
||||
cfg.Impersonate.Groups = append(cfg.Impersonate.Groups, userInfo.Groups...)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOrUpdate create or update a kubernetes object
|
||||
func CreateOrUpdate(ctx context.Context, cli client.Client, obj client.Object) (controllerutil.OperationResult, error) {
|
||||
bs, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return controllerutil.OperationResultNone, err
|
||||
}
|
||||
return controllerutil.CreateOrUpdate(ctx, cli, obj, func() error {
|
||||
createTimestamp := obj.GetCreationTimestamp()
|
||||
resourceVersion := obj.GetResourceVersion()
|
||||
deletionTimestamp := obj.GetDeletionTimestamp()
|
||||
generation := obj.GetGeneration()
|
||||
managedFields := obj.GetManagedFields()
|
||||
if e := json.Unmarshal(bs, obj); err != nil {
|
||||
return e
|
||||
}
|
||||
obj.SetCreationTimestamp(createTimestamp)
|
||||
obj.SetResourceVersion(resourceVersion)
|
||||
obj.SetDeletionTimestamp(deletionTimestamp)
|
||||
obj.SetGeneration(generation)
|
||||
obj.SetManagedFields(managedFields)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
@@ -34,11 +35,13 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// MutatingHandler adding user info to application annotations
|
||||
type MutatingHandler struct {
|
||||
Decoder *admission.Decoder
|
||||
skipUsers []string
|
||||
Decoder *admission.Decoder
|
||||
}
|
||||
|
||||
var _ admission.Handler = &MutatingHandler{}
|
||||
@@ -49,7 +52,7 @@ func (h *MutatingHandler) Handle(ctx context.Context, req admission.Request) adm
|
||||
return admission.Patched("")
|
||||
}
|
||||
|
||||
if slices.Contains(req.UserInfo.Groups, common.Group) {
|
||||
if slices.Contains(req.UserInfo.Groups, common.Group) || slices.Contains(h.skipUsers, req.UserInfo.Username) {
|
||||
return admission.Patched("")
|
||||
}
|
||||
|
||||
@@ -61,6 +64,7 @@ func (h *MutatingHandler) Handle(ctx context.Context, req admission.Request) adm
|
||||
if metav1.HasAnnotation(app.ObjectMeta, oam.AnnotationApplicationServiceAccountName) {
|
||||
return admission.Errored(http.StatusBadRequest, errors.New("service-account annotation is not permitted when authentication enabled"))
|
||||
}
|
||||
klog.Infof("[ApplicationMutatingHandler] Setting UserInfo into Application, UserInfo: %v, Application: %s/%s", req.UserInfo, app.GetNamespace(), app.GetName())
|
||||
auth.SetUserInfoInAnnotation(&app.ObjectMeta, req.UserInfo)
|
||||
|
||||
bs, err := json.Marshal(app)
|
||||
@@ -81,5 +85,12 @@ func (h *MutatingHandler) InjectDecoder(d *admission.Decoder) error {
|
||||
// RegisterMutatingHandler will register component mutation handler to the webhook
|
||||
func RegisterMutatingHandler(mgr manager.Manager) {
|
||||
server := mgr.GetWebhookServer()
|
||||
server.Register("/mutating-core-oam-dev-v1beta1-applications", &webhook.Admission{Handler: &MutatingHandler{}})
|
||||
handler := &MutatingHandler{}
|
||||
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.ControllerAutoImpersonation) {
|
||||
if userInfo := utils.GetUserInfoFromConfig(mgr.GetConfig()); userInfo != nil {
|
||||
klog.Infof("[ApplicationMutatingHandler] add skip user %s", userInfo.Username)
|
||||
handler.skipUsers = []string{userInfo.Username}
|
||||
}
|
||||
}
|
||||
server.Register("/mutating-core-oam-dev-v1beta1-applications", &webhook.Admission{Handler: handler})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
@@ -34,6 +37,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
)
|
||||
|
||||
@@ -48,6 +52,7 @@ func AuthCommandGroup(f velacmd.Factory, streams util.IOStreams) *cobra.Command
|
||||
}
|
||||
cmd.AddCommand(NewGenKubeConfigCommand(f, streams))
|
||||
cmd.AddCommand(NewListPrivilegesCommand(f, streams))
|
||||
cmd.AddCommand(NewGrantPrivilegesCommand(f, streams))
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -201,8 +206,8 @@ func (opt *ListPrivilegesOptions) Validate(f velacmd.Factory, cmd *cobra.Command
|
||||
}
|
||||
|
||||
// Run .
|
||||
func (opt *ListPrivilegesOptions) Run(f velacmd.Factory) error {
|
||||
ctx := context.Background()
|
||||
func (opt *ListPrivilegesOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
ctx := cmd.Context()
|
||||
m, err := auth.ListPrivileges(ctx, f.Client(), opt.Clusters, &opt.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -270,7 +275,7 @@ func NewListPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Complete(f, cmd)
|
||||
cmdutil.CheckErr(o.Validate(f, cmd))
|
||||
cmdutil.CheckErr(o.Run(f))
|
||||
cmdutil.CheckErr(o.Run(f, cmd))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user to list privileges.")
|
||||
@@ -293,3 +298,174 @@ func NewListPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.
|
||||
WithResponsiveWriter().
|
||||
Build()
|
||||
}
|
||||
|
||||
// GrantPrivilegesOptions options for grant privileges
|
||||
type GrantPrivilegesOptions struct {
|
||||
auth.Identity
|
||||
KubeConfig string
|
||||
GrantNamespaces []string
|
||||
GrantClusters []string
|
||||
ReadOnly bool
|
||||
CreateNamespace bool
|
||||
|
||||
util.IOStreams
|
||||
}
|
||||
|
||||
// Complete .
|
||||
func (opt *GrantPrivilegesOptions) Complete(f velacmd.Factory, cmd *cobra.Command) {
|
||||
if opt.KubeConfig != "" {
|
||||
identity, err := auth.ReadIdentityFromKubeConfig(opt.KubeConfig)
|
||||
cmdutil.CheckErr(err)
|
||||
opt.Identity = *identity
|
||||
opt.Identity.Groups = nil
|
||||
}
|
||||
if opt.Identity.ServiceAccount != "" {
|
||||
opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd)
|
||||
}
|
||||
opt.Regularize()
|
||||
if len(opt.GrantClusters) == 0 {
|
||||
opt.GrantClusters = []string{types.ClusterLocalName}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate .
|
||||
func (opt *GrantPrivilegesOptions) Validate(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
if opt.User == "" && len(opt.Groups) == 0 && opt.ServiceAccount == "" {
|
||||
return fmt.Errorf("at least one idenity (user/group/serviceaccount) should be set")
|
||||
}
|
||||
for _, cluster := range opt.GrantClusters {
|
||||
if _, err := prismclusterv1alpha1.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil {
|
||||
return fmt.Errorf("failed to find cluster %s: %w", cluster, err)
|
||||
}
|
||||
if !opt.CreateNamespace {
|
||||
for _, namespace := range opt.GrantNamespaces {
|
||||
if err := f.Client().Get(multicluster.ContextWithClusterName(cmd.Context(), cluster), apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil {
|
||||
return fmt.Errorf("failed to find namespace %s in cluster %s: %w", namespace, cluster, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run .
|
||||
func (opt *GrantPrivilegesOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
ctx := cmd.Context()
|
||||
if opt.CreateNamespace {
|
||||
for _, cluster := range opt.GrantClusters {
|
||||
if _, err := prismclusterv1alpha1.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil {
|
||||
return fmt.Errorf("failed to find cluster %s: %w", cluster, err)
|
||||
}
|
||||
for _, namespace := range opt.GrantNamespaces {
|
||||
_ctx := multicluster.ContextWithClusterName(cmd.Context(), cluster)
|
||||
ns := &corev1.Namespace{}
|
||||
if err := f.Client().Get(_ctx, apitypes.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
ns.SetName(namespace)
|
||||
if err = f.Client().Create(_ctx, ns); err != nil {
|
||||
return fmt.Errorf("failed to create namespace %s in cluster %s: %w", namespace, cluster, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to find namespace %s in cluster %s: %w", namespace, cluster, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var privileges []auth.PrivilegeDescription
|
||||
for _, cluster := range opt.GrantClusters {
|
||||
for _, namespace := range opt.GrantNamespaces {
|
||||
privileges = append(privileges, &auth.ScopedPrivilege{Cluster: cluster, Namespace: namespace, ReadOnly: opt.ReadOnly})
|
||||
}
|
||||
}
|
||||
if err := auth.GrantPrivileges(ctx, f.Client(), privileges, &opt.Identity, opt.IOStreams.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(opt.IOStreams.Out, "Privileges granted.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
grantPrivilegesLong = templates.LongDesc(i18n.T(`
|
||||
Grant privileges for user
|
||||
|
||||
Grant privileges to user/group/serviceaccount. By using --for-namespace and --for-cluster,
|
||||
you can grant all read/write privileges for all resources in the specified namespace and
|
||||
cluster. If --for-namespace is not set, the privileges will be granted cluster-wide.
|
||||
|
||||
Setting --create-namespace will automatically create namespace if the namespace of the
|
||||
granted privilege does not exists. By default, this flag is not enabled and errors will be
|
||||
returned if the namespace is not found in the corresponding cluster.
|
||||
|
||||
Setting --readonly will only grant read privileges for all resources in the destination. This
|
||||
can be useful if you want to give somebody the privileges to view resources but do not want to
|
||||
allow them to edit any resource.
|
||||
|
||||
If multiple identity information are set, all the identity information will be bond to the
|
||||
intended privileges respectively.
|
||||
|
||||
If --kubeconfig is set, the user/serviceaccount information in the kubeconfig will be used as
|
||||
the identity to grant privileges. Groups will be ignored.`))
|
||||
|
||||
grantPrivilegesExample = templates.Examples(i18n.T(`
|
||||
# Grant privileges for User alice in the namespace demo of the control plane
|
||||
vela auth grant-privileges --user alice --for-namespace demo
|
||||
|
||||
# Grant privileges for User alice in the namespace demo in cluster-1, create demo namespace if not exist
|
||||
vela auth grant-privileges --user alice --for-namespace demo --for-cluster cluster-1 --create-namespace
|
||||
|
||||
# Grant cluster-scoped privileges for Group org:dev-team in the control plane
|
||||
vela auth grant-privileges --group org:dev-team
|
||||
|
||||
# Grant privileges for Group org:dev-team and org:test-team in the namespace test on the control plane and managed cluster example-cluster
|
||||
vela auth grant-privileges --group org:dev-team --group org:test-team --for-namespace test --for-cluster local --for-cluster example-cluster
|
||||
|
||||
# Grant read privileges for ServiceAccount observer in test namespace on the control plane
|
||||
vela auth grant-privileges --serviceaccount observer -n test --for-namespace test --readonly
|
||||
|
||||
# Grant privileges for identity in kubeconfig in cluster-1
|
||||
vela auth grant-privileges --kubeconfig ./example.kubeconfig --for-cluster cluster-1`))
|
||||
)
|
||||
|
||||
// NewGrantPrivilegesCommand grant privileges to given identity
|
||||
func NewGrantPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
|
||||
o := &GrantPrivilegesOptions{IOStreams: streams}
|
||||
cmd := &cobra.Command{
|
||||
Use: "grant-privileges",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Grant privileges for user/group/serviceaccount"),
|
||||
Long: grantPrivilegesLong,
|
||||
Example: grantPrivilegesExample,
|
||||
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))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user to grant privileges.")
|
||||
cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The group to grant privileges.")
|
||||
cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount to grant privileges.")
|
||||
cmd.Flags().StringVarP(&o.KubeConfig, "kubeconfig", "", o.KubeConfig, "The kubeconfig to grant privileges. If set, it will override all the other identity flags.")
|
||||
cmd.Flags().StringSliceVarP(&o.GrantClusters, "for-cluster", "", o.GrantClusters, "The clusters privileges to grant. If empty, the control plane will be used.")
|
||||
cmd.Flags().StringSliceVarP(&o.GrantNamespaces, "for-namespace", "", o.GrantNamespaces, "The namespaces privileges to grant. If empty, cluster-scoped privileges will be granted.")
|
||||
cmd.Flags().BoolVarP(&o.ReadOnly, "readonly", "", o.ReadOnly, "If set, only read privileges of resources will be granted. Otherwise, read/write privileges will be granted.")
|
||||
cmd.Flags().BoolVarP(&o.CreateNamespace, "create-namespace", "", o.CreateNamespace, "If set, non-exist namespace will be created automatically.")
|
||||
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)
|
||||
}))
|
||||
|
||||
return velacmd.NewCommandBuilder(f, cmd).
|
||||
WithNamespaceFlag(velacmd.UsageOption("The namespace of the serviceaccount. This flag only works when `--serviceaccount` is set.")).
|
||||
WithStreams(streams).
|
||||
WithResponsiveWriter().
|
||||
Build()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
||||
@@ -151,7 +150,7 @@ func (opt *KubeApplyOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
if err = copiedObj.UnmarshalJSON(bs); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := controllerutil.CreateOrPatch(ctx, f.Client(), copiedObj, nil)
|
||||
res, err := utils.CreateOrUpdate(ctx, f.Client(), copiedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,14 @@ limitations under the License.
|
||||
package e2e_multicluster_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/api/core/v1"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
)
|
||||
|
||||
var _ = Describe("Test multicluster Auth commands", func() {
|
||||
@@ -58,6 +64,28 @@ var _ = Describe("Test multicluster Auth commands", func() {
|
||||
Expect(outputs).Should(ContainSubstring("cluster-admin"))
|
||||
})
|
||||
|
||||
It("Test vela grant-privileges for user and create namespace", func() {
|
||||
_, err := execCommand("auth", "grant-privileges", "--user", "alice", "--for-namespace", "alice", "--create-namespace", "--for-cluster", "local", "--for-cluster", WorkerClusterName)
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(k8sClient.Get(multicluster.ContextWithClusterName(context.Background(), "local"), apitypes.NamespacedName{Name: "alice"}, &metav1.Namespace{})).Should(Succeed())
|
||||
Expect(k8sClient.Get(multicluster.ContextWithClusterName(context.Background(), WorkerClusterName), apitypes.NamespacedName{Name: "alice"}, &metav1.Namespace{})).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test vela grant-privileges for groups and readonly", func() {
|
||||
_, err := execCommand("auth", "grant-privileges", "--group", "kubevela:dev-team", "--group", "kubevela:test-team", "--readonly")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test vela grant-privileges for serviceaccount", func() {
|
||||
_, err := execCommand("auth", "grant-privileges", "--serviceaccount", "default", "-n", "default", "--for-namespace", "default")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test vela grant-privileges for kubeconfig with cluster-scoped privileges", func() {
|
||||
_, err := execCommand("auth", "grant-privileges", "--kubeconfig", WorkerClusterKubeConfigPath, "--for-cluster", WorkerClusterName)
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user