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}