@@ -24,21 +24,31 @@ import (
2424)
2525
2626type (
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.
6776func (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
11088func (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
13198func (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