config.go

  1package jira
  2
  3import (
  4	"bufio"
  5	"encoding/json"
  6	"fmt"
  7	"io/ioutil"
  8	"os"
  9	"strconv"
 10	"strings"
 11	"syscall"
 12	"time"
 13
 14	"github.com/pkg/errors"
 15	"golang.org/x/crypto/ssh/terminal"
 16
 17	"github.com/MichaelMure/git-bug/bridge/core"
 18	"github.com/MichaelMure/git-bug/repository"
 19	"github.com/MichaelMure/git-bug/util/interrupt"
 20)
 21
 22const (
 23	target             = "jira"
 24	keyServer          = "server"
 25	keyProject         = "project"
 26	keyCredentialsFile = "credentials-file"
 27	keyUsername        = "username"
 28	keyPassword        = "password"
 29	keyMapOpenID       = "bug-open-id"
 30	keyMapCloseID      = "bug-closed-id"
 31	keyCreateDefaults  = "create-issue-defaults"
 32	keyCreateGitBug    = "create-issue-gitbug-id"
 33
 34	defaultTimeout = 60 * time.Second
 35)
 36
 37// Configure sets up the bridge configuration
 38func (g *Jira) Configure(
 39	repo repository.RepoCommon, params core.BridgeParams) (
 40	core.Configuration, error) {
 41	conf := make(core.Configuration)
 42	var err error
 43	var url string
 44	var project string
 45	var credentialsFile string
 46	var username string
 47	var password string
 48	var serverURL string
 49
 50	if params.Token != "" || params.TokenStdin {
 51		return nil, fmt.Errorf(
 52			"JIRA session tokens are extremely short lived. We don't store them " +
 53				"in the configuration, so they are not valid for this bridge.")
 54	}
 55
 56	if params.Owner != "" {
 57		return nil, fmt.Errorf("owner doesn't make sense for jira")
 58	}
 59
 60	serverURL = params.URL
 61	if url == "" {
 62		// terminal prompt
 63		serverURL, err = prompt("JIRA server URL", "URL")
 64		if err != nil {
 65			return nil, err
 66		}
 67	}
 68
 69	project = params.Project
 70	if project == "" {
 71		project, err = prompt("JIRA project key", "project")
 72		if err != nil {
 73			return nil, err
 74		}
 75	}
 76
 77	choice, err := promptCredentialOptions(serverURL)
 78	if err != nil {
 79		return nil, err
 80	}
 81
 82	if choice == 1 {
 83		credentialsFile, err = prompt("Credentials file path", "path")
 84		if err != nil {
 85			return nil, err
 86		}
 87	}
 88
 89	username, err = prompt("JIRA username", "username")
 90	if err != nil {
 91		return nil, err
 92	}
 93
 94	password, err = PromptPassword()
 95	if err != nil {
 96		return nil, err
 97	}
 98
 99	jsonData, err := json.Marshal(
100		&SessionQuery{Username: username, Password: password})
101	if err != nil {
102		return nil, err
103	}
104
105	fmt.Printf("Attempting to login with credentials...\n")
106	client := NewClient(serverURL, nil)
107	err = client.RefreshTokenRaw(jsonData)
108
109	// verify access to the project with credentials
110	_, err = client.GetProject(project)
111	if err != nil {
112		return nil, fmt.Errorf(
113			"Project %s doesn't exist on %s, or authentication credentials for (%s)"+
114				" are invalid",
115			project, serverURL, username)
116	}
117
118	conf[core.KeyTarget] = target
119	conf[keyServer] = serverURL
120	conf[keyProject] = project
121	if choice == 1 {
122		conf[keyCredentialsFile] = credentialsFile
123		err = ioutil.WriteFile(credentialsFile, jsonData, 0644)
124		if err != nil {
125			return nil, errors.Wrap(
126				err, fmt.Sprintf("Unable to write credentials to %s", credentialsFile))
127		}
128	} else if choice == 2 {
129		conf[keyUsername] = username
130		conf[keyPassword] = password
131	} else if choice == 3 {
132		conf[keyUsername] = username
133	}
134	err = g.ValidateConfig(conf)
135	if err != nil {
136		return nil, err
137	}
138
139	return conf, nil
140}
141
142// ValidateConfig returns true if all required keys are present
143func (*Jira) ValidateConfig(conf core.Configuration) error {
144	if v, ok := conf[core.KeyTarget]; !ok {
145		return fmt.Errorf("missing %s key", core.KeyTarget)
146	} else if v != target {
147		return fmt.Errorf("unexpected target name: %v", v)
148	}
149
150	if _, ok := conf[keyProject]; !ok {
151		return fmt.Errorf("missing %s key", keyProject)
152	}
153
154	return nil
155}
156
157const credentialsText = `
158How would you like to store your JIRA login credentials?
159[1]: sidecar JSON file: Your credentials will be stored in a JSON sidecar next
160     to your git config. Note that it will contain your JIRA password in clear
161     text.
162[2]: git-config: Your credentials will be stored in the git config. Note that
163     it will contain your JIRA password in clear text.
164[3]: username in config, askpass: Your username will be stored in the git
165     config. We will ask you for your password each time you execute the bridge.
166`
167
168func promptCredentialOptions(serverURL string) (int, error) {
169	fmt.Print(credentialsText)
170	for {
171		fmt.Print("Select option: ")
172
173		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
174		fmt.Println()
175		if err != nil {
176			return -1, err
177		}
178
179		line = strings.TrimRight(line, "\n")
180
181		index, err := strconv.Atoi(line)
182		if err != nil || (index != 1 && index != 2 && index != 3) {
183			fmt.Println("invalid input")
184			continue
185		}
186
187		return index, nil
188	}
189}
190
191func prompt(description, name string) (string, error) {
192	for {
193		fmt.Printf("%s: ", description)
194
195		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
196		if err != nil {
197			return "", err
198		}
199
200		line = strings.TrimRight(line, "\n")
201		if line == "" {
202			fmt.Printf("%s is empty\n", name)
203			continue
204		}
205
206		return line, nil
207	}
208}
209
210// PromptPassword performs interactive input collection to get the user password
211func PromptPassword() (string, error) {
212	termState, err := terminal.GetState(int(syscall.Stdin))
213	if err != nil {
214		return "", err
215	}
216
217	cancel := interrupt.RegisterCleaner(func() error {
218		return terminal.Restore(int(syscall.Stdin), termState)
219	})
220	defer cancel()
221
222	for {
223		fmt.Print("password: ")
224		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
225		// new line for coherent formatting, ReadPassword clip the normal new line
226		// entered by the user
227		fmt.Println()
228
229		if err != nil {
230			return "", err
231		}
232
233		if len(bytePassword) > 0 {
234			return string(bytePassword), nil
235		}
236
237		fmt.Println("password is empty")
238	}
239}