github.com/argoproj/argo-cd/v3@v3.2.1/server/account/account.go (about) 1 package account 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "regexp" 8 "sort" 9 "time" 10 11 "github.com/google/uuid" 12 log "github.com/sirupsen/logrus" 13 "google.golang.org/grpc/codes" 14 "google.golang.org/grpc/status" 15 "k8s.io/kubectl/pkg/util/slice" 16 17 "github.com/argoproj/argo-cd/v3/common" 18 "github.com/argoproj/argo-cd/v3/pkg/apiclient/account" 19 "github.com/argoproj/argo-cd/v3/server/rbacpolicy" 20 "github.com/argoproj/argo-cd/v3/util/password" 21 "github.com/argoproj/argo-cd/v3/util/rbac" 22 "github.com/argoproj/argo-cd/v3/util/session" 23 "github.com/argoproj/argo-cd/v3/util/settings" 24 ) 25 26 // Server provides a Session service 27 type Server struct { 28 sessionMgr *session.SessionManager 29 settingsMgr *settings.SettingsManager 30 enf *rbac.Enforcer 31 } 32 33 // NewServer returns a new instance of the Session service 34 func NewServer(sessionMgr *session.SessionManager, settingsMgr *settings.SettingsManager, enf *rbac.Enforcer) *Server { 35 return &Server{sessionMgr, settingsMgr, enf} 36 } 37 38 // UpdatePassword updates the password of the currently authenticated account or the account specified in the request. 39 func (s *Server) UpdatePassword(ctx context.Context, q *account.UpdatePasswordRequest) (*account.UpdatePasswordResponse, error) { 40 username := session.GetUserIdentifier(ctx) 41 42 updatedUsername := username 43 if q.Name != "" { 44 updatedUsername = q.Name 45 } 46 47 // check for permission is user is trying to change someone else's password 48 // assuming user is trying to update someone else if username is different or issuer is not Argo CD 49 issuer := session.Iss(ctx) 50 if updatedUsername != username || issuer != session.SessionManagerClaimsIssuer { 51 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceAccounts, rbac.ActionUpdate, q.Name); err != nil { 52 return nil, fmt.Errorf("permission denied: %w", err) 53 } 54 } 55 56 if issuer == session.SessionManagerClaimsIssuer { 57 // local user is changing own password or another user password 58 59 // user is changing own password. 60 // ensure token belongs to a user, not project 61 if q.Name == "" && rbacpolicy.IsProjectSubject(username) { 62 return nil, status.Errorf(codes.InvalidArgument, "password can only be changed for local users, not user %q", username) 63 } 64 65 err := s.sessionMgr.VerifyUsernamePassword(username, q.CurrentPassword) 66 if err != nil { 67 return nil, status.Errorf(codes.InvalidArgument, "current password does not match") 68 } 69 } else { 70 // SSO user is changing or local user password 71 72 iat, err := session.Iat(ctx) 73 if err != nil { 74 return nil, fmt.Errorf("failed to get issue time: %w", err) 75 } 76 if time.Since(iat) > common.ChangePasswordSSOTokenMaxAge { 77 return nil, errors.New("SSO token is too old. Please use 'argocd relogin' to get a new token") 78 } 79 } 80 81 // Need to validate password complexity with regular expression 82 passwordPattern, err := s.settingsMgr.GetPasswordPattern() 83 if err != nil { 84 return nil, fmt.Errorf("failed to get password pattern: %w", err) 85 } 86 87 validPasswordRegexp, err := regexp.Compile(passwordPattern) 88 if err != nil { 89 return nil, fmt.Errorf("failed to compile password regex: %w", err) 90 } 91 92 if !validPasswordRegexp.MatchString(q.NewPassword) { 93 err := fmt.Errorf("new password does not match the following expression: %s", passwordPattern) 94 return nil, err 95 } 96 97 hashedPassword, err := password.HashPassword(q.NewPassword) 98 if err != nil { 99 return nil, fmt.Errorf("failed to hash password: %w", err) 100 } 101 102 err = s.settingsMgr.UpdateAccount(updatedUsername, func(acc *settings.Account) error { 103 acc.PasswordHash = hashedPassword 104 now := time.Now().UTC() 105 acc.PasswordMtime = &now 106 return nil 107 }) 108 if err != nil { 109 return nil, fmt.Errorf("failed to update account password: %w", err) 110 } 111 112 if updatedUsername == username { 113 log.Infof("user '%s' updated password", username) 114 } else { 115 log.Infof("user '%s' updated password of user '%s'", username, updatedUsername) 116 } 117 return &account.UpdatePasswordResponse{}, nil 118 } 119 120 // CanI checks if the current account has permission to perform an action 121 func (s *Server) CanI(ctx context.Context, r *account.CanIRequest) (*account.CanIResponse, error) { 122 if !slice.ContainsString(rbac.Actions, r.Action, nil) { 123 return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbac.Actions, r.Action) 124 } 125 if !slice.ContainsString(rbac.Resources, r.Resource, nil) { 126 return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbac.Resources, r.Resource) 127 } 128 129 ok := s.enf.Enforce(ctx.Value("claims"), r.Resource, r.Action, r.Subresource) 130 if ok { 131 return &account.CanIResponse{Value: "yes"}, nil 132 } 133 return &account.CanIResponse{Value: "no"}, nil 134 } 135 136 func toAPIAccount(name string, a settings.Account) *account.Account { 137 var capabilities []string 138 for _, c := range a.Capabilities { 139 capabilities = append(capabilities, string(c)) 140 } 141 var tokens []*account.Token 142 for _, t := range a.Tokens { 143 tokens = append(tokens, &account.Token{Id: t.ID, ExpiresAt: t.ExpiresAt, IssuedAt: t.IssuedAt}) 144 } 145 sort.Slice(tokens, func(i, j int) bool { 146 return tokens[i].IssuedAt > tokens[j].IssuedAt 147 }) 148 return &account.Account{ 149 Name: name, 150 Enabled: a.Enabled, 151 Capabilities: capabilities, 152 Tokens: tokens, 153 } 154 } 155 156 func (s *Server) ensureHasAccountPermission(ctx context.Context, action string, account string) error { 157 id := session.GetUserIdentifier(ctx) 158 159 // account has always has access to itself 160 if id == account && session.Iss(ctx) == session.SessionManagerClaimsIssuer { 161 return nil 162 } 163 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceAccounts, action, account); err != nil { 164 return fmt.Errorf("permission denied for account %s with action %s: %w", account, action, err) 165 } 166 return nil 167 } 168 169 // ListAccounts returns the list of accounts 170 func (s *Server) ListAccounts(ctx context.Context, _ *account.ListAccountRequest) (*account.AccountsList, error) { 171 resp := account.AccountsList{} 172 accounts, err := s.settingsMgr.GetAccounts() 173 if err != nil { 174 return nil, fmt.Errorf("failed to get accounts: %w", err) 175 } 176 for name, a := range accounts { 177 if err := s.ensureHasAccountPermission(ctx, rbac.ActionGet, name); err == nil { 178 resp.Items = append(resp.Items, toAPIAccount(name, a)) 179 } 180 } 181 sort.Slice(resp.Items, func(i, j int) bool { 182 return resp.Items[i].Name < resp.Items[j].Name 183 }) 184 return &resp, nil 185 } 186 187 // GetAccount returns an account 188 func (s *Server) GetAccount(ctx context.Context, r *account.GetAccountRequest) (*account.Account, error) { 189 if err := s.ensureHasAccountPermission(ctx, rbac.ActionGet, r.Name); err != nil { 190 return nil, fmt.Errorf("permission denied to get account %s: %w", r.Name, err) 191 } 192 a, err := s.settingsMgr.GetAccount(r.Name) 193 if err != nil { 194 return nil, fmt.Errorf("failed to get account %s: %w", r.Name, err) 195 } 196 return toAPIAccount(r.Name, *a), nil 197 } 198 199 // CreateToken creates a token 200 func (s *Server) CreateToken(ctx context.Context, r *account.CreateTokenRequest) (*account.CreateTokenResponse, error) { 201 if err := s.ensureHasAccountPermission(ctx, rbac.ActionUpdate, r.Name); err != nil { 202 return nil, fmt.Errorf("permission denied to create token for account %s: %w", r.Name, err) 203 } 204 205 id := r.Id 206 if id == "" { 207 uniqueId, err := uuid.NewRandom() 208 if err != nil { 209 return nil, fmt.Errorf("failed to generate unique ID: %w", err) 210 } 211 id = uniqueId.String() 212 } 213 214 var tokenString string 215 err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error { 216 if account.TokenIndex(id) > -1 { 217 return fmt.Errorf("account already has token with id '%s'", id) 218 } 219 if !account.HasCapability(settings.AccountCapabilityApiKey) { 220 return fmt.Errorf("account '%s' does not have %s capability", r.Name, settings.AccountCapabilityApiKey) 221 } 222 223 now := time.Now() 224 var err error 225 tokenString, err = s.sessionMgr.Create(fmt.Sprintf("%s:%s", r.Name, settings.AccountCapabilityApiKey), r.ExpiresIn, id) 226 if err != nil { 227 return err 228 } 229 230 var expiresAt int64 231 if r.ExpiresIn > 0 { 232 expiresAt = now.Add(time.Duration(r.ExpiresIn) * time.Second).Unix() 233 } 234 account.Tokens = append(account.Tokens, settings.Token{ 235 ID: id, 236 IssuedAt: now.Unix(), 237 ExpiresAt: expiresAt, 238 }) 239 return nil 240 }) 241 if err != nil { 242 return nil, fmt.Errorf("failed to update account with new token: %w", err) 243 } 244 return &account.CreateTokenResponse{Token: tokenString}, nil 245 } 246 247 // DeleteToken deletes a token 248 func (s *Server) DeleteToken(ctx context.Context, r *account.DeleteTokenRequest) (*account.EmptyResponse, error) { 249 if err := s.ensureHasAccountPermission(ctx, rbac.ActionUpdate, r.Name); err != nil { 250 return nil, fmt.Errorf("permission denied to delete account %s: %w", r.Name, err) 251 } 252 253 err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error { 254 if index := account.TokenIndex(r.Id); index > -1 { 255 account.Tokens = append(account.Tokens[:index], account.Tokens[index+1:]...) 256 return nil 257 } 258 return status.Errorf(codes.NotFound, "token with id '%s' does not exist", r.Id) 259 }) 260 if err != nil { 261 return nil, fmt.Errorf("failed to delete account %s: %w", r.Name, err) 262 } 263 return &account.EmptyResponse{}, nil 264 }