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}