github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/authentication/auth.go (about) 1 package authentication 2 3 import ( 4 "errors" 5 "os" 6 "time" 7 8 "github.com/ActiveState/cli/internal/condition" 9 "github.com/ActiveState/cli/internal/config" 10 "github.com/ActiveState/cli/internal/constants" 11 "github.com/ActiveState/cli/internal/errs" 12 "github.com/ActiveState/cli/internal/locale" 13 "github.com/ActiveState/cli/internal/logging" 14 "github.com/ActiveState/cli/internal/multilog" 15 "github.com/ActiveState/cli/internal/profile" 16 "github.com/ActiveState/cli/internal/rollbar" 17 "github.com/ActiveState/cli/internal/singleton/uniqid" 18 "github.com/ActiveState/cli/pkg/platform/api" 19 "github.com/ActiveState/cli/pkg/platform/api/mono" 20 "github.com/ActiveState/cli/pkg/platform/api/mono/mono_client" 21 "github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/authentication" 22 apiAuth "github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/authentication" 23 "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" 24 model "github.com/ActiveState/cli/pkg/platform/model/auth" 25 "github.com/go-openapi/runtime" 26 httptransport "github.com/go-openapi/runtime/client" 27 "github.com/go-openapi/strfmt" 28 ) 29 30 var persist *Auth 31 32 type ErrUnauthorized struct{ *locale.LocalizedError } 33 34 type ErrTokenRequired struct{ *locale.LocalizedError } 35 36 var errNotYetGranted = locale.NewInputError("err_auth_device_noauth") 37 38 // Auth is the base structure used to record the authenticated state 39 type Auth struct { 40 client *mono_client.Mono 41 clientAuth *runtime.ClientAuthInfoWriter 42 bearerToken string 43 user *mono_models.User 44 cfg Configurable 45 } 46 47 type Configurable interface { 48 Set(string, interface{}) error 49 GetString(string) string 50 Close() error 51 } 52 53 const ApiTokenConfigKey = "apiToken" 54 55 // LegacyGet returns a cached version of Auth 56 func LegacyGet() (*Auth, error) { 57 if persist == nil { 58 cfg, err := config.New() 59 if err != nil { 60 return nil, errs.Wrap(err, "Could not get configuration required by auth") 61 } 62 63 persist = New(cfg) 64 if err := persist.Sync(); err != nil { 65 logging.Warning("Could not sync authenticated state: %s", err.Error()) 66 } 67 } 68 return persist, nil 69 } 70 71 func LegacyClose() { 72 if persist == nil { 73 return 74 } 75 persist.Close() 76 } 77 78 // Reset clears the cache 79 func Reset() { 80 persist = nil 81 } 82 83 // New creates a new version of Auth 84 func New(cfg Configurable) *Auth { 85 defer profile.Measure("auth:New", time.Now()) 86 auth := &Auth{ 87 cfg: cfg, 88 } 89 90 return auth 91 } 92 93 func (s *Auth) SyncRequired() bool { 94 expectAuth := s.AvailableAPIToken() != "" 95 return expectAuth != s.Authenticated() 96 } 97 98 // Sync will ensure that the authenticated state is in sync with what is in the config database. 99 // This is mainly useful if you want to instrument the auth package without creating unnecessary API calls. 100 func (s *Auth) Sync() error { 101 defer profile.Measure("auth:Sync", time.Now()) 102 103 if token := s.AvailableAPIToken(); token != "" { 104 logging.Debug("Authenticating with stored API token: %s..", desensitizeToken(token)) 105 if err := s.Authenticate(); err != nil { 106 return errs.Wrap(err, "Failed to authenticate with API token") 107 } 108 } else { 109 // Ensure properties aren't out of sync 110 s.resetSession() 111 } 112 return nil 113 } 114 115 func (s *Auth) Close() error { 116 if err := s.cfg.Close(); err != nil { 117 return errs.Wrap(err, "Could not close cfg from Auth") 118 } 119 return nil 120 } 121 122 // Authenticated checks whether we are currently authenticated 123 func (s *Auth) Authenticated() bool { 124 return s.clientAuth != nil 125 } 126 127 // ClientAuth returns the auth type required by swagger api calls 128 func (s *Auth) ClientAuth() runtime.ClientAuthInfoWriter { 129 if s.clientAuth == nil { 130 return nil 131 } 132 return *s.clientAuth 133 } 134 135 // BearerToken returns the current bearerToken 136 func (s *Auth) BearerToken() string { 137 return s.bearerToken 138 } 139 140 func (s *Auth) updateRollbarPerson() { 141 uid := s.UserID() 142 if uid == nil { 143 return 144 } 145 rollbar.UpdateRollbarPerson(uid.String(), s.WhoAmI(), s.Email()) 146 } 147 148 func (s *Auth) resetSession() { 149 s.bearerToken = "" 150 s.user = nil 151 } 152 153 func (s *Auth) Refresh() error { 154 s.resetSession() 155 156 apiToken := s.AvailableAPIToken() 157 if apiToken == "" { 158 return nil 159 } 160 161 return s.AuthenticateWithToken(apiToken) 162 } 163 164 // Authenticate will try to authenticate using stored credentials 165 func (s *Auth) Authenticate() error { 166 if s.Authenticated() { 167 s.updateRollbarPerson() 168 return nil 169 } 170 171 apiToken := s.AvailableAPIToken() 172 if apiToken == "" { 173 return locale.NewInputError("err_no_credentials") 174 } 175 176 return s.AuthenticateWithToken(apiToken) 177 } 178 179 // AuthenticateWithModel will try to authenticate using the given swagger model 180 func (s *Auth) AuthenticateWithModel(credentials *mono_models.Credentials) error { 181 logging.Debug("AuthenticateWithModel") 182 183 params := authentication.NewPostLoginParams() 184 params.SetCredentials(credentials) 185 186 loginOK, err := mono.Get().Authentication.PostLogin(params) 187 if err != nil { 188 tips := []string{ 189 locale.Tl("relog_tip", "If you're having trouble authenticating try logging out and logging back in again."), 190 locale.Tl("logout_tip", "Logout with '[ACTIONABLE]state auth logout[/RESET]'."), 191 locale.Tl("logout_tip", "Login with '[ACTIONABLE]state auth[/RESET]'."), 192 } 193 194 switch err.(type) { 195 case *apiAuth.PostLoginUnauthorized: 196 return errs.AddTips(&ErrUnauthorized{locale.WrapExternalError(err, "err_unauthorized")}, tips...) 197 case *apiAuth.PostLoginRetryWith: 198 return errs.AddTips(&ErrTokenRequired{locale.WrapExternalError(err, "err_auth_fail_totp")}, tips...) 199 default: 200 if os.IsTimeout(err) { 201 return locale.NewExternalError("err_api_auth_timeout", "Timed out waiting for authentication response. Please try again.") 202 } 203 if api.ErrorCode(err) == 403 { 204 return locale.NewExternalError("err_auth_forbidden", "You are not allowed to login now. Please try again later.") 205 } 206 if !condition.IsNetworkingError(err) { 207 multilog.Error("Authentication API returned %v", err) 208 } 209 return errs.AddTips(locale.WrapError(err, "err_api_auth", "Authentication failed: {{.V0}}", err.Error()), tips...) 210 } 211 } 212 213 if err := s.updateSession(loginOK.Payload); err != nil { 214 return errs.Wrap(err, "Storing JWT failed") 215 } 216 217 return nil 218 } 219 220 func (s *Auth) AuthenticateWithDevice(deviceCode strfmt.UUID) (apiKey string, err error) { 221 logging.Debug("AuthenticateWithDevice") 222 223 jwtToken, apiKeyToken, err := model.CheckDeviceAuthorization(deviceCode) 224 if err != nil { 225 return "", errs.Wrap(err, "Authorization failed") 226 } 227 228 if jwtToken == nil { 229 return "", errNotYetGranted 230 } 231 232 if err := s.updateSession(jwtToken); err != nil { 233 return "", errs.Wrap(err, "Storing JWT failed") 234 } 235 236 return apiKeyToken.Token, nil 237 } 238 239 func (s *Auth) AuthenticateWithDevicePolling(deviceCode strfmt.UUID, interval time.Duration) (string, error) { 240 logging.Debug("AuthenticateWithDevicePolling, polling: %v", interval.String()) 241 for start := time.Now(); time.Since(start) < 5*time.Minute; { 242 token, err := s.AuthenticateWithDevice(deviceCode) 243 if err == nil { 244 return token, nil 245 } else if !errors.Is(err, errNotYetGranted) { 246 return "", errs.Wrap(err, "Device authentication failed") 247 } 248 time.Sleep(interval) // then try again 249 } 250 251 return "", locale.NewExternalError("err_auth_device_timeout") 252 } 253 254 // AuthenticateWithToken will try to authenticate using the given token 255 func (s *Auth) AuthenticateWithToken(token string) error { 256 logging.Debug("AuthenticateWithToken") 257 return s.AuthenticateWithModel(&mono_models.Credentials{ 258 Token: token, 259 }) 260 } 261 262 // updateSession authenticates with the given access token obtained via a Platform 263 // API request and response (e.g. username/password loging or device authentication). 264 func (s *Auth) updateSession(accessToken *mono_models.JWT) error { 265 defer s.updateRollbarPerson() 266 267 s.user = accessToken.User 268 s.bearerToken = accessToken.Token 269 clientAuth := httptransport.BearerToken(s.bearerToken) 270 s.clientAuth = &clientAuth 271 272 persist = s 273 274 return nil 275 } 276 277 // WhoAmI returns the username of the currently authenticated user, or an empty string if not authenticated 278 func (s *Auth) WhoAmI() string { 279 if s.user != nil { 280 return s.user.Username 281 } 282 return "" 283 } 284 285 func (s *Auth) CanWrite(organization string) bool { 286 if s.user == nil { 287 return false 288 } 289 for _, org := range s.user.Organizations { 290 if org.URLname != organization { 291 continue 292 } 293 return org.Role == string(mono_models.RoleAdmin) || org.Role == string(mono_models.RoleEditor) 294 } 295 return false 296 } 297 298 // Email return the email of the authenticated user 299 func (s *Auth) Email() string { 300 if s.user != nil { 301 return s.user.Email 302 } 303 return "" 304 } 305 306 // UserID returns the user ID for the currently authenticated user, or nil if not authenticated 307 func (s *Auth) UserID() *strfmt.UUID { 308 if s.user != nil { 309 return &s.user.UserID 310 } 311 return nil 312 } 313 314 // Logout will destroy any session tokens and reset the current Auth instance 315 func (s *Auth) Logout() error { 316 err := s.cfg.Set(ApiTokenConfigKey, "") 317 if err != nil { 318 multilog.Error("Could not clear apiToken in config") 319 return locale.WrapError(err, "err_logout_cfg", "Could not update config, if this persists please try running '[ACTIONABLE]state clean config[/RESET]'.") 320 } 321 322 s.client = nil 323 s.clientAuth = nil 324 s.bearerToken = "" 325 s.user = nil 326 327 // This is a bit of a hack, but it's safe to assume that the global legacy use-case should be reset whenever we logout a specific instance 328 // Handling it any other way would be far too error-prone by comparison 329 Reset() 330 331 return nil 332 } 333 334 // Client will return an API client that has authentication set up 335 func (s *Auth) Client() (*mono_client.Mono, error) { 336 if s.client == nil { 337 s.client = mono.NewWithAuth(s.clientAuth) 338 } 339 if !s.Authenticated() { 340 if err := s.Authenticate(); err != nil { 341 return nil, errs.Wrap(err, "Authentication failed") 342 } 343 } 344 return s.client, nil 345 } 346 347 // CreateToken will create an API token for the current authenticated user 348 func (s *Auth) CreateToken() error { 349 client, err := s.Client() 350 if err != nil { 351 return err 352 } 353 354 tokensOK, err := client.Authentication.ListTokens(nil, s.ClientAuth()) 355 if err != nil { 356 return locale.WrapError(err, "err_token_list", "", err.Error()) 357 } 358 359 for _, token := range tokensOK.Payload { 360 if token.Name == constants.APITokenName { 361 logging.Debug("Deleting stale token") 362 params := authentication.NewDeleteTokenParams() 363 params.SetTokenID(token.TokenID) 364 _, err := client.Authentication.DeleteToken(params, s.ClientAuth()) 365 if err != nil { 366 return locale.WrapError(err, "err_token_delete", "", err.Error()) 367 } 368 break 369 } 370 } 371 372 key := constants.APITokenName + ":" + uniqid.Text() 373 token, err := s.NewAPIKey(key) 374 if err != nil { 375 return err 376 } 377 378 err = s.SaveToken(token) 379 if err != nil { 380 return errs.Wrap(err, "SaveToken failed") 381 } 382 383 return nil 384 } 385 386 // SaveToken will save an API token 387 func (s *Auth) SaveToken(token string) error { 388 logging.Debug("Saving token: %s..", desensitizeToken(token)) 389 err := s.cfg.Set(ApiTokenConfigKey, token) 390 if err != nil { 391 return locale.WrapError(err, "err_set_token", "Could not set token in config") 392 } 393 394 return nil 395 } 396 397 // NewAPIKey returns a new api key from the backend or the relevant failure. 398 func (s *Auth) NewAPIKey(name string) (string, error) { 399 params := authentication.NewAddTokenParams() 400 params.SetTokenOptions(&mono_models.TokenEditable{Name: name, DeviceID: uniqid.Text()}) 401 402 client, err := s.Client() 403 if err != nil { 404 return "", err 405 } 406 407 tokenOK, err := client.Authentication.AddToken(params, s.ClientAuth()) 408 if err != nil { 409 return "", locale.WrapError(err, "err_token_create", "", err.Error()) 410 } 411 412 logging.Debug("Created token: %s..", desensitizeToken(tokenOK.Payload.Token)) 413 414 return tokenOK.Payload.Token, nil 415 } 416 417 func (s *Auth) AvailableAPIToken() (v string) { 418 if tkn := os.Getenv(constants.APIKeyEnvVarName); tkn != "" { 419 logging.Debug("Using API token passed via env var") 420 return tkn 421 } 422 return s.cfg.GetString(ApiTokenConfigKey) 423 } 424 425 func desensitizeToken(v string) string { 426 if len(v) <= 2 { 427 return "invalid token value" 428 } 429 return v[0:2] 430 }