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}