Refactor authenticator
Signed-off-by: hongming <hongming@kubesphere.io>
This commit is contained in:
@@ -21,27 +21,18 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/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"
|
||||
)
|
||||
|
||||
@@ -51,139 +42,20 @@ var (
|
||||
AccountIsNotActiveError = fmt.Errorf("account is not active")
|
||||
)
|
||||
|
||||
// PasswordAuthenticator is an interface implemented by authenticator which take a
|
||||
// username and password.
|
||||
type PasswordAuthenticator interface {
|
||||
Authenticate(username, password string) (authuser.Info, string, error)
|
||||
Authenticate(ctx context.Context, username, password string) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type OAuthAuthenticator 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
|
||||
Authenticate(ctx context.Context, provider string, req *http.Request) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
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 NewOAuthAuthenticator(ksClient kubesphere.Interface,
|
||||
ksInformer informers.SharedInformerFactory,
|
||||
options *authoptions.AuthenticationOptions) OAuthAuthenticator {
|
||||
oauth2Authenticator := &oauth2Authenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: ksInformer.Iam().V1alpha2().Users().Lister()},
|
||||
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.GetGenericProvider(providerOptions.Name); 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())
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -193,95 +65,32 @@ func preRegistrationUser(idp string, identity identityprovider.Identity) authuse
|
||||
iamv1alpha2.ExtraUsername: {identity.GetUsername()},
|
||||
iamv1alpha2.ExtraEmail: {identity.GetEmail()},
|
||||
},
|
||||
Groups: []string{iamv1alpha2.PreRegistrationUserGroup},
|
||||
}
|
||||
}
|
||||
|
||||
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()},
|
||||
}
|
||||
}
|
||||
|
||||
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.GetOAuthProvider(providerOptions.Name)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
users, err := u.userLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if user.Spec.Email == username {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
|
||||
}
|
||||
|
||||
func (u *userGetter) findLinkedAccount(idp, uid string) (*iamv1alpha2.User, error) {
|
||||
// findMappedUser returns the user which mapped to the identity
|
||||
func (u *userGetter) findMappedUser(idp, uid string) (*iamv1alpha2.User, error) {
|
||||
selector := labels.SelectorFromSet(labels.Set{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid,
|
||||
|
||||
83
pkg/models/auth/oauth.go
Normal file
83
pkg/models/auth/oauth.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
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 (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
)
|
||||
|
||||
type oauthAuthenticator struct {
|
||||
*userGetter
|
||||
options *authentication.Options
|
||||
}
|
||||
|
||||
func NewOAuthAuthenticator(userLister iamv1alpha2listers.UserLister,
|
||||
options *authentication.Options) OAuthAuthenticator {
|
||||
authenticator := &oauthAuthenticator{
|
||||
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)
|
||||
// identity provider not registered
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
identityProvider, err := identityprovider.GetOAuthProvider(options.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
identity, err := identityProvider.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 {
|
||||
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
|
||||
}
|
||||
|
||||
return nil, "", errors.NewNotFound(iamv1alpha2.Resource(iamv1alpha2.ResourcesSingularUser), identity.GetUsername())
|
||||
}
|
||||
203
pkg/models/auth/oauth_test.go
Normal file
203
pkg/models/auth/oauth_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
)
|
||||
|
||||
func Test_oauthAuthenticator_Authenticate(t *testing.T) {
|
||||
|
||||
oauthOptions := &authentication.Options{
|
||||
OAuthOptions: &oauth.Options{
|
||||
IdentityProviders: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "fake",
|
||||
MappingMethod: "auto",
|
||||
Type: "FakeIdentityProvider",
|
||||
Provider: oauth.DynamicOptions{
|
||||
"identities": map[string]interface{}{
|
||||
"code1": map[string]string{
|
||||
"uid": "100001",
|
||||
"email": "user1@kubesphere.io",
|
||||
"username": "user1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
identityprovider.RegisterOAuthProvider(&fakeProviderFactory{})
|
||||
if err := identityprovider.SetupWithOptions(oauthOptions.OAuthOptions.IdentityProviders); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ksClient := fakeks.NewSimpleClientset()
|
||||
ksInformerFactory := ksinformers.NewSharedInformerFactory(ksClient, 0)
|
||||
err := ksInformerFactory.Iam().V1alpha2().Users().Informer().GetIndexer().Add(newUser("user1", "100001", "fake"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
provider string
|
||||
req *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
oauthAuthenticator OAuthAuthenticator
|
||||
args args
|
||||
userInfo user.Info
|
||||
provider string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Should successfully",
|
||||
oauthAuthenticator: NewOAuthAuthenticator(
|
||||
ksInformerFactory.Iam().V1alpha2().Users().Lister(),
|
||||
oauthOptions,
|
||||
),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
provider: "fake",
|
||||
req: must(http.NewRequest(http.MethodGet, "https://ks-console.kubesphere.io/oauth/callback/test?code=code1&state=100001", nil)),
|
||||
},
|
||||
userInfo: &user.DefaultInfo{
|
||||
Name: "user1",
|
||||
},
|
||||
provider: "fake",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Should successfully",
|
||||
oauthAuthenticator: NewOAuthAuthenticator(
|
||||
ksInformerFactory.Iam().V1alpha2().Users().Lister(),
|
||||
oauthOptions,
|
||||
),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
provider: "fake1",
|
||||
req: must(http.NewRequest(http.MethodGet, "https://ks-console.kubesphere.io/oauth/callback/test?code=code1&state=100001", nil)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
userInfo, provider, err := tt.oauthAuthenticator.Authenticate(tt.args.ctx, tt.args.provider, tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(userInfo, tt.userInfo) {
|
||||
t.Errorf("Authenticate() got = %v, want %v", userInfo, tt.userInfo)
|
||||
}
|
||||
if provider != tt.provider {
|
||||
t.Errorf("Authenticate() got = %v, want %v", provider, tt.provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must(r *http.Request, err error) *http.Request {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func newUser(username string, uid string, idp string) *iamv1alpha2.User {
|
||||
return &iamv1alpha2.User{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamv1alpha2.ResourceKindUser,
|
||||
APIVersion: iamv1alpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: username,
|
||||
Labels: map[string]string{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type fakeProviderFactory struct {
|
||||
}
|
||||
|
||||
type fakeProvider struct {
|
||||
Identities map[string]fakeIdentity `json:"identities"`
|
||||
}
|
||||
|
||||
type fakeIdentity struct {
|
||||
UID string `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetUserID() string {
|
||||
return f.UID
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetUsername() string {
|
||||
return f.Username
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetEmail() string {
|
||||
return f.Email
|
||||
}
|
||||
|
||||
func (fakeProviderFactory) Type() string {
|
||||
return "FakeIdentityProvider"
|
||||
}
|
||||
|
||||
func (fakeProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var fakeProvider fakeProvider
|
||||
if err := mapstructure.Decode(options, &fakeProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fakeProvider, nil
|
||||
}
|
||||
|
||||
func (f fakeProvider) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
code := req.URL.Query().Get("code")
|
||||
if identity, ok := f.Identities[code]; ok {
|
||||
return identity, nil
|
||||
}
|
||||
return nil, fmt.Errorf("authorization failed")
|
||||
}
|
||||
134
pkg/models/auth/password.go
Normal file
134
pkg/models/auth/password.go
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
|
||||
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.
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
)
|
||||
|
||||
type passwordAuthenticator struct {
|
||||
userGetter *userGetter
|
||||
authOptions *authentication.Options
|
||||
}
|
||||
|
||||
func NewPasswordAuthenticator(userLister iamv1alpha2listers.UserLister,
|
||||
options *authentication.Options) PasswordAuthenticator {
|
||||
passwordAuthenticator := &passwordAuthenticator{
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
authOptions: options,
|
||||
}
|
||||
return passwordAuthenticator
|
||||
}
|
||||
|
||||
func (p *passwordAuthenticator) Authenticate(ctx context.Context, 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.GetGenericProvider(providerOptions.Name); 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.findMappedUser(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 PasswordVerify(encryptedPassword, password string) error {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil {
|
||||
return IncorrectPasswordError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -23,99 +23,87 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
)
|
||||
|
||||
// TokenManagementInterface Cache issued token, support revocation of tokens after issuance
|
||||
type TokenManagementInterface interface {
|
||||
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
|
||||
Verify(token string) (user.Info, error)
|
||||
// IssueTo issues a token a User, return error if issuing process failed
|
||||
IssueTo(user user.Info) (*oauth.Token, error)
|
||||
// Verify the given token and returns token.VerifiedResponse
|
||||
Verify(token string) (*token.VerifiedResponse, error)
|
||||
// IssueTo issue a token for the specified user
|
||||
IssueTo(request *token.IssueRequest) (string, error)
|
||||
// Revoke revoke the specified token
|
||||
Revoke(token string) error
|
||||
// RevokeAllUserTokens revoke all user tokens
|
||||
RevokeAllUserTokens(username string) error
|
||||
}
|
||||
|
||||
type tokenOperator struct {
|
||||
issuer token.Issuer
|
||||
options *authoptions.AuthenticationOptions
|
||||
options *authentication.Options
|
||||
cache cache.Interface
|
||||
}
|
||||
|
||||
func NewTokenOperator(cache cache.Interface, options *authoptions.AuthenticationOptions) TokenManagementInterface {
|
||||
func (t tokenOperator) Revoke(token string) error {
|
||||
pattern := fmt.Sprintf("kubesphere:user:*:token:%s", token)
|
||||
if keys, err := t.cache.Keys(pattern); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
} else if len(keys) > 0 {
|
||||
if err := t.cache.Del(keys...); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTokenOperator(cache cache.Interface, issuer token.Issuer, options *authentication.Options) TokenManagementInterface {
|
||||
operator := &tokenOperator{
|
||||
issuer: token.NewTokenIssuer(options.JwtSecret, options.MaximumClockSkew),
|
||||
issuer: issuer,
|
||||
options: options,
|
||||
cache: cache,
|
||||
}
|
||||
return operator
|
||||
}
|
||||
|
||||
func (t tokenOperator) Verify(tokenStr string) (user.Info, error) {
|
||||
authenticated, tokenType, err := t.issuer.Verify(tokenStr)
|
||||
func (t *tokenOperator) Verify(tokenStr string) (*token.VerifiedResponse, error) {
|
||||
response, err := t.issuer.Verify(tokenStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.options.OAuthOptions.AccessTokenMaxAge == 0 ||
|
||||
tokenType == token.StaticToken {
|
||||
return authenticated, nil
|
||||
response.TokenType == token.StaticToken {
|
||||
return response, nil
|
||||
}
|
||||
if err := t.tokenCacheValidate(authenticated.GetName(), tokenStr); err != nil {
|
||||
if err := t.tokenCacheValidate(response.User.GetName(), tokenStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authenticated, nil
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) IssueTo(user user.Info) (*oauth.Token, error) {
|
||||
accessTokenExpiresIn := t.options.OAuthOptions.AccessTokenMaxAge
|
||||
refreshTokenExpiresIn := accessTokenExpiresIn + t.options.OAuthOptions.AccessTokenInactivityTimeout
|
||||
|
||||
accessToken, err := t.issuer.IssueTo(user, token.AccessToken, accessTokenExpiresIn)
|
||||
func (t *tokenOperator) IssueTo(request *token.IssueRequest) (string, error) {
|
||||
tokenStr, err := t.issuer.IssueTo(request)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
refreshToken, err := t.issuer.IssueTo(user, token.RefreshToken, refreshTokenExpiresIn)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &oauth.Token{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int(accessTokenExpiresIn.Seconds()),
|
||||
}
|
||||
|
||||
if !t.options.MultipleLogin {
|
||||
if err = t.RevokeAllUserTokens(user.GetName()); err != nil {
|
||||
if request.ExpiresIn > 0 {
|
||||
if err = t.cacheToken(request.User.GetName(), tokenStr, request.ExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if accessTokenExpiresIn > 0 {
|
||||
if err = t.cacheToken(user.GetName(), accessToken, accessTokenExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if err = t.cacheToken(user.GetName(), refreshToken, refreshTokenExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return tokenStr, nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
// RevokeAllUserTokens revoke all user tokens in the cache
|
||||
func (t *tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
pattern := fmt.Sprintf("kubesphere:user:%s:token:*", username)
|
||||
if keys, err := t.cache.Keys(pattern); err != nil {
|
||||
klog.Error(err)
|
||||
@@ -129,7 +117,8 @@ func (t tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
// tokenCacheValidate verify that the token is in the cache
|
||||
func (t *tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
|
||||
if exist, err := t.cache.Exists(key); err != nil {
|
||||
return err
|
||||
@@ -141,7 +130,8 @@ func (t tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) cacheToken(username, token string, duration time.Duration) error {
|
||||
// cacheToken cache the token for a period of time
|
||||
func (t *tokenOperator) cacheToken(username, token string, duration time.Duration) error {
|
||||
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
|
||||
if err := t.cache.Set(key, token, duration); err != nil {
|
||||
klog.Error(err)
|
||||
|
||||
@@ -19,13 +19,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
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/models/auth"
|
||||
@@ -43,7 +44,7 @@ type IdentityManagementInterface interface {
|
||||
PasswordVerify(username string, password string) error
|
||||
}
|
||||
|
||||
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
|
||||
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authentication.Options) IdentityManagementInterface {
|
||||
im := &imOperator{
|
||||
ksClient: ksClient,
|
||||
userGetter: userGetter,
|
||||
@@ -57,7 +58,7 @@ type imOperator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter resources.Interface
|
||||
loginRecordGetter resources.Interface
|
||||
options *authoptions.AuthenticationOptions
|
||||
options *authentication.Options
|
||||
}
|
||||
|
||||
// UpdateUser returns user information after update.
|
||||
|
||||
Reference in New Issue
Block a user