1package jira
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/MichaelMure/git-bug/bridge/core"
8 "github.com/MichaelMure/git-bug/bridge/core/auth"
9 "github.com/MichaelMure/git-bug/cache"
10 "github.com/MichaelMure/git-bug/input"
11 "github.com/MichaelMure/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/MichaelMure/git-bug/blob/master/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 }
37}
38
39// Configure sets up the bridge configuration
40func (j *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
41 var err error
42
43 baseURL := params.BaseURL
44 if baseURL == "" {
45 // terminal prompt
46 baseURL, err = input.Prompt("JIRA server URL", "URL", input.Required, input.IsURL)
47 if err != nil {
48 return nil, err
49 }
50 }
51
52 project := params.Project
53 if project == "" {
54 project, err = input.Prompt("JIRA project key", "project", input.Required)
55 if err != nil {
56 return nil, err
57 }
58 }
59
60 fmt.Println(credTypeText)
61 credTypeInput, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
62 if err != nil {
63 return nil, err
64 }
65 credType := []string{"SESSION", "TOKEN"}[credTypeInput]
66
67 var login string
68 var cred auth.Credential
69
70 switch {
71 case params.CredPrefix != "":
72 cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
73 if err != nil {
74 return nil, err
75 }
76 l, ok := cred.GetMetadata(auth.MetaKeyLogin)
77 if !ok {
78 return nil, fmt.Errorf("credential doesn't have a login")
79 }
80 login = l
81 default:
82 if params.Login == "" {
83 // TODO: validate username
84 login, err = input.Prompt("JIRA login", "login", input.Required)
85 } else {
86 // TODO: validate username
87 login = params.Login
88 }
89 if err != nil {
90 return nil, err
91 }
92 cred, err = promptCredOptions(repo, login, baseURL)
93 if err != nil {
94 return nil, err
95 }
96 }
97
98 conf := make(core.Configuration)
99 conf[core.ConfigKeyTarget] = target
100 conf[confKeyBaseUrl] = baseURL
101 conf[confKeyProject] = project
102 conf[confKeyCredentialType] = credType
103 conf[confKeyDefaultLogin] = login
104
105 err = j.ValidateConfig(conf)
106 if err != nil {
107 return nil, err
108 }
109
110 fmt.Printf("Attempting to login with credentials...\n")
111 client, err := buildClient(context.TODO(), baseURL, credType, cred)
112 if err != nil {
113 return nil, err
114 }
115
116 // verify access to the project with credentials
117 fmt.Printf("Checking project ...\n")
118 _, err = client.GetProject(project)
119 if err != nil {
120 return nil, fmt.Errorf(
121 "Project %s doesn't exist on %s, or authentication credentials for (%s)"+
122 " are invalid",
123 project, baseURL, login)
124 }
125
126 // don't forget to store the now known valid token
127 if !auth.IdExist(repo, cred.ID()) {
128 err = auth.Store(repo, cred)
129 if err != nil {
130 return nil, err
131 }
132 }
133
134 err = core.FinishConfig(repo, metaKeyJiraLogin, login)
135 if err != nil {
136 return nil, err
137 }
138
139 fmt.Print(moreConfigText)
140 return conf, nil
141}
142
143// ValidateConfig returns true if all required keys are present
144func (*Jira) ValidateConfig(conf core.Configuration) error {
145 if v, ok := conf[core.ConfigKeyTarget]; !ok {
146 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
147 } else if v != target {
148 return fmt.Errorf("unexpected target name: %v", v)
149 }
150 if _, ok := conf[confKeyBaseUrl]; !ok {
151 return fmt.Errorf("missing %s key", confKeyBaseUrl)
152 }
153 if _, ok := conf[confKeyProject]; !ok {
154 return fmt.Errorf("missing %s key", confKeyProject)
155 }
156 if _, ok := conf[confKeyCredentialType]; !ok {
157 return fmt.Errorf("missing %s key", confKeyCredentialType)
158 }
159 if _, ok := conf[confKeyDefaultLogin]; !ok {
160 return fmt.Errorf("missing %s key", confKeyDefaultLogin)
161 }
162
163 return nil
164}
165
166func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
167 creds, err := auth.List(repo,
168 auth.WithTarget(target),
169 auth.WithKind(auth.KindToken),
170 auth.WithMeta(auth.MetaKeyLogin, login),
171 auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
172 )
173 if err != nil {
174 return nil, err
175 }
176
177 cred, index, err := input.PromptCredential(target, "password", creds, []string{
178 "enter my password",
179 "ask my password each time",
180 })
181 switch {
182 case err != nil:
183 return nil, err
184 case cred != nil:
185 return cred, nil
186 case index == 0:
187 password, err := input.PromptPassword("Password", "password", input.Required)
188 if err != nil {
189 return nil, err
190 }
191 lp := auth.NewLoginPassword(target, login, password)
192 lp.SetMetadata(auth.MetaKeyLogin, login)
193 lp.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
194 return lp, nil
195 case index == 1:
196 l := auth.NewLogin(target, login)
197 l.SetMetadata(auth.MetaKeyLogin, login)
198 l.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
199 return l, nil
200 default:
201 panic("missed case")
202 }
203}