Merge pull request #3140 from wansir/identity-provider
improve identity provider plugin
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package auth
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
KindTokenReview = "TokenReview"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
Token string `json:"token" description:"access token"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Authenticated bool `json:"authenticated" description:"is authenticated"`
|
||||
User map[string]interface{} `json:"user,omitempty" description:"user info"`
|
||||
}
|
||||
|
||||
type TokenReview struct {
|
||||
APIVersion string `json:"apiVersion" description:"Kubernetes API version"`
|
||||
Kind string `json:"kind" description:"kind of the API object"`
|
||||
Spec *Spec `json:"spec,omitempty"`
|
||||
Status *Status `json:"status,omitempty" description:"token review status"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" description:"username"`
|
||||
Password string `json:"password" description:"password"`
|
||||
}
|
||||
|
||||
func (request *TokenReview) Validate() error {
|
||||
if request.Spec == nil || request.Spec.Token == "" {
|
||||
return fmt.Errorf("token must not be null")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package iam
|
||||
|
||||
type PasswordReset struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/emicklei/go-restful"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -28,32 +29,49 @@ import (
|
||||
var sanitizer = strings.NewReplacer(`&`, "&", `<`, "<", `>`, ">")
|
||||
|
||||
func HandleInternalError(response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), http.StatusInternalServerError)
|
||||
handle(http.StatusInternalServerError, response, req, err)
|
||||
}
|
||||
|
||||
// HandleBadRequest writes http.StatusBadRequest and log error
|
||||
func HandleBadRequest(response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), http.StatusBadRequest)
|
||||
handle(http.StatusBadRequest, response, req, err)
|
||||
}
|
||||
|
||||
func HandleNotFound(response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), http.StatusNotFound)
|
||||
handle(http.StatusNotFound, response, req, err)
|
||||
}
|
||||
|
||||
func HandleForbidden(response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), http.StatusForbidden)
|
||||
handle(http.StatusForbidden, response, req, err)
|
||||
}
|
||||
|
||||
func HandleUnauthorized(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusUnauthorized, response, req, err)
|
||||
}
|
||||
|
||||
func HandleTooManyRequests(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusTooManyRequests, response, req, err)
|
||||
}
|
||||
|
||||
func HandleConflict(response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), http.StatusConflict)
|
||||
handle(http.StatusConflict, response, req, err)
|
||||
}
|
||||
|
||||
func HandleError(response *restful.Response, req *restful.Request, err error) {
|
||||
var statusCode int
|
||||
switch t := err.(type) {
|
||||
case errors.APIStatus:
|
||||
statusCode = int(t.Status().Code)
|
||||
case restful.ServiceError:
|
||||
statusCode = t.Code
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
handle(statusCode, response, req, err)
|
||||
}
|
||||
|
||||
func handle(statusCode int, response *restful.Response, req *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(2)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), statusCode)
|
||||
}
|
||||
|
||||
@@ -58,13 +58,20 @@ const (
|
||||
GlobalRoleAnnotation = "iam.kubesphere.io/globalrole"
|
||||
WorkspaceRoleAnnotation = "iam.kubesphere.io/workspacerole"
|
||||
ClusterRoleAnnotation = "iam.kubesphere.io/clusterrole"
|
||||
UninitializedAnnotation = "iam.kubesphere.io/uninitialized"
|
||||
RoleAnnotation = "iam.kubesphere.io/role"
|
||||
RoleTemplateLabel = "iam.kubesphere.io/role-template"
|
||||
ScopeLabelFormat = "scope.kubesphere.io/%s"
|
||||
UserReferenceLabel = "iam.kubesphere.io/user-ref"
|
||||
IdentifyProviderLabel = "iam.kubesphere.io/identify-provider"
|
||||
PasswordEncryptedAnnotation = "iam.kubesphere.io/password-encrypted"
|
||||
OriginUIDLabel = "iam.kubesphere.io/origin-uid"
|
||||
FieldEmail = "email"
|
||||
ExtraEmail = FieldEmail
|
||||
ExtraIdentityProvider = "idp"
|
||||
ExtraUID = "uid"
|
||||
ExtraUsername = "username"
|
||||
ExtraDisplayName = "displayName"
|
||||
ExtraUninitialized = "uninitialized"
|
||||
InGroup = "ingroup"
|
||||
NotInGroup = "notingroup"
|
||||
AggregateTo = "aggregateTo"
|
||||
@@ -76,6 +83,8 @@ const (
|
||||
NamespaceAdmin = "admin"
|
||||
WorkspaceAdminFormat = "%s-admin"
|
||||
ClusterAdmin = "cluster-admin"
|
||||
PreRegistrationUser = "system:pre-registration"
|
||||
PreRegistrationUserGroup = "pre-registration"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
@@ -87,6 +96,7 @@ const (
|
||||
// +kubebuilder:printcolumn:name="Email",type="string",JSONPath=".spec.email"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.state"
|
||||
// +kubebuilder:resource:categories="iam",scope="Cluster"
|
||||
// +kubebuilder:subresource:status
|
||||
type User struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata.
|
||||
@@ -126,7 +136,7 @@ const (
|
||||
UserActive UserState = "Active"
|
||||
// UserDisabled means the user is disabled.
|
||||
UserDisabled UserState = "Disabled"
|
||||
// UserDisabled means the user is disabled.
|
||||
// UserAuthLimitExceeded means restrict user login.
|
||||
UserAuthLimitExceeded UserState = "AuthLimitExceeded"
|
||||
|
||||
AuthenticatedSuccessfully = "authenticated successfully"
|
||||
@@ -136,7 +146,7 @@ const (
|
||||
type UserStatus struct {
|
||||
// The user status
|
||||
// +optional
|
||||
State UserState `json:"state,omitempty"`
|
||||
State *UserState `json:"state,omitempty"`
|
||||
// +optional
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// +optional
|
||||
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/rbac"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/user"
|
||||
"net/http"
|
||||
rt "runtime"
|
||||
"time"
|
||||
@@ -167,9 +171,15 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
|
||||
// Installation happens before all informers start to cache objects, so
|
||||
// any attempt to list objects using listers will get empty results.
|
||||
func (s *APIServer) installKubeSphereAPIs() {
|
||||
imOperator := im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory, s.Config.AuthenticationOptions)
|
||||
amOperator := am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes())
|
||||
rbacAuthorizer := authorizerfactory.NewRBACAuthorizer(amOperator)
|
||||
imOperator := im.NewOperator(s.KubernetesClient.KubeSphere(),
|
||||
user.New(s.InformerFactory.KubeSphereSharedInformerFactory(),
|
||||
s.InformerFactory.KubernetesSharedInformerFactory()),
|
||||
loginrecord.New(s.InformerFactory.KubeSphereSharedInformerFactory()),
|
||||
s.Config.AuthenticationOptions)
|
||||
amOperator := am.NewOperator(s.KubernetesClient.KubeSphere(),
|
||||
s.KubernetesClient.Kubernetes(),
|
||||
s.InformerFactory)
|
||||
rbacAuthorizer := rbac.NewRBACAuthorizer(amOperator)
|
||||
|
||||
urlruntime.Must(configv1alpha2.AddToContainer(s.container, s.Config))
|
||||
urlruntime.Must(resourcev1alpha3.AddToContainer(s.container, s.InformerFactory))
|
||||
@@ -187,20 +197,22 @@ func (s *APIServer) installKubeSphereAPIs() {
|
||||
s.Config.MultiClusterOptions.ProxyPublishService,
|
||||
s.Config.MultiClusterOptions.ProxyPublishAddress,
|
||||
s.Config.MultiClusterOptions.AgentImage))
|
||||
urlruntime.Must(iamapi.AddToContainer(s.container, imOperator,
|
||||
am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()),
|
||||
urlruntime.Must(iamapi.AddToContainer(s.container, imOperator, amOperator,
|
||||
group.New(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()),
|
||||
s.Config.AuthenticationOptions))
|
||||
rbacAuthorizer))
|
||||
|
||||
urlruntime.Must(oauth.AddToContainer(s.container, imOperator,
|
||||
im.NewTokenOperator(
|
||||
auth.NewTokenOperator(
|
||||
s.CacheClient,
|
||||
s.Config.AuthenticationOptions),
|
||||
im.NewPasswordAuthenticator(
|
||||
auth.NewPasswordAuthenticator(
|
||||
s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
|
||||
s.Config.AuthenticationOptions),
|
||||
im.NewLoginRecorder(s.KubernetesClient.KubeSphere()),
|
||||
auth.NewOAuth2Authenticator(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
|
||||
s.Config.AuthenticationOptions),
|
||||
auth.NewLoginRecorder(s.KubernetesClient.KubeSphere()),
|
||||
s.Config.AuthenticationOptions))
|
||||
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
|
||||
urlruntime.Must(networkv1alpha2.AddToContainer(s.container, s.Config.NetworkOptions.WeaveScopeHost))
|
||||
@@ -211,7 +223,7 @@ func (s *APIServer) installKubeSphereAPIs() {
|
||||
s.KubernetesClient.KubeSphere(),
|
||||
s.S3Client,
|
||||
s.Config.DevopsOptions.Host,
|
||||
am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes())))
|
||||
amOperator))
|
||||
urlruntime.Must(devopsv1alpha3.AddToContainer(s.container,
|
||||
s.DevopsClient,
|
||||
s.KubernetesClient.Kubernetes(),
|
||||
@@ -285,7 +297,7 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
|
||||
excludedPaths := []string{"/oauth/*", "/kapis/config.kubesphere.io/*", "/kapis/version"}
|
||||
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
|
||||
amOperator := am.NewReadOnlyOperator(s.InformerFactory)
|
||||
authorizers = unionauthorizer.New(pathAuthorizer, authorizerfactory.NewRBACAuthorizer(amOperator))
|
||||
authorizers = unionauthorizer.New(pathAuthorizer, rbac.NewRBACAuthorizer(amOperator))
|
||||
}
|
||||
|
||||
handler = filters.WithAuthorization(handler, authorizers)
|
||||
@@ -295,12 +307,16 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
|
||||
handler = filters.WithMultipleClusterDispatcher(handler, clusterDispatcher)
|
||||
}
|
||||
|
||||
loginRecorder := im.NewLoginRecorder(s.KubernetesClient.KubeSphere())
|
||||
loginRecorder := auth.NewLoginRecorder(s.KubernetesClient.KubeSphere())
|
||||
// authenticators are unordered
|
||||
authn := unionauth.New(anonymous.NewAuthenticator(),
|
||||
basictoken.New(basic.NewBasicAuthenticator(im.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(), s.Config.AuthenticationOptions))),
|
||||
bearertoken.New(jwttoken.NewTokenAuthenticator(im.NewTokenOperator(s.CacheClient, s.Config.AuthenticationOptions), s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister())))
|
||||
handler = filters.WithAuthentication(handler, authn, loginRecorder)
|
||||
basictoken.New(basic.NewBasicAuthenticator(auth.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
|
||||
s.Config.AuthenticationOptions), loginRecorder)),
|
||||
bearertoken.New(jwttoken.NewTokenAuthenticator(auth.NewTokenOperator(s.CacheClient,
|
||||
s.Config.AuthenticationOptions),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister())))
|
||||
handler = filters.WithAuthentication(handler, authn)
|
||||
handler = filters.WithRequestInfo(handler, requestInfoResolver)
|
||||
s.Server.Handler = handler
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@ package basic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
)
|
||||
|
||||
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
|
||||
@@ -30,28 +33,36 @@ import (
|
||||
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
|
||||
// because some resources are public accessible.
|
||||
type basicAuthenticator struct {
|
||||
authenticator im.PasswordAuthenticator
|
||||
authenticator auth.PasswordAuthenticator
|
||||
loginRecorder auth.LoginRecorder
|
||||
}
|
||||
|
||||
func NewBasicAuthenticator(authenticator im.PasswordAuthenticator) authenticator.Password {
|
||||
func NewBasicAuthenticator(authenticator auth.PasswordAuthenticator, loginRecorder auth.LoginRecorder) authenticator.Password {
|
||||
return &basicAuthenticator{
|
||||
authenticator: authenticator,
|
||||
loginRecorder: loginRecorder,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
|
||||
providedUser, err := t.authenticator.Authenticate(username, password)
|
||||
|
||||
authenticated, provider, err := t.authenticator.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if t.loginRecorder != nil && err == auth.IncorrectPasswordError {
|
||||
var sourceIP, userAgent string
|
||||
if requestInfo, ok := request.RequestInfoFrom(ctx); ok {
|
||||
sourceIP = requestInfo.SourceIP
|
||||
userAgent = requestInfo.UserAgent
|
||||
}
|
||||
if err := t.loginRecorder.RecordLogin(username, iamv1alpha2.BasicAuth, provider, sourceIP, userAgent, err); err != nil {
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: providedUser.GetName(),
|
||||
UID: providedUser.GetUID(),
|
||||
Groups: append(providedUser.GetGroups(), user.AllAuthenticated),
|
||||
Name: authenticated.GetName(),
|
||||
Groups: append(authenticated.GetGroups(), user.AllAuthenticated),
|
||||
},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ package jwttoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
)
|
||||
|
||||
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
|
||||
@@ -33,11 +33,11 @@ import (
|
||||
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
|
||||
// because some resources are public accessible.
|
||||
type tokenAuthenticator struct {
|
||||
tokenOperator im.TokenManagementInterface
|
||||
tokenOperator auth.TokenManagementInterface
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
}
|
||||
|
||||
func NewTokenAuthenticator(tokenOperator im.TokenManagementInterface, userLister iamv1alpha2listers.UserLister) authenticator.Token {
|
||||
func NewTokenAuthenticator(tokenOperator auth.TokenManagementInterface, userLister iamv1alpha2listers.UserLister) authenticator.Token {
|
||||
return &tokenAuthenticator{
|
||||
tokenOperator: tokenOperator,
|
||||
userLister: userLister,
|
||||
@@ -51,6 +51,16 @@ func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if providedUser.GetName() == iamv1alpha2.PreRegistrationUser {
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: providedUser.GetName(),
|
||||
Extra: providedUser.GetExtra(),
|
||||
Groups: providedUser.GetGroups(),
|
||||
},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
dbUser, err := t.userLister.Get(providedUser.GetName())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -58,8 +68,7 @@ func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: providedUser.GetName(),
|
||||
UID: providedUser.GetUID(),
|
||||
Name: dbUser.GetName(),
|
||||
Groups: append(dbUser.Spec.Groups, user.AllAuthenticated),
|
||||
},
|
||||
}, true, nil
|
||||
|
||||
@@ -20,15 +20,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io/ioutil"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/yaml.v3"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
type AliyunIDaaS struct {
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&idaasProviderFactory{})
|
||||
}
|
||||
|
||||
type aliyunIDaaS struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `json:"clientID" yaml:"clientID"`
|
||||
|
||||
@@ -39,7 +43,7 @@ type AliyunIDaaS struct {
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or github.Endpoint.
|
||||
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
@@ -49,15 +53,15 @@ type AliyunIDaaS struct {
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
}
|
||||
|
||||
// Endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type Endpoint struct {
|
||||
type endpoint struct {
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
UserInfoURL string `json:"user_info_url" yaml:"userInfoUrl"`
|
||||
}
|
||||
|
||||
type IDaaSIdentity struct {
|
||||
type idaasIdentity struct {
|
||||
Sub string `json:"sub"`
|
||||
OuID string `json:"ou_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
@@ -67,72 +71,73 @@ type IDaaSIdentity struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type UserInfoResp struct {
|
||||
type userInfoResp struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
IDaaSIdentity IDaaSIdentity `json:"data"`
|
||||
IDaaSIdentity idaasIdentity `json:"data"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&AliyunIDaaS{})
|
||||
type idaasProviderFactory struct {
|
||||
}
|
||||
|
||||
func (a *AliyunIDaaS) Type() string {
|
||||
func (g *idaasProviderFactory) Type() string {
|
||||
return "AliyunIDaasProvider"
|
||||
}
|
||||
|
||||
func (a *AliyunIDaaS) Setup(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
data, err := yaml.Marshal(options)
|
||||
if err != nil {
|
||||
func (g *idaasProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var idaas aliyunIDaaS
|
||||
if err := mapstructure.Decode(options, &idaas); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var provider AliyunIDaaS
|
||||
err = yaml.Unmarshal(data, &provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &provider, nil
|
||||
return &idaas, nil
|
||||
}
|
||||
|
||||
func (a IDaaSIdentity) GetName() string {
|
||||
func (a idaasIdentity) GetUserID() string {
|
||||
return a.Sub
|
||||
}
|
||||
|
||||
func (a idaasIdentity) GetUsername() string {
|
||||
return a.Username
|
||||
}
|
||||
|
||||
func (a IDaaSIdentity) GetEmail() string {
|
||||
func (a idaasIdentity) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
func (g *AliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
func (a idaasIdentity) GetDisplayName() string {
|
||||
return a.Nickname
|
||||
}
|
||||
|
||||
func (a *aliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
config := oauth2.Config{
|
||||
ClientID: g.ClientID,
|
||||
ClientSecret: g.ClientSecret,
|
||||
ClientID: a.ClientID,
|
||||
ClientSecret: a.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: g.Endpoint.AuthURL,
|
||||
TokenURL: g.Endpoint.TokenURL,
|
||||
AuthURL: a.Endpoint.AuthURL,
|
||||
TokenURL: a.Endpoint.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: g.RedirectURL,
|
||||
Scopes: g.Scopes,
|
||||
RedirectURL: a.RedirectURL,
|
||||
Scopes: a.Scopes,
|
||||
}
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL)
|
||||
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var UserInfoResp UserInfoResp
|
||||
var UserInfoResp userInfoResp
|
||||
err = json.Unmarshal(data, &UserInfoResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
builtinGenericProviders = make(map[string]GenericProviderFactory)
|
||||
)
|
||||
|
||||
type GenericProvider interface {
|
||||
// Authenticate from remote server
|
||||
Authenticate(username string, password string) (Identity, error)
|
||||
}
|
||||
|
||||
type GenericProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
Create(options *oauth.DynamicOptions) (GenericProvider, error)
|
||||
}
|
||||
|
||||
func CreateGenericProvider(providerType string, options *oauth.DynamicOptions) (GenericProvider, error) {
|
||||
if factory, ok := builtinGenericProviders[providerType]; ok {
|
||||
return factory.Create(options)
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
func RegisterGenericProvider(factory GenericProviderFactory) {
|
||||
builtinGenericProviders[factory.Type()] = factory
|
||||
}
|
||||
@@ -19,8 +19,8 @@ package github
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
@@ -31,7 +31,11 @@ const (
|
||||
UserInfoURL = "https://api.github.com/user"
|
||||
)
|
||||
|
||||
type Github struct {
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&githubProviderFactory{})
|
||||
}
|
||||
|
||||
type github struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `json:"clientID" yaml:"clientID"`
|
||||
|
||||
@@ -41,8 +45,8 @@ type Github struct {
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or github.Endpoint.
|
||||
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
// google.Endpoint or github.endpoint.
|
||||
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
@@ -52,14 +56,14 @@ type Github struct {
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
}
|
||||
|
||||
// Endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type Endpoint struct {
|
||||
type endpoint struct {
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
}
|
||||
|
||||
type GithubIdentity struct {
|
||||
type githubIdentity struct {
|
||||
Login string `json:"login"`
|
||||
ID int `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
@@ -98,36 +102,38 @@ type GithubIdentity struct {
|
||||
Collaborators int `json:"collaborators"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&Github{})
|
||||
type githubProviderFactory struct {
|
||||
}
|
||||
|
||||
func (g *Github) Type() string {
|
||||
func (g *githubProviderFactory) Type() string {
|
||||
return "GitHubIdentityProvider"
|
||||
}
|
||||
|
||||
func (g *Github) Setup(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
data, err := yaml.Marshal(options)
|
||||
if err != nil {
|
||||
func (g *githubProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var github github
|
||||
if err := mapstructure.Decode(options, &github); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var provider Github
|
||||
err = yaml.Unmarshal(data, &provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &provider, nil
|
||||
return &github, nil
|
||||
}
|
||||
|
||||
func (g GithubIdentity) GetName() string {
|
||||
func (g githubIdentity) GetUserID() string {
|
||||
return g.Login
|
||||
}
|
||||
|
||||
func (g GithubIdentity) GetEmail() string {
|
||||
func (g githubIdentity) GetUsername() string {
|
||||
return g.Login
|
||||
}
|
||||
|
||||
func (g githubIdentity) GetEmail() string {
|
||||
return g.Email
|
||||
}
|
||||
|
||||
func (g *Github) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
func (g githubIdentity) GetDisplayName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (g *github) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
config := oauth2.Config{
|
||||
ClientID: g.ClientID,
|
||||
ClientSecret: g.ClientSecret,
|
||||
@@ -141,27 +147,23 @@ func (g *Github) IdentityExchange(code string) (identityprovider.Identity, error
|
||||
}
|
||||
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var githubIdentity GithubIdentity
|
||||
|
||||
var githubIdentity githubIdentity
|
||||
err = json.Unmarshal(data, &githubIdentity)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ limitations under the License.
|
||||
package identityprovider
|
||||
|
||||
type Identity interface {
|
||||
GetName() string
|
||||
// required
|
||||
GetUserID() string
|
||||
// optional
|
||||
GetUsername() string
|
||||
// optional
|
||||
GetDisplayName() string
|
||||
// optional
|
||||
GetEmail() string
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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 identityprovider
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -26,24 +24,23 @@ import (
|
||||
"github.com/go-ldap/ldap"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io/ioutil"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
LdapIdentityProvider = "LDAPIdentityProvider"
|
||||
ldapIdentityProvider = "LDAPIdentityProvider"
|
||||
defaultReadTimeout = 15000
|
||||
)
|
||||
|
||||
type LdapProvider interface {
|
||||
Authenticate(username string, password string) (*iamv1alpha2.User, error)
|
||||
func init() {
|
||||
identityprovider.RegisterGenericProvider(&ldapProviderFactory{})
|
||||
}
|
||||
|
||||
type ldapOptions struct {
|
||||
type ldapProvider struct {
|
||||
// Host and optional port of the LDAP server in the form "host:port".
|
||||
// If the port is not supplied, 389 for insecure or StartTLS connections, 636
|
||||
Host string `json:"host,omitempty" yaml:"managerDN"`
|
||||
@@ -73,105 +70,125 @@ type ldapOptions struct {
|
||||
UserMemberAttribute string `json:"userMemberAttribute,omitempty" yaml:"userMemberAttribute"`
|
||||
// Attribute on a group object storing the information for primary group membership.
|
||||
GroupMemberAttribute string `json:"groupMemberAttribute,omitempty" yaml:"groupMemberAttribute"`
|
||||
// login attribute used for comparing user entries.
|
||||
// The following three fields are direct mappings of attributes on the user entry.
|
||||
// login attribute used for comparing user entries.
|
||||
LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"`
|
||||
MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"`
|
||||
DisplayNameAttribute string `json:"displayNameAttribute" yaml:"displayNameAttribute"`
|
||||
}
|
||||
|
||||
type ldapProvider struct {
|
||||
options ldapOptions
|
||||
type ldapProviderFactory struct {
|
||||
}
|
||||
|
||||
func NewLdapProvider(options *oauth.DynamicOptions) (LdapProvider, error) {
|
||||
var ldapOptions ldapOptions
|
||||
if err := mapstructure.Decode(options, &ldapOptions); err != nil {
|
||||
func (l *ldapProviderFactory) Type() string {
|
||||
return ldapIdentityProvider
|
||||
}
|
||||
|
||||
func (l *ldapProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.GenericProvider, error) {
|
||||
var ldapProvider ldapProvider
|
||||
if err := mapstructure.Decode(options, &ldapProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ldapOptions.ReadTimeout <= 0 {
|
||||
ldapOptions.ReadTimeout = defaultReadTimeout
|
||||
if ldapProvider.ReadTimeout <= 0 {
|
||||
ldapProvider.ReadTimeout = defaultReadTimeout
|
||||
}
|
||||
return &ldapProvider{options: ldapOptions}, nil
|
||||
return &ldapProvider, nil
|
||||
}
|
||||
|
||||
func (l ldapProvider) Authenticate(username string, password string) (*iamv1alpha2.User, error) {
|
||||
type ldapIdentity struct {
|
||||
Username string
|
||||
Email string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetUserID() string {
|
||||
return l.Username
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetUsername() string {
|
||||
return l.Username
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetEmail() string {
|
||||
return l.Email
|
||||
}
|
||||
|
||||
func (l *ldapIdentity) GetDisplayName() string {
|
||||
return l.DisplayName
|
||||
}
|
||||
|
||||
func (l ldapProvider) Authenticate(username string, password string) (identityprovider.Identity, error) {
|
||||
conn, err := l.newConn()
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
conn.SetTimeout(time.Duration(l.options.ReadTimeout) * time.Millisecond)
|
||||
conn.SetTimeout(time.Duration(l.ReadTimeout) * time.Millisecond)
|
||||
defer conn.Close()
|
||||
err = conn.Bind(l.options.ManagerDN, l.options.ManagerPassword)
|
||||
err = conn.Bind(l.ManagerDN, l.ManagerPassword)
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := fmt.Sprintf("(&(%s=%s)%s)", l.options.LoginAttribute, username, l.options.UserSearchFilter)
|
||||
filter := fmt.Sprintf("(&(%s=%s)%s)", l.LoginAttribute, username, l.UserSearchFilter)
|
||||
result, err := conn.Search(&ldap.SearchRequest{
|
||||
BaseDN: l.options.UserSearchBase,
|
||||
BaseDN: l.UserSearchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
SizeLimit: 1,
|
||||
TimeLimit: 0,
|
||||
TypesOnly: false,
|
||||
Filter: filter,
|
||||
Attributes: []string{l.options.LoginAttribute, l.options.MailAttribute, l.options.DisplayNameAttribute},
|
||||
Attributes: []string{l.LoginAttribute, l.MailAttribute, l.DisplayNameAttribute},
|
||||
})
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Entries) == 1 {
|
||||
entry := result.Entries[0]
|
||||
err = conn.Bind(entry.DN, password)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
email := entry.GetAttributeValue(l.options.MailAttribute)
|
||||
displayName := entry.GetAttributeValue(l.options.DisplayNameAttribute)
|
||||
return &iamv1alpha2.User{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: username,
|
||||
Annotations: map[string]string{
|
||||
constants.DisplayNameAnnotationKey: displayName,
|
||||
},
|
||||
},
|
||||
Spec: iamv1alpha2.UserSpec{
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
},
|
||||
}, nil
|
||||
if len(result.Entries) != 1 {
|
||||
return nil, errors.NewUnauthorized("incorrect password")
|
||||
}
|
||||
|
||||
return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, fmt.Errorf("could not find user %s in LDAP directory", username))
|
||||
entry := result.Entries[0]
|
||||
if err = conn.Bind(entry.DN, password); err != nil {
|
||||
klog.Error(err)
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
|
||||
return nil, errors.NewUnauthorized("incorrect password")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
email := entry.GetAttributeValue(l.MailAttribute)
|
||||
displayName := entry.GetAttributeValue(l.DisplayNameAttribute)
|
||||
return &ldapIdentity{
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *ldapProvider) newConn() (*ldap.Conn, error) {
|
||||
if !l.options.StartTLS {
|
||||
return ldap.Dial("tcp", l.options.Host)
|
||||
if !l.StartTLS {
|
||||
return ldap.Dial("tcp", l.Host)
|
||||
}
|
||||
tlsConfig := tls.Config{}
|
||||
if l.options.InsecureSkipVerify {
|
||||
if l.InsecureSkipVerify {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
var caCert []byte
|
||||
var err error
|
||||
// Load CA cert
|
||||
if l.options.RootCA != "" {
|
||||
if caCert, err = ioutil.ReadFile(l.options.RootCA); err != nil {
|
||||
if l.RootCA != "" {
|
||||
if caCert, err = ioutil.ReadFile(l.RootCA); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if l.options.RootCAData != "" {
|
||||
if caCert, err = base64.StdEncoding.DecodeString(l.options.RootCAData); err != nil {
|
||||
if l.RootCAData != "" {
|
||||
if caCert, err = base64.StdEncoding.DecodeString(l.RootCAData); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
@@ -179,5 +196,5 @@ func (l *ldapProvider) newConn() (*ldap.Conn, error) {
|
||||
if caCert != nil {
|
||||
tlsConfig.RootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
return ldap.DialTLS("tcp", l.options.Host, &tlsConfig)
|
||||
return ldap.DialTLS("tcp", l.Host, &tlsConfig)
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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 identityprovider
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -42,12 +40,11 @@ mailAttribute: mail
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provider, err := NewLdapProvider(&dynamicOptions)
|
||||
got, err := new(ldapProviderFactory).Create(&dynamicOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := provider.(*ldapProvider).options
|
||||
expected := ldapOptions{
|
||||
expected := &ldapProvider{
|
||||
Host: "test.sn.mynetname.net:389",
|
||||
StartTLS: false,
|
||||
InsecureSkipVerify: false,
|
||||
@@ -81,14 +78,14 @@ func TestLdapProvider_Authenticate(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var dynamicOptions oauth.DynamicOptions
|
||||
if err := yaml.Unmarshal(options, &dynamicOptions); err != nil {
|
||||
if err = yaml.Unmarshal(options, &dynamicOptions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provider, err := NewLdapProvider(&dynamicOptions)
|
||||
ldapProvider, err := new(ldapProviderFactory).Create(&dynamicOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := provider.Authenticate("test", "test"); err != nil {
|
||||
if _, err = ldapProvider.Authenticate("test", "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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 identityprovider
|
||||
|
||||
import (
|
||||
@@ -24,23 +21,29 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
oauthProviders = make(map[string]OAuthProvider, 0)
|
||||
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
|
||||
builtinOAuthProviders = make(map[string]OAuthProviderFactory)
|
||||
identityProviderNotFound = errors.New("identity provider not found")
|
||||
)
|
||||
|
||||
type OAuthProvider interface {
|
||||
Type() string
|
||||
Setup(options *oauth.DynamicOptions) (OAuthProvider, error)
|
||||
// IdentityExchange exchange identity from remote server
|
||||
IdentityExchange(code string) (Identity, error)
|
||||
}
|
||||
|
||||
func GetOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
|
||||
if provider, ok := oauthProviders[providerType]; ok {
|
||||
return provider.Setup(options)
|
||||
}
|
||||
return nil, ErrorIdentityProviderNotFound
|
||||
type OAuthProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
Create(options *oauth.DynamicOptions) (OAuthProvider, error)
|
||||
}
|
||||
|
||||
func RegisterOAuthProvider(provider OAuthProvider) {
|
||||
oauthProviders[provider.Type()] = provider
|
||||
func CreateOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
|
||||
if provider, ok := builtinOAuthProviders[providerType]; ok {
|
||||
return provider.Create(options)
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
func RegisterOAuthProvider(factory OAuthProviderFactory) {
|
||||
builtinOAuthProviders[factory.Type()] = factory
|
||||
}
|
||||
|
||||
@@ -35,12 +35,13 @@ const (
|
||||
// GrantHandlerDeny auto-denies client authorization grant requests
|
||||
GrantHandlerDeny GrantHandlerType = "deny"
|
||||
// MappingMethodAuto The default value.The user will automatically create and mapping when login successful.
|
||||
// Fails if a user with that user name is already mapped to another identity.
|
||||
// Fails if a user with that username is already mapped to another identity.
|
||||
MappingMethodAuto MappingMethod = "auto"
|
||||
// MappingMethodLookup Looks up an existing identity, user identity mapping, and user, but does not automatically
|
||||
// provision users or identities. Using this method requires you to manually provision users.
|
||||
MappingMethodLookup MappingMethod = "lookup"
|
||||
// MappingMethodMixed A user entity can be mapped with multiple identifyProvider.
|
||||
// not supported yet.
|
||||
MappingMethodMixed MappingMethod = "mixed"
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/aliyunidaas"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/ldap"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"time"
|
||||
)
|
||||
@@ -66,7 +67,6 @@ func (options *AuthenticationOptions) Validate() []error {
|
||||
if len(options.JwtSecret) == 0 {
|
||||
errs = append(errs, fmt.Errorf("jwt secret is empty"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,10 @@ const (
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
UID string `json:"uid"`
|
||||
TokenType TokenType `json:"token_type"`
|
||||
Username string `json:"username"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Extra map[string][]string `json:"extra,omitempty"`
|
||||
TokenType TokenType `json:"token_type"`
|
||||
// Currently, we are not using any field in jwt.StandardClaims
|
||||
jwt.StandardClaims
|
||||
}
|
||||
@@ -51,7 +52,7 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (user.Info, TokenType, error
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
return &user.DefaultInfo{Name: clm.Username, UID: clm.UID}, clm.TokenType, nil
|
||||
return &user.DefaultInfo{Name: clm.Username, Groups: clm.Groups, Extra: clm.Extra}, clm.TokenType, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) IssueTo(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error) {
|
||||
@@ -59,7 +60,8 @@ func (s *jwtTokenIssuer) IssueTo(user user.Info, tokenType TokenType, expiresIn
|
||||
notBefore := issueAt
|
||||
clm := &Claims{
|
||||
Username: user.GetName(),
|
||||
UID: user.GetUID(),
|
||||
Groups: user.GetGroups(),
|
||||
Extra: user.GetExtra(),
|
||||
TokenType: tokenType,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: issueAt,
|
||||
|
||||
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1
|
||||
// Following code copied from k8s.io/kubernetes/pkg/apis/rbac/v1 to avoid import collision
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
// NOTE: This file is copied from k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac.
|
||||
|
||||
package authorizerfactory
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
rbacv1helpers "kubesphere.io/kubesphere/pkg/api/rbac/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -159,14 +158,14 @@ func ruleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule
|
||||
combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
|
||||
}
|
||||
|
||||
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
|
||||
rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
|
||||
rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName())
|
||||
return VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
|
||||
ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
|
||||
ResourceNameMatches(rule, requestAttributes.GetName())
|
||||
}
|
||||
|
||||
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
|
||||
return VerbMatches(rule, requestAttributes.GetVerb()) &&
|
||||
NonResourceURLMatches(rule, requestAttributes.GetPath())
|
||||
}
|
||||
|
||||
func regoPolicyAllows(requestAttributes authorizer.Attributes, regoPolicy string) bool {
|
||||
@@ -1,20 +1,22 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes 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
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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.
|
||||
|
||||
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 authorizerfactory
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -26,26 +26,23 @@ import (
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WithAuthentication installs authentication handler to handler chain.
|
||||
// The following part is a little bit ugly, WithAuthentication also logs user failed login attempt
|
||||
// if using basic auth. But only treats request with requestURI `/oauth/authorize` as login attempt
|
||||
func WithAuthentication(handler http.Handler, auth authenticator.Request, loginRecorder im.LoginRecorder) http.Handler {
|
||||
if auth == nil {
|
||||
func WithAuthentication(handler http.Handler, authRequest authenticator.Request) http.Handler {
|
||||
if authRequest == nil {
|
||||
klog.Warningf("Authentication is disabled")
|
||||
return handler
|
||||
}
|
||||
s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
resp, ok, err := auth.AuthenticateRequest(req)
|
||||
username, _, usingBasicAuth := req.BasicAuth()
|
||||
resp, ok, err := authRequest.AuthenticateRequest(req)
|
||||
_, _, usingBasicAuth := req.BasicAuth()
|
||||
|
||||
defer func() {
|
||||
// if we authenticated successfully, go ahead and remove the bearer token so that no one
|
||||
@@ -56,41 +53,17 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request, loginR
|
||||
}()
|
||||
|
||||
if err != nil || !ok {
|
||||
if err != nil {
|
||||
klog.Errorf("Unable to authenticate the request due to error: %v", err)
|
||||
if usingBasicAuth && err.Error() == im.AuthFailedIncorrectPassword.Error() { // log failed login attempts
|
||||
go func(user string) {
|
||||
if loginRecorder != nil && len(user) != 0 {
|
||||
err = loginRecorder.RecordLogin(user, iamv1alpha2.BasicAuth, "", err, req)
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", user, err)
|
||||
}
|
||||
}(username)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
requestInfo, found := request.RequestInfoFrom(ctx)
|
||||
if !found {
|
||||
responsewriters.InternalError(w, req, errors.New("no RequestInfo found in the context"))
|
||||
return
|
||||
}
|
||||
|
||||
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||
if err != nil && err.Error() == im.AuthRateLimitExceeded.Error() {
|
||||
responsewriters.ErrorNegotiated(apierrors.NewTooManyRequests(fmt.Sprintf("Unauthorized: %s", err), 60), s, gv, w, req)
|
||||
} else {
|
||||
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)), s, gv, w, req)
|
||||
}
|
||||
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)), s, gv, w, req)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if loginRecorder != nil && usingBasicAuth && strings.HasPrefix(req.URL.Path, "/oauth/authorize") {
|
||||
err = loginRecorder.RecordLogin(username, iamv1alpha2.BasicAuth, "", nil, req)
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
}()
|
||||
|
||||
req = req.WithContext(request.WithUser(req.Context(), resp.User))
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
netutils "kubesphere.io/kubesphere/pkg/utils/net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -74,6 +75,12 @@ type RequestInfo struct {
|
||||
|
||||
// Scope of requested resource.
|
||||
ResourceScope string
|
||||
|
||||
// Source IP
|
||||
SourceIP string
|
||||
|
||||
// User agent
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
type RequestInfoFactory struct {
|
||||
@@ -119,6 +126,8 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||
},
|
||||
Workspace: api.WorkspaceNone,
|
||||
Cluster: api.ClusterNone,
|
||||
SourceIP: netutils.GetRequestIP(req),
|
||||
UserAgent: req.UserAgent(),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
162
pkg/controller/loginrecord/loginrecord_controller.go
Normal file
162
pkg/controller/loginrecord/loginrecord_controller.go
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package loginrecord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"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"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
iamv1alpha2informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam/v1alpha2"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/controller/utils/controller"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// SuccessSynced is used as part of the Event 'reason' when a Foo is synced
|
||||
successSynced = "Synced"
|
||||
// is synced successfully
|
||||
messageResourceSynced = "LoginRecord synced successfully"
|
||||
controllerName = "loginrecord-controller"
|
||||
)
|
||||
|
||||
type loginRecordController struct {
|
||||
controller.BaseController
|
||||
k8sClient kubernetes.Interface
|
||||
ksClient kubesphere.Interface
|
||||
loginRecordLister iamv1alpha2listers.LoginRecordLister
|
||||
loginRecordSynced cache.InformerSynced
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
userSynced cache.InformerSynced
|
||||
loginHistoryRetentionPeriod time.Duration
|
||||
// recorder is an event recorder for recording Event resources to the
|
||||
// Kubernetes API.
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func NewLoginRecordController(k8sClient kubernetes.Interface,
|
||||
ksClient kubesphere.Interface,
|
||||
loginRecordInformer iamv1alpha2informers.LoginRecordInformer,
|
||||
userInformer iamv1alpha2informers.UserInformer,
|
||||
loginHistoryRetentionPeriod time.Duration) *loginRecordController {
|
||||
|
||||
klog.V(4).Info("Creating event broadcaster")
|
||||
eventBroadcaster := record.NewBroadcaster()
|
||||
eventBroadcaster.StartLogging(klog.Infof)
|
||||
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: k8sClient.CoreV1().Events("")})
|
||||
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerName})
|
||||
ctl := &loginRecordController{
|
||||
BaseController: controller.BaseController{
|
||||
Workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "LoginRecords"),
|
||||
Synced: []cache.InformerSynced{loginRecordInformer.Informer().HasSynced, userInformer.Informer().HasSynced},
|
||||
Name: controllerName,
|
||||
},
|
||||
k8sClient: k8sClient,
|
||||
ksClient: ksClient,
|
||||
loginRecordLister: loginRecordInformer.Lister(),
|
||||
userLister: userInformer.Lister(),
|
||||
loginHistoryRetentionPeriod: loginHistoryRetentionPeriod,
|
||||
recorder: recorder,
|
||||
}
|
||||
ctl.Handler = ctl.reconcile
|
||||
klog.Info("Setting up event handlers")
|
||||
loginRecordInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: ctl.Enqueue,
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
ctl.Enqueue(new)
|
||||
},
|
||||
DeleteFunc: ctl.Enqueue,
|
||||
})
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (c *loginRecordController) Start(stopCh <-chan struct{}) error {
|
||||
return c.Run(5, stopCh)
|
||||
}
|
||||
|
||||
func (c *loginRecordController) reconcile(key string) error {
|
||||
loginRecord, err := c.loginRecordLister.Get(key)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
utilruntime.HandleError(fmt.Errorf("login record '%s' in work queue no longer exists", key))
|
||||
return nil
|
||||
}
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !loginRecord.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// The object is being deleted
|
||||
// Our finalizer has finished, so the reconciler can do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.updateUserLastLoginTime(loginRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// login record beyonds retention period
|
||||
if loginRecord.CreationTimestamp.Add(c.loginHistoryRetentionPeriod).Before(now) {
|
||||
if err = c.ksClient.IamV1alpha2().LoginRecords().Delete(loginRecord.Name, metav1.NewDeleteOptions(0)); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
} else { // put item back into the queue
|
||||
c.Workqueue.AddAfter(key, loginRecord.CreationTimestamp.Add(c.loginHistoryRetentionPeriod).Sub(now))
|
||||
}
|
||||
c.recorder.Event(loginRecord, corev1.EventTypeNormal, successSynced, messageResourceSynced)
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUserLastLoginTime accepts a login object and set user lastLoginTime field
|
||||
func (c *loginRecordController) updateUserLastLoginTime(loginRecord *iamv1alpha2.LoginRecord) error {
|
||||
username, ok := loginRecord.Labels[iamv1alpha2.UserReferenceLabel]
|
||||
if !ok || len(username) == 0 {
|
||||
klog.V(4).Info("login doesn't belong to any user")
|
||||
return nil
|
||||
}
|
||||
user, err := c.userLister.Get(username)
|
||||
if err != nil {
|
||||
// ignore not found error
|
||||
if errors.IsNotFound(err) {
|
||||
klog.V(4).Infof("user %s doesn't exist any more, login record will be deleted later", username)
|
||||
return nil
|
||||
}
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
// update lastLoginTime
|
||||
if user.DeletionTimestamp.IsZero() &&
|
||||
(user.Status.LastLoginTime == nil || user.Status.LastLoginTime.Before(&loginRecord.CreationTimestamp)) {
|
||||
user.Status.LastLoginTime = &loginRecord.CreationTimestamp
|
||||
user, err = c.ksClient.IamV1alpha2().Users().UpdateStatus(user)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
265
pkg/controller/loginrecord/loginrecord_controller_test.go
Normal file
265
pkg/controller/loginrecord/loginrecord_controller_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package loginrecord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
k8sfake "k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
alwaysReady = func() bool { return true }
|
||||
noResyncPeriodFunc = func() time.Duration { return 0 }
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
t *testing.T
|
||||
|
||||
ksclient *fake.Clientset
|
||||
k8sclient *k8sfake.Clientset
|
||||
// Objects to put in the store.
|
||||
user *iamv1alpha2.User
|
||||
loginRecord *iamv1alpha2.LoginRecord
|
||||
// Actions expected to happen on the client.
|
||||
kubeactions []core.Action
|
||||
actions []core.Action
|
||||
// Objects from here preloaded into NewSimpleFake.
|
||||
kubeobjects []runtime.Object
|
||||
objects []runtime.Object
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
f := &fixture{}
|
||||
f.t = t
|
||||
f.objects = []runtime.Object{}
|
||||
f.kubeobjects = []runtime.Object{}
|
||||
return f
|
||||
}
|
||||
|
||||
func newUser(name string) *iamv1alpha2.User {
|
||||
return &iamv1alpha2.User{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: iamv1alpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: iamv1alpha2.UserSpec{
|
||||
Email: fmt.Sprintf("%s@kubesphere.io", name),
|
||||
Lang: "zh-CN",
|
||||
Description: "fake user",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newLoginRecord(username string) *iamv1alpha2.LoginRecord {
|
||||
return &iamv1alpha2.LoginRecord{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: iamv1alpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: username,
|
||||
CreationTimestamp: metav1.Now(),
|
||||
Labels: map[string]string{iamv1alpha2.UserReferenceLabel: username},
|
||||
},
|
||||
Spec: iamv1alpha2.LoginRecordSpec{
|
||||
Type: iamv1alpha2.Token,
|
||||
Success: true,
|
||||
Reason: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fixture) newController() (*loginRecordController, ksinformers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {
|
||||
f.ksclient = fake.NewSimpleClientset(f.objects...)
|
||||
f.k8sclient = k8sfake.NewSimpleClientset(f.kubeobjects...)
|
||||
|
||||
ksInformers := ksinformers.NewSharedInformerFactory(f.ksclient, noResyncPeriodFunc())
|
||||
k8sInformers := kubeinformers.NewSharedInformerFactory(f.k8sclient, noResyncPeriodFunc())
|
||||
if err := ksInformers.Iam().V1alpha2().Users().Informer().GetIndexer().Add(f.user); err != nil {
|
||||
f.t.Errorf("add user:%s", err)
|
||||
}
|
||||
if err := ksInformers.Iam().V1alpha2().LoginRecords().Informer().GetIndexer().Add(f.loginRecord); err != nil {
|
||||
f.t.Errorf("add login record:%s", err)
|
||||
}
|
||||
|
||||
c := NewLoginRecordController(f.k8sclient, f.ksclient,
|
||||
ksInformers.Iam().V1alpha2().LoginRecords(),
|
||||
ksInformers.Iam().V1alpha2().Users(),
|
||||
time.Minute*5)
|
||||
c.userSynced = alwaysReady
|
||||
c.loginRecordSynced = alwaysReady
|
||||
c.recorder = &record.FakeRecorder{}
|
||||
|
||||
return c, ksInformers, k8sInformers
|
||||
}
|
||||
|
||||
func (f *fixture) run(userName string) {
|
||||
f.runController(userName, true, false)
|
||||
}
|
||||
|
||||
func (f *fixture) runExpectError(userName string) {
|
||||
f.runController(userName, true, true)
|
||||
}
|
||||
|
||||
func (f *fixture) runController(user string, startInformers bool, expectError bool) {
|
||||
c, i, k8sI := f.newController()
|
||||
if startInformers {
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
i.Start(stopCh)
|
||||
k8sI.Start(stopCh)
|
||||
}
|
||||
|
||||
err := c.reconcile(user)
|
||||
if !expectError && err != nil {
|
||||
f.t.Errorf("error syncing user: %v", err)
|
||||
} else if expectError && err == nil {
|
||||
f.t.Error("expected error syncing user, got nil")
|
||||
}
|
||||
|
||||
actions := filterInformerActions(f.ksclient.Actions())
|
||||
for j, action := range actions {
|
||||
if len(f.actions) < j+1 {
|
||||
f.t.Errorf("%d unexpected actions: %+v", len(actions)-len(f.actions), actions[j:])
|
||||
break
|
||||
}
|
||||
|
||||
expectedAction := f.actions[j]
|
||||
checkAction(expectedAction, action, f.t)
|
||||
}
|
||||
|
||||
if len(f.actions) > len(actions) {
|
||||
f.t.Errorf("%d additional expected actions:%+v", len(f.actions)-len(actions), f.actions[len(actions):])
|
||||
}
|
||||
|
||||
k8sActions := filterInformerActions(f.k8sclient.Actions())
|
||||
for k, action := range k8sActions {
|
||||
if len(f.kubeactions) < k+1 {
|
||||
f.t.Errorf("%d unexpected actions: %+v", len(k8sActions)-len(f.kubeactions), k8sActions[k:])
|
||||
break
|
||||
}
|
||||
|
||||
expectedAction := f.kubeactions[k]
|
||||
checkAction(expectedAction, action, f.t)
|
||||
}
|
||||
|
||||
if len(f.kubeactions) > len(k8sActions) {
|
||||
f.t.Errorf("%d additional expected actions:%+v", len(f.kubeactions)-len(k8sActions), f.kubeactions[len(k8sActions):])
|
||||
}
|
||||
}
|
||||
|
||||
// checkAction verifies that expected and actual actions are equal and both have
|
||||
// same attached resources
|
||||
func checkAction(expected, actual core.Action, t *testing.T) {
|
||||
if !(expected.Matches(actual.GetVerb(), actual.GetResource().Resource) && actual.GetSubresource() == expected.GetSubresource()) {
|
||||
t.Errorf("Expected\n\t%#v\ngot\n\t%#v", expected, actual)
|
||||
return
|
||||
}
|
||||
|
||||
if reflect.TypeOf(actual) != reflect.TypeOf(expected) {
|
||||
t.Errorf("Action has wrong type. Expected: %t. Got: %t", expected, actual)
|
||||
return
|
||||
}
|
||||
|
||||
switch a := actual.(type) {
|
||||
case core.CreateActionImpl:
|
||||
e, _ := expected.(core.CreateActionImpl)
|
||||
expObject := e.GetObject()
|
||||
object := a.GetObject()
|
||||
if !reflect.DeepEqual(expObject, object) {
|
||||
t.Errorf("Action %s %s has wrong object\nDiff:\n %s",
|
||||
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object))
|
||||
}
|
||||
case core.UpdateActionImpl:
|
||||
e, _ := expected.(core.UpdateActionImpl)
|
||||
expObject := e.GetObject()
|
||||
object := a.GetObject()
|
||||
expUser := expObject.(*iamv1alpha2.User)
|
||||
user := object.(*iamv1alpha2.User)
|
||||
expUser.Status.LastTransitionTime = nil
|
||||
user.Status.LastTransitionTime = nil
|
||||
if !reflect.DeepEqual(expUser, user) {
|
||||
t.Errorf("Action %s %s has wrong object\nDiff:\n %s",
|
||||
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object))
|
||||
}
|
||||
case core.PatchActionImpl:
|
||||
e, _ := expected.(core.PatchActionImpl)
|
||||
expPatch := e.GetPatch()
|
||||
patch := a.GetPatch()
|
||||
if !reflect.DeepEqual(expPatch, patch) {
|
||||
t.Errorf("Action %s %s has wrong patch\nDiff:\n %s",
|
||||
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expPatch, patch))
|
||||
}
|
||||
default:
|
||||
t.Errorf("Uncaptured Action %s %s, you should explicitly add a case to capture it",
|
||||
actual.GetVerb(), actual.GetResource().Resource)
|
||||
}
|
||||
}
|
||||
|
||||
// filterInformerActions filters list and watch actions for testing resources.
|
||||
// Since list and watch don't change resource state we can filter it to lower
|
||||
// nose level in our tests.
|
||||
func filterInformerActions(actions []core.Action) []core.Action {
|
||||
var ret []core.Action
|
||||
for _, action := range actions {
|
||||
ret = append(ret, action)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *fixture) expectUpdateUserStatusAction(user *iamv1alpha2.User) {
|
||||
expect := user.DeepCopy()
|
||||
action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "users"}, "", expect)
|
||||
action.Subresource = "status"
|
||||
expect.Status.LastLoginTime = &f.loginRecord.CreationTimestamp
|
||||
f.actions = append(f.actions, action)
|
||||
}
|
||||
|
||||
func getKey(user *iamv1alpha2.User, t *testing.T) string {
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(user)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error getting key for user %v: %v", user.Name, err)
|
||||
return ""
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func TestDoNothing(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
user := newUser("test")
|
||||
loginRecord := newLoginRecord("test")
|
||||
|
||||
f.user = user
|
||||
f.loginRecord = loginRecord
|
||||
f.objects = append(f.objects, user, loginRecord)
|
||||
|
||||
f.expectUpdateUserStatusAction(user)
|
||||
f.run(getKey(user, t))
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
* /
|
||||
*/
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
iamv1alpha2informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam/v1alpha2"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoginRecordController struct {
|
||||
k8sClient kubernetes.Interface
|
||||
ksClient kubesphere.Interface
|
||||
loginRecordInformer iamv1alpha2informers.LoginRecordInformer
|
||||
loginRecordLister iamv1alpha2listers.LoginRecordLister
|
||||
loginRecordSynced cache.InformerSynced
|
||||
// workqueue is a rate limited work queue. This is used to queue work to be
|
||||
// processed instead of performing it as soon as a change happens. This
|
||||
// means we can ensure we only process a fixed amount of resources at a
|
||||
// time, and makes it easy to ensure we are never processing the same item
|
||||
// simultaneously in two different workers.
|
||||
workqueue workqueue.RateLimitingInterface
|
||||
// recorder is an event recorder for recording Event resources to the
|
||||
// Kubernetes API.
|
||||
recorder record.EventRecorder
|
||||
loginHistoryRetentionPeriod time.Duration
|
||||
}
|
||||
|
||||
func NewLoginRecordController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
|
||||
loginRecordInformer iamv1alpha2informers.LoginRecordInformer,
|
||||
loginHistoryRetentionPeriod time.Duration) *LoginRecordController {
|
||||
|
||||
klog.V(4).Info("Creating event broadcaster")
|
||||
eventBroadcaster := record.NewBroadcaster()
|
||||
eventBroadcaster.StartLogging(klog.Infof)
|
||||
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: k8sClient.CoreV1().Events("")})
|
||||
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "loginrecord-controller"})
|
||||
ctl := &LoginRecordController{
|
||||
k8sClient: k8sClient,
|
||||
ksClient: ksClient,
|
||||
loginRecordInformer: loginRecordInformer,
|
||||
loginRecordLister: loginRecordInformer.Lister(),
|
||||
loginRecordSynced: loginRecordInformer.Informer().HasSynced,
|
||||
loginHistoryRetentionPeriod: loginHistoryRetentionPeriod,
|
||||
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "loginrecord"),
|
||||
recorder: recorder,
|
||||
}
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) Run(threadiness int, stopCh <-chan struct{}) error {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.workqueue.ShutDown()
|
||||
|
||||
// Start the informer factories to begin populating the informer caches
|
||||
klog.Info("Starting LoginRecord controller")
|
||||
|
||||
// Wait for the caches to be synced before starting workers
|
||||
klog.Info("Waiting for informer caches to sync")
|
||||
|
||||
if ok := cache.WaitForCacheSync(stopCh, c.loginRecordSynced); !ok {
|
||||
return fmt.Errorf("failed to wait for caches to sync")
|
||||
}
|
||||
|
||||
klog.Info("Starting workers")
|
||||
// Launch two workers to process Foo resources
|
||||
for i := 0; i < threadiness; i++ {
|
||||
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||
}
|
||||
|
||||
go wait.Until(func() {
|
||||
if err := c.sync(); err != nil {
|
||||
klog.Errorf("Error periodically sync user status, %v", err)
|
||||
}
|
||||
}, time.Hour, stopCh)
|
||||
|
||||
klog.Info("Started workers")
|
||||
<-stopCh
|
||||
klog.Info("Shutting down workers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) enqueueLoginRecord(obj interface{}) {
|
||||
var key string
|
||||
var err error
|
||||
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
c.workqueue.Add(key)
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) runWorker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) processNextWorkItem() bool {
|
||||
obj, shutdown := c.workqueue.Get()
|
||||
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
// We wrap this block in a func so we can defer c.workqueue.Done.
|
||||
err := func(obj interface{}) error {
|
||||
// We call Done here so the workqueue knows we have finished
|
||||
// processing this item. We also must remember to call Forget if we
|
||||
// do not want this work item being re-queued. For example, we do
|
||||
// not call Forget if a transient error occurs, instead the item is
|
||||
// put back on the workqueue and attempted again after a back-off
|
||||
// period.
|
||||
defer c.workqueue.Done(obj)
|
||||
var key string
|
||||
var ok bool
|
||||
// We expect strings to come off the workqueue. These are of the
|
||||
// form namespace/name. We do this as the delayed nature of the
|
||||
// workqueue means the items in the informer cache may actually be
|
||||
// more up to date that when the item was initially put onto the
|
||||
// workqueue.
|
||||
if key, ok = obj.(string); !ok {
|
||||
// As the item in the workqueue is actually invalid, we call
|
||||
// Forget here else we'd go into a loop of attempting to
|
||||
// process a work item that is invalid.
|
||||
c.workqueue.Forget(obj)
|
||||
utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
|
||||
return nil
|
||||
}
|
||||
// Run the reconcile, passing it the namespace/name string of the
|
||||
// Foo resource to be synced.
|
||||
if err := c.reconcile(key); err != nil {
|
||||
// Put the item back on the workqueue to handle any transient errors.
|
||||
c.workqueue.AddRateLimited(key)
|
||||
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
|
||||
}
|
||||
// Finally, if no error occurs we Forget this item so it does not
|
||||
// get queued again until another change happens.
|
||||
c.workqueue.Forget(obj)
|
||||
klog.Infof("Successfully synced %s:%s", "key", key)
|
||||
return nil
|
||||
}(obj)
|
||||
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) reconcile(key string) error {
|
||||
loginRecord, err := c.loginRecordLister.Get(key)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
utilruntime.HandleError(fmt.Errorf("login record '%s' in work queue no longer exists", key))
|
||||
return nil
|
||||
}
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if loginRecord.CreationTimestamp.Add(c.loginHistoryRetentionPeriod).Before(now) { // login record beyonds retention period
|
||||
if err = c.ksClient.IamV1alpha2().LoginRecords().Delete(loginRecord.Name, metav1.NewDeleteOptions(0)); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
} else { // put item back into the queue
|
||||
c.workqueue.AddAfter(key, loginRecord.CreationTimestamp.Add(c.loginHistoryRetentionPeriod).Sub(now))
|
||||
}
|
||||
c.recorder.Event(loginRecord, corev1.EventTypeNormal, successSynced, messageResourceSynced)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) Start(stopCh <-chan struct{}) error {
|
||||
return c.Run(4, stopCh)
|
||||
}
|
||||
|
||||
func (c *LoginRecordController) sync() error {
|
||||
records, err := c.loginRecordLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
key, err := cache.MetaNamespaceKeyFunc(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.workqueue.AddRateLimited(key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,8 +19,8 @@ package user
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"kubesphere.io/kubesphere/pkg/controller/utils/controller"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
@@ -62,46 +61,36 @@ const (
|
||||
// is synced successfully
|
||||
messageResourceSynced = "User synced successfully"
|
||||
controllerName = "user-controller"
|
||||
|
||||
// user finalizer
|
||||
finalizer = "finalizers.kubesphere.io/users"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
k8sClient kubernetes.Interface
|
||||
ksClient kubesphere.Interface
|
||||
kubeconfig kubeconfig.Interface
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
userSynced cache.InformerSynced
|
||||
loginRecordLister iamv1alpha2listers.LoginRecordLister
|
||||
loginRecordSynced cache.InformerSynced
|
||||
cmSynced cache.InformerSynced
|
||||
fedUserCache cache.Store
|
||||
fedUserController cache.Controller
|
||||
ldapClient ldapclient.Interface
|
||||
devopsClient devops.Interface
|
||||
// workqueue is a rate limited work queue. This is used to queue work to be
|
||||
// processed instead of performing it as soon as a change happens. This
|
||||
// means we can ensure we only process a fixed amount of resources at a
|
||||
// time, and makes it easy to ensure we are never processing the same item
|
||||
// simultaneously in two different workers.
|
||||
workqueue workqueue.RateLimitingInterface
|
||||
// recorder is an event recorder for recording Event resources to the
|
||||
// Kubernetes API.
|
||||
recorder record.EventRecorder
|
||||
type userController struct {
|
||||
controller.BaseController
|
||||
k8sClient kubernetes.Interface
|
||||
ksClient kubesphere.Interface
|
||||
kubeconfig kubeconfig.Interface
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
loginRecordLister iamv1alpha2listers.LoginRecordLister
|
||||
fedUserCache cache.Store
|
||||
ldapClient ldapclient.Interface
|
||||
devopsClient devops.Interface
|
||||
authenticationOptions *authoptions.AuthenticationOptions
|
||||
multiClusterEnabled bool
|
||||
// recorder is an event recorder for recording Event resources to the
|
||||
// Kubernetes API.
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func NewUserController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
|
||||
config *rest.Config, userInformer iamv1alpha2informers.UserInformer,
|
||||
fedUserCache cache.Store, fedUserController cache.Controller,
|
||||
func NewUserController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface, config *rest.Config,
|
||||
userInformer iamv1alpha2informers.UserInformer,
|
||||
loginRecordInformer iamv1alpha2informers.LoginRecordInformer,
|
||||
fedUserCache cache.Store, fedUserController cache.Controller,
|
||||
configMapInformer corev1informers.ConfigMapInformer,
|
||||
ldapClient ldapclient.Interface,
|
||||
devopsClient devops.Interface,
|
||||
authenticationOptions *authoptions.AuthenticationOptions,
|
||||
multiClusterEnabled bool) *Controller {
|
||||
multiClusterEnabled bool) *userController {
|
||||
|
||||
utilruntime.Must(kubespherescheme.AddToScheme(scheme.Scheme))
|
||||
|
||||
@@ -113,152 +102,48 @@ func NewUserController(k8sClient kubernetes.Interface, ksClient kubesphere.Inter
|
||||
if config != nil {
|
||||
kubeconfigOperator = kubeconfig.NewOperator(k8sClient, configMapInformer, config)
|
||||
}
|
||||
ctl := &Controller{
|
||||
ctl := &userController{
|
||||
BaseController: controller.BaseController{
|
||||
Workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "User"),
|
||||
Synced: []cache.InformerSynced{
|
||||
userInformer.Informer().HasSynced,
|
||||
configMapInformer.Informer().HasSynced,
|
||||
loginRecordInformer.Informer().HasSynced,
|
||||
},
|
||||
Name: controllerName,
|
||||
},
|
||||
k8sClient: k8sClient,
|
||||
ksClient: ksClient,
|
||||
kubeconfig: kubeconfigOperator,
|
||||
userLister: userInformer.Lister(),
|
||||
userSynced: userInformer.Informer().HasSynced,
|
||||
loginRecordLister: loginRecordInformer.Lister(),
|
||||
loginRecordSynced: loginRecordInformer.Informer().HasSynced,
|
||||
cmSynced: configMapInformer.Informer().HasSynced,
|
||||
fedUserCache: fedUserCache,
|
||||
fedUserController: fedUserController,
|
||||
ldapClient: ldapClient,
|
||||
devopsClient: devopsClient,
|
||||
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Users"),
|
||||
recorder: recorder,
|
||||
multiClusterEnabled: multiClusterEnabled,
|
||||
authenticationOptions: authenticationOptions,
|
||||
}
|
||||
|
||||
if multiClusterEnabled {
|
||||
ctl.Synced = append(ctl.Synced, fedUserController.HasSynced)
|
||||
}
|
||||
ctl.Handler = ctl.reconcile
|
||||
klog.Info("Setting up event handlers")
|
||||
userInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: ctl.enqueueUser,
|
||||
AddFunc: ctl.Enqueue,
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
ctl.enqueueUser(new)
|
||||
},
|
||||
DeleteFunc: ctl.enqueueUser,
|
||||
})
|
||||
|
||||
loginRecordInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(new interface{}) {
|
||||
if err := ctl.enqueueLogin(new); err != nil {
|
||||
klog.Errorf("Failed to enqueue login object, error: %v", err)
|
||||
}
|
||||
ctl.Enqueue(new)
|
||||
},
|
||||
DeleteFunc: ctl.Enqueue,
|
||||
})
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.workqueue.ShutDown()
|
||||
|
||||
// Start the informer factories to begin populating the informer caches
|
||||
klog.Info("Starting User controller")
|
||||
|
||||
// Wait for the caches to be synced before starting workers
|
||||
klog.Info("Waiting for informer caches to sync")
|
||||
|
||||
synced := make([]cache.InformerSynced, 0)
|
||||
synced = append(synced, c.userSynced, c.loginRecordSynced, c.cmSynced)
|
||||
if c.multiClusterEnabled {
|
||||
synced = append(synced, c.fedUserController.HasSynced)
|
||||
}
|
||||
|
||||
if ok := cache.WaitForCacheSync(stopCh, synced...); !ok {
|
||||
return fmt.Errorf("failed to wait for caches to sync")
|
||||
}
|
||||
|
||||
klog.Info("Starting workers")
|
||||
// Launch two workers to process Foo resources
|
||||
for i := 0; i < threadiness; i++ {
|
||||
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||
}
|
||||
|
||||
klog.Info("Started workers")
|
||||
<-stopCh
|
||||
klog.Info("Shutting down workers")
|
||||
return nil
|
||||
func (c *userController) Start(stopCh <-chan struct{}) error {
|
||||
return c.Run(5, stopCh)
|
||||
}
|
||||
|
||||
func (c *Controller) enqueueUser(obj interface{}) {
|
||||
var key string
|
||||
var err error
|
||||
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
c.workqueue.Add(key)
|
||||
}
|
||||
|
||||
func (c *Controller) runWorker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) processNextWorkItem() bool {
|
||||
obj, shutdown := c.workqueue.Get()
|
||||
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
err := func(obj interface{}) error {
|
||||
defer c.workqueue.Done(obj)
|
||||
var key string
|
||||
var ok bool
|
||||
if key, ok = obj.(string); !ok {
|
||||
c.workqueue.Forget(obj)
|
||||
utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
|
||||
return nil
|
||||
}
|
||||
if err := c.reconcile(key); err != nil {
|
||||
c.workqueue.AddRateLimited(key)
|
||||
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
|
||||
}
|
||||
c.workqueue.Forget(obj)
|
||||
klog.Infof("Successfully synced %s:%s", "key", key)
|
||||
return nil
|
||||
}(obj)
|
||||
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// enqueueLogin accepts a login object and set user lastLoginTime field
|
||||
func (c *Controller) enqueueLogin(object interface{}) error {
|
||||
login := object.(*iamv1alpha2.LoginRecord)
|
||||
username, ok := login.Labels[iamv1alpha2.UserReferenceLabel]
|
||||
|
||||
if !ok || len(username) == 0 {
|
||||
return fmt.Errorf("login doesn't belong to any user")
|
||||
}
|
||||
|
||||
user, err := c.userLister.Get(username)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("user %s doesn't exist any more, login record will be deleted later", username)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Status.LastLoginTime == nil || user.Status.LastLoginTime.Before(&login.CreationTimestamp) {
|
||||
user.Status.LastLoginTime = &login.CreationTimestamp
|
||||
user, err = c.ksClient.IamV1alpha2().Users().Update(user)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) reconcile(key string) error {
|
||||
|
||||
func (c *userController) reconcile(key string) error {
|
||||
// Get the user with this name
|
||||
user, err := c.userLister.Get(key)
|
||||
if err != nil {
|
||||
@@ -307,7 +192,7 @@ func (c *Controller) reconcile(key string) error {
|
||||
|
||||
if c.devopsClient != nil {
|
||||
// unassign jenkins role, unassign multiple times is allowed
|
||||
if err := c.unassignDevOpsAdminRole(user); err != nil {
|
||||
if err = c.unassignDevOpsAdminRole(user); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
@@ -340,7 +225,7 @@ func (c *Controller) reconcile(key string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if user, err = c.ensurePasswordIsEncrypted(user); err != nil {
|
||||
if user, err = c.encryptPassword(user); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
@@ -361,7 +246,7 @@ func (c *Controller) reconcile(key string) error {
|
||||
if c.devopsClient != nil {
|
||||
// assign jenkins role after user create, assign multiple times is allowed
|
||||
// used as logged-in users can do anything
|
||||
if err := c.assignDevOpsAdminRole(user); err != nil {
|
||||
if err = c.assignDevOpsAdminRole(user); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
@@ -379,14 +264,9 @@ func (c *Controller) reconcile(key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) Start(stopCh <-chan struct{}) error {
|
||||
return c.Run(5, stopCh)
|
||||
}
|
||||
|
||||
func (c *Controller) ensurePasswordIsEncrypted(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
encrypted := user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] == "true"
|
||||
// password is not encrypted
|
||||
if !encrypted {
|
||||
func (c *userController) encryptPassword(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
// password is not empty and not encrypted
|
||||
if user.Spec.EncryptedPassword != "" && !isEncrypted(user.Spec.EncryptedPassword) {
|
||||
password, err := encrypt(user.Spec.EncryptedPassword)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
@@ -395,22 +275,16 @@ func (c *Controller) ensurePasswordIsEncrypted(user *iamv1alpha2.User) (*iamv1al
|
||||
user = user.DeepCopy()
|
||||
user.Spec.EncryptedPassword = password
|
||||
if user.Annotations == nil {
|
||||
user.Annotations = make(map[string]string, 0)
|
||||
user.Annotations = make(map[string]string)
|
||||
}
|
||||
// ensure plain text password won't be kept anywhere
|
||||
delete(user.Annotations, corev1.LastAppliedConfigAnnotation)
|
||||
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = "true"
|
||||
user.Status = iamv1alpha2.UserStatus{
|
||||
State: iamv1alpha2.UserActive,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
return c.ksClient.IamV1alpha2().Users().Update(user)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureNotControlledByKubefed(user *iamv1alpha2.User) error {
|
||||
func (c *userController) ensureNotControlledByKubefed(user *iamv1alpha2.User) error {
|
||||
if user.Labels[constants.KubefedManagedLabel] != "false" {
|
||||
if user.Labels == nil {
|
||||
user.Labels = make(map[string]string, 0)
|
||||
@@ -425,7 +299,7 @@ func (c *Controller) ensureNotControlledByKubefed(user *iamv1alpha2.User) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) multiClusterSync(user *iamv1alpha2.User) error {
|
||||
func (c *userController) multiClusterSync(user *iamv1alpha2.User) error {
|
||||
|
||||
if err := c.ensureNotControlledByKubefed(user); err != nil {
|
||||
klog.Error(err)
|
||||
@@ -458,7 +332,7 @@ func (c *Controller) multiClusterSync(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) createFederatedUser(user *iamv1alpha2.User) error {
|
||||
func (c *userController) createFederatedUser(user *iamv1alpha2.User) error {
|
||||
federatedUser := &iamv1alpha2.FederatedUser{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamv1alpha2.FedUserKind,
|
||||
@@ -506,14 +380,13 @@ func (c *Controller) createFederatedUser(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) updateFederatedUser(fedUser *iamv1alpha2.FederatedUser) error {
|
||||
func (c *userController) updateFederatedUser(fedUser *iamv1alpha2.FederatedUser) error {
|
||||
data, err := json.Marshal(fedUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli := c.k8sClient.(*kubernetes.Clientset)
|
||||
|
||||
err = cli.RESTClient().Put().
|
||||
AbsPath(fmt.Sprintf("/apis/%s/%s/%s/%s", iamv1alpha2.FedUserResource.Group,
|
||||
iamv1alpha2.FedUserResource.Version, iamv1alpha2.FedUserResource.Name, fedUser.Name)).
|
||||
@@ -529,7 +402,7 @@ func (c *Controller) updateFederatedUser(fedUser *iamv1alpha2.FederatedUser) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) assignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
func (c *userController) assignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
if err := c.devopsClient.AssignGlobalRole(modelsdevops.JenkinsAdminRoleName, user.Name); err != nil {
|
||||
klog.Errorf("%+v", err)
|
||||
return err
|
||||
@@ -537,7 +410,7 @@ func (c *Controller) assignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) unassignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
func (c *userController) unassignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
if err := c.devopsClient.UnAssignGlobalRole(modelsdevops.JenkinsAdminRoleName, user.Name); err != nil {
|
||||
klog.Errorf("%+v", err)
|
||||
return err
|
||||
@@ -545,9 +418,8 @@ func (c *Controller) unassignDevOpsAdminRole(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ldapSync(user *iamv1alpha2.User) error {
|
||||
encrypted, _ := strconv.ParseBool(user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation])
|
||||
if encrypted {
|
||||
func (c *userController) ldapSync(user *iamv1alpha2.User) error {
|
||||
if isEncrypted(user.Spec.EncryptedPassword) {
|
||||
return nil
|
||||
}
|
||||
_, err := c.ldapClient.Get(user.Name)
|
||||
@@ -564,14 +436,12 @@ func (c *Controller) ldapSync(user *iamv1alpha2.User) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) deleteGroupBindings(user *iamv1alpha2.User) error {
|
||||
|
||||
func (c *userController) deleteGroupBindings(user *iamv1alpha2.User) error {
|
||||
// Groupbindings that created by kubeshpere will be deleted directly.
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{iamv1alpha2.UserReferenceLabel: user.Name}).String(),
|
||||
}
|
||||
deleteOptions := metav1.NewDeleteOptions(0)
|
||||
|
||||
if err := c.ksClient.IamV1alpha2().GroupBindings().
|
||||
DeleteCollection(deleteOptions, listOptions); err != nil {
|
||||
klog.Error(err)
|
||||
@@ -580,12 +450,11 @@ func (c *Controller) deleteGroupBindings(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) deleteRoleBindings(user *iamv1alpha2.User) error {
|
||||
func (c *userController) deleteRoleBindings(user *iamv1alpha2.User) error {
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{iamv1alpha2.UserReferenceLabel: user.Name}).String(),
|
||||
}
|
||||
deleteOptions := metav1.NewDeleteOptions(0)
|
||||
|
||||
if err := c.ksClient.IamV1alpha2().GlobalRoleBindings().
|
||||
DeleteCollection(deleteOptions, listOptions); err != nil {
|
||||
klog.Error(err)
|
||||
@@ -619,7 +488,7 @@ func (c *Controller) deleteRoleBindings(user *iamv1alpha2.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) deleteLoginRecords(user *iamv1alpha2.User) error {
|
||||
func (c *userController) deleteLoginRecords(user *iamv1alpha2.User) error {
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{iamv1alpha2.UserReferenceLabel: user.Name}).String(),
|
||||
}
|
||||
@@ -634,28 +503,60 @@ func (c *Controller) deleteLoginRecords(user *iamv1alpha2.User) error {
|
||||
}
|
||||
|
||||
// syncUserStatus will reconcile user state based on user login records
|
||||
func (c *Controller) syncUserStatus(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
func (c *userController) syncUserStatus(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
// disabled user, nothing to do
|
||||
if user == nil || (user.Status.State == iamv1alpha2.UserDisabled) {
|
||||
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserDisabled {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// mapped user from other identity provider always active until disabled
|
||||
if user.Spec.EncryptedPassword == "" &&
|
||||
user.Labels[iamv1alpha2.IdentifyProviderLabel] != "" &&
|
||||
(user.Status.State == nil || *user.Status.State != iamv1alpha2.UserActive) {
|
||||
expected := user.DeepCopy()
|
||||
active := iamv1alpha2.UserActive
|
||||
expected.Status = iamv1alpha2.UserStatus{
|
||||
State: &active,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
return c.ksClient.IamV1alpha2().Users().UpdateStatus(expected)
|
||||
}
|
||||
|
||||
// becomes inactive after setting a blank password
|
||||
if user.Spec.EncryptedPassword == "" &&
|
||||
user.Labels[iamv1alpha2.IdentifyProviderLabel] == "" {
|
||||
expected := user.DeepCopy()
|
||||
expected.Status = iamv1alpha2.UserStatus{
|
||||
State: nil,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
return c.ksClient.IamV1alpha2().Users().UpdateStatus(expected)
|
||||
}
|
||||
|
||||
// becomes active after password encrypted
|
||||
if isEncrypted(user.Spec.EncryptedPassword) &&
|
||||
user.Status.State == nil {
|
||||
expected := user.DeepCopy()
|
||||
active := iamv1alpha2.UserActive
|
||||
expected.Status = iamv1alpha2.UserStatus{
|
||||
State: &active,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
return c.ksClient.IamV1alpha2().Users().UpdateStatus(expected)
|
||||
}
|
||||
|
||||
// blocked user, check if need to unblock user
|
||||
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
if user.Status.LastTransitionTime != nil &&
|
||||
user.Status.LastTransitionTime.Add(c.authenticationOptions.AuthenticateRateLimiterDuration).Before(time.Now()) {
|
||||
expected := user.DeepCopy()
|
||||
// unblock user
|
||||
if user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] == "true" {
|
||||
expected.Status = iamv1alpha2.UserStatus{
|
||||
State: iamv1alpha2.UserActive,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Status, user.Status) {
|
||||
return c.ksClient.IamV1alpha2().Users().Update(expected)
|
||||
active := iamv1alpha2.UserActive
|
||||
expected.Status = iamv1alpha2.UserStatus{
|
||||
State: &active,
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
return c.ksClient.IamV1alpha2().Users().UpdateStatus(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +571,8 @@ func (c *Controller) syncUserStatus(user *iamv1alpha2.User) (*iamv1alpha2.User,
|
||||
now := time.Now()
|
||||
failedLoginAttempts := 0
|
||||
for _, loginRecord := range records {
|
||||
if !loginRecord.Spec.Success && loginRecord.CreationTimestamp.Add(c.authenticationOptions.AuthenticateRateLimiterDuration).After(now) {
|
||||
if !loginRecord.Spec.Success &&
|
||||
loginRecord.CreationTimestamp.Add(c.authenticationOptions.AuthenticateRateLimiterDuration).After(now) {
|
||||
failedLoginAttempts++
|
||||
}
|
||||
}
|
||||
@@ -678,26 +580,29 @@ func (c *Controller) syncUserStatus(user *iamv1alpha2.User) (*iamv1alpha2.User,
|
||||
// block user if failed login attempts exceeds maximum tries setting
|
||||
if failedLoginAttempts >= c.authenticationOptions.AuthenticateRateLimiterMaxTries {
|
||||
expect := user.DeepCopy()
|
||||
limitExceed := iamv1alpha2.UserAuthLimitExceeded
|
||||
expect.Status = iamv1alpha2.UserStatus{
|
||||
State: iamv1alpha2.UserAuthLimitExceeded,
|
||||
State: &limitExceed,
|
||||
Reason: fmt.Sprintf("Failed login attempts exceed %d in last %s", failedLoginAttempts, c.authenticationOptions.AuthenticateRateLimiterDuration),
|
||||
LastTransitionTime: &metav1.Time{Time: time.Now()},
|
||||
}
|
||||
|
||||
// block user for AuthenticateRateLimiterDuration duration, after that put it back to the queue to unblock
|
||||
c.workqueue.AddAfter(user.Name, c.authenticationOptions.AuthenticateRateLimiterDuration)
|
||||
|
||||
return c.ksClient.IamV1alpha2().Users().Update(expect)
|
||||
c.Workqueue.AddAfter(user.Name, c.authenticationOptions.AuthenticateRateLimiterDuration)
|
||||
return c.ksClient.IamV1alpha2().Users().UpdateStatus(expect)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// isEncrypted returns whether the given password is encrypted
|
||||
func isEncrypted(password string) bool {
|
||||
// bcrypt.Cost returns the hashing cost used to create the given hashed
|
||||
cost, _ := bcrypt.Cost([]byte(password))
|
||||
// cost > 0 means the password has been encrypted
|
||||
return cost > 0
|
||||
}
|
||||
|
||||
@@ -80,32 +80,32 @@ func newUser(name string) *iamv1alpha2.User {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fixture) newController() (*Controller, ksinformers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {
|
||||
func (f *fixture) newController() (*userController, ksinformers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {
|
||||
f.ksclient = fake.NewSimpleClientset(f.objects...)
|
||||
f.k8sclient = k8sfake.NewSimpleClientset(f.kubeobjects...)
|
||||
ldapClient := ldapclient.NewSimpleLdap()
|
||||
|
||||
ksinformers := ksinformers.NewSharedInformerFactory(f.ksclient, noResyncPeriodFunc())
|
||||
k8sinformers := kubeinformers.NewSharedInformerFactory(f.k8sclient, noResyncPeriodFunc())
|
||||
ksInformers := ksinformers.NewSharedInformerFactory(f.ksclient, noResyncPeriodFunc())
|
||||
k8sInformers := kubeinformers.NewSharedInformerFactory(f.k8sclient, noResyncPeriodFunc())
|
||||
|
||||
for _, user := range f.userLister {
|
||||
err := ksinformers.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 := NewUserController(f.k8sclient, f.ksclient, nil,
|
||||
ksinformers.Iam().V1alpha2().Users(),
|
||||
ksInformers.Iam().V1alpha2().Users(),
|
||||
ksInformers.Iam().V1alpha2().LoginRecords(),
|
||||
nil, nil,
|
||||
ksinformers.Iam().V1alpha2().LoginRecords(),
|
||||
k8sinformers.Core().V1().ConfigMaps(),
|
||||
k8sInformers.Core().V1().ConfigMaps(),
|
||||
ldapClient, nil,
|
||||
options.NewAuthenticateOptions(), false)
|
||||
c.userSynced = alwaysReady
|
||||
c.Synced = []cache.InformerSynced{alwaysReady}
|
||||
c.recorder = &record.FakeRecorder{}
|
||||
|
||||
return c, ksinformers, k8sinformers
|
||||
return c, ksInformers, k8sInformers
|
||||
}
|
||||
|
||||
func (f *fixture) run(userName string) {
|
||||
@@ -230,14 +230,14 @@ func filterInformerActions(actions []core.Action) []core.Action {
|
||||
|
||||
func (f *fixture) expectUpdateUserStatusAction(user *iamv1alpha2.User) {
|
||||
expect := user.DeepCopy()
|
||||
//expect.Status.State = iamv1alpha2.UserActive
|
||||
expect.Finalizers = []string{"finalizers.kubesphere.io/users"}
|
||||
action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "users"}, "", expect)
|
||||
f.actions = append(f.actions, action)
|
||||
|
||||
expect = expect.DeepCopy()
|
||||
expect.Status.State = iamv1alpha2.UserActive
|
||||
expect.Annotations = map[string]string{iamv1alpha2.PasswordEncryptedAnnotation: "true"}
|
||||
action = core.NewUpdateAction(schema.GroupVersionResource{Resource: "users"}, "", expect)
|
||||
action.Subresource = "status"
|
||||
f.actions = append(f.actions, action)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,7 @@ func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admi
|
||||
}
|
||||
|
||||
allUsers := v1alpha2.UserList{}
|
||||
|
||||
err = a.Client.List(ctx, &allUsers, &client.ListOptions{})
|
||||
|
||||
if err != nil {
|
||||
if err = a.Client.List(ctx, &allUsers, &client.ListOptions{}); err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
@@ -51,7 +48,6 @@ func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admi
|
||||
}
|
||||
|
||||
alreadyExist := emailAlreadyExist(allUsers, user)
|
||||
|
||||
if alreadyExist {
|
||||
return admission.Errored(http.StatusConflict, fmt.Errorf("user email: %s already exists", user.Spec.Email))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"net/http"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
@@ -25,9 +26,7 @@ import (
|
||||
v1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
@@ -42,9 +41,9 @@ const (
|
||||
|
||||
var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"}
|
||||
|
||||
func AddToContainer(container *restful.Container, im im.IdentityManagementInterface, am am.AccessManagementInterface, group group.GroupOperator, options *authoptions.AuthenticationOptions) error {
|
||||
func AddToContainer(container *restful.Container, im im.IdentityManagementInterface, am am.AccessManagementInterface, group group.GroupOperator, authorizer authorizer.Authorizer) error {
|
||||
ws := runtime.NewWebService(GroupVersion)
|
||||
handler := newIAMHandler(im, am, group, options)
|
||||
handler := newIAMHandler(im, am, group, authorizer)
|
||||
|
||||
// users
|
||||
ws.Route(ws.POST("/users").
|
||||
@@ -69,7 +68,7 @@ func AddToContainer(container *restful.Container, im im.IdentityManagementInterf
|
||||
ws.Route(ws.PUT("/users/{user}/password").
|
||||
To(handler.ModifyPassword).
|
||||
Doc("Reset password of the specified user.").
|
||||
Reads(iam.PasswordReset{}).
|
||||
Reads(PasswordReset{}).
|
||||
Param(ws.PathParameter("user", "username")).
|
||||
Returns(http.StatusOK, api.StatusOK, errors.None).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.UserTag}))
|
||||
|
||||
@@ -20,62 +20,101 @@ import (
|
||||
"fmt"
|
||||
"github.com/emicklei/go-restful"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/api/auth"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
im im.IdentityManagementInterface
|
||||
options *authoptions.AuthenticationOptions
|
||||
tokenOperator im.TokenManagementInterface
|
||||
authenticator im.PasswordAuthenticator
|
||||
loginRecorder im.LoginRecorder
|
||||
const (
|
||||
KindTokenReview = "TokenReview"
|
||||
passwordGrantType = "password"
|
||||
refreshTokenGrantType = "refresh_token"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
Token string `json:"token" description:"access token"`
|
||||
}
|
||||
|
||||
func newHandler(im im.IdentityManagementInterface, tokenOperator im.TokenManagementInterface, authenticator im.PasswordAuthenticator, loginRecorder im.LoginRecorder, options *authoptions.AuthenticationOptions) *handler {
|
||||
return &handler{im: im, tokenOperator: tokenOperator, authenticator: authenticator, loginRecorder: loginRecorder, options: options}
|
||||
type Status struct {
|
||||
Authenticated bool `json:"authenticated" description:"is authenticated"`
|
||||
User map[string]interface{} `json:"user,omitempty" description:"user info"`
|
||||
}
|
||||
|
||||
type TokenReview struct {
|
||||
APIVersion string `json:"apiVersion" description:"Kubernetes API version"`
|
||||
Kind string `json:"kind" description:"kind of the API object"`
|
||||
Spec *Spec `json:"spec,omitempty"`
|
||||
Status *Status `json:"status,omitempty" description:"token review status"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" description:"username"`
|
||||
Password string `json:"password" description:"password"`
|
||||
}
|
||||
|
||||
func (request *TokenReview) Validate() error {
|
||||
if request.Spec == nil || request.Spec.Token == "" {
|
||||
return fmt.Errorf("token must not be null")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
im im.IdentityManagementInterface
|
||||
options *authoptions.AuthenticationOptions
|
||||
tokenOperator auth.TokenManagementInterface
|
||||
passwordAuthenticator auth.PasswordAuthenticator
|
||||
oauth2Authenticator auth.OAuth2Authenticator
|
||||
loginRecorder auth.LoginRecorder
|
||||
}
|
||||
|
||||
func newHandler(im im.IdentityManagementInterface,
|
||||
tokenOperator auth.TokenManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauth2Authenticator auth.OAuth2Authenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authoptions.AuthenticationOptions) *handler {
|
||||
return &handler{im: im,
|
||||
tokenOperator: tokenOperator,
|
||||
passwordAuthenticator: passwordAuthenticator,
|
||||
oauth2Authenticator: oauth2Authenticator,
|
||||
loginRecorder: loginRecorder,
|
||||
options: options}
|
||||
}
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
|
||||
var tokenReview auth.TokenReview
|
||||
var tokenReview TokenReview
|
||||
|
||||
err := req.ReadEntity(&tokenReview)
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = tokenReview.Validate(); err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
api.HandleInternalError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
success := auth.TokenReview{APIVersion: tokenReview.APIVersion,
|
||||
Kind: auth.KindTokenReview,
|
||||
Status: &auth.Status{
|
||||
success := TokenReview{APIVersion: tokenReview.APIVersion,
|
||||
Kind: KindTokenReview,
|
||||
Status: &Status{
|
||||
Authenticated: true,
|
||||
User: map[string]interface{}{"username": authenticated.GetName(), "uid": authenticated.GetUID()},
|
||||
},
|
||||
@@ -93,34 +132,33 @@ func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
|
||||
conf, err := h.options.OAuthOptions.OAuthClient(clientId)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
if responseType != "token" {
|
||||
err := apierrors.NewBadRequest(fmt.Sprintf("Response type %s is not supported", responseType))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err := apierrors.NewUnauthorized("Unauthorized")
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := conf.ResolveRedirectURL(redirectURI)
|
||||
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -133,105 +171,41 @@ func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
|
||||
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) oAuthCallBack(req *restful.Request, resp *restful.Response) {
|
||||
|
||||
func (h *handler) oauthCallBack(req *restful.Request, resp *restful.Response) {
|
||||
code := req.QueryParameter("code")
|
||||
name := req.PathParameter("callback")
|
||||
provider := req.PathParameter("callback")
|
||||
|
||||
if code == "" {
|
||||
err := apierrors.NewUnauthorized("Unauthorized: missing code")
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
providerOptions, err := h.options.OAuthOptions.IdentityProviderOptions(name)
|
||||
|
||||
authenticated, provider, err := h.oauth2Authenticator.Authenticate(provider, code)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleUnauthorized(resp, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
}
|
||||
|
||||
oauthIdentityProvider, err := identityprovider.GetOAuthProvider(providerOptions.Type, providerOptions.Provider)
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleInternalError(resp, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
identity, err := oauthIdentityProvider.IdentityExchange(code)
|
||||
|
||||
if err != nil {
|
||||
err = apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.im.DescribeUser(identity.GetName())
|
||||
if err != nil {
|
||||
// create user if not exist
|
||||
if (oauth.MappingMethodAuto == providerOptions.MappingMethod ||
|
||||
oauth.MappingMethodMixed == providerOptions.MappingMethod) &&
|
||||
apierrors.IsNotFound(err) {
|
||||
create := &iamv1alpha2.User{
|
||||
ObjectMeta: v1.ObjectMeta{Name: identity.GetName(),
|
||||
Annotations: map[string]string{iamv1alpha2.IdentifyProviderLabel: providerOptions.Name}},
|
||||
Spec: iamv1alpha2.UserSpec{Email: identity.GetEmail()},
|
||||
}
|
||||
if authenticated, err = h.im.CreateUser(create); err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleInternalError(resp, req, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
klog.Error(err)
|
||||
api.HandleInternalError(resp, req, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if oauth.MappingMethodLookup == providerOptions.MappingMethod &&
|
||||
authenticated == nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s cannot bound to this identify provider", identity.GetName()))
|
||||
klog.Error(err)
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
// oauth.MappingMethodAuto
|
||||
// Fails if a user with that user name is already mapped to another identity.
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodAuto && authenticated.Annotations[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s is already bound to other identify provider", identity.GetName()))
|
||||
klog.Error(err)
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(&user.DefaultInfo{
|
||||
Name: authenticated.Name,
|
||||
UID: string(authenticated.UID),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
resp.WriteError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.Name, iamv1alpha2.OAuth, providerOptions.Name, nil, req.Request); err != nil {
|
||||
klog.Error(err)
|
||||
err := apierrors.NewInternalError(err)
|
||||
resp.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
|
||||
}
|
||||
|
||||
resp.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) Login(request *restful.Request, response *restful.Response) {
|
||||
var loginRequest auth.LoginRequest
|
||||
var loginRequest LoginRequest
|
||||
err := request.ReadEntity(&loginRequest)
|
||||
if err != nil || loginRequest.Username == "" || loginRequest.Password == "" {
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, fmt.Errorf("empty username or password"))
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
h.passwordGrant(loginRequest.Username, loginRequest.Password, request, response)
|
||||
@@ -240,71 +214,53 @@ func (h *handler) Login(request *restful.Request, response *restful.Response) {
|
||||
func (h *handler) Token(req *restful.Request, response *restful.Response) {
|
||||
grantType, err := req.BodyParameter("grant_type")
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(response, req, err)
|
||||
return
|
||||
}
|
||||
switch grantType {
|
||||
case "password":
|
||||
username, err := req.BodyParameter("username")
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(response, req, err)
|
||||
return
|
||||
}
|
||||
password, err := req.BodyParameter("password")
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(response, req, err)
|
||||
return
|
||||
}
|
||||
case passwordGrantType:
|
||||
username, _ := req.BodyParameter("username")
|
||||
password, _ := req.BodyParameter("password")
|
||||
h.passwordGrant(username, password, req, response)
|
||||
break
|
||||
case "refresh_token":
|
||||
case refreshTokenGrantType:
|
||||
h.refreshTokenGrant(req, response)
|
||||
break
|
||||
default:
|
||||
err := apierrors.NewBadRequest(fmt.Sprintf("Grant type %s is not supported", grantType))
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
api.HandleBadRequest(response, req, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) passwordGrant(username string, password string, req *restful.Request, response *restful.Response) {
|
||||
authenticated, err := h.authenticator.Authenticate(username, password)
|
||||
authenticated, provider, err := h.passwordAuthenticator.Authenticate(username, password)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
switch err {
|
||||
case im.AuthFailedIncorrectPassword:
|
||||
if err := h.loginRecorder.RecordLogin(username, iamv1alpha2.Token, "", err, req.Request); err != nil {
|
||||
klog.Error(err)
|
||||
response.WriteError(http.StatusInternalServerError, apierrors.NewInternalError(err))
|
||||
return
|
||||
case auth.IncorrectPasswordError:
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err := h.loginRecorder.RecordLogin(username, iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
response.WriteError(http.StatusUnauthorized, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
case im.AuthFailedIdentityMappingNotMatch:
|
||||
response.WriteError(http.StatusUnauthorized, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
case im.AuthRateLimitExceeded:
|
||||
response.WriteError(http.StatusTooManyRequests, apierrors.NewTooManyRequests(fmt.Sprintf("Unauthorized: %s", err), 60))
|
||||
case auth.RateLimitExceededError:
|
||||
api.HandleTooManyRequests(response, req, apierrors.NewTooManyRequestsError(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
default:
|
||||
response.WriteError(http.StatusInternalServerError, apierrors.NewInternalError(err))
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
response.WriteError(http.StatusInternalServerError, apierrors.NewInternalError(err))
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, "", nil, req.Request); err != nil {
|
||||
klog.Error(err)
|
||||
response.WriteError(http.StatusInternalServerError, apierrors.NewInternalError(err))
|
||||
return
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", username, err)
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
@@ -313,22 +269,46 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R
|
||||
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response) {
|
||||
refreshToken, err := req.BodyParameter("refresh_token")
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
api.HandleBadRequest(response, req, err)
|
||||
api.HandleBadRequest(response, req, apierrors.NewBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.tokenOperator.Verify(refreshToken)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
response.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// update token after registration
|
||||
if authenticated.GetName() == iamv1alpha2.PreRegistrationUser &&
|
||||
authenticated.GetExtra() != nil &&
|
||||
len(authenticated.GetExtra()[iamv1alpha2.ExtraIdentityProvider]) > 0 &&
|
||||
len(authenticated.GetExtra()[iamv1alpha2.ExtraUID]) > 0 {
|
||||
|
||||
idp := authenticated.GetExtra()[iamv1alpha2.ExtraIdentityProvider][0]
|
||||
uid := authenticated.GetExtra()[iamv1alpha2.ExtraUID][0]
|
||||
queryParam := query.New()
|
||||
queryParam.LabelSelector = labels.SelectorFromSet(labels.Set{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid}).String()
|
||||
result, err := h.im.ListUsers(queryParam)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
return
|
||||
}
|
||||
if len(result.Items) != 1 {
|
||||
err := apierrors.NewUnauthorized("authenticated user does not exist")
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
return
|
||||
}
|
||||
authenticated = &user.DefaultInfo{Name: result.Items[0].(*iamv1alpha2.User).Name}
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
response.WriteError(http.StatusUnauthorized, err)
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"github.com/emicklei/go-restful"
|
||||
restfulspec "github.com/emicklei/go-restful-openapi"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/api/auth"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
"net/http"
|
||||
)
|
||||
@@ -34,22 +34,28 @@ import (
|
||||
// Most authentication integrations place an authenticating proxy in front of this endpoint, or configure ks-apiserver
|
||||
// to validate credentials against a backing identity provider.
|
||||
// Requests to <ks-apiserver>/oauth/authorize can come from user-agents that cannot display interactive login pages, such as the CLI.
|
||||
func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tokenOperator im.TokenManagementInterface, authenticator im.PasswordAuthenticator, loginRecorder im.LoginRecorder, options *authoptions.AuthenticationOptions) error {
|
||||
func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
tokenOperator auth.TokenManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauth2Authenticator auth.OAuth2Authenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authoptions.AuthenticationOptions) error {
|
||||
|
||||
ws := &restful.WebService{}
|
||||
ws.Path("/oauth").
|
||||
Consumes(restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON)
|
||||
|
||||
handler := newHandler(im, tokenOperator, authenticator, loginRecorder, options)
|
||||
handler := newHandler(im, tokenOperator, passwordAuthenticator, oauth2Authenticator, loginRecorder, options)
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
ws.Route(ws.POST("/authenticate").
|
||||
Doc("TokenReview attempts to authenticate a token to a known user. Note: TokenReview requests may be "+
|
||||
"cached by the webhook token authenticator plugin in the kube-apiserver.").
|
||||
Reads(auth.TokenReview{}).
|
||||
Reads(TokenReview{}).
|
||||
To(handler.TokenReview).
|
||||
Returns(http.StatusOK, api.StatusOK, auth.TokenReview{}).
|
||||
Returns(http.StatusOK, api.StatusOK, TokenReview{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
// Only support implicit grant flow
|
||||
@@ -98,7 +104,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
|
||||
"otherwise, REQUIRED. The scope of the access token as described by [RFC6479] Section 3.3.").Required(false)).
|
||||
Param(ws.QueryParameter("state", "if the \"state\" parameter was present in the client authorization request."+
|
||||
"The exact value received from the client.").Required(true)).
|
||||
To(handler.oAuthCallBack).
|
||||
To(handler.oauthCallBack).
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
@@ -113,7 +119,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
|
||||
To(handler.Login).
|
||||
Deprecate().
|
||||
Doc("KubeSphere APIs support token-based authentication via the Authtoken request header. The POST Login API is used to retrieve the authentication token. After the authentication token is obtained, it must be inserted into the Authtoken header for all requests.").
|
||||
Reads(auth.LoginRequest{}).
|
||||
Reads(LoginRequest{}).
|
||||
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
|
||||
@@ -21,26 +21,25 @@ import (
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
"kubesphere.io/kubesphere/pkg/models/components"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha2"
|
||||
resourcev1alpha2 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2/resource"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
"kubesphere.io/kubesphere/pkg/server/params"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
resourceGetterV1alpha3 *resource.ResourceGetter
|
||||
resourceGetterV1alpha3 *resourcev1alpha3.ResourceGetter
|
||||
resourcesGetterV1alpha2 *resourcev1alpha2.ResourceGetter
|
||||
componentsGetter components.ComponentsGetter
|
||||
}
|
||||
|
||||
func New(factory informers.InformerFactory) *Handler {
|
||||
func New(resourceGetterV1alpha3 *resourcev1alpha3.ResourceGetter, resourcesGetterV1alpha2 *resourcev1alpha2.ResourceGetter, componentsGetter components.ComponentsGetter) *Handler {
|
||||
return &Handler{
|
||||
resourceGetterV1alpha3: resource.NewResourceGetter(factory),
|
||||
resourcesGetterV1alpha2: resourcev1alpha2.NewResourceGetter(factory),
|
||||
componentsGetter: components.NewComponentsGetter(factory.KubernetesSharedInformerFactory()),
|
||||
resourceGetterV1alpha3: resourceGetterV1alpha3,
|
||||
resourcesGetterV1alpha2: resourcesGetterV1alpha2,
|
||||
componentsGetter: componentsGetter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +54,7 @@ func (h *Handler) handleGetResources(request *restful.Request, response *restful
|
||||
return
|
||||
}
|
||||
|
||||
if err != resource.ErrResourceNotSupported {
|
||||
if err != resourcev1alpha3.ErrResourceNotSupported {
|
||||
klog.Error(err, resourceType)
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
@@ -87,7 +86,7 @@ func (h *Handler) handleListResources(request *restful.Request, response *restfu
|
||||
return
|
||||
}
|
||||
|
||||
if err != resource.ErrResourceNotSupported {
|
||||
if err != resourcev1alpha3.ErrResourceNotSupported {
|
||||
klog.Error(err, resourceType)
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
|
||||
@@ -29,7 +29,10 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
"kubesphere.io/kubesphere/pkg/models/components"
|
||||
resourcev1alpha2 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2/resource"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
fakeapp "sigs.k8s.io/application/pkg/client/clientset/versioned/fake"
|
||||
"testing"
|
||||
)
|
||||
@@ -88,7 +91,9 @@ func TestResourceV1alpha2Fallback(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := New(factory)
|
||||
handler := New(resourcev1alpha3.NewResourceGetter(factory),
|
||||
resourcev1alpha2.NewResourceGetter(factory),
|
||||
components.NewComponentsGetter(factory.KubernetesSharedInformerFactory()))
|
||||
|
||||
for _, test := range tests {
|
||||
got, err := listResources(test.namespace, test.resource, test.query, handler)
|
||||
|
||||
@@ -25,6 +25,9 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
"kubesphere.io/kubesphere/pkg/models/components"
|
||||
resourcev1alpha2 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha2/resource"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -47,7 +50,9 @@ func Resource(resource string) schema.GroupResource {
|
||||
func AddToContainer(c *restful.Container, informerFactory informers.InformerFactory) error {
|
||||
|
||||
webservice := runtime.NewWebService(GroupVersion)
|
||||
handler := New(informerFactory)
|
||||
handler := New(resourcev1alpha3.NewResourceGetter(informerFactory),
|
||||
resourcev1alpha2.NewResourceGetter(informerFactory),
|
||||
components.NewComponentsGetter(informerFactory.KubernetesSharedInformerFactory()))
|
||||
|
||||
webservice.Route(webservice.GET("/{resources}").
|
||||
To(handler.handleListResources).
|
||||
|
||||
259
pkg/models/auth/authenticator.go
Normal file
259
pkg/models/auth/authenticator.go
Normal file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"net/mail"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
)
|
||||
|
||||
var (
|
||||
RateLimitExceededError = fmt.Errorf("auth rate limit exceeded")
|
||||
IncorrectPasswordError = fmt.Errorf("incorrect password")
|
||||
AccountIsNotActiveError = fmt.Errorf("account is not active")
|
||||
)
|
||||
|
||||
type PasswordAuthenticator interface {
|
||||
Authenticate(username, password string) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type OAuth2Authenticator interface {
|
||||
Authenticate(provider, code string) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type passwordAuthenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
authOptions *authoptions.AuthenticationOptions
|
||||
}
|
||||
|
||||
type oauth2Authenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
authOptions *authoptions.AuthenticationOptions
|
||||
}
|
||||
|
||||
type userGetter struct {
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
}
|
||||
|
||||
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authoptions.AuthenticationOptions) PasswordAuthenticator {
|
||||
passwordAuthenticator := &passwordAuthenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
authOptions: options,
|
||||
}
|
||||
return passwordAuthenticator
|
||||
}
|
||||
|
||||
func NewOAuth2Authenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authoptions.AuthenticationOptions) OAuth2Authenticator {
|
||||
oauth2Authenticator := &oauth2Authenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
authOptions: options,
|
||||
}
|
||||
return oauth2Authenticator
|
||||
}
|
||||
|
||||
func (p *passwordAuthenticator) Authenticate(username, password string) (authuser.Info, string, error) {
|
||||
// empty username or password are not allowed
|
||||
if username == "" || password == "" {
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
// generic identity provider has higher priority
|
||||
for _, providerOptions := range p.authOptions.OAuthOptions.IdentityProviders {
|
||||
// the admin account in kubesphere has the highest priority
|
||||
if username == constants.AdminUserName {
|
||||
break
|
||||
}
|
||||
if genericProvider, _ := identityprovider.CreateGenericProvider(providerOptions.Type, providerOptions.Provider); genericProvider != nil {
|
||||
authenticated, err := genericProvider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.IsUnauthorized(err) {
|
||||
continue
|
||||
}
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
linkedAccount, err := p.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
|
||||
// using this method requires you to manually provision users.
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodLookup && linkedAccount == nil {
|
||||
continue
|
||||
}
|
||||
if linkedAccount != nil {
|
||||
return &authuser.DefaultInfo{Name: linkedAccount.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kubesphere account
|
||||
user, err := p.userGetter.findUser(username)
|
||||
if err != nil {
|
||||
// ignore not found error
|
||||
if !errors.IsNotFound(err) {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// check user status
|
||||
if user != nil && (user.Status.State == nil || *user.Status.State != iamv1alpha2.UserActive) {
|
||||
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
klog.Errorf("%s, username: %s", RateLimitExceededError, username)
|
||||
return nil, "", RateLimitExceededError
|
||||
} else {
|
||||
// state not active
|
||||
klog.Errorf("%s, username: %s", AccountIsNotActiveError, username)
|
||||
return nil, "", AccountIsNotActiveError
|
||||
}
|
||||
}
|
||||
|
||||
// if the password is not empty, means that the password has been reset, even if the user was mapping from IDP
|
||||
if user != nil && user.Spec.EncryptedPassword != "" {
|
||||
if err = PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
u := &authuser.DefaultInfo{
|
||||
Name: user.Name,
|
||||
}
|
||||
// check if the password is initialized
|
||||
if uninitialized := user.Annotations[iamv1alpha2.UninitializedAnnotation]; uninitialized != "" {
|
||||
u.Extra = map[string][]string{
|
||||
iamv1alpha2.ExtraUninitialized: {uninitialized},
|
||||
}
|
||||
}
|
||||
return u, "", nil
|
||||
}
|
||||
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
|
||||
func preRegistrationUser(idp string, identity identityprovider.Identity) authuser.Info {
|
||||
return &authuser.DefaultInfo{
|
||||
Name: iamv1alpha2.PreRegistrationUser,
|
||||
Extra: map[string][]string{
|
||||
iamv1alpha2.ExtraIdentityProvider: {idp},
|
||||
iamv1alpha2.ExtraUID: {identity.GetUserID()},
|
||||
iamv1alpha2.ExtraUsername: {identity.GetUsername()},
|
||||
iamv1alpha2.ExtraEmail: {identity.GetEmail()},
|
||||
iamv1alpha2.ExtraDisplayName: {identity.GetDisplayName()},
|
||||
},
|
||||
Groups: []string{iamv1alpha2.PreRegistrationUserGroup},
|
||||
}
|
||||
}
|
||||
|
||||
func (o oauth2Authenticator) Authenticate(provider, code string) (authuser.Info, string, error) {
|
||||
providerOptions, err := o.authOptions.OAuthOptions.IdentityProviderOptions(provider)
|
||||
// identity provider not registered
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
oauthIdentityProvider, err := identityprovider.CreateOAuthProvider(providerOptions.Type, providerOptions.Provider)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
authenticated, err := oauthIdentityProvider.IdentityExchange(code)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
user, err := o.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodLookup {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
if user != nil {
|
||||
return &authuser.DefaultInfo{Name: user.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
|
||||
return nil, "", errors.NewNotFound(iamv1alpha2.Resource("user"), authenticated.GetUsername())
|
||||
}
|
||||
|
||||
func PasswordVerify(encryptedPassword, password string) error {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil {
|
||||
return IncorrectPasswordError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findUser
|
||||
func (u *userGetter) findUser(username string) (*iamv1alpha2.User, error) {
|
||||
if _, err := mail.ParseAddress(username); err != nil {
|
||||
return u.userLister.Get(username)
|
||||
} else {
|
||||
users, err := u.userLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
for _, find := range users {
|
||||
if find.Spec.Email == username {
|
||||
return find, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
|
||||
}
|
||||
|
||||
func (u *userGetter) findLinkedAccount(idp, uid string) (*iamv1alpha2.User, error) {
|
||||
selector := labels.SelectorFromSet(labels.Set{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid,
|
||||
})
|
||||
|
||||
users, err := u.userLister.List(selector)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if len(users) != 1 {
|
||||
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), uid)
|
||||
}
|
||||
|
||||
return users[0], err
|
||||
}
|
||||
40
pkg/models/auth/authenticator_test.go
Normal file
40
pkg/models/auth/authenticator_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncryptPassword(t *testing.T) {
|
||||
password := "P@88w0rd"
|
||||
encryptedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = PasswordVerify(encryptedPassword, password); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
* /
|
||||
*/
|
||||
Copyright 2020 KubeSphere Authors
|
||||
|
||||
package im
|
||||
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 (
|
||||
"fmt"
|
||||
@@ -24,13 +22,11 @@ import (
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
"kubesphere.io/kubesphere/pkg/utils/net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LoginRecorder interface {
|
||||
RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, authErr error, req *http.Request) error
|
||||
RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, sourceIP string, userAgent string, authErr error) error
|
||||
}
|
||||
|
||||
type loginRecorder struct {
|
||||
@@ -43,7 +39,7 @@ func NewLoginRecorder(ksClient kubesphere.Interface) LoginRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, authErr error, req *http.Request) error {
|
||||
func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, sourceIP string, userAgent string, authErr error) error {
|
||||
// This is a temporary solution in case of user login with email,
|
||||
// '@' is not allowed in Kubernetes object name.
|
||||
username = strings.Replace(username, "@", "-", -1)
|
||||
@@ -60,8 +56,8 @@ func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.Login
|
||||
Provider: provider,
|
||||
Success: true,
|
||||
Reason: iamv1alpha2.AuthenticatedSuccessfully,
|
||||
SourceIP: net.GetRequestIP(req),
|
||||
UserAgent: req.UserAgent(),
|
||||
SourceIP: sourceIP,
|
||||
UserAgent: userAgent,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
*/
|
||||
|
||||
package im
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -18,22 +18,30 @@ package am
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
devopslisters "kubesphere.io/kubesphere/pkg/client/listers/devops/v1alpha3"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/clusterrole"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/clusterrolebinding"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrole"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrolebinding"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/role"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/rolebinding"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/workspacerole"
|
||||
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/workspacerolebinding"
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
@@ -87,45 +95,55 @@ type AccessManagementInterface interface {
|
||||
}
|
||||
|
||||
type amOperator struct {
|
||||
resourceGetter *resourcev1alpha3.ResourceGetter
|
||||
ksclient kubesphere.Interface
|
||||
k8sclient kubernetes.Interface
|
||||
globalRoleBindingGetter resourcev1alpha3.Interface
|
||||
workspaceRoleBindingGetter resourcev1alpha3.Interface
|
||||
clusterRoleBindingGetter resourcev1alpha3.Interface
|
||||
roleBindingGetter resourcev1alpha3.Interface
|
||||
globalRoleGetter resourcev1alpha3.Interface
|
||||
workspaceRoleGetter resourcev1alpha3.Interface
|
||||
clusterRoleGetter resourcev1alpha3.Interface
|
||||
roleGetter resourcev1alpha3.Interface
|
||||
devopsProjectLister devopslisters.DevOpsProjectLister
|
||||
namespaceLister listersv1.NamespaceLister
|
||||
ksclient kubesphere.Interface
|
||||
k8sclient kubernetes.Interface
|
||||
}
|
||||
|
||||
func NewReadOnlyOperator(factory informers.InformerFactory) AccessManagementInterface {
|
||||
return &amOperator{
|
||||
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
|
||||
globalRoleBindingGetter: globalrolebinding.New(factory.KubeSphereSharedInformerFactory()),
|
||||
workspaceRoleBindingGetter: workspacerolebinding.New(factory.KubeSphereSharedInformerFactory()),
|
||||
clusterRoleBindingGetter: clusterrolebinding.New(factory.KubernetesSharedInformerFactory()),
|
||||
roleBindingGetter: rolebinding.New(factory.KubernetesSharedInformerFactory()),
|
||||
globalRoleGetter: globalrole.New(factory.KubeSphereSharedInformerFactory()),
|
||||
workspaceRoleGetter: workspacerole.New(factory.KubeSphereSharedInformerFactory()),
|
||||
clusterRoleGetter: clusterrole.New(factory.KubernetesSharedInformerFactory()),
|
||||
roleGetter: role.New(factory.KubernetesSharedInformerFactory()),
|
||||
devopsProjectLister: factory.KubeSphereSharedInformerFactory().Devops().V1alpha3().DevOpsProjects().Lister(),
|
||||
namespaceLister: factory.KubernetesSharedInformerFactory().Core().V1().Namespaces().Lister(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewOperator(factory informers.InformerFactory, ksclient kubesphere.Interface, k8sclient kubernetes.Interface) AccessManagementInterface {
|
||||
return &amOperator{
|
||||
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
|
||||
ksclient: ksclient,
|
||||
k8sclient: k8sclient,
|
||||
}
|
||||
func NewOperator(ksClient kubesphere.Interface, k8sClient kubernetes.Interface, factory informers.InformerFactory) AccessManagementInterface {
|
||||
amOperator := NewReadOnlyOperator(factory).(*amOperator)
|
||||
amOperator.ksclient = ksClient
|
||||
amOperator.k8sclient = k8sClient
|
||||
return amOperator
|
||||
}
|
||||
|
||||
func (am *amOperator) GetGlobalRoleOfUser(username string) (*iamv1alpha2.GlobalRole, error) {
|
||||
|
||||
userRoleBindings, err := am.ListGlobalRoleBindings(username)
|
||||
|
||||
if len(userRoleBindings) > 0 {
|
||||
role, err := am.GetGlobalRole(userRoleBindings[0].RoleRef.Name)
|
||||
globalRoleBindings, err := am.ListGlobalRoleBindings(username)
|
||||
if len(globalRoleBindings) > 0 {
|
||||
// Usually, only one globalRoleBinding will be found which is created from ks-console.
|
||||
if len(globalRoleBindings) > 1 {
|
||||
klog.Warningf("conflict global role binding, username: %s", username)
|
||||
}
|
||||
globalRole, err := am.GetGlobalRole(globalRoleBindings[0].RoleRef.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if len(userRoleBindings) > 1 {
|
||||
klog.Warningf("conflict global role binding, username: %s", username)
|
||||
}
|
||||
|
||||
out := role.DeepCopy()
|
||||
if out.Annotations == nil {
|
||||
out.Annotations = make(map[string]string, 0)
|
||||
}
|
||||
out.Annotations[iamv1alpha2.GlobalRoleAnnotation] = role.Name
|
||||
return out, nil
|
||||
return globalRole, nil
|
||||
}
|
||||
|
||||
err = errors.NewNotFound(iamv1alpha2.Resource(iamv1alpha2.ResourcesSingularGlobalRoleBinding), username)
|
||||
@@ -244,7 +262,7 @@ func (am *amOperator) GetClusterRoleOfUser(username string) (*rbacv1.ClusterRole
|
||||
}
|
||||
|
||||
func (am *amOperator) ListWorkspaceRoleBindings(username string, groups []string, workspace string) ([]*iamv1alpha2.WorkspaceRoleBinding, error) {
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding, "", query.New())
|
||||
roleBindings, err := am.workspaceRoleBindingGetter.List("", query.New())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -265,8 +283,7 @@ func (am *amOperator) ListWorkspaceRoleBindings(username string, groups []string
|
||||
|
||||
func (am *amOperator) ListClusterRoleBindings(username string) ([]*rbacv1.ClusterRoleBinding, error) {
|
||||
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralClusterRoleBinding, "", query.New())
|
||||
|
||||
roleBindings, err := am.clusterRoleBindingGetter.List("", query.New())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -283,14 +300,12 @@ func (am *amOperator) ListClusterRoleBindings(username string) ([]*rbacv1.Cluste
|
||||
}
|
||||
|
||||
func (am *amOperator) ListGlobalRoleBindings(username string) ([]*iamv1alpha2.GlobalRoleBinding, error) {
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralGlobalRoleBinding, "", query.New())
|
||||
|
||||
roleBindings, err := am.globalRoleBindingGetter.List("", query.New())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*iamv1alpha2.GlobalRoleBinding, 0)
|
||||
|
||||
for _, obj := range roleBindings.Items {
|
||||
roleBinding := obj.(*iamv1alpha2.GlobalRoleBinding)
|
||||
if contains(roleBinding.Subjects, username, nil) {
|
||||
@@ -302,7 +317,7 @@ func (am *amOperator) ListGlobalRoleBindings(username string) ([]*iamv1alpha2.Gl
|
||||
}
|
||||
|
||||
func (am *amOperator) ListRoleBindings(username string, groups []string, namespace string) ([]*rbacv1.RoleBinding, error) {
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace, query.New())
|
||||
roleBindings, err := am.roleBindingGetter.List(namespace, query.New())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -335,23 +350,23 @@ func contains(subjects []rbacv1.Subject, username string, groups []string) bool
|
||||
}
|
||||
|
||||
func (am *amOperator) ListRoles(namespace string, query *query.Query) (*api.ListResult, error) {
|
||||
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralRole, namespace, query)
|
||||
return am.roleGetter.List(namespace, query)
|
||||
}
|
||||
|
||||
func (am *amOperator) ListClusterRoles(query *query.Query) (*api.ListResult, error) {
|
||||
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralClusterRole, "", query)
|
||||
return am.clusterRoleGetter.List("", query)
|
||||
}
|
||||
|
||||
func (am *amOperator) ListWorkspaceRoles(queryParam *query.Query) (*api.ListResult, error) {
|
||||
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRole, "", queryParam)
|
||||
return am.workspaceRoleGetter.List("", queryParam)
|
||||
}
|
||||
|
||||
func (am *amOperator) ListGlobalRoles(query *query.Query) (*api.ListResult, error) {
|
||||
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralGlobalRole, "", query)
|
||||
return am.globalRoleGetter.List("", query)
|
||||
}
|
||||
|
||||
func (am *amOperator) GetGlobalRole(globalRole string) (*iamv1alpha2.GlobalRole, error) {
|
||||
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralGlobalRole, "", globalRole)
|
||||
obj, err := am.globalRoleGetter.Get("", globalRole)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -938,7 +953,7 @@ func (am *amOperator) GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace st
|
||||
}
|
||||
|
||||
func (am *amOperator) GetWorkspaceRole(workspace string, name string) (*iamv1alpha2.WorkspaceRole, error) {
|
||||
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralWorkspaceRole, "", name)
|
||||
obj, err := am.workspaceRoleGetter.Get("", name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -956,7 +971,7 @@ func (am *amOperator) GetWorkspaceRole(workspace string, name string) (*iamv1alp
|
||||
}
|
||||
|
||||
func (am *amOperator) GetNamespaceRole(namespace string, name string) (*rbacv1.Role, error) {
|
||||
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralRole, namespace, name)
|
||||
obj, err := am.roleGetter.Get(namespace, name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -965,7 +980,7 @@ func (am *amOperator) GetNamespaceRole(namespace string, name string) (*rbacv1.R
|
||||
}
|
||||
|
||||
func (am *amOperator) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
|
||||
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralClusterRole, "", name)
|
||||
obj, err := am.clusterRoleGetter.Get("", name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -973,28 +988,25 @@ func (am *amOperator) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
|
||||
return obj.(*rbacv1.ClusterRole), nil
|
||||
}
|
||||
func (am *amOperator) GetDevOpsRelatedNamespace(devops string) (string, error) {
|
||||
obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops)
|
||||
devopsProject, err := am.devopsProjectLister.Get(devops)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return "", err
|
||||
}
|
||||
devopsProject := obj.(*devopsv1alpha3.DevOpsProject)
|
||||
|
||||
return devopsProject.Status.AdminNamespace, nil
|
||||
}
|
||||
|
||||
func (am *amOperator) GetDevOpsControlledWorkspace(devops string) (string, error) {
|
||||
obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops)
|
||||
devopsProject, err := am.devopsProjectLister.Get(devops)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return "", err
|
||||
}
|
||||
devopsProject := obj.(*devopsv1alpha3.DevOpsProject)
|
||||
return devopsProject.Labels[tenantv1alpha1.WorkspaceLabel], nil
|
||||
}
|
||||
|
||||
func (am *amOperator) GetNamespaceControlledWorkspace(namespace string) (string, error) {
|
||||
obj, err := am.resourceGetter.Get("namespaces", "", namespace)
|
||||
ns, err := am.namespaceLister.Get(namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return "", nil
|
||||
@@ -1002,24 +1014,22 @@ func (am *amOperator) GetNamespaceControlledWorkspace(namespace string) (string,
|
||||
klog.Error(err)
|
||||
return "", err
|
||||
}
|
||||
ns := obj.(*corev1.Namespace)
|
||||
return ns.Labels[tenantv1alpha1.WorkspaceLabel], nil
|
||||
}
|
||||
|
||||
func (am *amOperator) ListGroupWorkspaceRoleBindings(workspace, group string) ([]*iamv1alpha2.WorkspaceRoleBinding, error) {
|
||||
q := workspaceQuery(workspace)
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding, "", q)
|
||||
|
||||
queryParam := query.New()
|
||||
queryParam.LabelSelector = labels.FormatLabels(map[string]string{tenantv1alpha1.WorkspaceLabel: workspace})
|
||||
roleBindings, err := am.workspaceRoleBindingGetter.List("", queryParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*iamv1alpha2.WorkspaceRoleBinding, 0)
|
||||
|
||||
for _, obj := range roleBindings.Items {
|
||||
roleBinding := obj.(*iamv1alpha2.WorkspaceRoleBinding)
|
||||
inSpecifiedWorkspace := workspace == "" || roleBinding.Labels[tenantv1alpha1.WorkspaceLabel] == workspace
|
||||
if containsgroup(roleBinding.Subjects, group) && inSpecifiedWorkspace {
|
||||
if containsGroup(roleBinding.Subjects, group) && inSpecifiedWorkspace {
|
||||
result = append(result, roleBinding)
|
||||
}
|
||||
}
|
||||
@@ -1062,15 +1072,13 @@ func (am *amOperator) DeleteWorkspaceRoleBinding(workspaceName, name string) err
|
||||
}
|
||||
|
||||
func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.RoleBinding, error) {
|
||||
q := workspaceQuery(workspace)
|
||||
namespaces, err := am.resourceGetter.List("namespaces", "", q)
|
||||
namespaces, err := am.namespaceLister.List(labels.SelectorFromSet(labels.Set{tenantv1alpha1.WorkspaceLabel: workspace}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*rbacv1.RoleBinding, 0)
|
||||
for _, ns := range namespaces.Items {
|
||||
namespace := ns.(*corev1.Namespace)
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace.Name, query.New())
|
||||
for _, namespace := range namespaces {
|
||||
roleBindings, err := am.roleBindingGetter.List(namespace.Name, query.New())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -1078,7 +1086,7 @@ func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.
|
||||
|
||||
for _, obj := range roleBindings.Items {
|
||||
roleBinding := obj.(*rbacv1.RoleBinding)
|
||||
if containsgroup(roleBinding.Subjects, group) {
|
||||
if containsGroup(roleBinding.Subjects, group) {
|
||||
result = append(result, roleBinding)
|
||||
}
|
||||
}
|
||||
@@ -1087,23 +1095,20 @@ func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.
|
||||
}
|
||||
|
||||
func (am *amOperator) ListGroupDevOpsRoleBindings(workspace, group string) ([]*rbacv1.RoleBinding, error) {
|
||||
q := workspaceQuery(workspace)
|
||||
namespaces, err := am.resourceGetter.List(devopsv1alpha3.ResourcePluralDevOpsProject, "", q)
|
||||
devOpsProjects, err := am.devopsProjectLister.List(labels.SelectorFromSet(labels.Set{tenantv1alpha1.WorkspaceLabel: workspace}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*rbacv1.RoleBinding, 0)
|
||||
for _, ns := range namespaces.Items {
|
||||
namespace := ns.(*devopsv1alpha3.DevOpsProject)
|
||||
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace.Name, query.New())
|
||||
for _, devOpsProject := range devOpsProjects {
|
||||
roleBindings, err := am.roleBindingGetter.List(devOpsProject.Name, query.New())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range roleBindings.Items {
|
||||
roleBinding := obj.(*rbacv1.RoleBinding)
|
||||
if containsgroup(roleBinding.Subjects, group) {
|
||||
if containsGroup(roleBinding.Subjects, group) {
|
||||
result = append(result, roleBinding)
|
||||
}
|
||||
}
|
||||
@@ -1143,7 +1148,7 @@ func (am *amOperator) DeleteRoleBinding(namespace, name string) error {
|
||||
return am.k8sclient.RbacV1().RoleBindings(namespace).Delete(name, metav1.NewDeleteOptions(0))
|
||||
}
|
||||
|
||||
func containsgroup(subjects []rbacv1.Subject, group string) bool {
|
||||
func containsGroup(subjects []rbacv1.Subject, group string) bool {
|
||||
for _, subject := range subjects {
|
||||
if subject.Kind == rbacv1.GroupKind && subject.Name == group {
|
||||
return true
|
||||
@@ -1151,9 +1156,3 @@ func containsgroup(subjects []rbacv1.Subject, group string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func workspaceQuery(workspace string) *query.Query {
|
||||
q := query.New()
|
||||
q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", tenantv1alpha1.WorkspaceLabel, workspace))
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
|
||||
"github.com/go-ldap/ldap"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
)
|
||||
|
||||
var (
|
||||
AuthRateLimitExceeded = fmt.Errorf("auth rate limit exceeded")
|
||||
AuthFailedIncorrectPassword = fmt.Errorf("incorrect password")
|
||||
AuthFailedAccountIsNotActive = fmt.Errorf("account is not active")
|
||||
AuthFailedIdentityMappingNotMatch = fmt.Errorf("identity mapping not match")
|
||||
)
|
||||
|
||||
type PasswordAuthenticator interface {
|
||||
Authenticate(username, password string) (authuser.Info, error)
|
||||
}
|
||||
|
||||
type passwordAuthenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
options *authoptions.AuthenticationOptions
|
||||
}
|
||||
|
||||
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authoptions.AuthenticationOptions) PasswordAuthenticator {
|
||||
return &passwordAuthenticator{
|
||||
ksClient: ksClient,
|
||||
userLister: userLister,
|
||||
options: options}
|
||||
}
|
||||
|
||||
func (im *passwordAuthenticator) Authenticate(username, password string) (authuser.Info, error) {
|
||||
|
||||
user, err := im.searchUser(username)
|
||||
if err != nil {
|
||||
// internal error
|
||||
if !errors.IsNotFound(err) {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
providerOptions, ldapProvider := im.getLdapProvider()
|
||||
|
||||
// no identity provider
|
||||
// even auth failed, still return username to record login attempt
|
||||
if user == nil && (providerOptions == nil || providerOptions.MappingMethod != oauth.MappingMethodAuto) {
|
||||
return nil, AuthFailedIncorrectPassword
|
||||
}
|
||||
|
||||
if user != nil && user.Status.State != iamv1alpha2.UserActive {
|
||||
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
klog.Errorf("%s, username: %s", AuthRateLimitExceeded, username)
|
||||
return nil, AuthRateLimitExceeded
|
||||
} else {
|
||||
klog.Errorf("%s, username: %s", AuthFailedAccountIsNotActive, username)
|
||||
return nil, AuthFailedAccountIsNotActive
|
||||
}
|
||||
}
|
||||
|
||||
// able to login using the locally principal admin account and password in case of a disruption of LDAP services.
|
||||
if ldapProvider != nil && username != constants.AdminUserName {
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodLookup &&
|
||||
(user == nil || user.Labels[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name) {
|
||||
klog.Error(AuthFailedIdentityMappingNotMatch)
|
||||
return nil, AuthFailedIdentityMappingNotMatch
|
||||
}
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodAuto &&
|
||||
user != nil && user.Labels[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name {
|
||||
klog.Error(AuthFailedIdentityMappingNotMatch)
|
||||
return nil, AuthFailedIdentityMappingNotMatch
|
||||
}
|
||||
|
||||
authenticated, err := ldapProvider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) || ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
|
||||
return nil, AuthFailedIncorrectPassword
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if authenticated != nil && user == nil {
|
||||
authenticated.Labels = map[string]string{iamv1alpha2.IdentifyProviderLabel: providerOptions.Name}
|
||||
if authenticated, err = im.ksClient.IamV1alpha2().Users().Create(authenticated); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if authenticated != nil {
|
||||
return &authuser.DefaultInfo{
|
||||
Name: authenticated.Name,
|
||||
UID: string(authenticated.UID),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
|
||||
return &authuser.DefaultInfo{
|
||||
Name: user.Name,
|
||||
UID: string(user.UID),
|
||||
Groups: user.Spec.Groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, AuthFailedIncorrectPassword
|
||||
}
|
||||
|
||||
func (im *passwordAuthenticator) searchUser(username string) (*iamv1alpha2.User, error) {
|
||||
|
||||
if _, err := mail.ParseAddress(username); err != nil {
|
||||
return im.userLister.Get(username)
|
||||
} else {
|
||||
users, err := im.userLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
for _, find := range users {
|
||||
if find.Spec.Email == username {
|
||||
return find, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
|
||||
}
|
||||
|
||||
func (im *passwordAuthenticator) getLdapProvider() (*oauth.IdentityProviderOptions, identityprovider.LdapProvider) {
|
||||
for _, options := range im.options.OAuthOptions.IdentityProviders {
|
||||
if options.Type == identityprovider.LdapIdentityProvider {
|
||||
if provider, err := identityprovider.NewLdapProvider(options.Provider); err != nil {
|
||||
klog.Error(err)
|
||||
} else {
|
||||
return &options, provider
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
package im
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"fmt"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
resources "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
|
||||
)
|
||||
|
||||
type IdentityManagementInterface interface {
|
||||
@@ -35,41 +35,40 @@ type IdentityManagementInterface interface {
|
||||
UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error)
|
||||
DescribeUser(username string) (*iamv1alpha2.User, error)
|
||||
ModifyPassword(username string, password string) error
|
||||
ListLoginRecords(query *query.Query) (*api.ListResult, error)
|
||||
ListLoginRecords(username string, query *query.Query) (*api.ListResult, error)
|
||||
PasswordVerify(username string, password string) error
|
||||
}
|
||||
|
||||
func NewOperator(ksClient kubesphere.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
|
||||
im := &defaultIMOperator{
|
||||
ksClient: ksClient,
|
||||
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
|
||||
options: options,
|
||||
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
|
||||
im := &imOperator{
|
||||
ksClient: ksClient,
|
||||
userGetter: userGetter,
|
||||
loginRecordGetter: loginRecordGetter,
|
||||
options: options,
|
||||
}
|
||||
return im
|
||||
}
|
||||
|
||||
type defaultIMOperator struct {
|
||||
ksClient kubesphere.Interface
|
||||
resourceGetter *resourcev1alpha3.ResourceGetter
|
||||
options *authoptions.AuthenticationOptions
|
||||
type imOperator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter resources.Interface
|
||||
loginRecordGetter resources.Interface
|
||||
options *authoptions.AuthenticationOptions
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", user.Name)
|
||||
// UpdateUser returns user information after update.
|
||||
func (im *imOperator) UpdateUser(new *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
old, err := im.fetch(new.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
old := obj.(*iamv1alpha2.User).DeepCopy()
|
||||
if user.Annotations == nil {
|
||||
user.Annotations = make(map[string]string, 0)
|
||||
if old.Annotations == nil {
|
||||
old.Annotations = make(map[string]string, 0)
|
||||
}
|
||||
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = old.Annotations[iamv1alpha2.PasswordEncryptedAnnotation]
|
||||
user.Spec.EncryptedPassword = old.Spec.EncryptedPassword
|
||||
user.Status = old.Status
|
||||
|
||||
updated, err := im.ksClient.IamV1alpha2().Users().Update(user)
|
||||
// keep encrypted password
|
||||
new.Spec.EncryptedPassword = old.Spec.EncryptedPassword
|
||||
updated, err := im.ksClient.IamV1alpha2().Users().Update(old)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
@@ -77,18 +76,23 @@ func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
|
||||
return ensurePasswordNotOutput(updated), nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) ModifyPassword(username string, password string) error {
|
||||
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
|
||||
func (im *imOperator) fetch(username string) (*iamv1alpha2.User, error) {
|
||||
obj, err := im.userGetter.Get("", username)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
user := obj.(*iamv1alpha2.User).DeepCopy()
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (im *imOperator) ModifyPassword(username string, password string) error {
|
||||
user, err := im.fetch(username)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
user := obj.(*iamv1alpha2.User).DeepCopy()
|
||||
delete(user.Annotations, iamv1alpha2.PasswordEncryptedAnnotation)
|
||||
user.Spec.EncryptedPassword = password
|
||||
|
||||
_, err = im.ksClient.IamV1alpha2().Users().Update(user)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
@@ -97,13 +101,12 @@ func (im *defaultIMOperator) ModifyPassword(username string, password string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
|
||||
result, err = im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", query)
|
||||
func (im *imOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
|
||||
result, err = im.userGetter.List("", query)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]interface{}, 0)
|
||||
for _, item := range result.Items {
|
||||
user := item.(*iamv1alpha2.User)
|
||||
@@ -114,40 +117,34 @@ func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResu
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) PasswordVerify(username string, password string) error {
|
||||
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
|
||||
func (im *imOperator) PasswordVerify(username string, password string) error {
|
||||
obj, err := im.userGetter.Get("", username)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
user := obj.(*iamv1alpha2.User)
|
||||
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
|
||||
return nil
|
||||
if err = auth.PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
|
||||
return err
|
||||
}
|
||||
return AuthFailedIncorrectPassword
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
|
||||
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
|
||||
func (im *imOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
|
||||
obj, err := im.userGetter.Get("", username)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := obj.(*iamv1alpha2.User)
|
||||
return ensurePasswordNotOutput(user), nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) DeleteUser(username string) error {
|
||||
func (im *imOperator) DeleteUser(username string) error {
|
||||
return im.ksClient.IamV1alpha2().Users().Delete(username, metav1.NewDeleteOptions(0))
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
func (im *imOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
|
||||
user, err := im.ksClient.IamV1alpha2().Users().Create(user)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
@@ -156,8 +153,9 @@ func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (im *defaultIMOperator) ListLoginRecords(query *query.Query) (*api.ListResult, error) {
|
||||
result, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralLoginRecord, "", query)
|
||||
func (im *imOperator) ListLoginRecords(username string, q *query.Query) (*api.ListResult, error) {
|
||||
q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", iamv1alpha2.UserReferenceLabel, username))
|
||||
result, err := im.loginRecordGetter.List("", q)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
|
||||
@@ -15,25 +15,3 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncryptPassword(t *testing.T) {
|
||||
password := "P@88w0rd"
|
||||
encryptedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !checkPasswordHash(password, encryptedPassword) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(encryptedPassword)
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
@@ -133,9 +133,9 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter {
|
||||
}
|
||||
}
|
||||
|
||||
// tryResource will retrieve a getter with resource name, it doesn't guarantee find resource with correct group version
|
||||
// TryResource will retrieve a getter with resource name, it doesn't guarantee find resource with correct group version
|
||||
// need to refactor this use schema.GroupVersionResource
|
||||
func (r *ResourceGetter) tryResource(resource string) v1alpha3.Interface {
|
||||
func (r *ResourceGetter) TryResource(resource string) v1alpha3.Interface {
|
||||
for k, v := range r.getters {
|
||||
if k.Resource == resource {
|
||||
return v
|
||||
@@ -145,7 +145,7 @@ func (r *ResourceGetter) tryResource(resource string) v1alpha3.Interface {
|
||||
}
|
||||
|
||||
func (r *ResourceGetter) Get(resource, namespace, name string) (runtime.Object, error) {
|
||||
getter := r.tryResource(resource)
|
||||
getter := r.TryResource(resource)
|
||||
if getter == nil {
|
||||
return nil, ErrResourceNotSupported
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (r *ResourceGetter) Get(resource, namespace, name string) (runtime.Object,
|
||||
}
|
||||
|
||||
func (r *ResourceGetter) List(resource, namespace string, query *query.Query) (*api.ListResult, error) {
|
||||
getter := r.tryResource(resource)
|
||||
getter := r.TryResource(resource)
|
||||
if getter == nil {
|
||||
return nil, ErrResourceNotSupported
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
|
||||
tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/rbac"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
@@ -540,8 +540,8 @@ func prepare() Interface {
|
||||
RoleBindings().Informer().GetIndexer().Add(roleBinding)
|
||||
}
|
||||
|
||||
amOperator := am.NewOperator(fakeInformerFactory, ksClient, k8sClient)
|
||||
authorizer := authorizerfactory.NewRBACAuthorizer(amOperator)
|
||||
amOperator := am.NewOperator(ksClient, k8sClient, fakeInformerFactory)
|
||||
authorizer := rbac.NewRBACAuthorizer(amOperator)
|
||||
|
||||
return New(fakeInformerFactory, k8sClient, ksClient, nil, nil, nil, amOperator, authorizer)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user