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:
zryfish
2020-03-10 13:50:17 +08:00
committed by GitHub
parent 7270307b66
commit 641615b299
235 changed files with 5538 additions and 38064 deletions

View File

@@ -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
}