Skip to content

Commit c0b5aee

Browse files
committed
auth: optimize locking for token validation and revocation
Signed-off-by: Aaron Wilson <[email protected]>
1 parent 45a398f commit c0b5aee

File tree

5 files changed

+485
-67
lines changed

5 files changed

+485
-67
lines changed

.gitlab-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ test:short:authn:
480480
AIS_AUTHN_URL: "http://localhost:52001"
481481
BUCKET: "ais://test"
482482
RE: "TestAuth"
483+
TEST_RACE: "true"
483484
script:
484485
- ${SCRIPTS_DIR}/clean_deploy.sh --target-cnt $NUM_TARGET --proxy-cnt $NUM_PROXY --mountpath-cnt $FS_CNT
485486
- ais auth login $AIS_AUTHN_SU_NAME -p $AIS_AUTHN_SU_PASS

ais/prxauth.go

Lines changed: 165 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,31 @@ import (
2424
)
2525

2626
type (
27-
tokenList authn.TokenList // token strings
28-
tokenMap map[string]*tok.AISClaims // map token strings to claim structs
29-
27+
tokenList authn.TokenList // token strings
3028
authManager struct {
3129
// used for parsing and validating claims from token strings
32-
tokenParser *tok.TokenParser
33-
// cache of decrypted token claims
34-
tokenMap tokenMap
30+
tokenParser tok.Parser
31+
// provides thread-safe access to a cache of decrypted token claims
32+
tokenMap *TokenClaimMap
33+
// provides thread-safe access to an underlying map of tokens
34+
revokedTokens *RevokedTokensMap
35+
}
36+
37+
RevokedTokensMap struct {
3538
// list of invalid tokens(revoked or of deleted users)
3639
// Authn sends these tokens to primary for broadcasting
3740
revokedTokens map[string]bool
3841
// latest revoked version
3942
version int64
4043
// lock
41-
sync.Mutex
44+
sync.RWMutex
45+
}
46+
47+
TokenClaimMap struct {
48+
// map token strings to claim structs
49+
tokenMap map[string]*tok.AISClaims
50+
// lock
51+
sync.RWMutex
4252
}
4353
)
4454

@@ -56,70 +66,27 @@ func newAuthManager(config *cmn.Config) *authManager {
5666
Aud: config.Auth.Aud,
5767
}
5868
return &authManager{
59-
tokenMap: make(tokenMap),
60-
revokedTokens: make(map[string]bool), // TODO: preallocate
61-
version: 1,
69+
tokenMap: newTokenClaimMap(),
70+
revokedTokens: newRevokedTokensMap(),
6271
tokenParser: tok.NewTokenParser(secret, rsaPubKey, requiredClaims),
6372
}
6473
}
6574

6675
// Add tokens to the list of invalid ones and clean up the list from expired tokens.
6776
func (a *authManager) updateRevokedList(newRevoked *tokenList) (allRevoked *tokenList) {
68-
a.Lock()
69-
defer a.Unlock()
70-
71-
switch {
72-
case newRevoked.Version == 0: // Manually revoked tokens
73-
a.version++
74-
case newRevoked.Version > a.version:
75-
a.version = newRevoked.Version
76-
default:
77-
nlog.Errorf("Current token list v%d is greater than received v%d", a.version, newRevoked.Version)
77+
// Add new revoked tokens -- error if invalid version
78+
err := a.revokedTokens.update(newRevoked)
79+
if err != nil {
7880
return nil
7981
}
80-
81-
// Add new revoked tokens and remove them from the valid token list.
82-
for _, token := range newRevoked.Tokens {
83-
a.revokedTokens[token] = true
84-
delete(a.tokenMap, token)
85-
}
86-
87-
allRevoked = &tokenList{
88-
Tokens: make([]string, 0, len(a.revokedTokens)),
89-
Version: a.version,
90-
}
91-
92-
// Clean up expired tokens from the revoked list.
93-
for token := range a.revokedTokens {
94-
_, err := a.tokenParser.ValidateToken(token)
95-
switch {
96-
case errors.Is(err, tok.ErrTokenExpired):
97-
delete(a.revokedTokens, token)
98-
case err == nil:
99-
allRevoked.Tokens = append(allRevoked.Tokens, token)
100-
default:
101-
nlog.Errorf("Unexpected token validation error: %v (token: %s)", err, token)
102-
}
103-
}
104-
if len(allRevoked.Tokens) == 0 {
105-
allRevoked = nil
106-
}
107-
return allRevoked
82+
// Remove revoked tokens from the token cache
83+
a.tokenMap.deleteMultiple(newRevoked.Tokens)
84+
// Clean up any expired tokens from the revoked list
85+
return a.revokedTokens.cleanup(a.tokenParser)
10886
}
10987

11088
func (a *authManager) revokedTokenList() (allRevoked *tokenList) {
111-
a.Lock()
112-
l := len(a.revokedTokens)
113-
if l == 0 {
114-
a.Unlock()
115-
return
116-
}
117-
allRevoked = &tokenList{Tokens: make([]string, 0, l), Version: a.version}
118-
for token := range a.revokedTokens {
119-
allRevoked.Tokens = append(allRevoked.Tokens, token)
120-
}
121-
a.Unlock()
122-
return
89+
return a.revokedTokens.getAll()
12390
}
12491

12592
// Checks if a token is valid:
@@ -129,17 +96,18 @@ func (a *authManager) revokedTokenList() (allRevoked *tokenList) {
12996
//
13097
// Caches and returns decoded token claims if it is valid
13198
func (a *authManager) validateToken(token string) (*tok.AISClaims, error) {
132-
a.Lock()
133-
defer a.Unlock()
134-
if _, ok := a.revokedTokens[token]; ok {
99+
if a.revokedTokens.contains(token) {
135100
return nil, fmt.Errorf("%w [err: %w]: %s", tok.ErrInvalidToken, tok.ErrTokenRevoked, token)
136101
}
137-
claims, ok := a.tokenMap[token]
102+
claims, ok := a.tokenMap.get(token)
138103
debug.Assert(!ok || claims != nil)
139104
// If token string exists in cache, only need to check claim expiry
140105
if ok && claims != nil {
141106
if claims.IsExpired() {
142-
delete(a.tokenMap, token)
107+
// This takes a separate write lock than the previous read, but:
108+
// - claims are immutable once cached
109+
// - we only delete expired claims, never modify them
110+
a.tokenMap.delete(token)
143111
return nil, fmt.Errorf("%w [err: %w]: %s", tok.ErrInvalidToken, tok.ErrTokenExpired, token)
144112
}
145113
return claims, nil
@@ -155,7 +123,8 @@ func (a *authManager) cacheNewToken(token string) (claims *tok.AISClaims, err er
155123
nlog.Infof("Received invalid token, error: %v", err)
156124
return nil, err
157125
}
158-
a.tokenMap[token] = claims
126+
// We aren't guaranteed someone else didn't do this, but worth a separate attempt to validate outside of lock
127+
a.tokenMap.set(token, claims)
159128
return claims, nil
160129
}
161130

@@ -326,3 +295,133 @@ func (p *proxy) access(hdr http.Header, bck *meta.Bck, ace apc.AccessAttrs) (err
326295
}
327296
return bck.Allow(ace)
328297
}
298+
299+
///////////////////
300+
// TokenClaimMap //
301+
///////////////////
302+
303+
func newTokenClaimMap() *TokenClaimMap {
304+
return &TokenClaimMap{
305+
tokenMap: make(map[string]*tok.AISClaims),
306+
}
307+
}
308+
309+
func (m *TokenClaimMap) get(token string) (*tok.AISClaims, bool) {
310+
m.RLock()
311+
claims, ok := m.tokenMap[token]
312+
m.RUnlock()
313+
return claims, ok
314+
}
315+
316+
func (m *TokenClaimMap) set(token string, claims *tok.AISClaims) {
317+
m.Lock()
318+
m.tokenMap[token] = claims
319+
m.Unlock()
320+
}
321+
322+
func (m *TokenClaimMap) delete(token string) {
323+
m.Lock()
324+
delete(m.tokenMap, token)
325+
m.Unlock()
326+
}
327+
328+
func (m *TokenClaimMap) deleteMultiple(tokens []string) {
329+
m.Lock()
330+
for _, token := range tokens {
331+
delete(m.tokenMap, token)
332+
}
333+
m.Unlock()
334+
}
335+
336+
//////////////////////
337+
// RevokedTokensMap //
338+
//////////////////////
339+
340+
func newRevokedTokensMap() *RevokedTokensMap {
341+
return &RevokedTokensMap{
342+
revokedTokens: make(map[string]bool),
343+
version: 1,
344+
}
345+
}
346+
347+
func (r *RevokedTokensMap) update(newRevoked *tokenList) error {
348+
// Lock over the whole operation as we must verify the final updated version matches the version number
349+
r.Lock()
350+
defer r.Unlock()
351+
err := r.updateVersion(newRevoked)
352+
if err != nil {
353+
return err
354+
}
355+
for _, token := range newRevoked.Tokens {
356+
r.revokedTokens[token] = true
357+
}
358+
return nil
359+
}
360+
361+
// Must be called under lock
362+
func (r *RevokedTokensMap) updateVersion(newRevoked *tokenList) error {
363+
currentVersion := r.version
364+
switch {
365+
case newRevoked.Version == 0:
366+
r.version++
367+
return nil
368+
case newRevoked.Version > currentVersion:
369+
r.version = newRevoked.Version
370+
return nil
371+
case newRevoked.Version == currentVersion:
372+
nlog.Warningf("received token list v%d equal to current token list v%d, ignoring", newRevoked.Version, currentVersion)
373+
default:
374+
nlog.Errorf("received token list v%d less than current token list v%d", newRevoked.Version, currentVersion)
375+
}
376+
return fmt.Errorf("received invalid token list version v%d compared to current token list v%d", newRevoked.Version, currentVersion)
377+
}
378+
379+
func (r *RevokedTokensMap) cleanup(tkParser tok.Parser) (allRevoked *tokenList) {
380+
r.Lock()
381+
defer r.Unlock()
382+
allRevoked = &tokenList{
383+
Tokens: make([]string, 0, len(r.revokedTokens)),
384+
Version: r.version,
385+
}
386+
387+
// Clean up expired tokens from the revoked list.
388+
for token := range r.revokedTokens {
389+
_, err := tkParser.ValidateToken(token)
390+
switch {
391+
case errors.Is(err, tok.ErrTokenExpired):
392+
delete(r.revokedTokens, token)
393+
case err == nil:
394+
allRevoked.Tokens = append(allRevoked.Tokens, token)
395+
default:
396+
// Keep tokens if error validating that's not expiration
397+
// We don't want to un-revoke this token in case expected claims revert to previous
398+
allRevoked.Tokens = append(allRevoked.Tokens, token)
399+
nlog.Errorf("Unexpected token validation error: %v (token: %s)", err, token)
400+
}
401+
}
402+
if len(allRevoked.Tokens) == 0 {
403+
allRevoked = nil
404+
}
405+
return allRevoked
406+
}
407+
408+
func (r *RevokedTokensMap) contains(token string) bool {
409+
r.RLock()
410+
_, ok := r.revokedTokens[token]
411+
r.RUnlock()
412+
return ok
413+
}
414+
415+
func (r *RevokedTokensMap) getAll() *tokenList {
416+
r.RLock()
417+
defer r.RUnlock()
418+
l := len(r.revokedTokens)
419+
if l == 0 {
420+
return nil
421+
}
422+
allRevoked := &tokenList{Tokens: make([]string, 0, l), Version: r.version}
423+
for token := range r.revokedTokens {
424+
allRevoked.Tokens = append(allRevoked.Tokens, token)
425+
}
426+
return allRevoked
427+
}

0 commit comments

Comments
 (0)