1
0
mirror of https://github.com/rancher/cli.git synced 2026-02-05 09:48:36 +01:00
Files
cli/cmd/login.go
2019-08-01 00:33:52 +01:00

359 lines
8.1 KiB
Go

package cmd
import (
"bufio"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"github.com/grantae/certinfo"
"github.com/rancher/cli/cliclient"
"github.com/rancher/cli/config"
managementClient "github.com/rancher/types/client/management/v3"
"github.com/urfave/cli"
)
type LoginData struct {
Project managementClient.Project
Index int
ClusterName string
}
type CACertResponse struct {
Name string `json:"name"`
Value string `json:"value"`
}
func LoginCommand() cli.Command {
return cli.Command{
Name: "login",
Aliases: []string{"l"},
Usage: "Login to a Rancher server",
Action: loginSetup,
ArgsUsage: "[SERVERURL]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "context",
Usage: "Set the context during login",
},
cli.StringFlag{
Name: "token,t",
Usage: "Token from the Rancher UI",
},
cli.StringFlag{
Name: "cacert",
Usage: "Location of the CACerts to use",
},
cli.StringFlag{
Name: "name",
Usage: "Name of the Server",
},
cli.BoolFlag{
Name: "skip-verify",
Usage: "Skip verification of the CACerts presented by the Server",
},
},
}
}
func loginSetup(ctx *cli.Context) error {
if ctx.NArg() == 0 {
return cli.ShowCommandHelp(ctx, "login")
}
cf, err := loadConfig(ctx)
if err != nil {
return err
}
serverName := ctx.String("name")
if serverName == "" {
serverName = "rancherDefault"
}
serverConfig := &config.ServerConfig{}
// Validate the url and drop the path
u, err := url.ParseRequestURI(ctx.Args().First())
if err != nil {
return fmt.Errorf("Failed to parse SERVERURL (%s), make sure it is a valid HTTPS URL (e.g. https://rancher.yourdomain.com or https://1.1.1.1). Error: %s", ctx.Args().First(), err)
}
u.Path = ""
serverConfig.URL = u.String()
if ctx.String("token") != "" {
auth := SplitOnColon(ctx.String("token"))
if len(auth) != 2 {
return errors.New("invalid token")
}
serverConfig.AccessKey = auth[0]
serverConfig.SecretKey = auth[1]
serverConfig.TokenKey = ctx.String("token")
} else {
// This can be removed once username and password is accepted
return errors.New("token flag is required")
}
if ctx.String("cacert") != "" {
cert, err := loadAndVerifyCert(ctx.String("cacert"))
if nil != err {
return err
}
serverConfig.CACerts = cert
}
c, err := cliclient.NewManagementClient(serverConfig)
if nil != err {
if _, ok := err.(*url.Error); ok && strings.Contains(err.Error(), "certificate signed by unknown authority") {
// no cert was provided and it's most likely a self signed cert if
// we get here so grab the cacert and see if the user accepts the server
c, err = getCertFromServer(ctx, serverConfig)
if nil != err {
return err
}
} else {
return err
}
}
proj, err := getProjectContext(ctx, c)
if nil != err {
return err
}
// Set the default server and proj for the user
serverConfig.Project = proj
cf.CurrentServer = serverName
cf.Servers[serverName] = serverConfig
cf.Write()
return nil
}
func getProjectContext(ctx *cli.Context, c *cliclient.MasterClient) (string, error) {
// If context is given
if ctx.String("context") != "" {
context := ctx.String("context")
// Check if given context is in valid format
_, _, err := parseClusterAndProjectID(context)
if err != nil {
return "", fmt.Errorf("Unable to parse context (%s). Please provide context as local:p-xxxxx, c-xxxxx:p-xxxxx, or c-xxxxx:project-xxxxx", context)
}
// Check if context exists
_, err = Lookup(c, context, "project")
if err != nil {
return "", fmt.Errorf("Unable to find context (%s). Make sure the context exists and you have permissions to use it. Error: %s", context, err)
}
return context, nil
}
projectCollection, err := c.ManagementClient.Project.List(defaultListOpts(ctx))
if err != nil {
return "", err
}
projDataLen := len(projectCollection.Data)
if projDataLen == 0 {
logrus.Warn("No projects found, context could not be set. Please create a project and run `rancher login` again.")
return "", nil
}
if projDataLen == 1 {
logrus.Infof("Only 1 project available: %s", projectCollection.Data[0].Name)
return projectCollection.Data[0].ID, nil
}
if projDataLen == 2 {
var sysProj bool
var defaultID string
for _, proj := range projectCollection.Data {
if proj.Name == "Default" {
defaultID = proj.ID
}
if proj.Name == "System" {
sysProj = true
}
if sysProj && defaultID != "" {
return defaultID, nil
}
}
}
clusterNames, err := getClusterNames(ctx, c)
if err != nil {
return "", err
}
writer := NewTableWriter([][]string{
{"NUMBER", "Index"},
{"CLUSTER NAME", "ClusterName"},
{"PROJECT ID", "Project.ID"},
{"PROJECT NAME", "Project.Name"},
{"PROJECT DESCRIPTION", "Project.Description"},
}, ctx)
for i, item := range projectCollection.Data {
writer.Write(&LoginData{
Project: item,
Index: i + 1,
ClusterName: clusterNames[item.ClusterID],
})
}
writer.Close()
if nil != writer.Err() {
return "", writer.Err()
}
fmt.Print("Select a Project:")
reader := bufio.NewReader(os.Stdin)
errMessage := fmt.Sprintf("Invalid input, enter a number between 1 and %v: ", len(projectCollection.Data))
var selection int
for {
input, err := reader.ReadString('\n')
if err != nil {
return "", err
}
input = strings.TrimSpace(input)
if input != "" {
i, err := strconv.Atoi(input)
if nil != err {
fmt.Print(errMessage)
continue
}
if i <= len(projectCollection.Data) && i != 0 {
selection = i - 1
break
}
fmt.Print(errMessage)
continue
}
}
return projectCollection.Data[selection].ID, nil
}
func getCertFromServer(ctx *cli.Context, cf *config.ServerConfig) (*cliclient.MasterClient, error) {
req, err := http.NewRequest("GET", cf.URL+"/v3/settings/cacerts", nil)
if nil != err {
return nil, err
}
req.SetBasicAuth(cf.AccessKey, cf.SecretKey)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
res, err := client.Do(req)
if nil != err {
return nil, err
}
defer res.Body.Close()
content, err := ioutil.ReadAll(res.Body)
if nil != err {
return nil, err
}
var certReponse *CACertResponse
err = json.Unmarshal(content, &certReponse)
if err != nil {
return nil, fmt.Errorf("Unable to parse response from %s/v3/settings/cacerts\nError: %s\nResponse:\n%s", cf.URL, err, content)
}
cert, err := verifyCert([]byte(certReponse.Value))
if nil != err {
return nil, err
}
// Get the server cert chain in a printable form
serverCerts, err := processServerChain(res)
if nil != err {
return nil, err
}
if !ctx.Bool("skip-verify") {
if ok := verifyUserAcceptsCert(serverCerts, cf.URL); !ok {
return nil, errors.New("CACert of server was not accepted, unable to login")
}
}
cf.CACerts = cert
return cliclient.NewManagementClient(cf)
}
func verifyUserAcceptsCert(certs []string, url string) bool {
fmt.Printf("The authenticity of server '%s' can't be established.\n", url)
fmt.Printf("Cert chain is : %v \n", certs)
fmt.Print("Do you want to continue connecting (yes/no)? ")
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input := scanner.Text()
input = strings.ToLower(strings.TrimSpace(input))
if input == "yes" || input == "y" {
return true
} else if input == "no" || input == "n" {
return false
}
fmt.Printf("Please type 'yes' or 'no': ")
}
return false
}
func processServerChain(res *http.Response) ([]string, error) {
var allCerts []string
for _, cert := range res.TLS.PeerCertificates {
result, err := certinfo.CertificateText(cert)
if err != nil {
return allCerts, err
}
allCerts = append(allCerts, result)
}
return allCerts, nil
}
func loginContext(ctx *cli.Context) error {
c, err := GetClient(ctx)
if err != nil {
return err
}
cluster, err := getClusterByID(c, c.UserConfig.FocusedCluster())
if nil != err {
return err
}
clusterName := getClusterName(cluster)
project, err := getProjectByID(c, c.UserConfig.Project)
if nil != err {
return err
}
fmt.Printf("Cluster:%s Project:%s\n", clusterName, project.Name)
return nil
}