github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/fly/commands/login.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/pf-qiu/concourse/v6/atc"
    16  	"github.com/pf-qiu/concourse/v6/fly/pty"
    17  	"github.com/pf-qiu/concourse/v6/fly/rc"
    18  	"github.com/pf-qiu/concourse/v6/go-concourse/concourse"
    19  	semisemanticversion "github.com/cppforlife/go-semi-semantic/version"
    20  	"github.com/skratchdot/open-golang/open"
    21  	"github.com/vito/go-interact/interact"
    22  	"golang.org/x/crypto/ssh/terminal"
    23  	"golang.org/x/oauth2"
    24  )
    25  
    26  type LoginCommand struct {
    27  	ATCURL         string       `short:"c" long:"concourse-url" description:"Concourse URL to authenticate with"`
    28  	Insecure       bool         `short:"k" long:"insecure" description:"Skip verification of the endpoint's SSL certificate"`
    29  	Username       string       `short:"u" long:"username" description:"Username for basic auth"`
    30  	Password       string       `short:"p" long:"password" description:"Password for basic auth"`
    31  	TeamName       string       `short:"n" long:"team-name" description:"Team to authenticate with"`
    32  	CACert         atc.PathFlag `long:"ca-cert" description:"Path to Concourse PEM-encoded CA certificate file."`
    33  	ClientCertPath atc.PathFlag `long:"client-cert" description:"Path to a PEM-encoded client certificate file."`
    34  	ClientKeyPath  atc.PathFlag `long:"client-key" description:"Path to a PEM-encoded client key file."`
    35  	OpenBrowser    bool         `short:"b" long:"open-browser" description:"Open browser to the auth endpoint"`
    36  
    37  	BrowserOnly bool
    38  }
    39  
    40  func (command *LoginCommand) Execute(args []string) error {
    41  	if Fly.Target == "" {
    42  		return errors.New("name for the target must be specified (--target/-t)")
    43  	}
    44  
    45  	var target rc.Target
    46  	var err error
    47  
    48  	var caCert string
    49  	if command.CACert != "" {
    50  		caCertBytes, err := ioutil.ReadFile(string(command.CACert))
    51  		if err != nil {
    52  			return err
    53  		}
    54  		caCert = string(caCertBytes)
    55  	}
    56  
    57  	if command.ATCURL != "" {
    58  		if command.TeamName == "" {
    59  			command.TeamName = atc.DefaultTeamName
    60  		}
    61  
    62  		target, err = rc.NewUnauthenticatedTarget(
    63  			Fly.Target,
    64  			command.ATCURL,
    65  			command.TeamName,
    66  			command.Insecure,
    67  			caCert,
    68  			string(command.ClientCertPath),
    69  			string(command.ClientKeyPath),
    70  			Fly.Verbose,
    71  		)
    72  	} else {
    73  		target, err = rc.LoadUnauthenticatedTarget(
    74  			Fly.Target,
    75  			command.TeamName,
    76  			command.Insecure,
    77  			caCert,
    78  			string(command.ClientCertPath),
    79  			string(command.ClientKeyPath),
    80  			Fly.Verbose,
    81  		)
    82  	}
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	client := target.Client()
    88  	command.TeamName = target.Team().Name()
    89  
    90  	fmt.Printf("logging in to team '%s'\n\n", command.TeamName)
    91  
    92  	if len(args) != 0 {
    93  		return errors.New("unexpected argument [" + strings.Join(args, ", ") + "]")
    94  	}
    95  
    96  	err = target.ValidateWithWarningOnly()
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	var tokenType string
   102  	var tokenValue string
   103  
   104  	version, err := target.Version()
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	semver, err := semisemanticversion.NewVersionFromString(version)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	legacySemver, err := semisemanticversion.NewVersionFromString("3.14.1")
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	devSemver, err := semisemanticversion.NewVersionFromString("0.0.0-dev")
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	isRawMode := pty.IsTerminal() && !command.BrowserOnly
   125  	if isRawMode {
   126  		state, err := terminal.MakeRaw(int(os.Stdin.Fd()))
   127  		if err != nil {
   128  			isRawMode = false
   129  		} else {
   130  			defer func() {
   131  				terminal.Restore(int(os.Stdin.Fd()), state)
   132  				fmt.Print("\r")
   133  			}()
   134  		}
   135  	}
   136  
   137  	if semver.Compare(legacySemver) <= 0 && semver.Compare(devSemver) != 0 {
   138  		// Legacy Auth Support
   139  		tokenType, tokenValue, err = command.legacyAuth(target, command.BrowserOnly, isRawMode)
   140  	} else {
   141  		if command.Username != "" && command.Password != "" {
   142  			tokenType, tokenValue, err = command.passwordGrant(client, command.Username, command.Password)
   143  		} else {
   144  			tokenType, tokenValue, err = command.authCodeGrant(client.URL(), command.BrowserOnly, isRawMode)
   145  		}
   146  	}
   147  
   148  	if errors.Is(err, pty.ErrInterrupted) {
   149  		fmt.Println("^C\r")
   150  		return nil
   151  	}
   152  
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	fmt.Println("")
   158  
   159  	err = command.verifyTeamExists(client.URL(), rc.TargetToken{
   160  		Type:  tokenType,
   161  		Value: tokenValue,
   162  	}, target.CACert(), target.ClientCertPath(), target.ClientKeyPath())
   163  
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	return command.saveTarget(
   169  		client.URL(),
   170  		&rc.TargetToken{
   171  			Type:  tokenType,
   172  			Value: tokenValue,
   173  		},
   174  		target.CACert(),
   175  		target.ClientCertPath(),
   176  		target.ClientKeyPath(),
   177  	)
   178  }
   179  
   180  func (command *LoginCommand) passwordGrant(client concourse.Client, username, password string) (string, string, error) {
   181  
   182  	oauth2Config := oauth2.Config{
   183  		ClientID:     "fly",
   184  		ClientSecret: "Zmx5",
   185  		Endpoint:     oauth2.Endpoint{TokenURL: client.URL() + "/sky/issuer/token"},
   186  		Scopes:       []string{"openid", "profile", "email", "federated:id", "groups"},
   187  	}
   188  
   189  	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client.HTTPClient())
   190  
   191  	token, err := oauth2Config.PasswordCredentialsToken(ctx, username, password)
   192  	if err != nil {
   193  		return "", "", err
   194  	}
   195  
   196  	return token.TokenType, token.AccessToken, nil
   197  }
   198  
   199  func (command *LoginCommand) authCodeGrant(targetUrl string, browserOnly bool, isRawMode bool) (string, string, error) {
   200  	var tokenStr string
   201  
   202  	stdinChannel := make(chan string)
   203  	tokenChannel := make(chan string)
   204  	errorChannel := make(chan error)
   205  	portChannel := make(chan string)
   206  
   207  	go listenForTokenCallback(tokenChannel, errorChannel, portChannel, targetUrl)
   208  
   209  	port := <-portChannel
   210  
   211  	var openURL string
   212  
   213  	fmt.Println("navigate to the following URL in your browser:\r")
   214  	fmt.Println("\r")
   215  
   216  	openURL = fmt.Sprintf("%s/login?fly_port=%s", targetUrl, port)
   217  
   218  	fmt.Printf("  %s\r\n", openURL)
   219  
   220  	if command.OpenBrowser {
   221  		// try to open the browser window, but don't get all hung up if it
   222  		// fails, since we already printed about it.
   223  		_ = open.Start(openURL)
   224  	}
   225  
   226  	if !browserOnly {
   227  		go waitForTokenInput(stdinChannel, errorChannel, isRawMode)
   228  	}
   229  
   230  	select {
   231  	case tokenStrMsg := <-tokenChannel:
   232  		tokenStr = tokenStrMsg
   233  	case tokenStrMsg := <-stdinChannel:
   234  		tokenStr = tokenStrMsg
   235  	case errorMsg := <-errorChannel:
   236  		return "", "", errorMsg
   237  	}
   238  
   239  	segments := strings.SplitN(tokenStr, " ", 2)
   240  
   241  	if len(segments) > 1 {
   242  		return segments[0], segments[1], nil
   243  	} else {
   244  		return "", "", fmt.Errorf("invalid token: %v", tokenStr)
   245  	}
   246  }
   247  
   248  func listenForTokenCallback(tokenChannel chan string, errorChannel chan error, portChannel chan string, targetUrl string) {
   249  	s := &http.Server{
   250  		Addr: "127.0.0.1:0",
   251  		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   252  			w.Header().Set("Access-Control-Allow-Origin", targetUrl)
   253  			tokenChannel <- r.FormValue("token")
   254  			if r.Header.Get("Upgrade-Insecure-Requests") != "" {
   255  				http.Redirect(w, r, fmt.Sprintf("%s/fly_success?noop=true", targetUrl), http.StatusFound)
   256  			}
   257  		}),
   258  	}
   259  
   260  	err := listenAndServeWithPort(s, portChannel)
   261  
   262  	if err != nil {
   263  		errorChannel <- err
   264  	}
   265  }
   266  
   267  func listenAndServeWithPort(srv *http.Server, portChannel chan string) error {
   268  	addr := srv.Addr
   269  	ln, err := net.Listen("tcp", addr)
   270  	if err != nil {
   271  		return err
   272  	}
   273  
   274  	_, port, err := net.SplitHostPort(ln.Addr().String())
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	portChannel <- port
   280  
   281  	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
   282  }
   283  
   284  type tcpKeepAliveListener struct {
   285  	*net.TCPListener
   286  }
   287  
   288  func waitForTokenInput(tokenChannel chan string, errorChannel chan error, isRawMode bool) {
   289  	fmt.Println()
   290  
   291  	for {
   292  		if isRawMode {
   293  			fmt.Print("or enter token manually (input hidden): ")
   294  		} else {
   295  			fmt.Print("or enter token manually: ")
   296  		}
   297  		tokenBytes, err := pty.ReadLine(os.Stdin)
   298  		token := strings.TrimSpace(string(tokenBytes))
   299  		if len(token) == 0 && err == io.EOF {
   300  			return
   301  		}
   302  		if err != nil && err != io.EOF {
   303  			errorChannel <- err
   304  			return
   305  		}
   306  
   307  		parts := strings.Split(token, " ")
   308  		if len(parts) != 2 {
   309  			fmt.Println("\rtoken must be of the format 'TYPE VALUE', e.g. 'Bearer ...'\r")
   310  			continue
   311  		}
   312  
   313  		tokenChannel <- token
   314  		break
   315  	}
   316  }
   317  
   318  func (command *LoginCommand) saveTarget(url string, token *rc.TargetToken, caCert string, clientCertPath string, clientKeyPath string) error {
   319  	err := rc.SaveTarget(
   320  		Fly.Target,
   321  		url,
   322  		command.Insecure,
   323  		command.TeamName,
   324  		&rc.TargetToken{
   325  			Type:  token.Type,
   326  			Value: token.Value,
   327  		},
   328  		caCert,
   329  		clientCertPath,
   330  		clientKeyPath,
   331  	)
   332  	if err != nil {
   333  		return err
   334  	}
   335  
   336  	fmt.Println("\rtarget saved\r")
   337  
   338  	return nil
   339  }
   340  
   341  func (command *LoginCommand) legacyAuth(target rc.Target, browserOnly bool, isRawMode bool) (string, string, error) {
   342  
   343  	httpClient := target.Client().HTTPClient()
   344  
   345  	authResponse, err := httpClient.Get(target.URL() + "/api/v1/teams/" + target.Team().Name() + "/auth/methods")
   346  	if err != nil {
   347  		return "", "", err
   348  	}
   349  
   350  	type authMethod struct {
   351  		Type        string `json:"type"`
   352  		DisplayName string `json:"display_name"`
   353  		AuthURL     string `json:"auth_url"`
   354  	}
   355  
   356  	defer authResponse.Body.Close()
   357  
   358  	var authMethods []authMethod
   359  	json.NewDecoder(authResponse.Body).Decode(&authMethods)
   360  
   361  	var chosenMethod authMethod
   362  
   363  	if command.Username != "" || command.Password != "" {
   364  		for _, method := range authMethods {
   365  			if method.Type == "basic" {
   366  				chosenMethod = method
   367  				break
   368  			}
   369  		}
   370  
   371  		if chosenMethod.Type == "" {
   372  			return "", "", errors.New("basic auth is not available")
   373  		}
   374  	} else {
   375  		choices := make([]interact.Choice, len(authMethods))
   376  
   377  		for i, method := range authMethods {
   378  			choices[i] = interact.Choice{
   379  				Display: method.DisplayName,
   380  				Value:   method,
   381  			}
   382  		}
   383  
   384  		if len(choices) == 0 {
   385  			chosenMethod = authMethod{
   386  				Type: "none",
   387  			}
   388  		}
   389  
   390  		if len(choices) == 1 {
   391  			chosenMethod = authMethods[0]
   392  		}
   393  
   394  		if len(choices) > 1 {
   395  			err = interact.NewInteraction("choose an auth method", choices...).Resolve(&chosenMethod)
   396  			if err != nil {
   397  				return "", "", err
   398  			}
   399  
   400  			fmt.Println("")
   401  		}
   402  	}
   403  
   404  	switch chosenMethod.Type {
   405  	case "oauth":
   406  		var tokenStr string
   407  
   408  		stdinChannel := make(chan string)
   409  		tokenChannel := make(chan string)
   410  		errorChannel := make(chan error)
   411  		portChannel := make(chan string)
   412  
   413  		go listenForTokenCallback(tokenChannel, errorChannel, portChannel, target.Client().URL())
   414  
   415  		port := <-portChannel
   416  
   417  		theURL := fmt.Sprintf("%s&fly_local_port=%s\n", chosenMethod.AuthURL, port)
   418  
   419  		fmt.Println("navigate to the following URL in your browser:\r")
   420  		fmt.Println("")
   421  		fmt.Printf("    %s\r\n", theURL)
   422  
   423  		if command.OpenBrowser {
   424  			// try to open the browser window, but don't get all hung up if it
   425  			// fails, since we already printed about it.
   426  			_ = open.Start(theURL)
   427  		}
   428  
   429  		if !browserOnly {
   430  			go waitForTokenInput(stdinChannel, errorChannel, isRawMode)
   431  		}
   432  
   433  		select {
   434  		case tokenStrMsg := <-tokenChannel:
   435  			tokenStr = tokenStrMsg
   436  		case tokenStrMsg := <-stdinChannel:
   437  			tokenStr = tokenStrMsg
   438  		case errorMsg := <-errorChannel:
   439  			return "", "", errorMsg
   440  		}
   441  
   442  		segments := strings.SplitN(tokenStr, " ", 2)
   443  
   444  		return segments[0], segments[1], nil
   445  
   446  	case "basic":
   447  		var username string
   448  		if command.Username != "" {
   449  			username = command.Username
   450  		} else {
   451  			err := interact.NewInteraction("username").Resolve(interact.Required(&username))
   452  			if err != nil {
   453  				return "", "", err
   454  			}
   455  		}
   456  
   457  		var password string
   458  		if command.Password != "" {
   459  			password = command.Password
   460  		} else {
   461  			var interactivePassword interact.Password
   462  			err := interact.NewInteraction("password").Resolve(interact.Required(&interactivePassword))
   463  			if err != nil {
   464  				return "", "", err
   465  			}
   466  			password = string(interactivePassword)
   467  		}
   468  
   469  		request, err := http.NewRequest("GET", target.URL()+"/api/v1/teams/"+target.Team().Name()+"/auth/token", nil)
   470  		if err != nil {
   471  			return "", "", err
   472  		}
   473  		request.SetBasicAuth(username, password)
   474  
   475  		tokenResponse, err := httpClient.Do(request)
   476  		if err != nil {
   477  			return "", "", err
   478  		}
   479  
   480  		type authToken struct {
   481  			Type  string `json:"token_type"`
   482  			Value string `json:"token_value"`
   483  		}
   484  
   485  		defer tokenResponse.Body.Close()
   486  
   487  		var token authToken
   488  		json.NewDecoder(tokenResponse.Body).Decode(&token)
   489  
   490  		return token.Type, token.Value, nil
   491  
   492  	case "none":
   493  		request, err := http.NewRequest("GET", target.URL()+"/api/v1/teams/"+target.Team().Name()+"/auth/token", nil)
   494  		if err != nil {
   495  			return "", "", err
   496  		}
   497  
   498  		tokenResponse, err := httpClient.Do(request)
   499  		if err != nil {
   500  			return "", "", err
   501  		}
   502  
   503  		type authToken struct {
   504  			Type  string `json:"token_type"`
   505  			Value string `json:"token_value"`
   506  		}
   507  
   508  		defer tokenResponse.Body.Close()
   509  
   510  		var token authToken
   511  		json.NewDecoder(tokenResponse.Body).Decode(&token)
   512  
   513  		return token.Type, token.Value, nil
   514  	}
   515  
   516  	return "", "", nil
   517  }
   518  
   519  func (command *LoginCommand) verifyTeamExists(clientUrl string, token rc.TargetToken, caCert string, clientCertPath string,
   520  	clientKeyPath string) error {
   521  	verifyTarget, err := rc.NewAuthenticatedTarget("verify",
   522  		clientUrl,
   523  		command.TeamName,
   524  		command.Insecure,
   525  		&token,
   526  		caCert,
   527  		clientCertPath,
   528  		clientKeyPath,
   529  		false)
   530  	if err != nil {
   531  		return err
   532  	}
   533  
   534  	userInfo, err := verifyTarget.Client().UserInfo()
   535  	if err != nil {
   536  		return err
   537  	}
   538  
   539  	if !userInfo.IsAdmin {
   540  		if userInfo.Teams != nil {
   541  			_, ok := userInfo.Teams[command.TeamName]
   542  			if !ok {
   543  				return errors.New("you are not a member of '" + command.TeamName + "' or the team does not exist")
   544  			}
   545  		} else {
   546  			return errors.New("unable to verify role on team")
   547  		}
   548  	} else {
   549  		teams, err := verifyTarget.Client().ListTeams()
   550  		if err != nil {
   551  			return err
   552  		}
   553  		var found bool
   554  		for _, team := range teams {
   555  			if team.Name == command.TeamName {
   556  				found = true
   557  				break
   558  			}
   559  		}
   560  		if !found {
   561  			return errors.New("team '" + command.TeamName + "' does not exist")
   562  		}
   563  	}
   564  
   565  	return nil
   566  }