login record CRD (#2565)

* Signed-off-by: hongming <talonwan@yunify.com>

support ldap identity provider

Signed-off-by: hongming <talonwan@yunify.com>

* add login record

Signed-off-by: Jeff <zw0948@gmail.com>

Co-authored-by: hongming <talonwan@yunify.com>
This commit is contained in:
zryfish
2020-07-23 22:10:39 +08:00
committed by GitHub
parent 50a6c7b2b5
commit 3d74bb0589
51 changed files with 2163 additions and 548 deletions

View File

@@ -0,0 +1,173 @@
/*
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"
"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"
"net/mail"
)
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 &authuser.DefaultInfo{Name: user.Name}, 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),
}, 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
}

View File

@@ -16,7 +16,6 @@ limitations under the License.
package im
import (
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
@@ -24,11 +23,9 @@ import (
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/query"
kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
"kubesphere.io/kubesphere/pkg/informers"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
"net/mail"
"time"
)
type IdentityManagementInterface interface {
@@ -37,34 +34,24 @@ type IdentityManagementInterface interface {
DeleteUser(username string) error
UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error)
DescribeUser(username string) (*iamv1alpha2.User, error)
Authenticate(username, password string) (*iamv1alpha2.User, error)
ModifyPassword(username string, password string) error
ListLoginRecords(query *query.Query) (*api.ListResult, error)
PasswordVerify(username string, password string) error
}
var (
AuthRateLimitExceeded = errors.New("user auth rate limit exceeded")
AuthFailedIncorrectPassword = errors.New("incorrect password")
UserAlreadyExists = errors.New("user already exists")
UserNotExists = errors.New("user not exists")
)
func NewOperator(ksClient kubesphereclient.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
func NewOperator(ksClient kubesphere.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
im := &defaultIMOperator{
ksClient: ksClient,
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
}
if options != nil {
im.authenticateRateLimiterDuration = options.AuthenticateRateLimiterDuration
im.authenticateRateLimiterMaxTries = options.AuthenticateRateLimiterMaxTries
options: options,
}
return im
}
type defaultIMOperator struct {
ksClient kubesphereclient.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
authenticateRateLimiterMaxTries int
authenticateRateLimiterDuration time.Duration
ksClient kubesphere.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
options *authoptions.AuthenticationOptions
}
func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
@@ -110,50 +97,6 @@ func (im *defaultIMOperator) ModifyPassword(username string, password string) er
return nil
}
func (im *defaultIMOperator) Authenticate(username, password string) (*iamv1alpha2.User, error) {
var user *iamv1alpha2.User
if _, err := mail.ParseAddress(username); err != nil {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
if err != nil {
klog.Error(err)
return nil, err
}
user = obj.(*iamv1alpha2.User)
} else {
objs, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", &query.Query{
Pagination: query.NoPagination,
Filters: map[query.Field]query.Value{iamv1alpha2.FieldEmail: query.Value(username)},
})
if err != nil {
klog.Error(err)
return nil, err
}
if len(objs.Items) != 1 {
if len(objs.Items) == 0 {
klog.Warningf("username or email: %s not exist", username)
} else {
klog.Errorf("duplicate user entries: %+v", objs)
}
return nil, AuthFailedIncorrectPassword
}
user = objs.Items[0].(*iamv1alpha2.User)
}
if im.authRateLimitExceeded(user) {
im.authFailRecord(user, AuthRateLimitExceeded)
return nil, AuthRateLimitExceeded
}
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return user, nil
}
im.authFailRecord(user, AuthFailedIncorrectPassword)
return nil, AuthFailedIncorrectPassword
}
func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
result, err = im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", query)
if err != nil {
@@ -171,6 +114,19 @@ 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)
if err != nil {
klog.Error(err)
return err
}
user := obj.(*iamv1alpha2.User)
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return nil
}
return AuthFailedIncorrectPassword
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
@@ -200,27 +156,13 @@ func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
return user, nil
}
func (im *defaultIMOperator) authRateLimitEnabled() bool {
if im.authenticateRateLimiterMaxTries <= 0 || im.authenticateRateLimiterDuration == 0 {
return false
func (im *defaultIMOperator) ListLoginRecords(query *query.Query) (*api.ListResult, error) {
result, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralLoginRecord, "", query)
if err != nil {
klog.Error(err)
return nil, err
}
return true
}
func (im *defaultIMOperator) authRateLimitExceeded(user *iamv1alpha2.User) bool {
if !im.authRateLimitEnabled() {
return false
}
// TODO record login history using CRD
return false
}
func (im *defaultIMOperator) authFailRecord(user *iamv1alpha2.User, err error) {
if !im.authRateLimitEnabled() {
return
}
// TODO record login history using CRD
return result, nil
}
func ensurePasswordNotOutput(user *iamv1alpha2.User) *iamv1alpha2.User {

View File

@@ -0,0 +1,72 @@
/*
*
* 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
)
type LoginRecorder interface {
RecordLogin(username string, authErr error, req *http.Request) error
}
type loginRecorder struct {
ksClient kubesphere.Interface
}
func NewLoginRecorder(ksClient kubesphere.Interface) LoginRecorder {
return &loginRecorder{
ksClient: ksClient,
}
}
func (l *loginRecorder) RecordLogin(username string, authErr error, req *http.Request) error {
loginEntry := &iamv1alpha2.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-", username),
Labels: map[string]string{
iamv1alpha2.UserReferenceLabel: username,
},
},
Spec: iamv1alpha2.LoginRecordSpec{
SourceIP: net.GetRequestIP(req),
Type: iamv1alpha2.LoginSuccess,
Reason: iamv1alpha2.AuthenticatedSuccessfully,
},
}
if authErr != nil {
loginEntry.Spec.Type = iamv1alpha2.LoginFailure
loginEntry.Spec.Reason = authErr.Error()
}
_, err := l.ksClient.IamV1alpha2().LoginRecords().Create(loginEntry)
if err != nil {
klog.Error(err)
return err
}
return nil
}

146
pkg/models/iam/im/token.go Normal file
View File

@@ -0,0 +1,146 @@
/*
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"
"k8s.io/apiserver/pkg/authentication/user"
"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"
"time"
)
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)
}
type tokenOperator struct {
issuer token.Issuer
options *authoptions.AuthenticationOptions
cache cache.Interface
}
func NewTokenOperator(cache cache.Interface, options *authoptions.AuthenticationOptions) TokenManagementInterface {
operator := &tokenOperator{
issuer: token.NewTokenIssuer(options.JwtSecret, options.MaximumClockSkew),
options: options,
cache: cache,
}
return operator
}
func (t tokenOperator) Verify(tokenStr string) (user.Info, error) {
authenticated, tokenType, err := t.issuer.Verify(tokenStr)
if err != nil {
klog.Error(err)
return nil, err
}
if t.options.OAuthOptions.AccessTokenMaxAge == 0 ||
tokenType == token.StaticToken {
return authenticated, nil
}
if err := t.tokenCacheValidate(authenticated.GetName(), tokenStr); err != nil {
klog.Error(err)
return nil, err
}
return authenticated, 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)
if err != nil {
klog.Error(err)
return nil, 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 {
klog.Error(err)
return nil, 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
}
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)
return err
} else if len(keys) > 0 {
if err := t.cache.Del(keys...); err != nil {
klog.Error(err)
return err
}
}
return nil
}
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
} else if !exist {
return fmt.Errorf("token not found in cache")
}
return nil
}
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)
return err
}
return nil
}