github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmd/authn/mgr.go (about)

     1  // Package authn is authentication server for AIStore.
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package main
     6  
     7  import (
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"time"
    13  
    14  	"github.com/NVIDIA/aistore/api/apc"
    15  	"github.com/NVIDIA/aistore/api/authn"
    16  	"github.com/NVIDIA/aistore/cmd/authn/tok"
    17  	"github.com/NVIDIA/aistore/cmn"
    18  	"github.com/NVIDIA/aistore/cmn/cos"
    19  	"github.com/NVIDIA/aistore/cmn/debug"
    20  	"github.com/NVIDIA/aistore/cmn/kvdb"
    21  	"github.com/NVIDIA/aistore/cmn/nlog"
    22  	jsoniter "github.com/json-iterator/go"
    23  	"golang.org/x/crypto/bcrypt"
    24  )
    25  
    26  type mgr struct {
    27  	clientH   *http.Client
    28  	clientTLS *http.Client
    29  	db        kvdb.Driver
    30  }
    31  
    32  var (
    33  	errInvalidCredentials = errors.New("invalid credentials")
    34  
    35  	predefinedRoles = []struct {
    36  		prefix string
    37  		desc   string
    38  		perms  apc.AccessAttrs
    39  	}{
    40  		{ClusterOwnerRole, "Admin access to %s", apc.AccessAll},
    41  		{BucketOwnerRole, "Full access to buckets in %s", apc.AccessRW},
    42  		{GuestRole, "Read-only access to buckets in %s", apc.AccessRO},
    43  	}
    44  )
    45  
    46  // If user DB exists, loads the data from the file and decrypts passwords
    47  func newMgr(driver kvdb.Driver) (m *mgr, err error) {
    48  	m = &mgr{
    49  		db: driver,
    50  	}
    51  	m.clientH, m.clientTLS = cmn.NewDefaultClients(time.Duration(Conf.Timeout.Default))
    52  	err = initializeDB(driver)
    53  	return
    54  }
    55  
    56  func (*mgr) String() string { return svcName }
    57  
    58  //
    59  // users ============================================================
    60  //
    61  
    62  // Registers a new user. It is info from a user, so the password
    63  // is not encrypted and a few fields are not filled(e.g, Access).
    64  func (m *mgr) addUser(info *authn.User) error {
    65  	if info.ID == "" || info.Password == "" {
    66  		return errInvalidCredentials
    67  	}
    68  
    69  	_, err := m.db.GetString(usersCollection, info.ID)
    70  	if err == nil {
    71  		return fmt.Errorf("user %q already registered", info.ID)
    72  	}
    73  	info.Password = encryptPassword(info.Password)
    74  	return m.db.Set(usersCollection, info.ID, info)
    75  }
    76  
    77  // Deletes an existing user
    78  func (m *mgr) delUser(userID string) error {
    79  	if userID == adminUserID {
    80  		return fmt.Errorf("cannot remove built-in %q account", adminUserID)
    81  	}
    82  	return m.db.Delete(usersCollection, userID)
    83  }
    84  
    85  // Updates an existing user. The function invalidates user tokens after
    86  // successful update.
    87  func (m *mgr) updateUser(userID string, updateReq *authn.User) error {
    88  	uInfo := &authn.User{}
    89  	err := m.db.Get(usersCollection, userID, uInfo)
    90  	if err != nil {
    91  		return cos.NewErrNotFound(m, "user "+userID)
    92  	}
    93  	if userID == adminUserID && len(updateReq.Roles) != 0 {
    94  		return errors.New("cannot change administrator's role")
    95  	}
    96  
    97  	if updateReq.Password != "" {
    98  		uInfo.Password = encryptPassword(updateReq.Password)
    99  	}
   100  	if len(updateReq.Roles) != 0 {
   101  		uInfo.Roles = updateReq.Roles
   102  	}
   103  	uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, updateReq.ClusterACLs, "")
   104  	uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, updateReq.BucketACLs, "")
   105  
   106  	return m.db.Set(usersCollection, userID, uInfo)
   107  }
   108  
   109  func (m *mgr) lookupUser(userID string) (*authn.User, error) {
   110  	uInfo := &authn.User{}
   111  	err := m.db.Get(usersCollection, userID, uInfo)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	// update ACLs with roles's ones
   117  	for _, role := range uInfo.Roles {
   118  		rInfo := &authn.Role{}
   119  		err := m.db.Get(rolesCollection, role, rInfo)
   120  		if err != nil {
   121  			continue
   122  		}
   123  		uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, rInfo.ClusterACLs, "")
   124  		uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, rInfo.BucketACLs, "")
   125  	}
   126  
   127  	return uInfo, nil
   128  }
   129  
   130  func (m *mgr) userList() (map[string]*authn.User, error) {
   131  	recs, err := m.db.GetAll(usersCollection, "")
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	users := make(map[string]*authn.User, 4)
   136  	for _, str := range recs {
   137  		uInfo := &authn.User{}
   138  		err := jsoniter.Unmarshal([]byte(str), uInfo)
   139  		cos.AssertNoErr(err)
   140  		users[uInfo.ID] = uInfo
   141  	}
   142  	return users, nil
   143  }
   144  
   145  //
   146  // roles ============================================================
   147  //
   148  
   149  // Registers a new role
   150  func (m *mgr) addRole(info *authn.Role) error {
   151  	if info.ID == "" {
   152  		return errors.New("role name is undefined")
   153  	}
   154  	if info.IsAdmin {
   155  		return fmt.Errorf("only built-in roles can have %q permissions", adminUserID)
   156  	}
   157  
   158  	_, err := m.db.GetString(rolesCollection, info.ID)
   159  	if err == nil {
   160  		return fmt.Errorf("role %q already exists", info.ID)
   161  	}
   162  	return m.db.Set(rolesCollection, info.ID, info)
   163  }
   164  
   165  // Deletes an existing role
   166  func (m *mgr) delRole(role string) error {
   167  	if role == authn.AdminRole {
   168  		return fmt.Errorf("cannot remove built-in %q role", authn.AdminRole)
   169  	}
   170  	return m.db.Delete(rolesCollection, role)
   171  }
   172  
   173  // Updates an existing role
   174  func (m *mgr) updateRole(role string, updateReq *authn.Role) error {
   175  	if role == authn.AdminRole {
   176  		return fmt.Errorf("cannot modify built-in %q role", authn.AdminRole)
   177  	}
   178  	rInfo := &authn.Role{}
   179  	err := m.db.Get(rolesCollection, role, rInfo)
   180  	if err != nil {
   181  		return cos.NewErrNotFound(m, "role "+role)
   182  	}
   183  
   184  	if updateReq.Desc != "" {
   185  		rInfo.Desc = updateReq.Desc
   186  	}
   187  	if len(updateReq.Roles) != 0 {
   188  		rInfo.Roles = updateReq.Roles
   189  	}
   190  	rInfo.ClusterACLs = mergeClusterACLs(rInfo.ClusterACLs, updateReq.ClusterACLs, "")
   191  	rInfo.BucketACLs = mergeBckACLs(rInfo.BucketACLs, updateReq.BucketACLs, "")
   192  
   193  	return m.db.Set(rolesCollection, role, rInfo)
   194  }
   195  
   196  func (m *mgr) lookupRole(roleID string) (*authn.Role, error) {
   197  	rInfo := &authn.Role{}
   198  	err := m.db.Get(rolesCollection, roleID, rInfo)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	return rInfo, nil
   203  }
   204  
   205  func (m *mgr) roleList() ([]*authn.Role, error) {
   206  	recs, err := m.db.GetAll(rolesCollection, "")
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	roles := make([]*authn.Role, 0, len(recs))
   211  	for _, str := range recs {
   212  		role := &authn.Role{}
   213  		err := jsoniter.Unmarshal([]byte(str), role)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		roles = append(roles, role)
   218  	}
   219  	return roles, nil
   220  }
   221  
   222  // Creates predefined roles for just added clusters. Errors are logged and
   223  // are not returned to a caller as it is not crucial.
   224  func (m *mgr) createRolesForCluster(clu *authn.CluACL) {
   225  	for _, pr := range predefinedRoles {
   226  		suffix := cos.Either(clu.Alias, clu.ID)
   227  		uid := pr.prefix + "-" + suffix
   228  		rInfo := &authn.Role{}
   229  		if err := m.db.Get(rolesCollection, uid, rInfo); err == nil {
   230  			continue
   231  		}
   232  		rInfo.ID = uid
   233  		cluName := clu.ID
   234  		if clu.Alias != "" {
   235  			cluName += "[" + clu.Alias + "]"
   236  		}
   237  		rInfo.Desc = fmt.Sprintf(pr.desc, cluName)
   238  		rInfo.ClusterACLs = []*authn.CluACL{
   239  			{ID: clu.ID, Access: pr.perms},
   240  		}
   241  		if err := m.db.Set(rolesCollection, uid, rInfo); err != nil {
   242  			nlog.Errorf("Failed to create role %s: %v", uid, err)
   243  		}
   244  	}
   245  }
   246  
   247  //
   248  // clusters ============================================================
   249  //
   250  
   251  func (m *mgr) clus() (map[string]*authn.CluACL, error) {
   252  	clusters, err := m.db.GetAll(clustersCollection, "")
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	clus := make(map[string]*authn.CluACL, len(clusters))
   257  	for cid, s := range clusters {
   258  		cInfo := &authn.CluACL{}
   259  		if err := jsoniter.Unmarshal([]byte(s), cInfo); err != nil {
   260  			nlog.Errorf("Failed to parse cluster %s info: %v", cid, err)
   261  			continue
   262  		}
   263  		clus[cInfo.ID] = cInfo
   264  	}
   265  	return clus, nil
   266  }
   267  
   268  // Returns a cluster ID which ID or Alias equal cluID or cluAlias.
   269  func (m *mgr) cluLookup(cluID, cluAlias string) string {
   270  	clus, err := m.clus()
   271  	if err != nil {
   272  		return ""
   273  	}
   274  	for cid, cInfo := range clus {
   275  		if cid != "" && (cid == cluID || cid == cluAlias) {
   276  			return cid
   277  		}
   278  		if cluAlias == "" {
   279  			continue
   280  		}
   281  		if cInfo.ID == cluAlias || cInfo.Alias == cluAlias {
   282  			return cid
   283  		}
   284  	}
   285  	return ""
   286  }
   287  
   288  // Get an existing cluster
   289  func (m *mgr) getCluster(cluID string) (*authn.CluACL, error) {
   290  	cid := m.cluLookup(cluID, cluID)
   291  	if cid == "" {
   292  		return nil, cos.NewErrNotFound(m, "cluster "+cluID)
   293  	}
   294  	clu := &authn.CluACL{}
   295  	err := m.db.Get(clustersCollection, cid, clu)
   296  	return clu, err
   297  }
   298  
   299  // Registers a new cluster
   300  func (m *mgr) addCluster(clu *authn.CluACL) error {
   301  	if clu.ID == "" {
   302  		return errors.New("cluster UUID is undefined")
   303  	}
   304  
   305  	cid := m.cluLookup(clu.ID, clu.Alias)
   306  	if cid != "" {
   307  		return fmt.Errorf("cluster %s[%s] already registered", clu.ID, cid)
   308  	}
   309  
   310  	// secret handshake
   311  	if err := m.validateSecret(clu); err != nil {
   312  		return err
   313  	}
   314  
   315  	if err := m.db.Set(clustersCollection, clu.ID, clu); err != nil {
   316  		return err
   317  	}
   318  	m.createRolesForCluster(clu)
   319  
   320  	go m.syncTokenList(clu)
   321  	return nil
   322  }
   323  
   324  func (m *mgr) updateCluster(cluID string, info *authn.CluACL) error {
   325  	if info.ID == "" {
   326  		return errors.New("cluster ID is undefined")
   327  	}
   328  	clu := &authn.CluACL{}
   329  	if err := m.db.Get(clustersCollection, cluID, clu); err != nil {
   330  		return err
   331  	}
   332  	if info.Alias != "" {
   333  		cid := m.cluLookup("", info.Alias)
   334  		if cid != "" && cid != clu.ID {
   335  			return fmt.Errorf("alias %q is used for cluster %q", info.Alias, cid)
   336  		}
   337  		clu.Alias = info.Alias
   338  	}
   339  	if len(info.URLs) != 0 {
   340  		clu.URLs = info.URLs
   341  	}
   342  
   343  	// secret handshake
   344  	if err := m.validateSecret(clu); err != nil {
   345  		return err
   346  	}
   347  
   348  	return m.db.Set(clustersCollection, cluID, clu)
   349  }
   350  
   351  // Unregister an existing cluster
   352  func (m *mgr) delCluster(cluID string) error {
   353  	cid := m.cluLookup(cluID, cluID)
   354  	if cid == "" {
   355  		return cos.NewErrNotFound(m, "cluster "+cluID)
   356  	}
   357  	return m.db.Delete(clustersCollection, cid)
   358  }
   359  
   360  //
   361  // tokens ============================================================
   362  //
   363  
   364  // Generates a token for a user if user credentials are valid. If the token is
   365  // already generated and is not expired yet the existing token is returned.
   366  // Token includes user ID, permissions, and token expiration time.
   367  // If a new token was generated then it sends the proxy a new valid token list
   368  func (m *mgr) issueToken(userID, pwd string, msg *authn.LoginMsg) (string, error) {
   369  	var (
   370  		err     error
   371  		expires time.Time
   372  		token   string
   373  		uInfo   = &authn.User{}
   374  		cid     string
   375  	)
   376  
   377  	err = m.db.Get(usersCollection, userID, uInfo)
   378  	if err != nil {
   379  		nlog.Errorln(err)
   380  		return "", errInvalidCredentials
   381  	}
   382  	if !isSamePassword(pwd, uInfo.Password) {
   383  		return "", errInvalidCredentials
   384  	}
   385  	if !uInfo.IsAdmin() {
   386  		if msg.ClusterID == "" {
   387  			return "", fmt.Errorf("Couldn't issue token for %q: cluster ID not set", userID)
   388  		}
   389  		cid = m.cluLookup(msg.ClusterID, msg.ClusterID)
   390  		if cid == "" {
   391  			return "", cos.NewErrNotFound(m, "cluster "+msg.ClusterID)
   392  		}
   393  		uInfo.ClusterACLs = mergeClusterACLs(make([]*authn.CluACL, 0, len(uInfo.ClusterACLs)), uInfo.ClusterACLs, cid)
   394  		uInfo.BucketACLs = mergeBckACLs(make([]*authn.BckACL, 0, len(uInfo.BucketACLs)), uInfo.BucketACLs, cid)
   395  	}
   396  
   397  	// update ACLs with roles's ones
   398  	for _, role := range uInfo.Roles {
   399  		rInfo := &authn.Role{}
   400  		err := m.db.Get(rolesCollection, role, rInfo)
   401  		if err != nil {
   402  			continue
   403  		}
   404  		uInfo.ClusterACLs = mergeClusterACLs(uInfo.ClusterACLs, rInfo.ClusterACLs, cid)
   405  		uInfo.BucketACLs = mergeBckACLs(uInfo.BucketACLs, rInfo.BucketACLs, cid)
   406  	}
   407  
   408  	// generate token
   409  	Conf.RLock()
   410  	defer Conf.RUnlock()
   411  	issued := time.Now()
   412  	expDelta := time.Duration(Conf.Server.ExpirePeriod)
   413  	if msg.ExpiresIn != nil {
   414  		expDelta = *msg.ExpiresIn
   415  	}
   416  	if expDelta == 0 {
   417  		expDelta = foreverTokenTime
   418  	}
   419  	expires = issued.Add(expDelta)
   420  
   421  	// put all useful info into token: who owns the token, when it was issued,
   422  	// when it expires and credentials to log in AWS, GCP etc.
   423  	// If a user is a super user, it is enough to pass only isAdmin marker
   424  	if uInfo.IsAdmin() {
   425  		token, err = tok.IssueAdminJWT(expires, userID, Conf.Server.Secret)
   426  	} else {
   427  		m.fixClusterIDs(uInfo.ClusterACLs)
   428  		token, err = tok.IssueJWT(expires, userID, uInfo.BucketACLs, uInfo.ClusterACLs, Conf.Server.Secret)
   429  	}
   430  	return token, err
   431  }
   432  
   433  // Before putting a list of cluster permissions to a token, cluster aliases
   434  // must be replaced with their IDs.
   435  func (m *mgr) fixClusterIDs(lst []*authn.CluACL) {
   436  	clus, err := m.clus()
   437  	if err != nil {
   438  		return
   439  	}
   440  	for _, cInfo := range lst {
   441  		if _, ok := clus[cInfo.ID]; ok {
   442  			continue
   443  		}
   444  		for _, clu := range clus {
   445  			if clu.Alias == cInfo.ID {
   446  				cInfo.ID = clu.ID
   447  			}
   448  		}
   449  	}
   450  }
   451  
   452  // Delete existing token, a.k.a log out
   453  // If the token was removed successfully then it sends the proxy a new valid token list
   454  func (m *mgr) revokeToken(token string) error {
   455  	err := m.db.Set(revokedCollection, token, "!")
   456  	if err != nil {
   457  		return err
   458  	}
   459  
   460  	// send the token in all case to allow an admin to revoke
   461  	// an existing token even after cluster restart
   462  	go m.broadcastRevoked(token)
   463  	return nil
   464  }
   465  
   466  // Create a list of non-expired and valid revoked tokens.
   467  // Obsolete and invalid tokens are removed from the database.
   468  func (m *mgr) generateRevokedTokenList() ([]string, error) {
   469  	tokens, err := m.db.List(revokedCollection, "")
   470  	if err != nil {
   471  		debug.AssertNoErr(err)
   472  		return nil, err
   473  	}
   474  
   475  	now := time.Now()
   476  	revokeList := make([]string, 0, len(tokens))
   477  	secret := Conf.Secret()
   478  	for _, token := range tokens {
   479  		tk, err := tok.DecryptToken(token, secret)
   480  		if err != nil {
   481  			m.db.Delete(revokedCollection, token)
   482  			continue
   483  		}
   484  		if tk.Expires.Before(now) {
   485  			nlog.Infof("removing %s", tk)
   486  			m.db.Delete(revokedCollection, token)
   487  			continue
   488  		}
   489  		revokeList = append(revokeList, token)
   490  	}
   491  	return revokeList, nil
   492  }
   493  
   494  //
   495  // private helpers ============================================================
   496  //
   497  
   498  func encryptPassword(password string) string {
   499  	b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
   500  	cos.AssertNoErr(err)
   501  	return hex.EncodeToString(b)
   502  }
   503  
   504  func isSamePassword(password, hashed string) bool {
   505  	b, err := hex.DecodeString(hashed)
   506  	if err != nil {
   507  		return false
   508  	}
   509  	return bcrypt.CompareHashAndPassword(b, []byte(password)) == nil
   510  }
   511  
   512  // If the DB is empty, the function prefills some data
   513  func initializeDB(driver kvdb.Driver) error {
   514  	users, err := driver.List(usersCollection, "")
   515  	if err != nil || len(users) != 0 {
   516  		// return on erros or when DB is already initialized
   517  		return err
   518  	}
   519  	role := &authn.Role{
   520  		ID:      authn.AdminRole,
   521  		Desc:    "AuthN administrator",
   522  		IsAdmin: true,
   523  	}
   524  	if err := driver.Set(rolesCollection, authn.AdminRole, role); err != nil {
   525  		return err
   526  	}
   527  	su := &authn.User{
   528  		ID:       adminUserID,
   529  		Password: encryptPassword(adminUserPass),
   530  		Roles:    []string{authn.AdminRole},
   531  	}
   532  	return driver.Set(usersCollection, adminUserID, su)
   533  }