diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 8a64adea3..694092cd2 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -239,8 +239,8 @@ func (s *APIServer) installKubeSphereAPIs() { userLister := s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister() urlruntime.Must(oauth.AddToContainer(s.container, imOperator, auth.NewTokenOperator(s.CacheClient, s.Issuer, s.Config.AuthenticationOptions), - auth.NewPasswordAuthenticator(userLister, s.Config.AuthenticationOptions), - auth.NewOAuthAuthenticator(userLister, s.Config.AuthenticationOptions), + auth.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(), userLister, s.Config.AuthenticationOptions), + auth.NewOAuthAuthenticator(s.KubernetesClient.KubeSphere(), userLister, s.Config.AuthenticationOptions), auth.NewLoginRecorder(s.KubernetesClient.KubeSphere(), userLister), s.Config.AuthenticationOptions)) urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.Config.ServiceMeshOptions, s.container, s.KubernetesClient.Kubernetes(), s.CacheClient)) @@ -337,7 +337,9 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) { // authenticators are unordered authn := unionauth.New(anonymous.NewAuthenticator(), - basictoken.New(basic.NewBasicAuthenticator(auth.NewPasswordAuthenticator(userLister, + basictoken.New(basic.NewBasicAuthenticator(auth.NewPasswordAuthenticator( + s.KubernetesClient.KubeSphere(), + userLister, s.Config.AuthenticationOptions), loginRecorder)), bearertoken.New(jwt.NewTokenAuthenticator( diff --git a/pkg/apiserver/authentication/identityprovider/aliyunidaas/idaas_test.go b/pkg/apiserver/authentication/identityprovider/aliyunidaas/idaas_test.go index 95bd402d0..8feca4b96 100644 --- a/pkg/apiserver/authentication/identityprovider/aliyunidaas/idaas_test.go +++ b/pkg/apiserver/authentication/identityprovider/aliyunidaas/idaas_test.go @@ -53,7 +53,7 @@ endpoint: userInfoUrl: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo" authURL: "https://xxxx.login.aliyunidaas.com/oauth/authorize" tokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token" -redirectURL: "https://console.kubesphere.io/oauth/redirect/idaas" +redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas" scopes: - read `)}, @@ -65,7 +65,7 @@ scopes: TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token", UserInfoURL: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo", }, - RedirectURL: "https://console.kubesphere.io/oauth/redirect/idaas", + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas", Scopes: []string{"read"}, Config: &oauth2.Config{ ClientID: "xxxx", @@ -75,7 +75,7 @@ scopes: TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token", AuthStyle: oauth2.AuthStyleAutoDetect, }, - RedirectURL: "https://console.kubesphere.io/oauth/redirect/idaas", + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas", Scopes: []string{"read"}, }, }, diff --git a/pkg/apiserver/authentication/identityprovider/github/github_test.go b/pkg/apiserver/authentication/identityprovider/github/github_test.go index 09bfa99c1..817ce095d 100644 --- a/pkg/apiserver/authentication/identityprovider/github/github_test.go +++ b/pkg/apiserver/authentication/identityprovider/github/github_test.go @@ -87,7 +87,7 @@ var _ = Describe("GitHub", func() { configYAML := ` clientID: de6ff8bed0304e487b6e clientSecret: 2b70536f79ec8d2939863509d05e2a71c268b9af -redirectURL: "https://console.kubesphere.io/oauth/redirect/github" +redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github" scopes: - user ` @@ -103,7 +103,7 @@ scopes: TokenURL: tokenURL, UserInfoURL: userInfoURL, }, - RedirectURL: "https://console.kubesphere.io/oauth/redirect/github", + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github", Scopes: []string{"user"}, Config: &oauth2.Config{ ClientID: "de6ff8bed0304e487b6e", @@ -112,7 +112,7 @@ scopes: AuthURL: authURL, TokenURL: tokenURL, }, - RedirectURL: "https://console.kubesphere.io/oauth/redirect/github", + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github", Scopes: []string{"user"}, }, } @@ -122,7 +122,7 @@ scopes: config := oauth.DynamicOptions{ "clientID": "de6ff8bed0304e487b6e", "clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af", - "redirectURL": "https://console.kubesphere.io/oauth/redirect/github", + "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/github", "insecureSkipVerify": true, "endpoint": oauth.DynamicOptions{ "authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL), @@ -136,7 +136,7 @@ scopes: expected := oauth.DynamicOptions{ "clientID": "de6ff8bed0304e487b6e", "clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af", - "redirectURL": "https://console.kubesphere.io/oauth/redirect/github", + "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/github", "insecureSkipVerify": true, "endpoint": oauth.DynamicOptions{ "authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL), @@ -147,7 +147,7 @@ scopes: Expect(config).Should(Equal(expected)) }) It("should login successfully", func() { - url, _ := url.Parse("https://console.kubesphere.io/oauth/redirect/test?code=00000") + url, _ := url.Parse("https://ks-console.kubesphere-system.svc/oauth/redirect/test?code=00000") req := &http.Request{URL: url} identity, err := provider.IdentityExchangeCallback(req) Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go b/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go index 871a7bcf1..5430e8924 100644 --- a/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go +++ b/pkg/apiserver/authentication/identityprovider/oidc/oidc_test.go @@ -171,7 +171,7 @@ var _ = Describe("OIDC", func() { "issuer": oidcServer.URL, "clientID": "kubesphere", "clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7", - "redirectURL": "https://console.kubesphere.io/oauth/redirect/oidc", + "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc", "insecureSkipVerify": true, } factory := oidcProviderFactory{} @@ -181,7 +181,7 @@ var _ = Describe("OIDC", func() { "issuer": oidcServer.URL, "clientID": "kubesphere", "clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7", - "redirectURL": "https://console.kubesphere.io/oauth/redirect/oidc", + "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc", "insecureSkipVerify": true, "endpoint": oauth.DynamicOptions{ "authURL": fmt.Sprintf("%s/authorize", oidcServer.URL), @@ -194,7 +194,7 @@ var _ = Describe("OIDC", func() { Expect(config).Should(Equal(expected)) }) It("should login successfully", func() { - url, _ := url.Parse("https://console.kubesphere.io/oauth/redirect/oidc?code=00000") + url, _ := url.Parse("https://ks-console.kubesphere-system.svc/oauth/redirect/oidc?code=00000") req := &http.Request{URL: url} identity, err := provider.IdentityExchangeCallback(req) Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/authentication/oauth/error.go b/pkg/apiserver/authentication/oauth/error.go index 2de057863..46fe769d9 100644 --- a/pkg/apiserver/authentication/oauth/error.go +++ b/pkg/apiserver/authentication/oauth/error.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The KubeSphere Authors. +Copyright 2021 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. @@ -18,6 +18,7 @@ package oauth import "fmt" +// The following error type is defined in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 var ( // ErrorInvalidClient // Client authentication failed (e.g., unknown client, no diff --git a/pkg/apiserver/authentication/oauth/options.go b/pkg/apiserver/authentication/oauth/options.go index 0efb3d063..4e6b72d4f 100644 --- a/pkg/apiserver/authentication/oauth/options.go +++ b/pkg/apiserver/authentication/oauth/options.go @@ -191,7 +191,7 @@ type Token struct { type Client struct { // The name of the OAuth client is used as the client_id parameter when making requests to /oauth/authorize // and /oauth/token. - Name string + Name string `json:"name" yaml:"name,omitempty"` // Secret is the unique secret associated with a client Secret string `json:"-" yaml:"secret,omitempty"` @@ -225,19 +225,7 @@ type Client struct { var ( // AllowAllRedirectURI Allow any redirect URI if the redirectURI is defined in request - AllowAllRedirectURI = "*" - DefaultTokenMaxAge = time.Second * 86400 - DefaultAccessTokenInactivityTimeout = time.Duration(0) - DefaultClients = []Client{{ - Name: "default", - Secret: "kubesphere", - RespondWithChallenges: true, - RedirectURIs: []string{AllowAllRedirectURI}, - GrantMethod: GrantHandlerAuto, - ScopeRestrictions: []string{"full"}, - AccessTokenMaxAge: &DefaultTokenMaxAge, - AccessTokenInactivityTimeout: &DefaultAccessTokenInactivityTimeout, - }} + AllowAllRedirectURI = "*" ) func (o *Options) OAuthClient(name string) (Client, error) { @@ -246,11 +234,6 @@ func (o *Options) OAuthClient(name string) (Client, error) { return found, nil } } - for _, defaultClient := range DefaultClients { - if defaultClient.Name == name { - return defaultClient, nil - } - } return Client{}, ErrorClientNotFound } diff --git a/pkg/apiserver/authentication/oauth/options_test.go b/pkg/apiserver/authentication/oauth/options_test.go index 1d7bfc6a1..7259994f8 100644 --- a/pkg/apiserver/authentication/oauth/options_test.go +++ b/pkg/apiserver/authentication/oauth/options_test.go @@ -19,77 +19,39 @@ package oauth import ( "encoding/json" "testing" - "time" - "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v3" ) -func TestDefaultAuthOptions(t *testing.T) { - oneDay := time.Second * 86400 - zero := time.Duration(0) - expect := Client{ - Name: "default", - RespondWithChallenges: true, - Secret: "kubesphere", - RedirectURIs: []string{AllowAllRedirectURI}, - GrantMethod: GrantHandlerAuto, - ScopeRestrictions: []string{"full"}, - AccessTokenMaxAge: &oneDay, - AccessTokenInactivityTimeout: &zero, - } - - options := NewOptions() - client, err := options.OAuthClient("default") - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(expect, client); len(diff) != 0 { - t.Errorf("%T differ (-got, +expected), %s", expect, diff) - } -} - func TestClientResolveRedirectURL(t *testing.T) { - options := NewOptions() - defaultClient, err := options.OAuthClient("default") - if err != nil { - t.Fatal(err) - } + tests := []struct { Name string client Client wantErr bool expectURL string }{ - { - Name: "default client test", - client: defaultClient, - wantErr: false, - expectURL: "https://localhost:8080/auth/cb", - }, { Name: "custom client test", client: Client{ - Name: "default", - RespondWithChallenges: true, - RedirectURIs: []string{"https://foo.bar.com/oauth/cb"}, - GrantMethod: GrantHandlerAuto, - ScopeRestrictions: []string{"full"}, - }, - wantErr: true, - expectURL: "https://foo.bar.com/oauth/err", - }, - { - Name: "custom client test", - client: Client{ - Name: "default", + Name: "custom", RespondWithChallenges: true, RedirectURIs: []string{AllowAllRedirectURI, "https://foo.bar.com/oauth/cb"}, GrantMethod: GrantHandlerAuto, - ScopeRestrictions: []string{"full"}, }, wantErr: false, - expectURL: "https://foo.bar.com/oauth/err2", + expectURL: "https://foo.bar.com/oauth/cb", + }, + { + Name: "custom client test", + client: Client{ + Name: "custom", + RespondWithChallenges: true, + RedirectURIs: []string{"https://foo.bar.com/oauth/cb"}, + GrantMethod: GrantHandlerAuto, + }, + wantErr: true, + expectURL: "https://foo.bar.com/oauth/cb2", }, } diff --git a/pkg/apiserver/authentication/token/issuer.go b/pkg/apiserver/authentication/token/issuer.go index 4f3803ae0..b6cf206ab 100644 --- a/pkg/apiserver/authentication/token/issuer.go +++ b/pkg/apiserver/authentication/token/issuer.go @@ -83,22 +83,30 @@ type Claims struct { jwt.StandardClaims // Private Claim Names // TokenType defined the type of the token - TokenType Type `json:"token_type"` - // Username is user identity same as `sub` - Username string `json:"username"` + TokenType Type `json:"token_type,omitempty"` + // Username user identity, deprecated field + Username string `json:"username,omitempty"` + // Extra contains the additional information + Extra map[string][]string `json:"extra,omitempty"` + + // Used for issuing authorization code + // Scopes can be used to request that specific sets of information be made available as Claim Values. + Scopes []string `json:"scopes,omitempty"` + + // The following is well-known ID Token fields + + // End-User's full name in displayable form including all name parts, + // possibly including titles and suffixes, ordered according to the End-User's locale and preferences. + Name string `json:"name,omitempty"` // String value used to associate a Client session with an ID Token, and to mitigate replay attacks. // The value is passed through unmodified from the Authentication Request to the ID Token. Nonce string `json:"nonce,omitempty"` - // Scopes can be used to request that specific sets of information be made available as Claim Values. - Scopes []string `json:"scopes,omitempty"` // End-User's preferred e-mail address. Email string `json:"email,omitempty"` // End-User's locale, represented as a BCP47 [RFC5646] language tag. Locale string `json:"locale,omitempty"` // Shorthand name by which the End-User wishes to be referred to at the RP, PreferredUsername string `json:"preferred_username,omitempty"` - // Extra contains the additional information - Extra map[string][]string `json:"extra,omitempty"` } type issuer struct { @@ -128,6 +136,9 @@ func (s *issuer) IssueTo(request *IssueRequest) (string, error) { if len(request.Audience) > 0 { claims.Audience = request.Audience } + if request.Name != "" { + claims.Name = request.Name + } if request.Nonce != "" { claims.Nonce = request.Nonce } diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index cc254954c..f2e01be24 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -18,13 +18,15 @@ package oauth import ( "fmt" - "gopkg.in/square/go-jose.v2" - "kubesphere.io/kubesphere/pkg/utils/sliceutil" "net/http" "net/url" "strings" "time" + "gopkg.in/square/go-jose.v2" + + "kubesphere.io/kubesphere/pkg/utils/sliceutil" + "github.com/form3tech-oss/jwt-go" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" @@ -443,7 +445,7 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err)) return case auth.RateLimitExceededError: - response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err)) + response.WriteHeaderAndEntity(http.StatusTooManyRequests, oauth.NewInvalidGrant(err)) return default: response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err)) @@ -602,6 +604,7 @@ func (h *handler) codeGrant(req *restful.Request, response *restful.Response) { }, Nonce: authorizeContext.Nonce, TokenType: token.IDToken, + Name: authorizeContext.User.GetName(), }, ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout, } @@ -655,3 +658,28 @@ func (h *handler) logout(req *restful.Request, resp *restful.Response) { resp.Header().Set("Content-Type", "text/plain") http.Redirect(resp, req.Request, redirectURL.String(), http.StatusFound) } + +// userinfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User. +func (h *handler) userinfo(req *restful.Request, response *restful.Response) { + authenticated, _ := request.UserFrom(req.Request.Context()) + if authenticated == nil || authenticated.GetName() == user.Anonymous { + response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.ErrorLoginRequired) + return + } + detail, err := h.im.DescribeUser(authenticated.GetName()) + if err != nil { + response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err)) + return + } + + result := token.Claims{ + StandardClaims: jwt.StandardClaims{ + Subject: detail.Name, + }, + Name: detail.Name, + Email: detail.Spec.Email, + Locale: detail.Spec.Lang, + PreferredUsername: detail.Name, + } + response.WriteEntity(result) +} diff --git a/pkg/kapis/oauth/register.go b/pkg/kapis/oauth/register.go index ca5c485bc..f03fe3bad 100644 --- a/pkg/kapis/oauth/register.go +++ b/pkg/kapis/oauth/register.go @@ -57,6 +57,8 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, Doc("The OpenID Provider's configuration information can be retrieved.")) ws.Route(ws.GET("/keys").To(handler.keys). Doc("OP's JSON Web Key Set [JWK] document.")) + ws.Route(ws.GET("/userinfo").To(handler.userinfo). + Doc("UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User.")) // Implement webhook authentication interface // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication @@ -100,15 +102,20 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, To(handler.authorize). Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag})) - // https://tools.ietf.org/html/rfc6749#section-4.3 + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.2 ws.Route(ws.POST("/token"). Consumes(contentTypeFormData). Doc("The resource owner password credentials grant type is suitable in\n"+ "cases where the resource owner has a trust relationship with the\n"+ "client, such as the device operating system or a highly privileged application."). - Param(ws.FormParameter("grant_type", "Value MUST be set to \"password\".").Required(true)). - Param(ws.FormParameter("username", "The resource owner username.").Required(true)). - Param(ws.FormParameter("password", "The resource owner password.").Required(true)). + Param(ws.FormParameter("grant_type", "OAuth defines four grant types: "+ + "authorization code, implicit, resource owner password credentials, and client credentials."). + Required(true)). + Param(ws.FormParameter("client_id", "Valid client credential.").Required(true)). + Param(ws.FormParameter("client_secret", "Valid client credential.").Required(true)). + Param(ws.FormParameter("username", "The resource owner username.").Required(false)). + Param(ws.FormParameter("password", "The resource owner password.").Required(false)). + Param(ws.FormParameter("code", "Valid authorization code.").Required(false)). To(handler.token). Returns(http.StatusOK, http.StatusText(http.StatusOK), &oauth.Token{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag})) diff --git a/pkg/models/auth/authenticator.go b/pkg/models/auth/authenticator.go index 01a532674..60f6d9aca 100644 --- a/pkg/models/auth/authenticator.go +++ b/pkg/models/auth/authenticator.go @@ -23,6 +23,9 @@ import ( "fmt" "net/http" "net/mail" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" @@ -68,6 +71,21 @@ func preRegistrationUser(idp string, identity identityprovider.Identity) authuse } } +func mappedUser(idp string, identity identityprovider.Identity) *iamv1alpha2.User { + // username convert + username := strings.ToLower(identity.GetUsername()) + return &iamv1alpha2.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: username, + Labels: map[string]string{ + iamv1alpha2.IdentifyProviderLabel: idp, + iamv1alpha2.OriginUIDLabel: identity.GetUserID(), + }, + }, + Spec: iamv1alpha2.UserSpec{Email: identity.GetEmail()}, + } +} + // findUser returns the user associated with the username or email func (u *userGetter) findUser(username string) (*iamv1alpha2.User, error) { if _, err := mail.ParseAddress(username); err != nil { diff --git a/pkg/models/auth/oauth.go b/pkg/models/auth/oauth.go index 430c603a6..fbcd87f47 100644 --- a/pkg/models/auth/oauth.go +++ b/pkg/models/auth/oauth.go @@ -22,6 +22,10 @@ import ( "context" "net/http" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + "kubesphere.io/kubesphere/pkg/apiserver/authentication" "k8s.io/apimachinery/pkg/api/errors" @@ -35,49 +39,60 @@ import ( ) type oauthAuthenticator struct { - *userGetter - options *authentication.Options + ksClient kubesphere.Interface + userGetter *userGetter + options *authentication.Options } -func NewOAuthAuthenticator(userLister iamv1alpha2listers.UserLister, +func NewOAuthAuthenticator(ksClient kubesphere.Interface, + userLister iamv1alpha2listers.UserLister, options *authentication.Options) OAuthAuthenticator { authenticator := &oauthAuthenticator{ + ksClient: ksClient, userGetter: &userGetter{userLister: userLister}, options: options, } return authenticator } -func (o oauthAuthenticator) Authenticate(ctx context.Context, provider string, req *http.Request) (authuser.Info, string, error) { - options, err := o.options.OAuthOptions.IdentityProviderOptions(provider) +func (o *oauthAuthenticator) Authenticate(_ context.Context, provider string, req *http.Request) (authuser.Info, string, error) { + providerOptions, err := o.options.OAuthOptions.IdentityProviderOptions(provider) // identity provider not registered if err != nil { klog.Error(err) return nil, "", err } - identityProvider, err := identityprovider.GetOAuthProvider(options.Name) + oauthIdentityProvider, err := identityprovider.GetOAuthProvider(providerOptions.Name) if err != nil { klog.Error(err) return nil, "", err } - identity, err := identityProvider.IdentityExchangeCallback(req) + authenticated, err := oauthIdentityProvider.IdentityExchangeCallback(req) if err != nil { klog.Error(err) return nil, "", err } - mappedUser, err := o.findMappedUser(options.Name, identity.GetUserID()) - if mappedUser == nil && options.MappingMethod == oauth.MappingMethodLookup { + user, err := o.userGetter.findMappedUser(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 mappedUser == nil && options.MappingMethod == oauth.MappingMethodAuto { - return preRegistrationUser(options.Name, identity), options.Name, nil - } - if mappedUser != nil { - return &authuser.DefaultInfo{Name: mappedUser.GetName()}, options.Name, nil + if user == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto { + if !providerOptions.DisableLoginConfirmation { + return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil + } + user, err = o.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{}) + if err != nil { + return nil, providerOptions.Name, err + } } - return nil, "", errors.NewNotFound(iamv1alpha2.Resource(iamv1alpha2.ResourcesSingularUser), identity.GetUsername()) + if user != nil { + return &authuser.DefaultInfo{Name: user.GetName()}, providerOptions.Name, nil + } + + return nil, "", errors.NewNotFound(iamv1alpha2.Resource("user"), authenticated.GetUsername()) } diff --git a/pkg/models/auth/oauth_test.go b/pkg/models/auth/oauth_test.go index b2f3d9607..61e87e581 100644 --- a/pkg/models/auth/oauth_test.go +++ b/pkg/models/auth/oauth_test.go @@ -88,6 +88,7 @@ func Test_oauthAuthenticator_Authenticate(t *testing.T) { { name: "Should successfully", oauthAuthenticator: NewOAuthAuthenticator( + nil, ksInformerFactory.Iam().V1alpha2().Users().Lister(), oauthOptions, ), @@ -105,6 +106,7 @@ func Test_oauthAuthenticator_Authenticate(t *testing.T) { { name: "Should successfully", oauthAuthenticator: NewOAuthAuthenticator( + nil, ksInformerFactory.Iam().V1alpha2().Users().Lister(), oauthOptions, ), diff --git a/pkg/models/auth/password.go b/pkg/models/auth/password.go index a4ed80e57..a2fb38904 100644 --- a/pkg/models/auth/password.go +++ b/pkg/models/auth/password.go @@ -21,6 +21,10 @@ package auth import ( "context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + "kubesphere.io/kubesphere/pkg/apiserver/authentication" "golang.org/x/crypto/bcrypt" @@ -36,20 +40,23 @@ import ( ) type passwordAuthenticator struct { + ksClient kubesphere.Interface userGetter *userGetter authOptions *authentication.Options } -func NewPasswordAuthenticator(userLister iamv1alpha2listers.UserLister, +func NewPasswordAuthenticator(ksClient kubesphere.Interface, + userLister iamv1alpha2listers.UserLister, options *authentication.Options) PasswordAuthenticator { passwordAuthenticator := &passwordAuthenticator{ + ksClient: ksClient, userGetter: &userGetter{userLister: userLister}, authOptions: options, } return passwordAuthenticator } -func (p *passwordAuthenticator) Authenticate(ctx context.Context, username, password string) (authuser.Info, string, error) { +func (p *passwordAuthenticator) Authenticate(_ context.Context, username, password string) (authuser.Info, string, error) { // empty username or password are not allowed if username == "" || password == "" { return nil, "", IncorrectPasswordError @@ -69,17 +76,27 @@ func (p *passwordAuthenticator) Authenticate(ctx context.Context, username, pass return nil, providerOptions.Name, err } linkedAccount, err := p.userGetter.findMappedUser(providerOptions.Name, authenticated.GetUserID()) + if err != nil { + return nil, providerOptions.Name, err + } // using this method requires you to manually provision users. if providerOptions.MappingMethod == oauth.MappingMethodLookup && linkedAccount == nil { continue } + // the user will automatically create and mapping when login successful. + if linkedAccount == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto { + if !providerOptions.DisableLoginConfirmation { + return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil + } + + linkedAccount, err = p.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{}) + if err != nil { + return nil, providerOptions.Name, err + } + } 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 - } } }