@@ -18,6 +18,7 @@ package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
@@ -25,15 +26,18 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
kubespherescheme "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme"
|
||||
userinformer "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam/v1alpha2"
|
||||
userlister "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/models/kubeconfig"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -46,9 +50,9 @@ const (
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
kubeClientset kubernetes.Interface
|
||||
kubesphereClientset kubesphereclient.Interface
|
||||
|
||||
k8sClient kubernetes.Interface
|
||||
ksClient kubesphere.Interface
|
||||
kubeconfig kubeconfig.Interface
|
||||
userInformer userinformer.UserInformer
|
||||
userLister userlister.UserLister
|
||||
userSynced cache.InformerSynced
|
||||
@@ -63,9 +67,8 @@ type Controller struct {
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func NewController(kubeclientset kubernetes.Interface,
|
||||
kubesphereklientset kubesphereclient.Interface,
|
||||
userInformer userinformer.UserInformer) *Controller {
|
||||
func NewController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
|
||||
config *rest.Config, userInformer userinformer.UserInformer) *Controller {
|
||||
// Create event broadcaster
|
||||
// Add sample-controller types to the default Kubernetes Scheme so Events can be
|
||||
// logged for sample-controller types.
|
||||
@@ -74,16 +77,21 @@ func NewController(kubeclientset kubernetes.Interface,
|
||||
klog.V(4).Info("Creating event broadcaster")
|
||||
eventBroadcaster := record.NewBroadcaster()
|
||||
eventBroadcaster.StartLogging(klog.Infof)
|
||||
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
|
||||
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: k8sClient.CoreV1().Events("")})
|
||||
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerName})
|
||||
var kubeconfigOperator kubeconfig.Interface
|
||||
if config != nil {
|
||||
kubeconfigOperator = kubeconfig.NewOperator(k8sClient, config, "")
|
||||
}
|
||||
ctl := &Controller{
|
||||
kubeClientset: kubeclientset,
|
||||
kubesphereClientset: kubesphereklientset,
|
||||
userInformer: userInformer,
|
||||
userLister: userInformer.Lister(),
|
||||
userSynced: userInformer.Informer().HasSynced,
|
||||
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Users"),
|
||||
recorder: recorder,
|
||||
k8sClient: k8sClient,
|
||||
ksClient: ksClient,
|
||||
kubeconfig: kubeconfigOperator,
|
||||
userInformer: userInformer,
|
||||
userLister: userInformer.Lister(),
|
||||
userSynced: userInformer.Informer().HasSynced,
|
||||
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Users"),
|
||||
recorder: recorder,
|
||||
}
|
||||
klog.Info("Setting up event handlers")
|
||||
userInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
@@ -205,26 +213,64 @@ func (c *Controller) reconcile(key string) error {
|
||||
utilruntime.HandleError(fmt.Errorf("user '%s' in work queue no longer exists", key))
|
||||
return nil
|
||||
}
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.updateUserStatus(user)
|
||||
user, err = c.encryptPassword(user.DeepCopy())
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if c.kubeconfig != nil {
|
||||
err = c.kubeconfig.CreateKubeConfig(user)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.recorder.Event(user, corev1.EventTypeNormal, successSynced, messageResourceSynced)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) updateUserStatus(user *iamv1alpha2.User) error {
|
||||
userCopy := user.DeepCopy()
|
||||
userCopy.Status.State = iamv1alpha2.UserActive
|
||||
_, err := c.kubesphereClientset.IamV1alpha2().Users().Update(userCopy)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Controller) Start(stopCh <-chan struct{}) error {
|
||||
return c.Run(4, stopCh)
|
||||
}
|
||||
|
||||
func (c *Controller) encryptPassword(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
encrypted, err := strconv.ParseBool(user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation])
|
||||
|
||||
// password is not encrypted
|
||||
if err != nil || !encrypted {
|
||||
password, err := encrypt(user.Spec.EncryptedPassword)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
user.Spec.EncryptedPassword = password
|
||||
if user.Annotations == nil {
|
||||
user.Annotations = make(map[string]string, 0)
|
||||
}
|
||||
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = "true"
|
||||
user.Status.State = iamv1alpha2.UserActive
|
||||
|
||||
updated, err := c.ksClient.IamV1alpha2().Users().Update(user)
|
||||
|
||||
return updated, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func encrypt(password string) (string, error) {
|
||||
// when user is already mapped to another identity, password is empty by default
|
||||
// unable to log in directly until password reset
|
||||
if password == "" {
|
||||
return "", nil
|
||||
}
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -44,8 +44,8 @@ var (
|
||||
type fixture struct {
|
||||
t *testing.T
|
||||
|
||||
client *fake.Clientset
|
||||
kubeclient *k8sfake.Clientset
|
||||
ksclient *fake.Clientset
|
||||
k8sclient *k8sfake.Clientset
|
||||
// Objects to put in the store.
|
||||
userLister []*iamv1alpha2.User
|
||||
// Actions expected to happen on the client.
|
||||
@@ -78,25 +78,25 @@ func newUser(name string) *iamv1alpha2.User {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fixture) newController() (*Controller, informers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {
|
||||
f.client = fake.NewSimpleClientset(f.objects...)
|
||||
f.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...)
|
||||
func (f *fixture) newController() (*Controller, ksinformers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {
|
||||
f.ksclient = fake.NewSimpleClientset(f.objects...)
|
||||
f.k8sclient = k8sfake.NewSimpleClientset(f.kubeobjects...)
|
||||
|
||||
i := informers.NewSharedInformerFactory(f.client, noResyncPeriodFunc())
|
||||
k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc())
|
||||
ksinformers := ksinformers.NewSharedInformerFactory(f.ksclient, noResyncPeriodFunc())
|
||||
k8sinformers := kubeinformers.NewSharedInformerFactory(f.k8sclient, noResyncPeriodFunc())
|
||||
|
||||
for _, user := range f.userLister {
|
||||
err := i.Iam().V1alpha2().Users().Informer().GetIndexer().Add(user)
|
||||
err := ksinformers.Iam().V1alpha2().Users().Informer().GetIndexer().Add(user)
|
||||
if err != nil {
|
||||
f.t.Errorf("add user:%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
c := NewController(f.kubeclient, f.client, i.Iam().V1alpha2().Users())
|
||||
c := NewController(f.k8sclient, f.ksclient, nil, ksinformers.Iam().V1alpha2().Users())
|
||||
c.userSynced = alwaysReady
|
||||
c.recorder = &record.FakeRecorder{}
|
||||
|
||||
return c, i, k8sI
|
||||
return c, ksinformers, k8sinformers
|
||||
}
|
||||
|
||||
func (f *fixture) run(userName string) {
|
||||
@@ -123,7 +123,7 @@ func (f *fixture) runController(user string, startInformers bool, expectError bo
|
||||
f.t.Error("expected error syncing user, got nil")
|
||||
}
|
||||
|
||||
actions := filterInformerActions(f.client.Actions())
|
||||
actions := filterInformerActions(f.ksclient.Actions())
|
||||
for i, action := range actions {
|
||||
if len(f.actions) < i+1 {
|
||||
f.t.Errorf("%d unexpected actions: %+v", len(actions)-len(f.actions), actions[i:])
|
||||
@@ -138,7 +138,7 @@ func (f *fixture) runController(user string, startInformers bool, expectError bo
|
||||
f.t.Errorf("%d additional expected actions:%+v", len(f.actions)-len(actions), f.actions[len(actions):])
|
||||
}
|
||||
|
||||
k8sActions := filterInformerActions(f.kubeclient.Actions())
|
||||
k8sActions := filterInformerActions(f.k8sclient.Actions())
|
||||
for i, action := range k8sActions {
|
||||
if len(f.kubeactions) < i+1 {
|
||||
f.t.Errorf("%d unexpected actions: %+v", len(k8sActions)-len(f.kubeactions), k8sActions[i:])
|
||||
@@ -220,6 +220,7 @@ func filterInformerActions(actions []core.Action) []core.Action {
|
||||
func (f *fixture) expectUpdateUserStatusAction(user *iamv1alpha2.User) {
|
||||
expect := user.DeepCopy()
|
||||
expect.Status.State = iamv1alpha2.UserActive
|
||||
expect.Annotations = map[string]string{iamv1alpha2.PasswordEncryptedAnnotation: "true"}
|
||||
action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "users"}, "", expect)
|
||||
f.actions = append(f.actions, action)
|
||||
}
|
||||
|
||||
@@ -20,17 +20,12 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"fmt"
|
||||
"kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
encryptedAnnotation = "iam.kubesphere.io/password-encrypted"
|
||||
)
|
||||
|
||||
type EmailValidator struct {
|
||||
@@ -38,11 +33,6 @@ type EmailValidator struct {
|
||||
decoder *admission.Decoder
|
||||
}
|
||||
|
||||
type PasswordCipher struct {
|
||||
Client client.Client
|
||||
decoder *admission.Decoder
|
||||
}
|
||||
|
||||
func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
user := &v1alpha2.User{}
|
||||
err := a.decoder.Decode(req, user)
|
||||
@@ -57,11 +47,14 @@ func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admi
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
if _, err := mail.ParseAddress(user.Spec.Email); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, fmt.Errorf("invalid email address:%s", user.Spec.Email))
|
||||
}
|
||||
|
||||
alreadyExist := emailAlreadyExist(allUsers, user)
|
||||
|
||||
if alreadyExist {
|
||||
return admission.Denied("user email already exists")
|
||||
return admission.Errored(http.StatusConflict, fmt.Errorf("user email: %s already exists", user.Spec.Email))
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
@@ -76,43 +69,6 @@ func emailAlreadyExist(users v1alpha2.UserList, user *v1alpha2.User) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *PasswordCipher) 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)
|
||||
}
|
||||
|
||||
encrypted, err := strconv.ParseBool(user.Annotations[encryptedAnnotation])
|
||||
|
||||
if err != nil || !encrypted {
|
||||
password, err := hashPassword(user.Spec.EncryptedPassword)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
user.Spec.EncryptedPassword = password
|
||||
user.Annotations[encryptedAnnotation] = "true"
|
||||
}
|
||||
|
||||
marshaledUser, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledUser)
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// InjectDecoder injects the decoder.
|
||||
func (a *PasswordCipher) InjectDecoder(d *admission.Decoder) error {
|
||||
a.decoder = d
|
||||
return nil
|
||||
}
|
||||
|
||||
// InjectDecoder injects the decoder.
|
||||
func (a *EmailValidator) InjectDecoder(d *admission.Decoder) error {
|
||||
a.decoder = d
|
||||
|
||||
Reference in New Issue
Block a user