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  }