feat: kubesphere 4.0 (#6115)

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

---------

Signed-off-by: ci-bot <ci-bot@kubesphere.io>
Co-authored-by: ks-ci-bot <ks-ci-bot@example.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -1,486 +1,293 @@
/*
Copyright 2019 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package user
import (
"context"
"fmt"
"reflect"
"strings"
"time"
clusterpredicate "kubesphere.io/kubesphere/pkg/controller/cluster/predicate"
"k8s.io/apimachinery/pkg/api/errors"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/models/kubeconfig"
"kubesphere.io/kubesphere/pkg/utils/clusterclient"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
"github.com/go-logr/logr"
rbacv1 "k8s.io/api/rbac/v1"
"golang.org/x/crypto/bcrypt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
typesv1beta1 "kubesphere.io/api/types/v1beta1"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"k8s.io/apimachinery/pkg/util/validation"
utilwait "k8s.io/apimachinery/pkg/util/wait"
"golang.org/x/crypto/bcrypt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/constants"
modelsdevops "kubesphere.io/kubesphere/pkg/models/devops"
"kubesphere.io/kubesphere/pkg/models/kubeconfig"
"kubesphere.io/kubesphere/pkg/simple/client/devops"
ldapclient "kubesphere.io/kubesphere/pkg/simple/client/ldap"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
clusterutils "kubesphere.io/kubesphere/pkg/controller/cluster/utils"
)
const (
// SuccessSynced is used as part of the Event 'reason' when a Foo is synced
successSynced = "Synced"
failedSynced = "FailedSync"
// is synced successfully
messageResourceSynced = "User synced successfully"
controllerName = "user-controller"
// user finalizer
finalizer = "finalizers.kubesphere.io/users"
interval = time.Second
timeout = 15 * time.Second
syncFailMessage = "Failed to sync: %s"
controllerName = "user"
finalizer = "finalizers.kubesphere.io/users"
)
var _ kscontroller.Controller = &Reconciler{}
var _ kscontroller.ClusterSelector = &Reconciler{}
var _ reconcile.Reconciler = &Reconciler{}
// Reconciler reconciles a User object
type Reconciler struct {
client.Client
KubeconfigClient kubeconfig.Interface
MultiClusterEnabled bool
DevopsClient devops.Interface
LdapClient ldapclient.Interface
AuthenticationOptions *authentication.Options
Logger logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
MaxConcurrentReconciles int
authenticationOptions *authentication.Options
logger logr.Logger
recorder record.EventRecorder
clusterClient clusterclient.Interface
}
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.Client == nil {
r.Client = mgr.GetClient()
}
if r.Logger.GetSink() == nil {
r.Logger = ctrl.Log.WithName("controllers").WithName(controllerName)
}
if r.Scheme == nil {
r.Scheme = mgr.GetScheme()
}
if r.Recorder == nil {
r.Recorder = mgr.GetEventRecorderFor(controllerName)
}
if r.MaxConcurrentReconciles <= 0 {
r.MaxConcurrentReconciles = 1
}
func (r *Reconciler) Enabled(clusterRole string) bool {
return strings.EqualFold(clusterRole, string(clusterv1alpha1.ClusterRoleHost))
}
func (r *Reconciler) Name() string {
return controllerName
}
func (r *Reconciler) SetupWithManager(mgr *kscontroller.Manager) error {
r.authenticationOptions = mgr.AuthenticationOptions
r.Client = mgr.GetClient()
r.logger = mgr.GetLogger().WithName(controllerName)
r.recorder = mgr.GetEventRecorderFor(controllerName)
r.clusterClient = mgr.ClusterClient
return ctrl.NewControllerManagedBy(mgr).
Named(controllerName).
WithOptions(controller.Options{
MaxConcurrentReconciles: r.MaxConcurrentReconciles,
}).
For(&iamv1alpha2.User{}).
WithOptions(controller.Options{MaxConcurrentReconciles: 2}).
For(&iamv1beta1.User{}).
Watches(&iamv1beta1.GlobalRoleBinding{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request {
var result []reconcile.Request
if username := object.GetLabels()[iamv1beta1.UserReferenceLabel]; username != "" {
result = append(result, reconcile.Request{NamespacedName: types.NamespacedName{Name: username}})
}
return result
}), builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
return e.ObjectOld.GetLabels()[iamv1beta1.UserReferenceLabel] != e.ObjectNew.GetLabels()[iamv1beta1.UserReferenceLabel]
},
CreateFunc: func(e event.CreateEvent) bool {
return e.Object.GetLabels()[iamv1beta1.UserReferenceLabel] != ""
},
DeleteFunc: func(e event.DeleteEvent) bool {
return e.Object.GetLabels()[iamv1beta1.UserReferenceLabel] != ""
},
})).
Watches(
&clusterv1alpha1.Cluster{},
handler.EnqueueRequestsFromMapFunc(r.mapper),
builder.WithPredicates(clusterpredicate.ClusterStatusChangedPredicate{}),
).
Complete(r)
}
func (r *Reconciler) mapper(ctx context.Context, o client.Object) []reconcile.Request {
cluster := o.(*clusterv1alpha1.Cluster)
var requests []reconcile.Request
if !clusterutils.IsClusterReady(cluster) {
return requests
}
users := &iamv1beta1.UserList{}
if err := r.List(ctx, users); err != nil {
r.logger.Error(err, "failed to list users")
return requests
}
for _, user := range users.Items {
requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: user.Name}})
}
return requests
}
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
logger := r.Logger.WithValues("user", req.NamespacedName)
user := &iamv1alpha2.User{}
err := r.Get(ctx, req.NamespacedName, user)
if err != nil {
logger := r.logger.WithValues("user", req.NamespacedName)
ctx = klog.NewContext(ctx, logger)
user := &iamv1beta1.User{}
if err := r.Get(ctx, req.NamespacedName, user); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if user.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object.
if !sliceutil.HasString(user.Finalizers, finalizer) {
user.ObjectMeta.Finalizers = append(user.ObjectMeta.Finalizers, finalizer)
if err = r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
logger.Error(err, "failed to update user")
return ctrl.Result{}, err
}
if !controllerutil.ContainsFinalizer(user, finalizer) {
expected := user.DeepCopy()
controllerutil.AddFinalizer(expected, finalizer)
return ctrl.Result{}, r.Patch(ctx, expected, client.MergeFrom(user))
}
} else {
// The object is being deleted
if sliceutil.HasString(user.ObjectMeta.Finalizers, finalizer) {
// we do not need to delete the user from ldapServer when ldapClient is nil
if r.LdapClient != nil {
if err = r.waitForDeleteFromLDAP(user.Name); err != nil {
// ignore timeout error
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
}
if controllerutil.ContainsFinalizer(user, finalizer) {
if err := r.deleteRelatedResources(ctx, user); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to delete related resources: %s", err)
}
if err = r.deleteRoleBindings(ctx, user); err != nil {
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
if err := r.deleteRelatedResourcesInMemberCluster(ctx, user); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to delete related resources: %s", err)
}
if err = r.deleteGroupBindings(ctx, user); err != nil {
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
if r.DevopsClient != nil {
// unassign jenkins role, unassign multiple times is allowed
if err = r.waitForUnassignDevOpsAdminRole(user); err != nil {
// ignore timeout error
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
}
}
if err = r.deleteLoginRecords(ctx, user); err != nil {
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
// remove our finalizer from the list and update it.
user.Finalizers = sliceutil.RemoveString(user.ObjectMeta.Finalizers, func(item string) bool {
return item == finalizer
})
if err = r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
klog.Error(err)
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
controllerutil.RemoveFinalizer(user, finalizer)
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return ctrl.Result{}, err
}
}
// Our finalizer has finished, so the reconciler can do nothing.
return ctrl.Result{}, nil
}
if err := r.updateGlobalRoleAnnotation(ctx, user); err != nil {
return reconcile.Result{}, err
}
if err := r.encryptPassword(ctx, user); err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileUserStatus(ctx, user); err != nil {
return ctrl.Result{}, err
}
if err := r.multiClusterSync(ctx, user); err != nil {
return ctrl.Result{}, err
}
// synchronization through kubefed-controller when multi cluster is enabled
if r.MultiClusterEnabled {
if err = r.multiClusterSync(ctx, user); err != nil {
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
}
// we do not need to sync ldap info when ldapClient is nil
if r.LdapClient != nil {
// ignore errors if timeout
if err = r.waitForSyncToLDAP(user); err != nil {
// ignore timeout error
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
}
}
// update user status if not managed by kubefed
managedByKubefed := user.Labels[constants.KubefedManagedLabel] == "true"
if !managedByKubefed {
if err = r.encryptPassword(ctx, user); err != nil {
klog.Error(err)
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
if err = r.syncUserStatus(ctx, user); err != nil {
klog.Error(err)
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
}
if r.KubeconfigClient != nil {
// ensure user KubeconfigClient configmap is created
if err = r.KubeconfigClient.CreateKubeConfig(user); err != nil {
klog.Error(err)
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
return ctrl.Result{}, err
}
}
if r.DevopsClient != nil {
// assign jenkins role after user create, assign multiple times is allowed
// used as logged-in users can do anything
if err = r.waitForAssignDevOpsAdminRole(user); err != nil {
// ignore timeout error
r.Recorder.Event(user, corev1.EventTypeWarning, failedSynced, fmt.Sprintf(syncFailMessage, err))
}
}
r.Recorder.Event(user, corev1.EventTypeNormal, successSynced, messageResourceSynced)
r.recorder.Event(user, corev1.EventTypeNormal, kscontroller.Synced, kscontroller.MessageResourceSynced)
// block user for AuthenticateRateLimiterDuration duration, after that put it back to the queue to unblock
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
return ctrl.Result{Requeue: true, RequeueAfter: r.AuthenticationOptions.AuthenticateRateLimiterDuration}, nil
if user.Status.State == iamv1beta1.UserAuthLimitExceeded {
return ctrl.Result{Requeue: true, RequeueAfter: r.authenticationOptions.AuthenticateRateLimiterDuration}, nil
}
return ctrl.Result{}, nil
}
// encryptPassword Encrypt and update the user password
func (r *Reconciler) encryptPassword(ctx context.Context, user *iamv1alpha2.User) error {
// password is not empty and not encrypted
func (r *Reconciler) encryptPassword(ctx context.Context, user *iamv1beta1.User) error {
// password must be encrypted if not empty
if user.Spec.EncryptedPassword != "" && !isEncrypted(user.Spec.EncryptedPassword) {
password, err := encrypt(user.Spec.EncryptedPassword)
encryptedPassword, err := encrypt(user.Spec.EncryptedPassword)
if err != nil {
klog.Error(err)
return err
}
user.Spec.EncryptedPassword = password
user.Spec.EncryptedPassword = encryptedPassword
if user.Annotations == nil {
user.Annotations = make(map[string]string)
}
user.Annotations[iamv1alpha2.LastPasswordChangeTimeAnnotation] = time.Now().UTC().Format(time.RFC3339)
user.Annotations[iamv1beta1.LastPasswordChangeTimeAnnotation] = time.Now().UTC().Format(time.RFC3339)
// ensure plain text password won't be kept anywhere
delete(user.Annotations, corev1.LastAppliedConfigAnnotation)
err = r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err = r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
return nil
}
func (r *Reconciler) ensureNotControlledByKubefed(ctx context.Context, user *iamv1alpha2.User) error {
if user.Labels[constants.KubefedManagedLabel] != "false" {
if user.Labels == nil {
user.Labels = make(map[string]string, 0)
func (r *Reconciler) deleteRelatedResources(ctx context.Context, user *iamv1beta1.User) error {
if err := r.DeleteAllOf(ctx, &iamv1beta1.LoginRecord{}, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
if err := r.DeleteAllOf(ctx, &iamv1beta1.GlobalRoleBinding{}, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
if err := r.DeleteAllOf(ctx, &iamv1beta1.WorkspaceRoleBinding{}, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
return nil
}
func (r *Reconciler) deleteRelatedResourcesInMemberCluster(ctx context.Context, user *iamv1beta1.User) error {
clusters, err := r.clusterClient.ListClusters(ctx)
if err != nil {
return fmt.Errorf("failed to list clusters: %s", err)
}
var notReadyClusters []string
for _, cluster := range clusters {
// skip if the cluster is not ready
if !clusterutils.IsClusterReady(&cluster) {
notReadyClusters = append(notReadyClusters, cluster.Name)
continue
}
user.Labels[constants.KubefedManagedLabel] = "false"
err := r.Update(ctx, user, &client.UpdateOptions{})
clusterClient, err := r.clusterClient.GetRuntimeClient(cluster.Name)
if err != nil {
klog.Error(err)
return fmt.Errorf("failed to get cluster client: %s", err)
}
if err = clusterClient.DeleteAllOf(ctx, &iamv1beta1.ClusterRoleBinding{}, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
}
return nil
}
func (r *Reconciler) multiClusterSync(ctx context.Context, user *iamv1alpha2.User) error {
if err := r.ensureNotControlledByKubefed(ctx, user); err != nil {
klog.Error(err)
return err
}
federatedUser := &typesv1beta1.FederatedUser{}
err := r.Get(ctx, types.NamespacedName{Name: user.Name}, federatedUser)
if err != nil {
if errors.IsNotFound(err) {
return r.createFederatedUser(ctx, user)
roleBindings := &iamv1beta1.RoleBindingList{}
if err = clusterClient.List(ctx, roleBindings, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
return err
}
if !reflect.DeepEqual(federatedUser.Spec.Template.Spec, user.Spec) ||
!reflect.DeepEqual(federatedUser.Spec.Template.Status, user.Status) ||
!reflect.DeepEqual(federatedUser.Spec.Template.Labels, user.Labels) {
federatedUser.Spec.Template.Labels = user.Labels
federatedUser.Spec.Template.Spec = user.Spec
federatedUser.Spec.Template.Status = user.Status
return r.Update(ctx, federatedUser, &client.UpdateOptions{})
}
return nil
}
func (r *Reconciler) createFederatedUser(ctx context.Context, user *iamv1alpha2.User) error {
federatedUser := &typesv1beta1.FederatedUser{
ObjectMeta: metav1.ObjectMeta{
Name: user.Name,
},
Spec: typesv1beta1.FederatedUserSpec{
Template: typesv1beta1.UserTemplate{
ObjectMeta: metav1.ObjectMeta{
Labels: user.Labels,
},
Spec: user.Spec,
Status: user.Status,
},
Placement: typesv1beta1.GenericPlacementFields{
ClusterSelector: &metav1.LabelSelector{},
},
},
}
// must bind user lifecycle
err := controllerutil.SetControllerReference(user, federatedUser, scheme.Scheme)
if err != nil {
return err
}
err = r.Create(ctx, federatedUser, &client.CreateOptions{})
if err != nil {
if errors.IsAlreadyExists(err) {
return nil
}
return err
}
return nil
}
func (r *Reconciler) waitForAssignDevOpsAdminRole(user *iamv1alpha2.User) error {
err := utilwait.PollImmediate(interval, timeout, func() (done bool, err error) {
if err := r.DevopsClient.AssignGlobalRole(modelsdevops.JenkinsAdminRoleName, user.Name); err != nil {
klog.Error(err)
return false, err
}
return true, nil
})
return err
}
func (r *Reconciler) waitForUnassignDevOpsAdminRole(user *iamv1alpha2.User) error {
err := utilwait.PollImmediate(interval, timeout, func() (done bool, err error) {
if err := r.DevopsClient.UnAssignGlobalRole(modelsdevops.JenkinsAdminRoleName, user.Name); err != nil {
return false, err
}
return true, nil
})
return err
}
func (r *Reconciler) waitForSyncToLDAP(user *iamv1alpha2.User) error {
if isEncrypted(user.Spec.EncryptedPassword) {
return nil
}
err := utilwait.PollImmediate(interval, timeout, func() (done bool, err error) {
_, err = r.LdapClient.Get(user.Name)
if err != nil {
if err == ldapclient.ErrUserNotExists {
err = r.LdapClient.Create(user)
if err != nil {
klog.Error(err)
return false, err
for _, roleBinding := range roleBindings.Items {
if err = clusterClient.Delete(ctx, &roleBinding); err != nil {
if errors.IsNotFound(err) {
continue
}
return true, nil
return err
}
klog.Error(err)
return false, err
}
err = r.LdapClient.Update(user)
if err != nil {
klog.Error(err)
return false, err
}
return true, nil
})
return err
}
func (r *Reconciler) waitForDeleteFromLDAP(username string) error {
err := utilwait.PollImmediate(interval, timeout, func() (done bool, err error) {
err = r.LdapClient.Delete(username)
if err != nil && err != ldapclient.ErrUserNotExists {
klog.Error(err)
return false, err
}
return true, nil
})
return err
}
func (r *Reconciler) deleteGroupBindings(ctx context.Context, user *iamv1alpha2.User) error {
// groupBindings that created by kubeshpere will be deleted directly.
groupBindings := &iamv1alpha2.GroupBinding{}
return r.Client.DeleteAllOf(ctx, groupBindings, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
}
func (r *Reconciler) deleteRoleBindings(ctx context.Context, user *iamv1alpha2.User) error {
if len(user.Name) > validation.LabelValueMaxLength {
// ignore invalid label value error
return nil
}
globalRoleBinding := &iamv1alpha2.GlobalRoleBinding{}
err := r.Client.DeleteAllOf(ctx, globalRoleBinding, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
if err != nil {
return err
}
workspaceRoleBinding := &iamv1alpha2.WorkspaceRoleBinding{}
err = r.Client.DeleteAllOf(ctx, workspaceRoleBinding, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
if err != nil {
return err
}
clusterRoleBinding := &rbacv1.ClusterRoleBinding{}
err = r.Client.DeleteAllOf(ctx, clusterRoleBinding, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
if err != nil {
return err
}
roleBindingList := &rbacv1.RoleBindingList{}
err = r.Client.List(ctx, roleBindingList, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
if err != nil {
return err
}
for _, roleBinding := range roleBindingList.Items {
err = r.Client.Delete(ctx, &roleBinding)
if err != nil {
if err = clusterClient.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: constants.KubeSphereNamespace, Name: fmt.Sprintf(kubeconfig.UserKubeConfigSecretNameFormat, user.Name)}}); err != nil {
if errors.IsNotFound(err) {
continue
}
return err
}
}
if len(notReadyClusters) > 0 {
err = fmt.Errorf("cluster not ready: %s", strings.Join(notReadyClusters, ","))
klog.FromContext(ctx).Error(err, "failed to delete related resources")
r.recorder.Event(user, corev1.EventTypeWarning, kscontroller.SyncFailed, fmt.Sprintf("cluster not ready: %s", strings.Join(notReadyClusters, ",")))
return err
}
return nil
}
func (r *Reconciler) deleteLoginRecords(ctx context.Context, user *iamv1alpha2.User) error {
loginRecord := &iamv1alpha2.LoginRecord{}
return r.Client.DeleteAllOf(ctx, loginRecord, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
}
// syncUserStatus Update the user status
func (r *Reconciler) syncUserStatus(ctx context.Context, user *iamv1alpha2.User) error {
// reconcileUserStatus updates the user status based on various conditions.
func (r *Reconciler) reconcileUserStatus(ctx context.Context, user *iamv1beta1.User) error {
// skip status sync if the user is disabled
if user.Status.State == iamv1alpha2.UserDisabled {
if user.Status.State == iamv1beta1.UserDisabled {
return nil
}
if user.Spec.EncryptedPassword == "" {
if user.Labels[iamv1alpha2.IdentifyProviderLabel] != "" {
// mapped user from other identity provider always active until disabled
if user.Status.State != iamv1alpha2.UserActive {
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserActive,
if user.Labels[iamv1beta1.IdentifyProviderLabel] != "" {
// mapped user from another identity provider always active until disabled
if user.Status.State != iamv1beta1.UserActive {
user.Status = iamv1beta1.UserStatus{
State: iamv1beta1.UserActive,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
err := r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
} else {
// empty password is not allowed for normal user
if user.Status.State != iamv1alpha2.UserDisabled {
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserDisabled,
if user.Status.State != iamv1beta1.UserDisabled {
user.Status = iamv1beta1.UserStatus{
State: iamv1beta1.UserDisabled,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
err := r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
@@ -491,38 +298,33 @@ func (r *Reconciler) syncUserStatus(ctx context.Context, user *iamv1alpha2.User)
// becomes active after password encrypted
if user.Status.State == "" && isEncrypted(user.Spec.EncryptedPassword) {
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserActive,
user.Status = iamv1beta1.UserStatus{
State: iamv1beta1.UserActive,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
err := r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
// blocked user, check if need to unblock user
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
// determine whether there is a requirement to unblock the user who has been blocked.
if user.Status.State == iamv1beta1.UserAuthLimitExceeded {
if user.Status.LastTransitionTime != nil &&
user.Status.LastTransitionTime.Add(r.AuthenticationOptions.AuthenticateRateLimiterDuration).Before(time.Now()) {
user.Status.LastTransitionTime.Add(r.authenticationOptions.AuthenticateRateLimiterDuration).Before(time.Now()) {
// unblock user
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserActive,
user.Status = iamv1beta1.UserStatus{
State: iamv1beta1.UserActive,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
err := r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
return nil
}
}
records := &iamv1alpha2.LoginRecordList{}
// normal user, check user's login records see if we need to block
err := r.List(ctx, records, client.MatchingLabels{iamv1alpha2.UserReferenceLabel: user.Name})
if err != nil {
klog.Error(err)
records := &iamv1beta1.LoginRecordList{}
if err := r.List(ctx, records, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
@@ -533,21 +335,19 @@ func (r *Reconciler) syncUserStatus(ctx context.Context, user *iamv1alpha2.User)
afterStateTransition := user.Status.LastTransitionTime == nil || loginRecord.CreationTimestamp.After(user.Status.LastTransitionTime.Time)
if !loginRecord.Spec.Success &&
afterStateTransition &&
loginRecord.CreationTimestamp.Add(r.AuthenticationOptions.AuthenticateRateLimiterDuration).After(now) {
loginRecord.CreationTimestamp.Add(r.authenticationOptions.AuthenticateRateLimiterDuration).After(now) {
failedLoginAttempts++
}
}
// block user if failed login attempts exceeds maximum tries setting
if failedLoginAttempts >= r.AuthenticationOptions.AuthenticateRateLimiterMaxTries {
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserAuthLimitExceeded,
Reason: fmt.Sprintf("Failed login attempts exceed %d in last %s", failedLoginAttempts, r.AuthenticationOptions.AuthenticateRateLimiterDuration),
if failedLoginAttempts >= r.authenticationOptions.AuthenticateRateLimiterMaxTries {
user.Status = iamv1beta1.UserStatus{
State: iamv1beta1.UserAuthLimitExceeded,
Reason: fmt.Sprintf("Failed login attempts exceed %d in last %s", failedLoginAttempts, r.authenticationOptions.AuthenticateRateLimiterDuration),
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
err = r.Update(ctx, user, &client.UpdateOptions{})
if err != nil {
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
@@ -555,6 +355,82 @@ func (r *Reconciler) syncUserStatus(ctx context.Context, user *iamv1alpha2.User)
return nil
}
func (r *Reconciler) updateGlobalRoleAnnotation(ctx context.Context, user *iamv1beta1.User) error {
globalRoles := &iamv1beta1.GlobalRoleBindingList{}
if err := r.List(ctx, globalRoles, client.MatchingLabels{iamv1beta1.UserReferenceLabel: user.Name}); err != nil {
return err
}
var globalRole string
if len(globalRoles.Items) == 1 {
globalRole = globalRoles.Items[0].RoleRef.Name
} else if len(globalRoles.Items) == 0 {
globalRole = ""
} else {
klog.Warningf("User %s has more than one global role bindings", user.Name)
globalRole = user.Annotations[iamv1beta1.GlobalRoleAnnotation]
}
if globalRole != user.Annotations[iamv1beta1.GlobalRoleAnnotation] {
if user.Annotations == nil {
user.Annotations = make(map[string]string)
}
user.Annotations[iamv1beta1.GlobalRoleAnnotation] = globalRole
if err := r.Update(ctx, user, &client.UpdateOptions{}); err != nil {
return err
}
}
return nil
}
func (r *Reconciler) multiClusterSync(ctx context.Context, user *iamv1beta1.User) error {
clusters, err := r.clusterClient.ListClusters(ctx)
if err != nil {
return fmt.Errorf("failed to list clusters: %s", err)
}
var notReadyClusters []string
for _, cluster := range clusters {
// skip if the cluster is not ready
if !clusterutils.IsClusterReady(&cluster) {
notReadyClusters = append(notReadyClusters, cluster.Name)
continue
}
if err := r.syncKubeConfigSecret(ctx, cluster, user); err != nil {
return fmt.Errorf("failed to sync user %s to cluster %s: %s", user.Name, cluster.Name, err)
}
}
if len(notReadyClusters) > 0 {
klog.FromContext(ctx).V(4).Info("cluster not ready", "clusters", strings.Join(notReadyClusters, ","))
r.recorder.Event(user, corev1.EventTypeWarning, kscontroller.SyncFailed, fmt.Sprintf("cluster not ready: %s", strings.Join(notReadyClusters, ",")))
}
return nil
}
func (r *Reconciler) syncKubeConfigSecret(ctx context.Context, cluster clusterv1alpha1.Cluster, user *iamv1beta1.User) error {
clusterClient, err := r.clusterClient.GetRuntimeClient(cluster.Name)
if err != nil {
return fmt.Errorf("failed to get cluster client: %s", err)
}
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: constants.KubeSphereNamespace, Name: fmt.Sprintf(kubeconfig.UserKubeConfigSecretNameFormat, user.Name)}}
op, err := controllerutil.CreateOrUpdate(ctx, clusterClient, secret, func() error {
if secret.Labels == nil {
secret.Labels = make(map[string]string)
}
secret.Labels[constants.UsernameLabelKey] = user.Name
if secret.Type == "" {
secret.Type = kubeconfig.SecretTypeKubeConfig
}
return nil
})
if err != nil {
return fmt.Errorf("failed to update kubeconfig secret: %s", err)
}
r.logger.V(4).Info("kubeconfig secret successfully synced", "cluster", cluster.Name, "operation", op, "name", secret.Name)
return nil
}
func encrypt(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err

View File

@@ -1,18 +1,7 @@
/*
Copyright 2019 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package user
@@ -23,34 +12,30 @@ import (
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/record"
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
runtimefakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"kubesphere.io/kubesphere/pkg/apis"
ldapclient "kubesphere.io/kubesphere/pkg/simple/client/ldap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
"kubesphere.io/kubesphere/pkg/scheme"
"kubesphere.io/kubesphere/pkg/utils/clusterclient"
)
func newUser(name string) *iamv1alpha2.User {
return &iamv1alpha2.User{
TypeMeta: metav1.TypeMeta{APIVersion: iamv1alpha2.SchemeGroupVersion.String()},
func newUser(name string) *iamv1beta1.User {
return &iamv1beta1.User{
TypeMeta: metav1.TypeMeta{APIVersion: iamv1beta1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: iamv1alpha2.UserSpec{
Spec: iamv1beta1.UserSpec{
Email: fmt.Sprintf("%s@kubesphere.io", name),
Lang: "zh-CN",
Description: "fake user",
@@ -66,36 +51,35 @@ func TestDoNothing(t *testing.T) {
user := newUser("test")
loginRecords := make([]runtime.Object, 0)
for i := 0; i < authenticateOptions.AuthenticateRateLimiterMaxTries+1; i++ {
loginRecord := iamv1alpha2.LoginRecord{
loginRecord := iamv1beta1.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", user.Name, i),
Labels: map[string]string{iamv1alpha2.UserReferenceLabel: user.Name},
Labels: map[string]string{iamv1beta1.UserReferenceLabel: user.Name},
// Ensure that the failed login record created after the user status change to active,
// otherwise, the failed login attempts will not be counted.
CreationTimestamp: metav1.NewTime(time.Now().Add(time.Minute)),
},
Spec: iamv1alpha2.LoginRecordSpec{
Spec: iamv1beta1.LoginRecordSpec{
Success: false,
},
}
loginRecords = append(loginRecords, &loginRecord)
}
sch := scheme.Scheme
if err := apis.AddToScheme(sch); err != nil {
t.Fatalf("unable add APIs to scheme: %v", err)
}
client := runtimefakeclient.NewClientBuilder().WithScheme(sch).WithRuntimeObjects(user).WithRuntimeObjects(loginRecords...).Build()
ldap := ldapclient.NewSimpleLdap()
client := runtimefakeclient.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(user).WithRuntimeObjects(loginRecords...).Build()
clusterClientSet, err := clusterclient.NewClusterClientSet(&informertest.FakeInformers{Scheme: scheme.Scheme})
if err != nil {
t.Fatal(err)
}
c := &Reconciler{
Recorder: &record.FakeRecorder{},
LdapClient: ldap,
Logger: ctrl.Log.WithName("controllers").WithName(controllerName),
recorder: &record.FakeRecorder{},
logger: ctrl.Log.WithName("controllers").WithName(controllerName),
Client: client,
AuthenticationOptions: authenticateOptions,
authenticationOptions: authenticateOptions,
clusterClient: clusterClientSet,
}
users := &iamv1alpha2.UserList{}
users := &iamv1beta1.UserList{}
w, err := client.Watch(context.Background(), users, &runtimeclient.ListOptions{})
if err != nil {
t.Fatal(err)
@@ -112,27 +96,33 @@ func TestDoNothing(t *testing.T) {
updateEvent := <-w.ResultChan()
assert.Equal(t, watch.Modified, updateEvent.Type)
assert.NotNil(t, updateEvent.Object)
user = updateEvent.Object.(*iamv1alpha2.User)
user = updateEvent.Object.(*iamv1beta1.User)
assert.NotNil(t, user)
assert.NotEmpty(t, user.Finalizers)
result, err = c.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: user.Name},
})
if err != nil {
t.Fatal(err)
}
updateEvent = <-w.ResultChan()
// encrypt password
assert.Equal(t, watch.Modified, updateEvent.Type)
assert.NotNil(t, updateEvent.Object)
user = updateEvent.Object.(*iamv1alpha2.User)
user = updateEvent.Object.(*iamv1beta1.User)
assert.NotNil(t, user)
assert.True(t, isEncrypted(user.Spec.EncryptedPassword))
// becomes active after password encrypted
updateEvent = <-w.ResultChan()
user = updateEvent.Object.(*iamv1alpha2.User)
assert.Equal(t, iamv1alpha2.UserActive, user.Status.State)
user = updateEvent.Object.(*iamv1beta1.User)
assert.Equal(t, iamv1beta1.UserActive, user.Status.State)
// block user
updateEvent = <-w.ResultChan()
user = updateEvent.Object.(*iamv1alpha2.User)
assert.Equal(t, iamv1alpha2.UserAuthLimitExceeded, user.Status.State)
user = updateEvent.Object.(*iamv1beta1.User)
assert.Equal(t, iamv1beta1.UserAuthLimitExceeded, user.Status.State)
assert.True(t, result.Requeue)
time.Sleep(result.RequeueAfter + time.Second)
@@ -145,6 +135,6 @@ func TestDoNothing(t *testing.T) {
// unblock user
updateEvent = <-w.ResultChan()
user = updateEvent.Object.(*iamv1alpha2.User)
assert.Equal(t, iamv1alpha2.UserActive, user.Status.State)
user = updateEvent.Object.(*iamv1beta1.User)
assert.Equal(t, iamv1beta1.UserActive, user.Status.State)
}

View File

@@ -1,63 +1,86 @@
/*
Copyright 2020 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package user
import (
"context"
"fmt"
"net/http"
"net/mail"
"k8s.io/apimachinery/pkg/runtime"
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"kubesphere.io/api/iam/v1alpha2"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
)
type EmailValidator struct {
Client client.Client
decoder *admission.Decoder
const webhookName = "user-webhook"
func (v *Webhook) Name() string {
return webhookName
}
func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
user := &v1alpha2.User{}
err := a.decoder.Decode(req, user)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
var _ kscontroller.Controller = &Webhook{}
var _ admission.CustomValidator = &Webhook{}
type Webhook struct {
client.Client
}
func (v *Webhook) SetupWithManager(mgr *kscontroller.Manager) error {
v.Client = mgr.GetClient()
return builder.WebhookManagedBy(mgr).
For(&iamv1beta1.User{}).
WithValidator(v).
WithDefaulter(v).
Complete()
}
func (v *Webhook) Default(ctx context.Context, obj runtime.Object) error {
return nil
}
// validate admits a pod if a specific annotation exists.
func (v *Webhook) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
user, ok := obj.(*iamv1beta1.User)
if !ok {
return nil, fmt.Errorf("expected a User but got a %T", obj)
}
allUsers := v1alpha2.UserList{}
if err = a.Client.List(ctx, &allUsers, &client.ListOptions{}); err != nil {
return admission.Errored(http.StatusInternalServerError, err)
allUsers := iamv1beta1.UserList{}
if err := v.List(ctx, &allUsers, &client.ListOptions{}); err != nil {
return nil, err
}
if _, err := mail.ParseAddress(user.Spec.Email); user.Spec.Email != "" && err != nil {
return admission.Errored(http.StatusBadRequest, fmt.Errorf("invalid email address:%s", user.Spec.Email))
return nil, fmt.Errorf("invalid email address:%s", user.Spec.Email)
}
alreadyExist := emailAlreadyExist(allUsers, user)
if alreadyExist {
return admission.Errored(http.StatusConflict, fmt.Errorf("user email: %s already exists", user.Spec.Email))
return nil, fmt.Errorf("user email: %s already exists", user.Spec.Email)
}
return admission.Allowed("")
return nil, nil
}
func emailAlreadyExist(users v1alpha2.UserList, user *v1alpha2.User) bool {
func (v *Webhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
return v.validate(ctx, obj)
}
func (v *Webhook) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
return v.validate(ctx, newObj)
}
func (v *Webhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
return v.validate(ctx, obj)
}
func emailAlreadyExist(users iamv1beta1.UserList, user *iamv1beta1.User) bool {
// empty email is allowed
if user.Spec.Email == "" {
return false
@@ -69,9 +92,3 @@ func emailAlreadyExist(users v1alpha2.UserList, user *v1alpha2.User) bool {
}
return false
}
// InjectDecoder injects the decoder.
func (a *EmailValidator) InjectDecoder(d *admission.Decoder) error {
a.decoder = d
return nil
}