未验证 提交 2acb68e3 编写于 作者: H hongming

feat: token management

Signed-off-by: Nhongming <talonwan@yunify.com>
上级 3c7074b4
......@@ -37,9 +37,10 @@ type ServerRunOptions struct {
MySQLOptions *mysql.MySQLOptions
AdminEmail string
AdminPassword string
TokenExpireTime string
TokenIdleTimeout string
JWTSecret string
AuthRateLimit string
EnableMultiLogin bool
}
func NewServerRunOptions() *ServerRunOptions {
......@@ -60,9 +61,10 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) {
s.GenericServerRunOptions.AddFlags(fs)
fs.StringVar(&s.AdminEmail, "admin-email", "admin@kubesphere.io", "default administrator's email")
fs.StringVar(&s.AdminPassword, "admin-password", "passw0rd", "default administrator's password")
fs.StringVar(&s.TokenExpireTime, "token-expire-time", "2h", "token expire time,valid time units are \"ns\",\"us\",\"ms\",\"s\",\"m\",\"h\"")
fs.StringVar(&s.TokenIdleTimeout, "token-idle-timeout", "30m", "tokens that are idle beyond that time will expire,0s means the token has no expiration time. valid time units are \"ns\",\"us\",\"ms\",\"s\",\"m\",\"h\"")
fs.StringVar(&s.JWTSecret, "jwt-secret", "", "jwt secret")
fs.StringVar(&s.AuthRateLimit, "auth-rate-limit", "5/30m", "specifies the maximum number of authentication attempts permitted and time interval,valid time units are \"s\",\"m\",\"h\"")
fs.BoolVar(&s.EnableMultiLogin, "enable-multi-login", false, "allow one account to have multiple sessions")
s.KubernetesOptions.AddFlags(fss.FlagSet("kubernetes"))
s.LdapOptions.AddFlags(fss.FlagSet("ldap"))
......
......@@ -24,6 +24,7 @@ import (
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/klog"
"kubesphere.io/kubesphere/cmd/ks-iam/app/options"
"kubesphere.io/kubesphere/pkg/apis"
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
"kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/iam"
......@@ -35,9 +36,6 @@ import (
"kubesphere.io/kubesphere/pkg/utils/signals"
"kubesphere.io/kubesphere/pkg/utils/term"
"net/http"
"time"
"kubesphere.io/kubesphere/pkg/apis"
)
func NewAPIServerCommand() *cobra.Command {
......@@ -94,15 +92,10 @@ func Run(s *options.ServerRunOptions, stopChan <-chan struct{}) error {
client.NewClientSetFactory(csop, stopChan)
expireTime, err := time.ParseDuration(s.TokenExpireTime)
if err != nil {
return err
}
waitForResourceSync(stopChan)
err = iam.Init(s.AdminEmail, s.AdminPassword, expireTime, s.AuthRateLimit)
err := iam.Init(s.AdminEmail, s.AdminPassword, s.TokenIdleTimeout, s.AuthRateLimit, s.EnableMultiLogin)
jwtutil.Setup(s.JWTSecret)
if err != nil {
......
......@@ -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 {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册