This is a huge commit, it does following things: (#1942)
1. Remove ks-iam standalone binary, move it to ks-apiserver 2. Generate all devops apis inside kubesphere repository, no need to import s2ioperator. 3. Reorganize ldap code, make it more flexible to use.
This commit is contained in:
@@ -20,216 +20,96 @@ package iam
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-ldap/ldap"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
"kubesphere.io/kubesphere/pkg/models"
|
||||
"kubesphere.io/kubesphere/pkg/server/params"
|
||||
ldappool "kubesphere.io/kubesphere/pkg/simple/client/ldap"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/ldap"
|
||||
"kubesphere.io/kubesphere/pkg/utils/jwtutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IdentityManagementInterface interface {
|
||||
CreateUser(user *User) (*User, error)
|
||||
CreateUser(user *iam.User) (*iam.User, error)
|
||||
DeleteUser(username string) error
|
||||
DescribeUser(username string) (*User, error)
|
||||
DescribeUser(username string) (*iam.User, error)
|
||||
Login(username, password, ip string) (*oauth2.Token, error)
|
||||
ModifyUser(user *User) (*User, error)
|
||||
ModifyUser(user *iam.User) (*iam.User, error)
|
||||
ListUsers(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error)
|
||||
GetUserRoles(username string) ([]*rbacv1.Role, error)
|
||||
GetUserRole(namespace string, username string) (*rbacv1.Role, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
authRateLimit string
|
||||
maxAuthFailed int
|
||||
authTimeInterval time.Duration
|
||||
tokenIdleTimeout time.Duration
|
||||
enableMultiLogin bool
|
||||
}
|
||||
|
||||
type imOperator struct {
|
||||
config Config
|
||||
ldap ldappool.Client
|
||||
redis redis.Client
|
||||
authenticateOptions *iam.AuthenticationOptions
|
||||
ldapClient ldap.Interface
|
||||
cacheClient cache.Interface
|
||||
}
|
||||
|
||||
const (
|
||||
authRateLimitRegex = `(\d+)/(\d+[s|m|h])`
|
||||
defaultMaxAuthFailed = 5
|
||||
defaultAuthTimeInterval = 30 * time.Minute
|
||||
mailAttribute = "mail"
|
||||
uidAttribute = "uid"
|
||||
descriptionAttribute = "description"
|
||||
preferredLanguageAttribute = "preferredLanguage"
|
||||
createTimestampAttribute = "createTimestampAttribute"
|
||||
dateTimeLayout = "20060102150405Z"
|
||||
)
|
||||
|
||||
var (
|
||||
AuthRateLimitExceeded = errors.New("user auth rate limit exceeded")
|
||||
UserAlreadyExists = errors.New("user already exists")
|
||||
UserNotExists = errors.New("user not exists")
|
||||
)
|
||||
|
||||
func NewIMOperator(ldap ldappool.Client, config Config) *imOperator {
|
||||
imOperator := &imOperator{ldap: ldap, config: config}
|
||||
return imOperator
|
||||
func NewIMOperator(ldapClient ldap.Interface, cacheClient cache.Interface, options *iam.AuthenticationOptions) *imOperator {
|
||||
return &imOperator{ldapClient: ldapClient, cacheClient: cacheClient, authenticateOptions: options}
|
||||
|
||||
}
|
||||
|
||||
// TODO init in controller
|
||||
func (im *imOperator) Init() error {
|
||||
|
||||
userSearchBase := &ldap.AddRequest{
|
||||
DN: im.ldap.UserSearchBase(),
|
||||
Attributes: []ldap.Attribute{{
|
||||
Type: "objectClass",
|
||||
Vals: []string{"organizationalUnit", "top"},
|
||||
}, {
|
||||
Type: "ou",
|
||||
Vals: []string{"Users"},
|
||||
}},
|
||||
Controls: nil,
|
||||
}
|
||||
|
||||
err := im.createIfNotExists(userSearchBase)
|
||||
|
||||
func (im *imOperator) ModifyUser(user *iam.User) (*iam.User, error) {
|
||||
err := im.ldapClient.Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groupSearchBase := &ldap.AddRequest{
|
||||
DN: im.ldap.GroupSearchBase(),
|
||||
Attributes: []ldap.Attribute{{
|
||||
Type: "objectClass",
|
||||
Vals: []string{"organizationalUnit", "top"},
|
||||
}, {
|
||||
Type: "ou",
|
||||
Vals: []string{"Groups"},
|
||||
}},
|
||||
Controls: nil,
|
||||
}
|
||||
|
||||
err = im.createIfNotExists(groupSearchBase)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *imOperator) createIfNotExists(createRequest *ldap.AddRequest) error {
|
||||
conn, err := im.ldap.NewConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
createRequest.DN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(objectClass=*)",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
_, err = conn.Search(searchRequest)
|
||||
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
|
||||
err = conn.Add(createRequest)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *imOperator) ModifyUser(user *User) (*User, error) {
|
||||
conn, err := im.ldap.NewConn()
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
dn := fmt.Sprintf("uid=%s,%s", user.Username, im.ldap.UserSearchBase())
|
||||
userModifyRequest := ldap.NewModifyRequest(dn, nil)
|
||||
|
||||
if user.Description != "" {
|
||||
userModifyRequest.Replace("description", []string{user.Description})
|
||||
}
|
||||
|
||||
if user.Lang != "" {
|
||||
userModifyRequest.Replace("preferredLanguage", []string{user.Lang})
|
||||
}
|
||||
|
||||
if user.Password != "" {
|
||||
userModifyRequest.Replace("userPassword", []string{user.Password})
|
||||
}
|
||||
|
||||
err = conn.Modify(userModifyRequest)
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// clear auth failed record
|
||||
if user.Password != "" {
|
||||
|
||||
records, err := im.redis.Keys(fmt.Sprintf("kubesphere:authfailed:%s:*", user.Username)).Result()
|
||||
|
||||
records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(user.Username, "*"))
|
||||
if err == nil {
|
||||
im.redis.Del(records...)
|
||||
im.cacheClient.Del(records...)
|
||||
}
|
||||
}
|
||||
|
||||
return im.DescribeUser(user.Username)
|
||||
return im.ldapClient.Get(user.Username)
|
||||
}
|
||||
|
||||
func authenticationFailedKeyForUsername(username, failedTimestamp string) string {
|
||||
return fmt.Sprintf("kubesphere:authfailed:%s:%s", username, failedTimestamp)
|
||||
}
|
||||
|
||||
func tokenKeyForUsername(username, token string) string {
|
||||
return fmt.Sprintf("kubesphere:users:%s:token:%s", username, token)
|
||||
}
|
||||
|
||||
func loginKeyForUsername(username, loginTimestamp, ip string) string {
|
||||
return fmt.Sprintf("kubesphere:users:%s:login-log:%s:%s", username, loginTimestamp, ip)
|
||||
}
|
||||
|
||||
func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error) {
|
||||
|
||||
records, err := im.redis.Keys(fmt.Sprintf("kubesphere:authfailed:%s:*", username)).Result()
|
||||
|
||||
records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(username, "*"))
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) >= im.config.maxAuthFailed {
|
||||
if len(records) >= im.authenticateOptions.MaxAuthenticateRetries {
|
||||
return nil, AuthRateLimitExceeded
|
||||
}
|
||||
|
||||
user, err := im.DescribeUser(username)
|
||||
|
||||
conn, err := im.ldap.NewConn()
|
||||
user, err := im.ldapClient.Get(username)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
dn := fmt.Sprintf("uid=%s,%s", user.Username, im.ldap.UserSearchBase())
|
||||
|
||||
// bind as the user to verify their password
|
||||
err = conn.Bind(dn, password)
|
||||
|
||||
err = im.ldapClient.Verify(user.Username, password)
|
||||
if err != nil {
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
|
||||
cacheKey := fmt.Sprintf("kubesphere:authfailed:%s:%d", user.Username, time.Now().UnixNano())
|
||||
im.redis.Set(cacheKey, "", im.config.authTimeInterval)
|
||||
if err == ldap.ErrInvalidCredentials {
|
||||
im.cacheClient.Set(authenticationFailedKeyForUsername(username, fmt.Sprintf("%d", time.Now().UnixNano())), "", 30*time.Minute)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -243,30 +123,25 @@ func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error
|
||||
}
|
||||
token := jwtutil.MustSigned(claims)
|
||||
|
||||
if !im.config.enableMultiLogin {
|
||||
tokenKey := tokenKeyForUsername(user.Username, "*")
|
||||
if !im.authenticateOptions.MultipleLogin {
|
||||
// multi login not allowed, remove the previous token
|
||||
cacheKey := fmt.Sprintf("kubesphere:users:%s:token:*", user.Username)
|
||||
sessions, err := im.redis.Keys(cacheKey).Result()
|
||||
|
||||
sessions, err := im.cacheClient.Keys(tokenKey)
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(sessions) > 0 {
|
||||
klog.V(4).Infoln("revoke token", sessions)
|
||||
err = im.redis.Del(sessions...).Err()
|
||||
err = im.cacheClient.Del(sessions...)
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cache token with expiration time
|
||||
cacheKey := fmt.Sprintf("kubesphere:users:%s:token:%s", user.Username, token)
|
||||
if err = im.redis.Set(cacheKey, token, im.config.tokenIdleTimeout).Err(); err != nil {
|
||||
klog.Errorln(err)
|
||||
if err = im.cacheClient.Set(tokenKey, token, im.authenticateOptions.TokenExpiration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -277,153 +152,39 @@ func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error
|
||||
|
||||
func (im *imOperator) loginRecord(username, ip string, loginTime time.Time) {
|
||||
if ip != "" {
|
||||
im.redis.RPush(fmt.Sprintf("kubesphere:users:%s:login-log", username), fmt.Sprintf("%s,%s", loginTime.UTC().Format("2006-01-02T15:04:05Z"), ip))
|
||||
im.redis.LTrim(fmt.Sprintf("kubesphere:users:%s:login-log", username), -10, -1)
|
||||
_ = im.cacheClient.Set(loginKeyForUsername(username, loginTime.UTC().Format("2006-01-02T15:04:05Z"), ip), "", 30*24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func (im *imOperator) LoginHistory(username string) ([]string, error) {
|
||||
data, err := im.redis.LRange(fmt.Sprintf("kubesphere:users:%s:login-log", username), -10, -1).Result()
|
||||
|
||||
keys, err := im.cacheClient.Keys(loginKeyForUsername(username, "*", "*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (im *imOperator) ListUsers(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (im *imOperator) DescribeUser(username string) (*User, error) {
|
||||
conn, err := im.ldap.NewConn()
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
filter := fmt.Sprintf("(&(objectClass=inetOrgPerson)(|(uid=%s)(mail=%s)))", username, username)
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
im.ldap.UserSearchBase(),
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
[]string{mailAttribute, descriptionAttribute, preferredLanguageAttribute, createTimestampAttribute},
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := conn.Search(searchRequest)
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Entries) != 1 {
|
||||
return nil, UserNotExists
|
||||
}
|
||||
|
||||
entry := result.Entries[0]
|
||||
|
||||
return convertLdapEntryToUser(entry), nil
|
||||
}
|
||||
|
||||
func convertLdapEntryToUser(entry *ldap.Entry) *User {
|
||||
username := entry.GetAttributeValue(uidAttribute)
|
||||
email := entry.GetAttributeValue(mailAttribute)
|
||||
description := entry.GetAttributeValue(descriptionAttribute)
|
||||
lang := entry.GetAttributeValue(preferredLanguageAttribute)
|
||||
createTimestamp, err := time.Parse(dateTimeLayout, entry.GetAttributeValue(createTimestampAttribute))
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
}
|
||||
return &User{Username: username, Email: email, Description: description, Lang: lang, CreateTime: createTimestamp}
|
||||
func (im *imOperator) DescribeUser(username string) (*iam.User, error) {
|
||||
return im.ldapClient.Get(username)
|
||||
}
|
||||
|
||||
func (im *imOperator) getLastLoginTime(username string) string {
|
||||
cacheKey := fmt.Sprintf("kubesphere:users:%s:login-log", username)
|
||||
lastLogin, err := im.redis.LRange(cacheKey, -1, -1).Result()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(lastLogin) > 0 {
|
||||
return strings.Split(lastLogin[0], ",")[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (im *imOperator) DeleteUser(username string) error {
|
||||
conn, err := im.ldap.NewConn()
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
deleteRequest := ldap.NewDelRequest(fmt.Sprintf("uid=%s,%s", username, im.ldap.UserSearchBase()), nil)
|
||||
|
||||
if err = conn.Del(deleteRequest); err != nil {
|
||||
klog.Errorln(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return im.ldapClient.Delete(username)
|
||||
}
|
||||
|
||||
func (im *imOperator) CreateUser(user *User) (*User, error) {
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
user.Email = strings.TrimSpace(user.Email)
|
||||
user.Password = strings.TrimSpace(user.Password)
|
||||
user.Description = strings.TrimSpace(user.Description)
|
||||
|
||||
existed, err := im.DescribeUser(user.Username)
|
||||
|
||||
func (im *imOperator) CreateUser(user *iam.User) (*iam.User, error) {
|
||||
err := im.ldapClient.Create(user)
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
return nil, UserAlreadyExists
|
||||
}
|
||||
|
||||
uidNumber := im.uidNumberNext()
|
||||
|
||||
createRequest := ldap.NewAddRequest(fmt.Sprintf("uid=%s,%s", user.Username, im.ldap.UserSearchBase()), nil)
|
||||
createRequest.Attribute("objectClass", []string{"inetOrgPerson", "posixAccount", "top"})
|
||||
createRequest.Attribute("cn", []string{user.Username}) // RFC4519: common name(s) for which the entity is known by
|
||||
createRequest.Attribute("sn", []string{" "}) // RFC2256: last (family) name(s) for which the entity is known by
|
||||
createRequest.Attribute("gidNumber", []string{"500"}) // RFC2307: An integer uniquely identifying a group in an administrative domain
|
||||
createRequest.Attribute("homeDirectory", []string{"/home/" + user.Username}) // The absolute path to the home directory
|
||||
createRequest.Attribute("uid", []string{user.Username}) // RFC4519: user identifier
|
||||
createRequest.Attribute("uidNumber", []string{strconv.Itoa(uidNumber)}) // RFC2307: An integer uniquely identifying a user in an administrative domain
|
||||
createRequest.Attribute("mail", []string{user.Email}) // RFC1274: RFC822 Mailbox
|
||||
createRequest.Attribute("userPassword", []string{user.Password}) // RFC4519/2307: password of user
|
||||
if user.Lang != "" {
|
||||
createRequest.Attribute("preferredLanguage", []string{user.Lang})
|
||||
}
|
||||
if user.Description != "" {
|
||||
createRequest.Attribute("description", []string{user.Description}) // RFC4519: descriptive information
|
||||
}
|
||||
|
||||
conn, err := im.ldap.NewConn()
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.Add(createRequest)
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user