|
|
@@ -18,9 +18,7 @@ import (
|
|
|
"bytes"
|
|
|
"encoding/binary"
|
|
|
"errors"
|
|
|
- "fmt"
|
|
|
"sort"
|
|
|
- "strconv"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
|
|
|
@@ -62,6 +60,7 @@ var (
|
|
|
ErrAuthNotEnabled = errors.New("auth: authentication is not enabled")
|
|
|
ErrAuthOldRevision = errors.New("auth: revision in header is old")
|
|
|
ErrInvalidAuthToken = errors.New("auth: invalid auth token")
|
|
|
+ ErrInvalidAuthOpts = errors.New("auth: invalid auth options")
|
|
|
|
|
|
// BcryptCost is the algorithm cost / strength for hashing auth passwords
|
|
|
BcryptCost = bcrypt.DefaultCost
|
|
|
@@ -131,10 +130,6 @@ type AuthStore interface {
|
|
|
// RoleList gets a list of all roles
|
|
|
RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
|
|
|
|
|
|
- // AuthInfoFromToken gets a username from the given Token and current revision number
|
|
|
- // (The revision number is used for preventing the TOCTOU problem)
|
|
|
- AuthInfoFromToken(token string) (*AuthInfo, bool)
|
|
|
-
|
|
|
// IsPutPermitted checks put permission of the user
|
|
|
IsPutPermitted(authInfo *AuthInfo, key []byte) error
|
|
|
|
|
|
@@ -147,8 +142,9 @@ type AuthStore interface {
|
|
|
// IsAdminPermitted checks admin permission of the user
|
|
|
IsAdminPermitted(authInfo *AuthInfo) error
|
|
|
|
|
|
- // GenSimpleToken produces a simple random string
|
|
|
- GenSimpleToken() (string, error)
|
|
|
+ // GenTokenPrefix produces a random string in a case of simple token
|
|
|
+ // in a case of JWT, it produces an empty string
|
|
|
+ GenTokenPrefix() (string, error)
|
|
|
|
|
|
// Revision gets current revision of authStore
|
|
|
Revision() uint64
|
|
|
@@ -166,6 +162,16 @@ type AuthStore interface {
|
|
|
AuthInfoFromTLS(ctx context.Context) *AuthInfo
|
|
|
}
|
|
|
|
|
|
+type TokenProvider interface {
|
|
|
+ info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool)
|
|
|
+ assign(ctx context.Context, username string, revision uint64) (string, error)
|
|
|
+ enable()
|
|
|
+ disable()
|
|
|
+
|
|
|
+ invalidateUser(string)
|
|
|
+ genTokenPrefix() (string, error)
|
|
|
+}
|
|
|
+
|
|
|
type authStore struct {
|
|
|
be backend.Backend
|
|
|
enabled bool
|
|
|
@@ -173,24 +179,9 @@ type authStore struct {
|
|
|
|
|
|
rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions
|
|
|
|
|
|
- simpleTokensMu sync.RWMutex
|
|
|
- simpleTokens map[string]string // token -> username
|
|
|
- simpleTokenKeeper *simpleTokenTTLKeeper
|
|
|
-
|
|
|
revision uint64
|
|
|
|
|
|
- indexWaiter func(uint64) <-chan struct{}
|
|
|
-}
|
|
|
-
|
|
|
-func newDeleterFunc(as *authStore) func(string) {
|
|
|
- return func(t string) {
|
|
|
- as.simpleTokensMu.Lock()
|
|
|
- defer as.simpleTokensMu.Unlock()
|
|
|
- if username, ok := as.simpleTokens[t]; ok {
|
|
|
- plog.Infof("deleting token %s for user %s", t, username)
|
|
|
- delete(as.simpleTokens, t)
|
|
|
- }
|
|
|
- }
|
|
|
+ tokenProvider TokenProvider
|
|
|
}
|
|
|
|
|
|
func (as *authStore) AuthEnable() error {
|
|
|
@@ -220,8 +211,7 @@ func (as *authStore) AuthEnable() error {
|
|
|
tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
|
|
|
|
|
|
as.enabled = true
|
|
|
-
|
|
|
- as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
|
|
+ as.tokenProvider.enable()
|
|
|
|
|
|
as.rangePermCache = make(map[string]*unifiedRangePermissions)
|
|
|
|
|
|
@@ -247,14 +237,7 @@ func (as *authStore) AuthDisable() {
|
|
|
b.ForceCommit()
|
|
|
|
|
|
as.enabled = false
|
|
|
-
|
|
|
- as.simpleTokensMu.Lock()
|
|
|
- as.simpleTokens = make(map[string]string) // invalidate all tokens
|
|
|
- as.simpleTokensMu.Unlock()
|
|
|
- if as.simpleTokenKeeper != nil {
|
|
|
- as.simpleTokenKeeper.stop()
|
|
|
- as.simpleTokenKeeper = nil
|
|
|
- }
|
|
|
+ as.tokenProvider.disable()
|
|
|
|
|
|
plog.Noticef("Authentication disabled")
|
|
|
}
|
|
|
@@ -265,10 +248,7 @@ func (as *authStore) Close() error {
|
|
|
if !as.enabled {
|
|
|
return nil
|
|
|
}
|
|
|
- if as.simpleTokenKeeper != nil {
|
|
|
- as.simpleTokenKeeper.stop()
|
|
|
- as.simpleTokenKeeper = nil
|
|
|
- }
|
|
|
+ as.tokenProvider.disable()
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
@@ -277,10 +257,6 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
|
|
return nil, ErrAuthNotEnabled
|
|
|
}
|
|
|
|
|
|
- // TODO(mitake): after adding jwt support, branching based on values of ctx is required
|
|
|
- index := ctx.Value("index").(uint64)
|
|
|
- simpleToken := ctx.Value("simpleToken").(string)
|
|
|
-
|
|
|
tx := as.be.BatchTx()
|
|
|
tx.Lock()
|
|
|
defer tx.Unlock()
|
|
|
@@ -290,10 +266,15 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
|
|
return nil, ErrAuthFailed
|
|
|
}
|
|
|
|
|
|
- token := fmt.Sprintf("%s.%d", simpleToken, index)
|
|
|
- as.assignSimpleTokenToUser(username, token)
|
|
|
+ // Password checking is already performed in the API layer, so we don't need to check for now.
|
|
|
+ // Staleness of password can be detected with OCC in the API layer, too.
|
|
|
+
|
|
|
+ token, err := as.tokenProvider.assign(ctx, username, as.revision)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
|
|
|
- plog.Infof("authorized %s, token is %s", username, token)
|
|
|
+ plog.Debugf("authorized %s, token is %s", username, token)
|
|
|
return &pb.AuthenticateResponse{Token: token}, nil
|
|
|
}
|
|
|
|
|
|
@@ -385,7 +366,7 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete
|
|
|
as.commitRevision(tx)
|
|
|
|
|
|
as.invalidateCachedPerm(r.Name)
|
|
|
- as.invalidateUser(r.Name)
|
|
|
+ as.tokenProvider.invalidateUser(r.Name)
|
|
|
|
|
|
plog.Noticef("deleted a user: %s", r.Name)
|
|
|
|
|
|
@@ -421,7 +402,7 @@ func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*p
|
|
|
as.commitRevision(tx)
|
|
|
|
|
|
as.invalidateCachedPerm(r.Name)
|
|
|
- as.invalidateUser(r.Name)
|
|
|
+ as.tokenProvider.invalidateUser(r.Name)
|
|
|
|
|
|
plog.Noticef("changed a password of a user: %s", r.Name)
|
|
|
|
|
|
@@ -650,14 +631,8 @@ func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse,
|
|
|
return &pb.AuthRoleAddResponse{}, nil
|
|
|
}
|
|
|
|
|
|
-func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
|
|
|
- as.simpleTokensMu.RLock()
|
|
|
- defer as.simpleTokensMu.RUnlock()
|
|
|
- t, ok := as.simpleTokens[token]
|
|
|
- if ok {
|
|
|
- as.simpleTokenKeeper.resetSimpleToken(token)
|
|
|
- }
|
|
|
- return &AuthInfo{Username: t, Revision: as.revision}, ok
|
|
|
+func (as *authStore) authInfoFromToken(ctx context.Context, token string) (*AuthInfo, bool) {
|
|
|
+ return as.tokenProvider.info(ctx, token, as.revision)
|
|
|
}
|
|
|
|
|
|
type permSlice []*authpb.Permission
|
|
|
@@ -887,7 +862,7 @@ func (as *authStore) isAuthEnabled() bool {
|
|
|
return as.enabled
|
|
|
}
|
|
|
|
|
|
-func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{}) *authStore {
|
|
|
+func NewAuthStore(be backend.Backend, tp TokenProvider) *authStore {
|
|
|
tx := be.BatchTx()
|
|
|
tx.Lock()
|
|
|
|
|
|
@@ -905,15 +880,14 @@ func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{})
|
|
|
|
|
|
as := &authStore{
|
|
|
be: be,
|
|
|
- simpleTokens: make(map[string]string),
|
|
|
revision: getRevision(tx),
|
|
|
- indexWaiter: indexWaiter,
|
|
|
enabled: enabled,
|
|
|
rangePermCache: make(map[string]*unifiedRangePermissions),
|
|
|
+ tokenProvider: tp,
|
|
|
}
|
|
|
|
|
|
if enabled {
|
|
|
- as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
|
|
+ as.tokenProvider.enable()
|
|
|
}
|
|
|
|
|
|
if as.revision == 0 {
|
|
|
@@ -956,25 +930,6 @@ func (as *authStore) Revision() uint64 {
|
|
|
return as.revision
|
|
|
}
|
|
|
|
|
|
-func (as *authStore) isValidSimpleToken(token string, ctx context.Context) bool {
|
|
|
- splitted := strings.Split(token, ".")
|
|
|
- if len(splitted) != 2 {
|
|
|
- return false
|
|
|
- }
|
|
|
- index, err := strconv.Atoi(splitted[1])
|
|
|
- if err != nil {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- select {
|
|
|
- case <-as.indexWaiter(uint64(index)):
|
|
|
- return true
|
|
|
- case <-ctx.Done():
|
|
|
- }
|
|
|
-
|
|
|
- return false
|
|
|
-}
|
|
|
-
|
|
|
func (as *authStore) AuthInfoFromTLS(ctx context.Context) *AuthInfo {
|
|
|
peer, ok := peer.FromContext(ctx)
|
|
|
if !ok || peer == nil || peer.AuthInfo == nil {
|
|
|
@@ -1009,14 +964,57 @@ func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
|
|
|
}
|
|
|
|
|
|
token := ts[0]
|
|
|
- if !as.isValidSimpleToken(token, ctx) {
|
|
|
- return nil, ErrInvalidAuthToken
|
|
|
- }
|
|
|
-
|
|
|
- authInfo, uok := as.AuthInfoFromToken(token)
|
|
|
+ authInfo, uok := as.authInfoFromToken(ctx, token)
|
|
|
if !uok {
|
|
|
plog.Warningf("invalid auth token: %s", token)
|
|
|
return nil, ErrInvalidAuthToken
|
|
|
}
|
|
|
return authInfo, nil
|
|
|
}
|
|
|
+
|
|
|
+func (as *authStore) GenTokenPrefix() (string, error) {
|
|
|
+ return as.tokenProvider.genTokenPrefix()
|
|
|
+}
|
|
|
+
|
|
|
+func decomposeOpts(optstr string) (string, map[string]string, error) {
|
|
|
+ opts := strings.Split(optstr, ",")
|
|
|
+ tokenType := opts[0]
|
|
|
+
|
|
|
+ typeSpecificOpts := make(map[string]string)
|
|
|
+ for i := 1; i < len(opts); i++ {
|
|
|
+ pair := strings.Split(opts[i], "=")
|
|
|
+
|
|
|
+ if len(pair) != 2 {
|
|
|
+ plog.Errorf("invalid token specific option: %s", optstr)
|
|
|
+ return "", nil, ErrInvalidAuthOpts
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, ok := typeSpecificOpts[pair[0]]; ok {
|
|
|
+ plog.Errorf("invalid token specific option, duplicated parameters (%s): %s", pair[0], optstr)
|
|
|
+ return "", nil, ErrInvalidAuthOpts
|
|
|
+ }
|
|
|
+
|
|
|
+ typeSpecificOpts[pair[0]] = pair[1]
|
|
|
+ }
|
|
|
+
|
|
|
+ return tokenType, typeSpecificOpts, nil
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+func NewTokenProvider(tokenOpts string, indexWaiter func(uint64) <-chan struct{}) (TokenProvider, error) {
|
|
|
+ tokenType, typeSpecificOpts, err := decomposeOpts(tokenOpts)
|
|
|
+ if err != nil {
|
|
|
+ return nil, ErrInvalidAuthOpts
|
|
|
+ }
|
|
|
+
|
|
|
+ switch tokenType {
|
|
|
+ case "simple":
|
|
|
+ plog.Warningf("simple token is not cryptographically signed")
|
|
|
+ return newTokenProviderSimple(indexWaiter), nil
|
|
|
+ case "jwt":
|
|
|
+ return newTokenProviderJWT(typeSpecificOpts)
|
|
|
+ default:
|
|
|
+ plog.Errorf("unknown token type: %s", tokenType)
|
|
|
+ return nil, ErrInvalidAuthOpts
|
|
|
+ }
|
|
|
+}
|