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 }