github.com/argoproj/argo-cd/v2@v2.10.9/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/v2/common" 18 "github.com/argoproj/argo-cd/v2/pkg/apiclient/account" 19 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 20 "github.com/argoproj/argo-cd/v2/util/password" 21 "github.com/argoproj/argo-cd/v2/util/rbac" 22 "github.com/argoproj/argo-cd/v2/util/session" 23 "github.com/argoproj/argo-cd/v2/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 issuer := session.Iss(ctx) 41 username := session.Sub(ctx) 42 updatedUsername := username 43 44 if q.Name != "" { 45 updatedUsername = q.Name 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 if updatedUsername != username || issuer != session.SessionManagerClaimsIssuer { 50 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceAccounts, rbacpolicy.ActionUpdate, q.Name); err != nil { 51 return nil, err 52 } 53 } 54 55 if issuer == session.SessionManagerClaimsIssuer { 56 // local user is changing own password or another user password 57 58 // user is changing own password. 59 // ensure token belongs to a user, not project 60 if q.Name == "" && rbacpolicy.IsProjectSubject(username) { 61 return nil, status.Errorf(codes.InvalidArgument, "password can only be changed for local users, not user %q", username) 62 } 63 64 err := s.sessionMgr.VerifyUsernamePassword(username, q.CurrentPassword) 65 if err != nil { 66 return nil, status.Errorf(codes.InvalidArgument, "current password does not match") 67 } 68 } else { 69 // SSO user is changing or local user password 70 71 iat, err := session.Iat(ctx) 72 if err != nil { 73 return nil, err 74 } 75 if time.Since(iat) > common.ChangePasswordSSOTokenMaxAge { 76 return nil, errors.New("SSO token is too old. Please use 'argocd relogin' to get a new token.") 77 } 78 } 79 80 //Need to validate password complexity with regular expression 81 passwordPattern, err := s.settingsMgr.GetPasswordPattern() 82 if err != nil { 83 return nil, err 84 } 85 86 validPasswordRegexp, err := regexp.Compile(passwordPattern) 87 if err != nil { 88 return nil, err 89 } 90 91 if !validPasswordRegexp.Match([]byte(q.NewPassword)) { 92 err := fmt.Errorf("New password does not match the following expression: %s.", passwordPattern) 93 return nil, err 94 } 95 96 hashedPassword, err := password.HashPassword(q.NewPassword) 97 if err != nil { 98 return nil, err 99 } 100 101 err = s.settingsMgr.UpdateAccount(updatedUsername, func(acc *settings.Account) error { 102 acc.PasswordHash = hashedPassword 103 now := time.Now().UTC() 104 acc.PasswordMtime = &now 105 return nil 106 }) 107 108 if err != nil { 109 return nil, 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 121 // CanI checks if the current account has permission to perform an action 122 func (s *Server) CanI(ctx context.Context, r *account.CanIRequest) (*account.CanIResponse, error) { 123 if !slice.ContainsString(rbacpolicy.Actions, r.Action, nil) { 124 return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbacpolicy.Actions, r.Action) 125 } 126 if !slice.ContainsString(rbacpolicy.Resources, r.Resource, nil) { 127 return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbacpolicy.Resources, r.Resource) 128 } 129 130 // Logs RBAC will be enforced only if an internal var serverRBACLogEnforceEnable (representing server.rbac.log.enforce.enable env var) 131 // is defined and has a "true" value 132 // Otherwise, no RBAC enforcement for logs will take place (meaning, can-i request on a logs resource will result in "yes", 133 // even if there is no explicit RBAC allow, or if there is an explicit RBAC deny) 134 if r.Resource == "logs" { 135 serverRBACLogEnforceEnable, err := s.settingsMgr.GetServerRBACLogEnforceEnable() 136 if err != nil { 137 return nil, err 138 } 139 140 if !serverRBACLogEnforceEnable { 141 return &account.CanIResponse{Value: "yes"}, nil 142 } 143 } 144 145 ok := s.enf.Enforce(ctx.Value("claims"), r.Resource, r.Action, r.Subresource) 146 if ok { 147 return &account.CanIResponse{Value: "yes"}, nil 148 } else { 149 return &account.CanIResponse{Value: "no"}, nil 150 } 151 } 152 153 func toApiAccount(name string, a settings.Account) *account.Account { 154 var capabilities []string 155 for _, c := range a.Capabilities { 156 capabilities = append(capabilities, string(c)) 157 } 158 var tokens []*account.Token 159 for _, t := range a.Tokens { 160 tokens = append(tokens, &account.Token{Id: t.ID, ExpiresAt: t.ExpiresAt, IssuedAt: t.IssuedAt}) 161 } 162 sort.Slice(tokens, func(i, j int) bool { 163 return tokens[i].IssuedAt > tokens[j].IssuedAt 164 }) 165 return &account.Account{ 166 Name: name, 167 Enabled: a.Enabled, 168 Capabilities: capabilities, 169 Tokens: tokens, 170 } 171 } 172 173 func (s *Server) ensureHasAccountPermission(ctx context.Context, action string, account string) error { 174 // account has always has access to itself 175 if session.Sub(ctx) == account && session.Iss(ctx) == session.SessionManagerClaimsIssuer { 176 return nil 177 } 178 if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceAccounts, action, account); err != nil { 179 return err 180 } 181 return nil 182 } 183 184 // ListAccounts returns the list of accounts 185 func (s *Server) ListAccounts(ctx context.Context, r *account.ListAccountRequest) (*account.AccountsList, error) { 186 resp := account.AccountsList{} 187 accounts, err := s.settingsMgr.GetAccounts() 188 if err != nil { 189 return nil, err 190 } 191 for name, a := range accounts { 192 if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionGet, name); err == nil { 193 resp.Items = append(resp.Items, toApiAccount(name, a)) 194 } 195 } 196 sort.Slice(resp.Items, func(i, j int) bool { 197 return resp.Items[i].Name < resp.Items[j].Name 198 }) 199 return &resp, nil 200 } 201 202 // GetAccount returns an account 203 func (s *Server) GetAccount(ctx context.Context, r *account.GetAccountRequest) (*account.Account, error) { 204 if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionGet, r.Name); err != nil { 205 return nil, err 206 } 207 a, err := s.settingsMgr.GetAccount(r.Name) 208 if err != nil { 209 return nil, err 210 } 211 return toApiAccount(r.Name, *a), nil 212 } 213 214 // CreateToken creates a token 215 func (s *Server) CreateToken(ctx context.Context, r *account.CreateTokenRequest) (*account.CreateTokenResponse, error) { 216 if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionUpdate, r.Name); err != nil { 217 return nil, err 218 } 219 220 id := r.Id 221 if id == "" { 222 uniqueId, err := uuid.NewRandom() 223 if err != nil { 224 return nil, err 225 } 226 id = uniqueId.String() 227 } 228 229 var tokenString string 230 err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error { 231 if account.TokenIndex(id) > -1 { 232 return fmt.Errorf("account already has token with id '%s'", id) 233 } 234 if !account.HasCapability(settings.AccountCapabilityApiKey) { 235 return fmt.Errorf("account '%s' does not have %s capability", r.Name, settings.AccountCapabilityApiKey) 236 } 237 238 now := time.Now() 239 var err error 240 tokenString, err = s.sessionMgr.Create(fmt.Sprintf("%s:%s", r.Name, settings.AccountCapabilityApiKey), r.ExpiresIn, id) 241 if err != nil { 242 return err 243 } 244 245 var expiresAt int64 246 if r.ExpiresIn > 0 { 247 expiresAt = now.Add(time.Duration(r.ExpiresIn) * time.Second).Unix() 248 } 249 account.Tokens = append(account.Tokens, settings.Token{ 250 ID: id, 251 IssuedAt: now.Unix(), 252 ExpiresAt: expiresAt, 253 }) 254 return nil 255 }) 256 if err != nil { 257 return nil, err 258 } 259 return &account.CreateTokenResponse{Token: tokenString}, nil 260 } 261 262 // DeleteToken deletes a token 263 func (s *Server) DeleteToken(ctx context.Context, r *account.DeleteTokenRequest) (*account.EmptyResponse, error) { 264 if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionUpdate, r.Name); err != nil { 265 return nil, err 266 } 267 268 err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error { 269 if index := account.TokenIndex(r.Id); index > -1 { 270 account.Tokens = append(account.Tokens[:index], account.Tokens[index+1:]...) 271 return nil 272 } 273 return status.Errorf(codes.NotFound, "token with id '%s' does not exist", r.Id) 274 }) 275 if err != nil { 276 return nil, err 277 } 278 return &account.EmptyResponse{}, nil 279 }