mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
feature(kubeconfig): Do not invalidate kubeconfig upon Portainer restarting [EE-1854] (#5905)
This commit is contained in:
parent
22b72fb6e3
commit
048613a0c5
7 changed files with 106 additions and 24 deletions
|
@ -2,19 +2,20 @@ package jwt
|
|||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gorilla/securecookie"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// scope represents JWT scopes that are supported in JWT claims.
|
||||
type scope string
|
||||
|
||||
// Service represents a service for managing JWT tokens.
|
||||
type Service struct {
|
||||
secret []byte
|
||||
secrets map[scope][]byte
|
||||
userSessionTimeout time.Duration
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
@ -23,6 +24,7 @@ type claims struct {
|
|||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
Scope scope `json:"scope"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
|
@ -31,6 +33,11 @@ var (
|
|||
errInvalidJWTToken = errors.New("Invalid JWT token")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultScope = scope("default")
|
||||
kubeConfigScope = scope("kubeconfig")
|
||||
)
|
||||
|
||||
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
|
||||
func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Service, error) {
|
||||
userSessionTimeout, err := time.ParseDuration(userSessionDuration)
|
||||
|
@ -43,73 +50,122 @@ func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Ser
|
|||
return nil, errSecretGeneration
|
||||
}
|
||||
|
||||
kubeSecret, err := getOrCreateKubeSecret(dataStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
secret,
|
||||
map[scope][]byte{
|
||||
defaultScope: secret,
|
||||
kubeConfigScope: kubeSecret,
|
||||
},
|
||||
userSessionTimeout,
|
||||
dataStore,
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *Service) defaultExpireAt() (int64) {
|
||||
func getOrCreateKubeSecret(dataStore portainer.DataStore) ([]byte, error) {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeSecret := settings.OAuthSettings.KubeSecretKey
|
||||
if kubeSecret == nil {
|
||||
kubeSecret = securecookie.GenerateRandomKey(32)
|
||||
if kubeSecret == nil {
|
||||
return nil, errSecretGeneration
|
||||
}
|
||||
settings.OAuthSettings.KubeSecretKey = kubeSecret
|
||||
err = dataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return kubeSecret, nil
|
||||
}
|
||||
|
||||
func (service *Service) defaultExpireAt() int64 {
|
||||
return time.Now().Add(service.userSessionTimeout).Unix()
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT token.
|
||||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
return service.generateSignedToken(data, service.defaultExpireAt())
|
||||
return service.generateSignedToken(data, service.defaultExpireAt(), defaultScope)
|
||||
}
|
||||
|
||||
// GenerateTokenForOAuth generates a new JWT for OAuth login
|
||||
// token expiry time from the OAuth provider is considered
|
||||
// GenerateTokenForOAuth generates a new JWT token for OAuth login
|
||||
// token expiry time response from OAuth provider is considered
|
||||
func (service *Service) GenerateTokenForOAuth(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
|
||||
expireAt := service.defaultExpireAt()
|
||||
if expiryTime != nil && !expiryTime.IsZero() {
|
||||
expireAt = expiryTime.Unix()
|
||||
}
|
||||
return service.generateSignedToken(data, expireAt)
|
||||
return service.generateSignedToken(data, expireAt, defaultScope)
|
||||
}
|
||||
|
||||
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
|
||||
scope := parseScope(token)
|
||||
secret := service.secrets[scope]
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
msg := fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, msg
|
||||
}
|
||||
return service.secret, nil
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err == nil && parsedToken != nil {
|
||||
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
|
||||
tokenData := &portainer.TokenData{
|
||||
return &portainer.TokenData{
|
||||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
Role: portainer.UserRole(cl.Role),
|
||||
}
|
||||
return tokenData, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errInvalidJWTToken
|
||||
}
|
||||
|
||||
// parse a JWT token, fallback to defaultScope if no scope is present in the JWT
|
||||
func parseScope(token string) scope {
|
||||
unverifiedToken, _, _ := new(jwt.Parser).ParseUnverified(token, &claims{})
|
||||
if unverifiedToken != nil {
|
||||
if cl, ok := unverifiedToken.Claims.(*claims); ok {
|
||||
if cl.Scope == kubeConfigScope {
|
||||
return kubeConfigScope
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultScope
|
||||
}
|
||||
|
||||
// SetUserSessionDuration sets the user session duration
|
||||
func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration) {
|
||||
service.userSessionTimeout = userSessionDuration
|
||||
}
|
||||
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64) (string, error) {
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64, scope scope) (string, error) {
|
||||
secret, found := service.secrets[scope]
|
||||
if !found {
|
||||
return "", fmt.Errorf("invalid scope: %v", scope)
|
||||
}
|
||||
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
Scope: scope,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
|
||||
signedToken, err := token.SignedString(service.secret)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
signedToken, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue