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