mirror of https://gitlab.com/gitlab-org/cli.git
340 lines
8.5 KiB
Go
340 lines
8.5 KiB
Go
package login
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gitlab.com/gitlab-org/cli/commands/auth/authutils"
|
|
|
|
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/spf13/cobra"
|
|
"gitlab.com/gitlab-org/cli/api"
|
|
"gitlab.com/gitlab-org/cli/commands/cmdutils"
|
|
"gitlab.com/gitlab-org/cli/internal/config"
|
|
"gitlab.com/gitlab-org/cli/pkg/glinstance"
|
|
)
|
|
|
|
type LoginOptions struct {
|
|
IO *iostreams.IOStreams
|
|
Config func() (config.Config, error)
|
|
|
|
Interactive bool
|
|
|
|
Hostname string
|
|
Token string
|
|
}
|
|
|
|
var opts *LoginOptions
|
|
|
|
func NewCmdLogin(f *cmdutils.Factory) *cobra.Command {
|
|
opts = &LoginOptions{
|
|
IO: f.IO,
|
|
Config: f.Config,
|
|
}
|
|
|
|
var tokenStdin bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "login",
|
|
Args: cobra.ExactArgs(0),
|
|
Short: "Authenticate with a GitLab instance",
|
|
Long: heredoc.Docf(`
|
|
Authenticate with a GitLab instance.
|
|
You can pass in a token on standard input by using %[1]s--stdin%[1]s.
|
|
The minimum required scopes for the token are: %[1]sapi%[1]s, %[1]swrite_repository%[1]s.
|
|
`, "`"),
|
|
Example: heredoc.Docf(`
|
|
# start interactive setup
|
|
$ glab auth login
|
|
# authenticate against %[1]sgitlab.com%[1]s by reading the token from a file
|
|
$ glab auth login --stdin < myaccesstoken.txt
|
|
# authenticate with a self-hosted GitLab instance
|
|
$ glab auth login --hostname salsa.debian.org
|
|
`, "`"),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if !opts.IO.PromptEnabled() && !tokenStdin && opts.Token == "" {
|
|
return &cmdutils.FlagError{Err: errors.New("--stdin or --token required when not running interactively")}
|
|
}
|
|
|
|
if opts.Token != "" && tokenStdin {
|
|
return &cmdutils.FlagError{Err: errors.New("specify one of --token or --stdin. You cannot use both flags at the same time")}
|
|
}
|
|
|
|
if tokenStdin {
|
|
defer opts.IO.In.Close()
|
|
token, err := ioutil.ReadAll(opts.IO.In)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read token from STDIN: %w", err)
|
|
}
|
|
opts.Token = strings.TrimSpace(string(token))
|
|
}
|
|
|
|
if opts.IO.PromptEnabled() && opts.Token == "" && opts.IO.IsaTTY {
|
|
opts.Interactive = true
|
|
}
|
|
|
|
if cmd.Flags().Changed("hostname") {
|
|
if err := hostnameValidator(opts.Hostname); err != nil {
|
|
return &cmdutils.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
|
|
}
|
|
}
|
|
|
|
if !opts.Interactive {
|
|
if opts.Hostname == "" {
|
|
opts.Hostname = glinstance.Default()
|
|
}
|
|
}
|
|
|
|
return loginRun()
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitLab instance to authenticate with")
|
|
cmd.Flags().StringVarP(&opts.Token, "token", "t", "", "Your GitLab access token")
|
|
cmd.Flags().BoolVar(&tokenStdin, "stdin", false, "Read token from standard input")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func loginRun() error {
|
|
c := opts.IO.Color()
|
|
cfg, err := opts.Config()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Token != "" {
|
|
if opts.Hostname == "" {
|
|
return errors.New("empty hostname would leak oauth_token")
|
|
}
|
|
|
|
err := cfg.Set(opts.Hostname, "token", opts.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cfg.Write()
|
|
}
|
|
|
|
hostname := opts.Hostname
|
|
apiHostname := opts.Hostname
|
|
defaultHostname := glinstance.OverridableDefault()
|
|
isSelfHosted := false
|
|
|
|
if hostname == "" {
|
|
var hostType int
|
|
err := survey.AskOne(&survey.Select{
|
|
Message: "What GitLab instance do you want to log into?",
|
|
Options: []string{
|
|
defaultHostname,
|
|
"GitLab Self-hosted Instance",
|
|
},
|
|
}, &hostType)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
isSelfHosted = hostType == 1
|
|
|
|
hostname = defaultHostname
|
|
apiHostname = hostname
|
|
if isSelfHosted {
|
|
err := survey.AskOne(&survey.Input{
|
|
Message: "GitLab hostname:",
|
|
}, &hostname, survey.WithValidator(hostnameValidator))
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
err = survey.AskOne(&survey.Input{
|
|
Message: "API hostname:",
|
|
Help: "For instances with different hostname for the API endpoint",
|
|
Default: hostname,
|
|
}, &apiHostname, survey.WithValidator(hostnameValidator))
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "- Logging into %s\n", hostname)
|
|
|
|
if token := config.GetFromEnv("token"); token != "" {
|
|
fmt.Fprintf(opts.IO.StdErr, "%s you have GITLAB_TOKEN or OAUTH_TOKEN environment variable set. Unset if you don't want to use it for glab\n", c.Yellow("!WARNING:"))
|
|
}
|
|
existingToken, _, _ := cfg.GetWithSource(hostname, "token", false)
|
|
|
|
if existingToken != "" && opts.Interactive {
|
|
apiClient, err := cmdutils.LabClientFunc(hostname, cfg, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := api.CurrentUser(apiClient)
|
|
if err == nil {
|
|
username := user.Username
|
|
var keepGoing bool
|
|
err = survey.AskOne(&survey.Confirm{
|
|
Message: fmt.Sprintf(
|
|
"You're already logged into %s as %s. Do you want to re-authenticate?",
|
|
hostname,
|
|
username),
|
|
Default: false,
|
|
}, &keepGoing)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
if !keepGoing {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(opts.IO.StdErr)
|
|
fmt.Fprintln(opts.IO.StdErr, heredoc.Doc(getAccessTokenTip(hostname)))
|
|
var token string
|
|
err = survey.AskOne(&survey.Password{
|
|
Message: "Paste your authentication token:",
|
|
}, &token, survey.WithValidator(survey.Required))
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
if hostname == "" {
|
|
return errors.New("empty hostname would leak token")
|
|
}
|
|
|
|
err = cfg.Set(hostname, "token", token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = cfg.Set(hostname, "api_host", apiHostname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gitProtocol := "https"
|
|
apiProtocol := "https"
|
|
|
|
glabExecutable := "glab"
|
|
if exe, err := os.Executable(); err == nil {
|
|
glabExecutable = exe
|
|
}
|
|
credentialFlow := &authutils.GitCredentialFlow{Executable: glabExecutable}
|
|
|
|
if opts.Interactive {
|
|
err = survey.AskOne(&survey.Select{
|
|
Message: "Choose default git protocol",
|
|
Options: []string{
|
|
"SSH",
|
|
"HTTPS",
|
|
"HTTP",
|
|
},
|
|
Default: "HTTPS",
|
|
}, &gitProtocol)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
gitProtocol = strings.ToLower(gitProtocol)
|
|
if opts.Interactive && gitProtocol != "ssh" {
|
|
if err := credentialFlow.Prompt(hostname, gitProtocol); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if isSelfHosted {
|
|
err = survey.AskOne(&survey.Select{
|
|
Message: "Choose host API protocol",
|
|
Options: []string{
|
|
"HTTPS",
|
|
"HTTP",
|
|
},
|
|
Default: "HTTPS",
|
|
}, &apiProtocol)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
|
|
apiProtocol = strings.ToLower(apiProtocol)
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "- glab config set -h %s git_protocol %s\n", hostname, gitProtocol)
|
|
err = cfg.Set(hostname, "git_protocol", gitProtocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "%s Configured git protocol\n", c.GreenCheck())
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "- glab config set -h %s api_protocol %s\n", hostname, apiProtocol)
|
|
err = cfg.Set(hostname, "api_protocol", apiProtocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "%s Configured API protocol\n", c.GreenCheck())
|
|
}
|
|
apiClient, err := cmdutils.LabClientFunc(hostname, cfg, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := api.CurrentUser(apiClient)
|
|
if err != nil {
|
|
return fmt.Errorf("error using api: %w", err)
|
|
}
|
|
username := user.Username
|
|
|
|
err = cfg.Set(hostname, "user", username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cfg.Write()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if credentialFlow.ShouldSetup() {
|
|
err := credentialFlow.Setup(hostname, gitProtocol, username, token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdErr, "%s Logged in as %s\n", c.GreenCheck(), c.Bold(username))
|
|
|
|
return nil
|
|
}
|
|
|
|
func hostnameValidator(v interface{}) error {
|
|
val := fmt.Sprint(v)
|
|
if len(strings.TrimSpace(val)) < 1 {
|
|
return errors.New("a value is required")
|
|
}
|
|
re := regexp.MustCompile(`^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])(:[0-9]+)?(/[a-z0-9]*)*$`)
|
|
if !re.MatchString(val) {
|
|
return fmt.Errorf("invalid hostname %q", val)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getAccessTokenTip(hostname string) string {
|
|
glHostname := hostname
|
|
if glHostname == "" {
|
|
glHostname = glinstance.OverridableDefault()
|
|
}
|
|
return fmt.Sprintf(`
|
|
Tip: you can generate a Personal Access Token here https://%s/-/profile/personal_access_tokens
|
|
The minimum required scopes are 'api' and 'write_repository'.`, glHostname)
|
|
}
|