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 }