1package jira
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/git-bug/git-bug/bridge/core"
8 "github.com/git-bug/git-bug/bridge/core/auth"
9 "github.com/git-bug/git-bug/cache"
10 "github.com/git-bug/git-bug/commands/input"
11 "github.com/git-bug/git-bug/repository"
12)
13
14const moreConfigText = `
15NOTE: There are a few optional configuration values that you can additionally
16set in your git configuration to influence the behavior of the bridge. Please
17see the notes at:
18https://github.com/git-bug/git-bug/blob/trunk/doc/jira_bridge.md
19`
20
21const credTypeText = `
22JIRA has recently altered it's authentication strategies. Servers deployed
23prior to October 1st 2019 must use "SESSION" authentication, whereby the REST
24client logs in with an actual username and password, is assigned a session, and
25passes the session cookie with each request. JIRA Cloud and servers deployed
26after October 1st 2019 must use "TOKEN" authentication. You must create a user
27API token and the client will provide this along with your username with each
28request.`
29
30func (*Jira) ValidParams() map[string]interface{} {
31 return map[string]interface{}{
32 "BaseURL": nil,
33 "Login": nil,
34 "CredPrefix": nil,
35 "Project": nil,
36 "TokenRaw": nil,
37 }
38}
39
40// Configure sets up the bridge configuration
41func (j *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
42 var err error
43
44 baseURL := params.BaseURL
45 if baseURL == "" {
46 if !interactive {
47 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the JIRA server URL via the --base-url option.")
48 }
49 // terminal prompt
50 baseURL, err = input.Prompt("JIRA server URL", "URL", input.Required, input.IsURL)
51 if err != nil {
52 return nil, err
53 }
54 }
55
56 project := params.Project
57 if project == "" {
58 if !interactive {
59 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the JIRA project key via the --project option.")
60 }
61 project, err = input.Prompt("JIRA project key", "project", input.Required)
62 if err != nil {
63 return nil, err
64 }
65 }
66
67 var login string
68 var credType string
69 var cred auth.Credential
70
71 switch {
72 case params.CredPrefix != "":
73 cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
74 if err != nil {
75 return nil, err
76 }
77 l, ok := cred.GetMetadata(auth.MetaKeyLogin)
78 if !ok {
79 return nil, fmt.Errorf("credential doesn't have a login")
80 }
81 login = l
82 default:
83 if params.Login == "" {
84 if !interactive {
85 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
86 }
87 login, err = input.Prompt("JIRA login", "login", input.Required)
88 if err != nil {
89 return nil, err
90 }
91 } else {
92 login = params.Login
93 }
94 // TODO: validate username
95
96 if params.TokenRaw == "" {
97 if !interactive {
98 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
99 }
100 fmt.Println(credTypeText)
101 credTypeInput, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
102 if err != nil {
103 return nil, err
104 }
105 credType = []string{"SESSION", "TOKEN"}[credTypeInput]
106 cred, err = promptCredOptions(repo, login, baseURL)
107 if err != nil {
108 return nil, err
109 }
110 } else {
111 credType = "TOKEN"
112 }
113 }
114
115 conf := make(core.Configuration)
116 conf[core.ConfigKeyTarget] = target
117 conf[confKeyBaseUrl] = baseURL
118 conf[confKeyProject] = project
119 conf[confKeyCredentialType] = credType
120 conf[confKeyDefaultLogin] = login
121
122 err = j.ValidateConfig(conf)
123 if err != nil {
124 return nil, err
125 }
126
127 fmt.Printf("Attempting to login with credentials...\n")
128 client, err := buildClient(context.TODO(), baseURL, credType, cred)
129 if err != nil {
130 return nil, err
131 }
132
133 // verify access to the project with credentials
134 fmt.Printf("Checking project ...\n")
135 _, err = client.GetProject(project)
136 if err != nil {
137 return nil, fmt.Errorf(
138 "Project %s doesn't exist on %s, or authentication credentials for (%s)"+
139 " are invalid",
140 project, baseURL, login)
141 }
142
143 // don't forget to store the now known valid token
144 if !auth.IdExist(repo, cred.ID()) {
145 err = auth.Store(repo, cred)
146 if err != nil {
147 return nil, err
148 }
149 }
150
151 err = core.FinishConfig(repo, metaKeyJiraLogin, login)
152 if err != nil {
153 return nil, err
154 }
155
156 fmt.Print(moreConfigText)
157 return conf, nil
158}
159
160// ValidateConfig returns true if all required keys are present
161func (*Jira) ValidateConfig(conf core.Configuration) error {
162 if v, ok := conf[core.ConfigKeyTarget]; !ok {
163 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
164 } else if v != target {
165 return fmt.Errorf("unexpected target name: %v", v)
166 }
167 if _, ok := conf[confKeyBaseUrl]; !ok {
168 return fmt.Errorf("missing %s key", confKeyBaseUrl)
169 }
170 if _, ok := conf[confKeyProject]; !ok {
171 return fmt.Errorf("missing %s key", confKeyProject)
172 }
173 if _, ok := conf[confKeyCredentialType]; !ok {
174 return fmt.Errorf("missing %s key", confKeyCredentialType)
175 }
176 if _, ok := conf[confKeyDefaultLogin]; !ok {
177 return fmt.Errorf("missing %s key", confKeyDefaultLogin)
178 }
179
180 return nil
181}
182
183func promptCredOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) {
184 creds, err := auth.List(repo,
185 auth.WithTarget(target),
186 auth.WithKind(auth.KindToken),
187 auth.WithMeta(auth.MetaKeyLogin, login),
188 auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
189 )
190 if err != nil {
191 return nil, err
192 }
193
194 cred, index, err := input.PromptCredential(target, "password", creds, []string{
195 "enter my password",
196 "ask my password each time",
197 })
198 switch {
199 case err != nil:
200 return nil, err
201 case cred != nil:
202 return cred, nil
203 case index == 0:
204 password, err := input.PromptPassword("Password", "password", input.Required)
205 if err != nil {
206 return nil, err
207 }
208 lp := auth.NewLoginPassword(target, login, password)
209 lp.SetMetadata(auth.MetaKeyLogin, login)
210 lp.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
211 return lp, nil
212 case index == 1:
213 l := auth.NewLogin(target, login)
214 l.SetMetadata(auth.MetaKeyLogin, login)
215 l.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
216 return l, nil
217 default:
218 panic("missed case")
219 }
220}