github.com/argoproj/argo-cd/v3@v3.2.1/util/settings/accounts.go (about)

     1  package settings
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	log "github.com/sirupsen/logrus"
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  	corev1 "k8s.io/api/core/v1"
    14  	"k8s.io/client-go/util/retry"
    15  
    16  	"github.com/argoproj/argo-cd/v3/common"
    17  )
    18  
    19  const (
    20  	accountsKeyPrefix          = "accounts"
    21  	accountPasswordSuffix      = "password"
    22  	accountPasswordMtimeSuffix = "passwordMtime"
    23  	accountEnabledSuffix       = "enabled"
    24  	accountTokensSuffix        = "tokens"
    25  
    26  	// Admin superuser password storage
    27  	// settingAdminPasswordHashKey designates the key for a root password hash inside a Kubernetes secret.
    28  	settingAdminPasswordHashKey = "admin.password"
    29  	// settingAdminPasswordMtimeKey designates the key for a root password mtime inside a Kubernetes secret.
    30  	settingAdminPasswordMtimeKey = "admin.passwordMtime"
    31  	settingAdminEnabledKey       = "admin.enabled"
    32  	settingAdminTokensKey        = "admin.tokens"
    33  )
    34  
    35  type AccountCapability string
    36  
    37  const (
    38  	// AccountCapabilityLogin represents capability to create UI session tokens.
    39  	AccountCapabilityLogin AccountCapability = "login"
    40  	// AccountCapabilityLogin represents capability to generate API auth tokens.
    41  	AccountCapabilityApiKey AccountCapability = "apiKey" //nolint:revive //FIXME(var-naming)
    42  )
    43  
    44  // Token holds the information about the generated auth token.
    45  type Token struct {
    46  	ID        string `json:"id"`
    47  	IssuedAt  int64  `json:"iat"`
    48  	ExpiresAt int64  `json:"exp,omitempty"`
    49  }
    50  
    51  // Account holds local account information
    52  type Account struct {
    53  	PasswordHash  string
    54  	PasswordMtime *time.Time
    55  	Enabled       bool
    56  	Capabilities  []AccountCapability
    57  	Tokens        []Token
    58  }
    59  
    60  // FormatPasswordMtime return the formatted password modify time or empty string of password modify time is nil.
    61  func (a *Account) FormatPasswordMtime() string {
    62  	if a.PasswordMtime == nil {
    63  		return ""
    64  	}
    65  	return a.PasswordMtime.Format(time.RFC3339)
    66  }
    67  
    68  // FormatCapabilities returns comma separate list of user capabilities.
    69  func (a *Account) FormatCapabilities() string {
    70  	var items []string
    71  	for i := range a.Capabilities {
    72  		items = append(items, string(a.Capabilities[i]))
    73  	}
    74  	return strings.Join(items, ",")
    75  }
    76  
    77  // TokenIndex return an index of a token with the given identifier or -1 if token not found.
    78  func (a *Account) TokenIndex(id string) int {
    79  	for i := range a.Tokens {
    80  		if a.Tokens[i].ID == id {
    81  			return i
    82  		}
    83  	}
    84  	return -1
    85  }
    86  
    87  // HasCapability return true if the account has the specified capability.
    88  func (a *Account) HasCapability(capability AccountCapability) bool {
    89  	for _, c := range a.Capabilities {
    90  		if c == capability {
    91  			return true
    92  		}
    93  	}
    94  	return false
    95  }
    96  
    97  func (mgr *SettingsManager) saveAccount(name string, account Account) error {
    98  	return mgr.updateSecret(func(secret *corev1.Secret) error {
    99  		return mgr.updateConfigMap(func(cm *corev1.ConfigMap) error {
   100  			return saveAccount(secret, cm, name, account)
   101  		})
   102  	})
   103  }
   104  
   105  // AddAccount save an account with the given name and properties.
   106  func (mgr *SettingsManager) AddAccount(name string, account Account) error {
   107  	accounts, err := mgr.GetAccounts()
   108  	if err != nil {
   109  		return fmt.Errorf("error getting accounts: %w", err)
   110  	}
   111  	if _, ok := accounts[name]; ok {
   112  		return status.Errorf(codes.AlreadyExists, "account '%s' already exists", name)
   113  	}
   114  	return mgr.saveAccount(name, account)
   115  }
   116  
   117  // GetAccount return an account info by the specified name.
   118  func (mgr *SettingsManager) GetAccount(name string) (*Account, error) {
   119  	accounts, err := mgr.GetAccounts()
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	account, ok := accounts[name]
   124  	if !ok {
   125  		return nil, status.Errorf(codes.NotFound, "account '%s' does not exist", name)
   126  	}
   127  	return &account, nil
   128  }
   129  
   130  // UpdateAccount runs the callback function against an account that matches to the specified name
   131  // and persist changes applied by the callback.
   132  func (mgr *SettingsManager) UpdateAccount(name string, callback func(account *Account) error) error {
   133  	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   134  		account, err := mgr.GetAccount(name)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		err = callback(account)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		return mgr.saveAccount(name, *account)
   143  	})
   144  }
   145  
   146  // GetAccounts returns list of configured accounts
   147  func (mgr *SettingsManager) GetAccounts() (map[string]Account, error) {
   148  	cm, err := mgr.getConfigMap()
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	secret, err := mgr.getSecret()
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	return parseAccounts(secret, cm)
   157  }
   158  
   159  func updateAccountMap(cm *corev1.ConfigMap, key string, val string, defVal string) {
   160  	existingVal := cm.Data[key]
   161  	if existingVal != val {
   162  		if val == "" || val == defVal {
   163  			delete(cm.Data, key)
   164  		} else {
   165  			cm.Data[key] = val
   166  		}
   167  	}
   168  }
   169  
   170  func updateAccountSecret(secret *corev1.Secret, key string, val string, defVal string) {
   171  	existingVal := string(secret.Data[key])
   172  	if existingVal != val {
   173  		if val == "" || val == defVal {
   174  			delete(secret.Data, key)
   175  		} else {
   176  			secret.Data[key] = []byte(val)
   177  		}
   178  	}
   179  }
   180  
   181  func saveAccount(secret *corev1.Secret, cm *corev1.ConfigMap, name string, account Account) error {
   182  	tokens, err := json.Marshal(account.Tokens)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	if name == common.ArgoCDAdminUsername {
   187  		updateAccountSecret(secret, settingAdminPasswordHashKey, account.PasswordHash, "")
   188  		updateAccountSecret(secret, settingAdminPasswordMtimeKey, account.FormatPasswordMtime(), "")
   189  		updateAccountSecret(secret, settingAdminTokensKey, string(tokens), "[]")
   190  		updateAccountMap(cm, settingAdminEnabledKey, strconv.FormatBool(account.Enabled), "true")
   191  	} else {
   192  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordSuffix), account.PasswordHash, "")
   193  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordMtimeSuffix), account.FormatPasswordMtime(), "")
   194  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountTokensSuffix), string(tokens), "[]")
   195  		updateAccountMap(cm, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountEnabledSuffix), strconv.FormatBool(account.Enabled), "true")
   196  		updateAccountMap(cm, fmt.Sprintf("%s.%s", accountsKeyPrefix, name), account.FormatCapabilities(), "")
   197  	}
   198  	return nil
   199  }
   200  
   201  func parseAdminAccount(secret *corev1.Secret, cm *corev1.ConfigMap) (*Account, error) {
   202  	adminAccount := &Account{Enabled: true, Capabilities: []AccountCapability{AccountCapabilityLogin}}
   203  	if adminPasswordHash, ok := secret.Data[settingAdminPasswordHashKey]; ok {
   204  		adminAccount.PasswordHash = string(adminPasswordHash)
   205  	}
   206  	if adminPasswordMtimeBytes, ok := secret.Data[settingAdminPasswordMtimeKey]; ok {
   207  		if mTime, err := time.Parse(time.RFC3339, string(adminPasswordMtimeBytes)); err == nil {
   208  			adminAccount.PasswordMtime = &mTime
   209  		}
   210  	}
   211  
   212  	adminAccount.Tokens = make([]Token, 0)
   213  	if tokensStr, ok := secret.Data[settingAdminTokensKey]; ok && len(tokensStr) != 0 {
   214  		if err := json.Unmarshal(tokensStr, &adminAccount.Tokens); err != nil {
   215  			return nil, err
   216  		}
   217  	}
   218  
   219  	if enabledStr, ok := cm.Data[settingAdminEnabledKey]; ok {
   220  		if enabled, err := strconv.ParseBool(enabledStr); err == nil {
   221  			adminAccount.Enabled = enabled
   222  		} else {
   223  			log.Warnf("ConfigMap has invalid key %s: %v", settingAdminTokensKey, err)
   224  		}
   225  	}
   226  
   227  	return adminAccount, nil
   228  }
   229  
   230  func parseAccounts(secret *corev1.Secret, cm *corev1.ConfigMap) (map[string]Account, error) {
   231  	adminAccount, err := parseAdminAccount(secret, cm)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	accounts := map[string]Account{
   236  		common.ArgoCDAdminUsername: *adminAccount,
   237  	}
   238  
   239  	for key, v := range cm.Data {
   240  		if !strings.HasPrefix(key, accountsKeyPrefix+".") {
   241  			continue
   242  		}
   243  
   244  		val := v
   245  		var accountName, suffix string
   246  
   247  		parts := strings.Split(key, ".")
   248  		switch len(parts) {
   249  		case 2:
   250  			accountName = parts[1]
   251  		case 3:
   252  			accountName = parts[1]
   253  			suffix = parts[2]
   254  		default:
   255  			log.Warnf("Unexpected key %s in ConfigMap '%s'", key, cm.Name)
   256  			continue
   257  		}
   258  
   259  		account, ok := accounts[accountName]
   260  		if !ok {
   261  			account = Account{Enabled: true}
   262  			accounts[accountName] = account
   263  		}
   264  		switch suffix {
   265  		case "":
   266  			for _, capability := range strings.Split(val, ",") {
   267  				capability = strings.TrimSpace(capability)
   268  				if capability == "" {
   269  					continue
   270  				}
   271  
   272  				switch capability {
   273  				case string(AccountCapabilityLogin):
   274  					account.Capabilities = append(account.Capabilities, AccountCapabilityLogin)
   275  				case string(AccountCapabilityApiKey):
   276  					account.Capabilities = append(account.Capabilities, AccountCapabilityApiKey)
   277  				default:
   278  					log.Warnf("not supported account capability '%s' in config map key '%s'", capability, key)
   279  				}
   280  			}
   281  		case accountEnabledSuffix:
   282  			account.Enabled, err = strconv.ParseBool(val)
   283  			if err != nil {
   284  				return nil, err
   285  			}
   286  		}
   287  		accounts[accountName] = account
   288  	}
   289  
   290  	for name, account := range accounts {
   291  		if name == common.ArgoCDAdminUsername {
   292  			continue
   293  		}
   294  
   295  		if passwordHash, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordSuffix)]; ok {
   296  			account.PasswordHash = string(passwordHash)
   297  		}
   298  		if passwordMtime, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordMtimeSuffix)]; ok {
   299  			mTime, err := time.Parse(time.RFC3339, string(passwordMtime))
   300  			if err != nil {
   301  				return nil, err
   302  			}
   303  			account.PasswordMtime = &mTime
   304  		}
   305  		if tokensStr, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountTokensSuffix)]; ok {
   306  			account.Tokens = make([]Token, 0)
   307  			if len(tokensStr) != 0 {
   308  				if err := json.Unmarshal(tokensStr, &account.Tokens); err != nil {
   309  					log.Errorf("Account '%s' has invalid token in secret '%s'", name, secret.Name)
   310  				}
   311  			}
   312  		}
   313  		accounts[name] = account
   314  	}
   315  
   316  	return accounts, nil
   317  }