github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxauth.go (about)

     1  // Package ais provides core functionality for the AIStore object storage.
     2  /*
     3   * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package ais
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/NVIDIA/aistore/api/apc"
    14  	"github.com/NVIDIA/aistore/api/authn"
    15  	"github.com/NVIDIA/aistore/cmd/authn/tok"
    16  	"github.com/NVIDIA/aistore/cmn"
    17  	"github.com/NVIDIA/aistore/cmn/cos"
    18  	"github.com/NVIDIA/aistore/cmn/debug"
    19  	"github.com/NVIDIA/aistore/cmn/nlog"
    20  	"github.com/NVIDIA/aistore/core/meta"
    21  	"github.com/NVIDIA/aistore/memsys"
    22  )
    23  
    24  type (
    25  	tokenList   authn.TokenList       // token strings
    26  	tkList      map[string]*tok.Token // tk structs
    27  	authManager struct {
    28  		sync.Mutex
    29  		// cache of decrypted tokens
    30  		tkList tkList
    31  		// list of invalid tokens(revoked or of deleted users)
    32  		// Authn sends these tokens to primary for broadcasting
    33  		revokedTokens map[string]bool
    34  		version       int64
    35  	}
    36  )
    37  
    38  /////////////////
    39  // authManager //
    40  /////////////////
    41  
    42  func newAuthManager() *authManager {
    43  	return &authManager{tkList: make(tkList), revokedTokens: make(map[string]bool), version: 1}
    44  }
    45  
    46  // Add tokens to list of invalid ones. After that it cleans up the list
    47  // from expired tokens
    48  func (a *authManager) updateRevokedList(newRevoked *tokenList) (allRevoked *tokenList) {
    49  	a.Lock()
    50  	switch {
    51  	case newRevoked.Version == 0: // manually revoked
    52  		a.version++
    53  	case newRevoked.Version > a.version:
    54  		a.version = newRevoked.Version
    55  	default:
    56  		nlog.Errorf("Current token list v%d is greater than received v%d", a.version, newRevoked.Version)
    57  		a.Unlock()
    58  		return
    59  	}
    60  	// add new
    61  	for _, token := range newRevoked.Tokens {
    62  		a.revokedTokens[token] = true
    63  		delete(a.tkList, token)
    64  	}
    65  	allRevoked = &tokenList{
    66  		Tokens:  make([]string, 0, len(a.revokedTokens)),
    67  		Version: a.version,
    68  	}
    69  	var (
    70  		now    = time.Now()
    71  		secret = cmn.GCO.Get().Auth.Secret
    72  	)
    73  	for token := range a.revokedTokens {
    74  		tk, err := tok.DecryptToken(token, secret)
    75  		debug.AssertNoErr(err)
    76  		if tk.Expires.Before(now) {
    77  			delete(a.revokedTokens, token)
    78  		} else {
    79  			allRevoked.Tokens = append(allRevoked.Tokens, token)
    80  		}
    81  	}
    82  	a.Unlock()
    83  	if len(allRevoked.Tokens) == 0 {
    84  		allRevoked = nil
    85  	}
    86  	return
    87  }
    88  
    89  func (a *authManager) revokedTokenList() (allRevoked *tokenList) {
    90  	a.Lock()
    91  	l := len(a.revokedTokens)
    92  	if l == 0 {
    93  		a.Unlock()
    94  		return
    95  	}
    96  	allRevoked = &tokenList{Tokens: make([]string, 0, l), Version: a.version}
    97  	for token := range a.revokedTokens {
    98  		allRevoked.Tokens = append(allRevoked.Tokens, token)
    99  	}
   100  	a.Unlock()
   101  	return
   102  }
   103  
   104  // Checks if a token is valid:
   105  //   - must not be revoked one
   106  //   - must not be expired
   107  //   - must have all mandatory fields: userID, creds, issued, expires
   108  //
   109  // Returns decrypted token information if it is valid
   110  func (a *authManager) validateToken(token string) (tk *tok.Token, err error) {
   111  	a.Lock()
   112  	if _, ok := a.revokedTokens[token]; ok {
   113  		tk, err = nil, fmt.Errorf("%v: %s", tok.ErrTokenRevoked, tk)
   114  	} else {
   115  		tk, err = a.validateAddRm(token, time.Now())
   116  	}
   117  	a.Unlock()
   118  	return
   119  }
   120  
   121  // Decrypts and validates token. Adds it to authManager.token if not found. Removes if expired.
   122  // Must be called under lock.
   123  func (a *authManager) validateAddRm(token string, now time.Time) (*tok.Token, error) {
   124  	tk, ok := a.tkList[token]
   125  	if !ok || tk == nil {
   126  		var (
   127  			err    error
   128  			secret = cmn.GCO.Get().Auth.Secret
   129  		)
   130  		if tk, err = tok.DecryptToken(token, secret); err != nil {
   131  			nlog.Errorln(err)
   132  			return nil, tok.ErrInvalidToken
   133  		}
   134  		a.tkList[token] = tk
   135  	}
   136  	if tk.Expires.Before(now) {
   137  		delete(a.tkList, token)
   138  		return nil, fmt.Errorf("%v: %s", tok.ErrTokenExpired, tk)
   139  	}
   140  	return tk, nil
   141  }
   142  
   143  ///////////////
   144  // tokenList //
   145  ///////////////
   146  
   147  // interface guard
   148  var _ revs = (*tokenList)(nil)
   149  
   150  func (*tokenList) tag() string         { return revsTokenTag }
   151  func (t *tokenList) version() int64    { return t.Version } // no versioning: receivers keep adding tokens to their lists
   152  func (t *tokenList) marshal() []byte   { return cos.MustMarshal(t) }
   153  func (t *tokenList) jit(_ *proxy) revs { return t }
   154  func (*tokenList) sgl() *memsys.SGL    { return nil }
   155  func (t *tokenList) String() string    { return fmt.Sprintf("TokenList v%d", t.Version) }
   156  
   157  //
   158  // proxy cont-ed
   159  //
   160  
   161  // [METHOD] /v1/tokens
   162  func (p *proxy) tokenHandler(w http.ResponseWriter, r *http.Request) {
   163  	switch r.Method {
   164  	case http.MethodPost:
   165  		p.validateSecret(w, r)
   166  	case http.MethodDelete:
   167  		p.httpTokenDelete(w, r)
   168  	default:
   169  		cmn.WriteErr405(w, r, http.MethodDelete)
   170  	}
   171  }
   172  
   173  func (p *proxy) validateSecret(w http.ResponseWriter, r *http.Request) {
   174  	if _, err := p.parseURL(w, r, apc.URLPathTokens.L, 0, false); err != nil {
   175  		return
   176  	}
   177  	cksum := cos.NewCksumHash(cos.ChecksumSHA256)
   178  	cksum.H.Write([]byte(cmn.GCO.Get().Auth.Secret))
   179  	cksum.Finalize()
   180  
   181  	cluConf := &authn.ServerConf{}
   182  	if err := cmn.ReadJSON(w, r, cluConf); err != nil {
   183  		return
   184  	}
   185  	if cksum.Val() != cluConf.Secret {
   186  		p.writeErrf(w, r, "%s: invalid secret sha256(%q)", p, cos.SHead(cluConf.Secret))
   187  	}
   188  }
   189  
   190  func (p *proxy) httpTokenDelete(w http.ResponseWriter, r *http.Request) {
   191  	if _, err := p.parseURL(w, r, apc.URLPathTokens.L, 0, false); err != nil {
   192  		return
   193  	}
   194  	if p.forwardCP(w, r, nil, "revoke token") {
   195  		return
   196  	}
   197  	tokenList := &tokenList{}
   198  	if err := cmn.ReadJSON(w, r, tokenList); err != nil {
   199  		return
   200  	}
   201  	allRevoked := p.authn.updateRevokedList(tokenList)
   202  	if allRevoked != nil && p.owner.smap.get().isPrimary(p.si) {
   203  		msg := p.newAmsgStr(apc.ActNewPrimary, nil)
   204  		_ = p.metasyncer.sync(revsPair{allRevoked, msg})
   205  	}
   206  }
   207  
   208  // Validates a token from the request header
   209  func (p *proxy) validateToken(hdr http.Header) (*tok.Token, error) {
   210  	token, err := tok.ExtractToken(hdr)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	tk, err := p.authn.validateToken(token)
   215  	if err != nil {
   216  		nlog.Errorf("invalid token: %v", err)
   217  		return nil, err
   218  	}
   219  	return tk, nil
   220  }
   221  
   222  // When AuthN is on, accessing a bucket requires two permissions:
   223  //   - access to the bucket is granted to a user
   224  //   - bucket ACL allows the required operation
   225  //     Exception: a superuser can always PATCH the bucket/Set ACL
   226  //
   227  // If AuthN is off, only bucket permissions are checked.
   228  //
   229  //	Exceptions:
   230  //	- read-only access to a bucket is always granted
   231  //	- PATCH cannot be forbidden
   232  func (p *proxy) checkAccess(w http.ResponseWriter, r *http.Request, bck *meta.Bck, ace apc.AccessAttrs) (err error) {
   233  	if err = p.access(r.Header, bck, ace); err != nil {
   234  		p.writeErr(w, r, err, aceErrToCode(err))
   235  	}
   236  	return
   237  }
   238  
   239  func aceErrToCode(err error) (status int) {
   240  	switch err {
   241  	case nil:
   242  	case tok.ErrNoToken, tok.ErrInvalidToken:
   243  		status = http.StatusUnauthorized
   244  	default:
   245  		status = http.StatusForbidden
   246  	}
   247  	return status
   248  }
   249  
   250  func (p *proxy) access(hdr http.Header, bck *meta.Bck, ace apc.AccessAttrs) (err error) {
   251  	var (
   252  		tk     *tok.Token
   253  		bucket *cmn.Bck
   254  	)
   255  	if p.isIntraCall(hdr, false /*from primary*/) == nil {
   256  		return nil
   257  	}
   258  	if cmn.Rom.AuthEnabled() { // config.Auth.Enabled
   259  		tk, err = p.validateToken(hdr)
   260  		if err != nil {
   261  			// NOTE: making exception to allow 3rd party clients read remote ht://bucket
   262  			if err == tok.ErrNoToken && bck != nil && bck.IsHTTP() {
   263  				err = nil
   264  			}
   265  			return err
   266  		}
   267  		uid := p.owner.smap.Get().UUID
   268  		if bck != nil {
   269  			bucket = bck.Bucket()
   270  		}
   271  		if err := tk.CheckPermissions(uid, bucket, ace); err != nil {
   272  			return err
   273  		}
   274  	}
   275  	if bck == nil {
   276  		// cluster ACL: create/list buckets, node management, etc.
   277  		return nil
   278  	}
   279  
   280  	// bucket access conventions:
   281  	// - without AuthN: read-only access, PATCH, and ACL
   282  	// - with AuthN:    superuser can PATCH and change ACL
   283  	if !cmn.Rom.AuthEnabled() {
   284  		ace &^= (apc.AcePATCH | apc.AceBckSetACL | apc.AccessRO)
   285  	} else if tk.IsAdmin {
   286  		ace &^= (apc.AcePATCH | apc.AceBckSetACL)
   287  	}
   288  	if ace == 0 {
   289  		return nil
   290  	}
   291  	return bck.Allow(ace)
   292  }