@@ -20,13 +20,16 @@ package authenticate
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/klog"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
@@ -38,9 +41,12 @@ type Auth struct {
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Secret []byte
|
||||
Path string
|
||||
ExceptedPath []string
|
||||
Secret []byte
|
||||
Path string
|
||||
RedisOptions *redis.Options
|
||||
TokenIdleTimeout time.Duration
|
||||
RedisClient *redis.Client
|
||||
ExceptedPath []string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
@@ -87,7 +93,7 @@ func (h Auth) ServeHTTP(resp http.ResponseWriter, req *http.Request) (int, error
|
||||
|
||||
func (h Auth) InjectContext(req *http.Request, token *jwt.Token) (*http.Request, error) {
|
||||
|
||||
payLoad, ok := token.Claims.(jwt.MapClaims)
|
||||
payload, ok := token.Claims.(jwt.MapClaims)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("invalid payload")
|
||||
@@ -101,14 +107,14 @@ func (h Auth) InjectContext(req *http.Request, token *jwt.Token) (*http.Request,
|
||||
|
||||
usr := &user.DefaultInfo{}
|
||||
|
||||
username, ok := payLoad["username"].(string)
|
||||
username, ok := payload["username"].(string)
|
||||
|
||||
if ok && username != "" {
|
||||
req.Header.Set("X-Token-Username", username)
|
||||
usr.Name = username
|
||||
}
|
||||
|
||||
uid := payLoad["uid"]
|
||||
uid := payload["uid"]
|
||||
|
||||
if uid != nil {
|
||||
switch uid.(type) {
|
||||
@@ -123,7 +129,7 @@ func (h Auth) InjectContext(req *http.Request, token *jwt.Token) (*http.Request,
|
||||
}
|
||||
}
|
||||
|
||||
groups, ok := payLoad["groups"].([]string)
|
||||
groups, ok := payload["groups"].([]string)
|
||||
if ok && len(groups) > 0 {
|
||||
req.Header.Set("X-Token-Groups", strings.Join(groups, ","))
|
||||
usr.Groups = groups
|
||||
@@ -160,10 +166,46 @@ func (h Auth) Validate(uToken string) (*jwt.Token, error) {
|
||||
token, err := jwt.Parse(uToken, h.ProvideKey)
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
payload, ok := token.Claims.(jwt.MapClaims)
|
||||
|
||||
if !ok {
|
||||
err := fmt.Errorf("invalid payload")
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username, ok := payload["username"].(string)
|
||||
|
||||
if !ok {
|
||||
err := fmt.Errorf("invalid payload")
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok = payload["exp"]; ok {
|
||||
// allow static token when contain expiration time
|
||||
return token, nil
|
||||
}
|
||||
|
||||
tokenKey := fmt.Sprintf("kubesphere:users:%s:token:%s", username, uToken)
|
||||
|
||||
exist, err := h.Rule.RedisClient.Exists(tokenKey).Result()
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exist == 1 {
|
||||
// reset expiration time if token exist
|
||||
h.Rule.RedisClient.Expire(tokenKey, h.Rule.TokenIdleTimeout)
|
||||
return token, nil
|
||||
} else {
|
||||
return nil, errors.New("illegal token")
|
||||
}
|
||||
}
|
||||
|
||||
func (h Auth) HandleUnauthorized(w http.ResponseWriter, err error) int {
|
||||
|
||||
@@ -19,7 +19,9 @@ package authenticate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-redis/redis"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
@@ -33,17 +35,27 @@ func Setup(c *caddy.Controller) error {
|
||||
return err
|
||||
}
|
||||
|
||||
rule.RedisClient = redis.NewClient(rule.RedisOptions)
|
||||
|
||||
c.OnStartup(func() error {
|
||||
if err := rule.RedisClient.Ping().Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Authenticate middleware is initiated")
|
||||
return nil
|
||||
})
|
||||
|
||||
c.OnShutdown(func() error {
|
||||
return rule.RedisClient.Close()
|
||||
})
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return &Auth{Next: next, Rule: rule}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (Rule, error) {
|
||||
|
||||
rule := Rule{ExceptedPath: make([]string, 0)}
|
||||
@@ -61,6 +73,34 @@ func parse(c *caddy.Controller) (Rule, error) {
|
||||
|
||||
rule.Path = c.Val()
|
||||
|
||||
if c.NextArg() {
|
||||
return rule, c.ArgErr()
|
||||
}
|
||||
case "token-idle-timeout":
|
||||
if !c.NextArg() {
|
||||
return rule, c.ArgErr()
|
||||
}
|
||||
|
||||
if timeout, err := time.ParseDuration(c.Val()); err != nil {
|
||||
return rule, c.ArgErr()
|
||||
} else {
|
||||
rule.TokenIdleTimeout = timeout
|
||||
}
|
||||
|
||||
if c.NextArg() {
|
||||
return rule, c.ArgErr()
|
||||
}
|
||||
case "redis-url":
|
||||
if !c.NextArg() {
|
||||
return rule, c.ArgErr()
|
||||
}
|
||||
|
||||
if redisOptions, err := redis.ParseURL(c.Val()); err != nil {
|
||||
return rule, c.ArgErr()
|
||||
} else {
|
||||
rule.RedisOptions = redisOptions
|
||||
}
|
||||
|
||||
if c.NextArg() {
|
||||
return rule, c.ArgErr()
|
||||
}
|
||||
|
||||
@@ -53,10 +53,11 @@ import (
|
||||
var (
|
||||
adminEmail string
|
||||
adminPassword string
|
||||
tokenExpireTime time.Duration
|
||||
tokenIdleTimeout time.Duration
|
||||
maxAuthFailed int
|
||||
authTimeInterval time.Duration
|
||||
initUsers []initUser
|
||||
enableMultiLogin bool
|
||||
)
|
||||
|
||||
type initUser struct {
|
||||
@@ -69,13 +70,15 @@ const (
|
||||
authRateLimitRegex = `(\d+)/(\d+[s|m|h])`
|
||||
defaultMaxAuthFailed = 5
|
||||
defaultAuthTimeInterval = 30 * time.Minute
|
||||
defaultTokenIdleTimeout = 30 * time.Minute
|
||||
)
|
||||
|
||||
func Init(email, password string, expireTime time.Duration, authRateLimit string) error {
|
||||
func Init(email, password, idleTimeout, authRateLimit string, multiLogin bool) error {
|
||||
adminEmail = email
|
||||
adminPassword = password
|
||||
tokenExpireTime = expireTime
|
||||
tokenIdleTimeout = parseTokenIdleTimeout(idleTimeout)
|
||||
maxAuthFailed, authTimeInterval = parseAuthRateLimit(authRateLimit)
|
||||
enableMultiLogin = multiLogin
|
||||
|
||||
err := checkAndCreateDefaultUser()
|
||||
|
||||
@@ -94,6 +97,15 @@ func Init(email, password string, expireTime time.Duration, authRateLimit string
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTokenIdleTimeout(tokenExpirationTime string) time.Duration {
|
||||
duration, err := time.ParseDuration(tokenExpirationTime)
|
||||
if err != nil {
|
||||
return defaultTokenIdleTimeout
|
||||
} else {
|
||||
return duration
|
||||
}
|
||||
}
|
||||
|
||||
func parseAuthRateLimit(authRateLimit string) (int, time.Duration) {
|
||||
regex := regexp.MustCompile(authRateLimitRegex)
|
||||
groups := regex.FindStringSubmatch(authRateLimit)
|
||||
@@ -216,6 +228,9 @@ func createUserBaseDN() error {
|
||||
return err
|
||||
}
|
||||
conn, err := client.NewConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
groupsCreateRequest := ldap.NewAddRequest(client.UserSearchBase(), nil)
|
||||
groupsCreateRequest.Attribute("objectClass", []string{"organizationalUnit", "top"})
|
||||
@@ -230,6 +245,9 @@ func createGroupsBaseDN() error {
|
||||
return err
|
||||
}
|
||||
conn, err := client.NewConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
groupsCreateRequest := ldap.NewAddRequest(client.GroupSearchBase(), nil)
|
||||
groupsCreateRequest.Attribute("objectClass", []string{"organizationalUnit", "top"})
|
||||
@@ -295,7 +313,7 @@ func Login(username string, password string, ip string) (*models.Token, error) {
|
||||
klog.Infoln("auth failed", username, err)
|
||||
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
|
||||
loginFailedRecord := fmt.Sprintf("kubesphere:authfailed:%s:%d", username, time.Now().UnixNano())
|
||||
loginFailedRecord := fmt.Sprintf("kubesphere:authfailed:%s:%d", uid, time.Now().UnixNano())
|
||||
redisClient.Set(loginFailedRecord, "", authTimeInterval)
|
||||
}
|
||||
|
||||
@@ -304,14 +322,38 @@ func Login(username string, password string, ip string) (*models.Token, error) {
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
|
||||
if tokenExpireTime > 0 {
|
||||
claims["exp"] = time.Now().Add(tokenExpireTime).Unix()
|
||||
}
|
||||
// do not set expiration time
|
||||
claims["username"] = uid
|
||||
claims["email"] = email
|
||||
claims["iat"] = time.Now().Unix()
|
||||
|
||||
token := jwtutil.MustSigned(claims)
|
||||
|
||||
if !enableMultiLogin {
|
||||
// multi login not allowed, remove the previous token
|
||||
sessions, err := redisClient.Keys(fmt.Sprintf("kubesphere:users:%s:token:*", uid)).Result()
|
||||
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(sessions) > 0 {
|
||||
klog.V(4).Infoln("revoke token", sessions)
|
||||
err = redisClient.Del(sessions...).Err()
|
||||
if err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cache token with expiration time
|
||||
if err = redisClient.Set(fmt.Sprintf("kubesphere:users:%s:token:%s", uid, token), token, tokenIdleTimeout).Err(); err != nil {
|
||||
klog.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loginLog(uid, ip)
|
||||
|
||||
return &models.Token{Token: token}, nil
|
||||
@@ -443,7 +485,6 @@ func ListUsers(conditions *params.Conditions, orderBy string, reverse bool, limi
|
||||
|
||||
if i >= offset && len(items) < limit {
|
||||
|
||||
user.AvatarUrl = getAvatar(user.Username)
|
||||
user.LastLoginTime = getLastLoginTime(user.Username)
|
||||
clusterRole, err := GetUserClusterRole(user.Username)
|
||||
if err != nil {
|
||||
@@ -480,8 +521,6 @@ func DescribeUser(username string) (*models.User, error) {
|
||||
user.Groups = groups
|
||||
}
|
||||
|
||||
user.AvatarUrl = getAvatar(username)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -582,37 +621,6 @@ func getLastLoginTime(username string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func setAvatar(username, avatar string) error {
|
||||
redis, err := clientset.ClientSets().Redis()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = redis.HMSet("kubesphere:users:avatar", map[string]interface{}{"username": avatar}).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func getAvatar(username string) string {
|
||||
redis, err := clientset.ClientSets().Redis()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
avatar, err := redis.HMGet("kubesphere:users:avatar", username).Result()
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(avatar) > 0 {
|
||||
if url, ok := avatar[0].(string); ok {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func DeleteUser(username string) error {
|
||||
|
||||
client, err := clientset.ClientSets().Ldap()
|
||||
@@ -876,10 +884,6 @@ func CreateUser(user *models.User) (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AvatarUrl != "" {
|
||||
setAvatar(user.Username, user.AvatarUrl)
|
||||
}
|
||||
|
||||
if user.ClusterRole != "" {
|
||||
err := CreateClusterRoleBinding(user.Username, user.ClusterRole)
|
||||
|
||||
@@ -1022,15 +1026,6 @@ func UpdateUser(user *models.User) (*models.User, error) {
|
||||
userModifyRequest.Replace("userPassword", []string{user.Password})
|
||||
}
|
||||
|
||||
if user.AvatarUrl != "" {
|
||||
err = setAvatar(user.Username, user.AvatarUrl)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.Modify(userModifyRequest)
|
||||
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user