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 }