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