internal/platform/tokenloader/token_loader.go (167 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tokenloader
import (
"fmt"
"os"
"github.com/JetBrains/qodana-cli/internal/cloud"
"github.com/JetBrains/qodana-cli/internal/platform/git"
"github.com/JetBrains/qodana-cli/internal/platform/msg"
"github.com/JetBrains/qodana-cli/internal/platform/product"
"github.com/JetBrains/qodana-cli/internal/platform/qdenv"
"github.com/JetBrains/qodana-cli/internal/platform/utils"
"github.com/pterm/pterm"
log "github.com/sirupsen/logrus"
"github.com/zalando/go-keyring"
)
const keyringDefaultService = "qodana-cli"
type CloudTokenLoader interface {
GetQodanaToken() string
GetId() string
GetAnalyzer() product.Analyzer
GetRepositoryRoot() string
GetProjectDir() string
GetLogDir() string
}
func IsCloudTokenRequired(tokenLoader CloudTokenLoader) bool {
if tokenLoader.GetQodanaToken() != "" || os.Getenv(qdenv.QodanaLicenseOnlyToken) != "" {
return true
}
isQodanaLicenseSet := os.Getenv(qdenv.QodanaLicense) != ""
analyzer := tokenLoader.GetAnalyzer()
isFreeAnalyzer := !analyzer.GetLinter().IsPaid
isEapAnalyzer := analyzer.IsEAP()
return !isQodanaLicenseSet && !isFreeAnalyzer && !isEapAnalyzer
}
func LoadCloudUploadToken(tokenLoader CloudTokenLoader, refresh bool, requiresToken bool, interactive bool) string {
tokenFetchers := []func(bool) string{
func(_ bool) string { return tokenLoader.GetQodanaToken() },
func(refresh bool) string { return getTokenFromKeychain(refresh, tokenLoader.GetId()) },
}
if interactive && requiresToken {
fetcherFromUserInput := func(_ bool) string {
return getTokenFromUserInput(
tokenLoader.GetProjectDir(),
tokenLoader.GetRepositoryRoot(),
tokenLoader.GetId(),
tokenLoader.GetLogDir(),
)
}
tokenFetchers = append(tokenFetchers, fetcherFromUserInput)
}
for _, fetcher := range tokenFetchers {
if token := fetcher(refresh); token != "" {
return token
}
}
return ""
}
func ValidateCloudToken(tokenLoader CloudTokenLoader, refresh bool) string {
token := LoadCloudUploadToken(tokenLoader, refresh, true, true)
if token != "" {
ValidateTokenPrintProject(token)
}
return token
}
// ValidateTokenPrintProject validates given token by requesting linked project name.
func ValidateTokenPrintProject(token string) {
client := cloud.GetCloudApiEndpoints().NewCloudApiClient(token)
if projectName, err := client.RequestProjectName(); err != nil {
msg.ErrorMessage(cloud.InvalidTokenMessage)
os.Exit(1)
} else if !qdenv.IsContainer() {
msg.SuccessMessage("Linked %s project: %s", cloud.GetCloudRootEndpoint().Url, projectName)
}
}
// saveCloudToken saves token to the system keyring
func saveCloudToken(id string, token string) error {
err := keyring.Set(keyringDefaultService, id, token)
if err != nil {
return err
}
log.Debugf("Saved token to the system keyring with id %s", id)
return nil
}
// getCloudToken returns token from the system keyring
func getCloudToken(id string) (string, error) {
secret, err := keyring.Get(keyringDefaultService, id)
if err != nil {
return "", err
}
log.Debugf("Got token from the system keyring with id %s", id)
return secret, nil
}
func setupToken(projectDir string, repositoryRoot, id string, logdir string) string {
openCloud := msg.AskUserConfirm("Do you want to open the team page to get the token?")
if openCloud {
origin, err := git.RemoteUrl(repositoryRoot, logdir)
if err != nil {
msg.ErrorMessage("%s", err)
return ""
}
err = utils.OpenBrowser(cloud.GetCloudRootEndpoint().GetCloudTeamsPageUrl(origin, projectDir))
if err != nil {
msg.ErrorMessage("%s", err)
return ""
}
}
token, err := pterm.DefaultInteractiveTextInput.WithMask("*").WithTextStyle(msg.PrimaryStyle).Show(
fmt.Sprintf("> Enter the token (will be used for %s; enter 'q' to exit)", msg.PrimaryBold(projectDir)),
)
if token == "q" {
return "q"
}
if err != nil {
msg.ErrorMessage("%s", err)
return ""
}
if token == "" {
msg.ErrorMessage("Token cannot be empty")
return ""
}
client := cloud.GetCloudApiEndpoints().NewCloudApiClient(token)
_, err = client.RequestProjectName()
if err != nil {
msg.ErrorMessage("Invalid token, try again")
return ""
}
err = saveCloudToken(id, token)
if err != nil {
msg.ErrorMessage("Failed to save credentials: %s", err)
return ""
}
return token
}
func getTokenFromKeychain(refresh bool, id string) string {
log.Debugf("project id: %s", id)
if refresh || os.Getenv(qdenv.QodanaClearKeyring) != "" {
err := keyring.Delete(keyringDefaultService, id)
if err != nil {
log.Debugf("Failed to delete token from the system keyring: %s", err)
}
return ""
}
tokenFromKeychain, err := getCloudToken(id)
if err == nil && tokenFromKeychain != "" {
msg.WarningMessage(
"Got %s from the system keyring, declare %s env variable or run %s to override it",
msg.PrimaryBold(qdenv.QodanaToken),
msg.PrimaryBold(qdenv.QodanaToken),
msg.PrimaryBold("qodana init -f"),
)
log.Debugf("Loaded token from the system keyring with id %s", id)
return tokenFromKeychain
}
return ""
}
func getTokenFromUserInput(projectDir string, repositoryRoot string, id string, logDir string) string {
if msg.IsInteractive() {
msg.WarningMessage(cloud.EmptyTokenMessage, cloud.GetCloudRootEndpoint().Url)
var token string
for {
token = setupToken(projectDir, repositoryRoot, id, logDir)
if token == "q" {
return ""
}
if token != "" {
return token
}
}
}
return ""
}