config.go

  1package jira
  2
  3import (
  4	"bufio"
  5	"encoding/json"
  6	"fmt"
  7	"io/ioutil"
  8	"os"
  9	"strconv"
 10	"strings"
 11	"time"
 12
 13	"github.com/pkg/errors"
 14
 15	"github.com/MichaelMure/git-bug/bridge/core"
 16	"github.com/MichaelMure/git-bug/cache"
 17	"github.com/MichaelMure/git-bug/input"
 18)
 19
 20const (
 21	target             = "jira"
 22	keyServer          = "server"
 23	keyProject         = "project"
 24	keyCredentialsType = "credentials-type"
 25	keyCredentialsFile = "credentials-file"
 26	keyUsername        = "username"
 27	keyPassword        = "password"
 28	keyIDMap           = "bug-id-map"
 29	keyIDRevMap        = "bug-id-revmap"
 30	keyCreateDefaults  = "create-issue-defaults"
 31	keyCreateGitBug    = "create-issue-gitbug-id"
 32
 33	defaultTimeout = 60 * time.Second
 34)
 35
 36const moreConfigText = `
 37NOTE: There are a few optional configuration values that you can additionally
 38set in your git configuration to influence the behavior of the bridge. Please
 39see the notes at:
 40https://github.com/MichaelMure/git-bug/blob/master/doc/jira_bridge.md
 41`
 42
 43const credTypeText = `
 44JIRA has recently altered it's authentication strategies. Servers deployed
 45prior to October 1st 2019 must use "SESSION" authentication, whereby the REST
 46client logs in with an actual username and password, is assigned a session, and
 47passes the session cookie with each request. JIRA Cloud and servers deployed
 48after October 1st 2019 must use "TOKEN" authentication. You must create a user
 49API token and the client will provide this along with your username with each
 50request.
 51
 52Which authentication mechanism should this bridge use?
 53[1]: SESSION
 54[2]: TOKEN
 55`
 56const credentialsText = `
 57How would you like to store your JIRA login credentials?
 58[1]: sidecar JSON file: Your credentials will be stored in a JSON sidecar next
 59     to your git config. Note that it will contain your JIRA password in clear
 60     text.
 61[2]: git-config: Your credentials will be stored in the git config. Note that
 62     it will contain your JIRA password in clear text.
 63[3]: username in config, askpass: Your username will be stored in the git
 64     config. We will ask you for your password each time you execute the bridge.
 65`
 66
 67// Configure sets up the bridge configuration
 68func (g *Jira) Configure(
 69	repo *cache.RepoCache, params core.BridgeParams) (
 70	core.Configuration, error) {
 71	conf := make(core.Configuration)
 72	var err error
 73	var url string
 74	var project string
 75	var credentialsFile string
 76	var username string
 77	var password string
 78	var serverURL string
 79
 80	// if params.Token != "" || params.TokenStdin {
 81	// 	return nil, fmt.Errorf(
 82	// 		"JIRA session tokens are extremely short lived. We don't store them " +
 83	// 			"in the configuration, so they are not valid for this bridge.")
 84	// }
 85
 86	if params.Owner != "" {
 87		return nil, fmt.Errorf("owner doesn't make sense for jira")
 88	}
 89
 90	serverURL = params.URL
 91	if url == "" {
 92		// terminal prompt
 93		serverURL, err = prompt("JIRA server URL", "URL")
 94		if err != nil {
 95			return nil, err
 96		}
 97	}
 98
 99	project = params.Project
100	if project == "" {
101		project, err = prompt("JIRA project key", "project")
102		if err != nil {
103			return nil, err
104		}
105	}
106
107	credType, err := promptOptions(credTypeText, 1, 2)
108	if err != nil {
109		return nil, err
110	}
111
112	choice, err := promptOptions(credentialsText, 1, 3)
113	if err != nil {
114		return nil, err
115	}
116
117	if choice == 1 {
118		credentialsFile, err = prompt("Credentials file path", "path")
119		if err != nil {
120			return nil, err
121		}
122	}
123
124	username, err = prompt("JIRA username", "username")
125	if err != nil {
126		return nil, err
127	}
128
129	password, err = input.PromptPassword()
130	if err != nil {
131		return nil, err
132	}
133
134	jsonData, err := json.Marshal(
135		&SessionQuery{Username: username, Password: password})
136	if err != nil {
137		return nil, err
138	}
139
140	conf[core.ConfigKeyTarget] = target
141	conf[keyServer] = serverURL
142	conf[keyProject] = project
143
144	switch credType {
145	case 1:
146		conf[keyCredentialsType] = "SESSION"
147	case 2:
148		conf[keyCredentialsType] = "TOKEN"
149	}
150
151	switch choice {
152	case 1:
153		conf[keyCredentialsFile] = credentialsFile
154		err = ioutil.WriteFile(credentialsFile, jsonData, 0644)
155		if err != nil {
156			return nil, errors.Wrap(
157				err, fmt.Sprintf("Unable to write credentials to %s", credentialsFile))
158		}
159	case 2:
160		conf[keyUsername] = username
161		conf[keyPassword] = password
162	case 3:
163		conf[keyUsername] = username
164	}
165
166	err = g.ValidateConfig(conf)
167	if err != nil {
168		return nil, err
169	}
170
171	fmt.Printf("Attempting to login with credentials...\n")
172	client := NewClient(serverURL, nil)
173	err = client.Login(conf)
174	if err != nil {
175		return nil, err
176	}
177
178	// verify access to the project with credentials
179	fmt.Printf("Checking project ...\n")
180	_, err = client.GetProject(project)
181	if err != nil {
182		return nil, fmt.Errorf(
183			"Project %s doesn't exist on %s, or authentication credentials for (%s)"+
184				" are invalid",
185			project, serverURL, username)
186	}
187
188	fmt.Print(moreConfigText)
189	return conf, nil
190}
191
192// ValidateConfig returns true if all required keys are present
193func (*Jira) ValidateConfig(conf core.Configuration) error {
194	if v, ok := conf[core.ConfigKeyTarget]; !ok {
195		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
196	} else if v != target {
197		return fmt.Errorf("unexpected target name: %v", v)
198	}
199
200	if _, ok := conf[keyProject]; !ok {
201		return fmt.Errorf("missing %s key", keyProject)
202	}
203
204	return nil
205}
206
207func promptOptions(description string, minVal, maxVal int) (int, error) {
208	fmt.Print(description)
209	for {
210		fmt.Print("Select option: ")
211
212		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
213		fmt.Println()
214		if err != nil {
215			return -1, err
216		}
217
218		line = strings.TrimRight(line, "\n")
219
220		index, err := strconv.Atoi(line)
221		if err != nil {
222			fmt.Println("invalid input")
223			continue
224		}
225		if index < minVal || index > maxVal {
226			fmt.Println("invalid choice")
227			continue
228		}
229
230		return index, nil
231	}
232}
233
234func prompt(description, name string) (string, error) {
235	for {
236		fmt.Printf("%s: ", description)
237
238		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
239		if err != nil {
240			return "", err
241		}
242
243		line = strings.TrimRight(line, "\n")
244		if line == "" {
245			fmt.Printf("%s is empty\n", name)
246			continue
247		}
248
249		return line, nil
250	}
251}