github.com/argoproj/argo-cd/v2@v2.10.5/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  	v1 "k8s.io/api/core/v1"
    14  	"k8s.io/client-go/util/retry"
    15  
    16  	"github.com/argoproj/argo-cd/v2/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"
    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 *v1.Secret) error {
    99  		return mgr.updateConfigMap(func(cm *v1.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  	err := mgr.ensureSynced(false)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	secret, err := mgr.secrets.Secrets(mgr.namespace).Get(common.ArgoCDSecretName)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	cm, err := mgr.configmaps.ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	return parseAccounts(secret, cm)
   161  }
   162  
   163  func updateAccountMap(cm *v1.ConfigMap, key string, val string, defVal string) {
   164  	existingVal := cm.Data[key]
   165  	if existingVal != val {
   166  		if val == "" || val == defVal {
   167  			delete(cm.Data, key)
   168  		} else {
   169  			cm.Data[key] = val
   170  		}
   171  	}
   172  }
   173  
   174  func updateAccountSecret(secret *v1.Secret, key string, val string, defVal string) {
   175  	existingVal := string(secret.Data[key])
   176  	if existingVal != val {
   177  		if val == "" || val == defVal {
   178  			delete(secret.Data, key)
   179  		} else {
   180  			secret.Data[key] = []byte(val)
   181  		}
   182  	}
   183  }
   184  
   185  func saveAccount(secret *v1.Secret, cm *v1.ConfigMap, name string, account Account) error {
   186  	tokens, err := json.Marshal(account.Tokens)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	if name == common.ArgoCDAdminUsername {
   191  		updateAccountSecret(secret, settingAdminPasswordHashKey, account.PasswordHash, "")
   192  		updateAccountSecret(secret, settingAdminPasswordMtimeKey, account.FormatPasswordMtime(), "")
   193  		updateAccountSecret(secret, settingAdminTokensKey, string(tokens), "[]")
   194  		updateAccountMap(cm, settingAdminEnabledKey, strconv.FormatBool(account.Enabled), "true")
   195  	} else {
   196  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordSuffix), account.PasswordHash, "")
   197  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordMtimeSuffix), account.FormatPasswordMtime(), "")
   198  		updateAccountSecret(secret, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountTokensSuffix), string(tokens), "[]")
   199  		updateAccountMap(cm, fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountEnabledSuffix), strconv.FormatBool(account.Enabled), "true")
   200  		updateAccountMap(cm, fmt.Sprintf("%s.%s", accountsKeyPrefix, name), account.FormatCapabilities(), "")
   201  	}
   202  	return nil
   203  }
   204  
   205  func parseAdminAccount(secret *v1.Secret, cm *v1.ConfigMap) (*Account, error) {
   206  	adminAccount := &Account{Enabled: true, Capabilities: []AccountCapability{AccountCapabilityLogin}}
   207  	if adminPasswordHash, ok := secret.Data[settingAdminPasswordHashKey]; ok {
   208  		adminAccount.PasswordHash = string(adminPasswordHash)
   209  	}
   210  	if adminPasswordMtimeBytes, ok := secret.Data[settingAdminPasswordMtimeKey]; ok {
   211  		if mTime, err := time.Parse(time.RFC3339, string(adminPasswordMtimeBytes)); err == nil {
   212  			adminAccount.PasswordMtime = &mTime
   213  		}
   214  	}
   215  
   216  	adminAccount.Tokens = make([]Token, 0)
   217  	if tokensStr, ok := secret.Data[settingAdminTokensKey]; ok && string(tokensStr) != "" {
   218  		if err := json.Unmarshal(tokensStr, &adminAccount.Tokens); err != nil {
   219  			return nil, err
   220  		}
   221  	}
   222  
   223  	if enabledStr, ok := cm.Data[settingAdminEnabledKey]; ok {
   224  		if enabled, err := strconv.ParseBool(enabledStr); err == nil {
   225  			adminAccount.Enabled = enabled
   226  		} else {
   227  			log.Warnf("ConfigMap has invalid key %s: %v", settingAdminTokensKey, err)
   228  		}
   229  	}
   230  
   231  	return adminAccount, nil
   232  }
   233  
   234  func parseAccounts(secret *v1.Secret, cm *v1.ConfigMap) (map[string]Account, error) {
   235  	adminAccount, err := parseAdminAccount(secret, cm)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	accounts := map[string]Account{
   240  		common.ArgoCDAdminUsername: *adminAccount,
   241  	}
   242  
   243  	for key, v := range cm.Data {
   244  		if !strings.HasPrefix(key, fmt.Sprintf("%s.", accountsKeyPrefix)) {
   245  			continue
   246  		}
   247  
   248  		val := v
   249  		var accountName, suffix string
   250  
   251  		parts := strings.Split(key, ".")
   252  		switch len(parts) {
   253  		case 2:
   254  			accountName = parts[1]
   255  		case 3:
   256  			accountName = parts[1]
   257  			suffix = parts[2]
   258  		default:
   259  			log.Warnf("Unexpected key %s in ConfigMap '%s'", key, cm.Name)
   260  			continue
   261  		}
   262  
   263  		account, ok := accounts[accountName]
   264  		if !ok {
   265  			account = Account{Enabled: true}
   266  			accounts[accountName] = account
   267  		}
   268  		switch suffix {
   269  		case "":
   270  			for _, capability := range strings.Split(val, ",") {
   271  				capability = strings.TrimSpace(capability)
   272  				if capability == "" {
   273  					continue
   274  				}
   275  
   276  				switch capability {
   277  				case string(AccountCapabilityLogin):
   278  					account.Capabilities = append(account.Capabilities, AccountCapabilityLogin)
   279  				case string(AccountCapabilityApiKey):
   280  					account.Capabilities = append(account.Capabilities, AccountCapabilityApiKey)
   281  				default:
   282  					log.Warnf("not supported account capability '%s' in config map key '%s'", capability, key)
   283  				}
   284  			}
   285  		case accountEnabledSuffix:
   286  			account.Enabled, err = strconv.ParseBool(val)
   287  			if err != nil {
   288  				return nil, err
   289  			}
   290  		}
   291  		accounts[accountName] = account
   292  	}
   293  
   294  	for name, account := range accounts {
   295  		if name == common.ArgoCDAdminUsername {
   296  			continue
   297  		}
   298  
   299  		if passwordHash, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordSuffix)]; ok {
   300  			account.PasswordHash = string(passwordHash)
   301  		}
   302  		if passwordMtime, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountPasswordMtimeSuffix)]; ok {
   303  			if mTime, err := time.Parse(time.RFC3339, string(passwordMtime)); err != nil {
   304  				return nil, err
   305  			} else {
   306  				account.PasswordMtime = &mTime
   307  			}
   308  		}
   309  		if tokensStr, ok := secret.Data[fmt.Sprintf("%s.%s.%s", accountsKeyPrefix, name, accountTokensSuffix)]; ok {
   310  			account.Tokens = make([]Token, 0)
   311  			if string(tokensStr) != "" {
   312  				if err := json.Unmarshal(tokensStr, &account.Tokens); err != nil {
   313  					log.Errorf("Account '%s' has invalid token in secret '%s'", name, secret.Name)
   314  				}
   315  			}
   316  		}
   317  		accounts[name] = account
   318  	}
   319  
   320  	return accounts, nil
   321  }