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 login = params.Login
83 if login == "" {
84 // TODO: validate username
85 login, err = input.Prompt("JIRA login", "login", input.Required)
86 if err != nil {
87 return nil, err
88 }
89 }
90 cred, err = promptCredOptions(repo, login, baseURL)
91 if err != nil {
92 return nil, err
93 }
94 }
95
96 conf := make(core.Configuration)
97 conf[core.ConfigKeyTarget] = target
98 conf[confKeyBaseUrl] = baseURL
99 conf[confKeyProject] = project
100 conf[confKeyCredentialType] = credType
101 conf[confKeyDefaultLogin] = login
102
103 err = j.ValidateConfig(conf)
104 if err != nil {
105 return nil, err
106 }
107
108 fmt.Printf("Attempting to login with credentials...\n")
109 client, err := buildClient(context.TODO(), baseURL, credType, cred)
110 if err != nil {
111 return nil, err
112 }
113
114 // verify access to the project with credentials
115 fmt.Printf("Checking project ...\n")
116 _, err = client.GetProject(project)
117 if err != nil {
118 return nil, fmt.Errorf(
119 "Project %s doesn't exist on %s, or authentication credentials for (%s)"+
120 " are invalid",
121 project, baseURL, login)
122 }
123
124 // don't forget to store the now known valid token
125 if !auth.IdExist(repo, cred.ID()) {
126 err = auth.Store(repo, cred)
127 if err != nil {
128 return nil, err
129 }
130 }
131
132 err = core.FinishConfig(repo, metaKeyJiraLogin, login)
133 if err != nil {
134 return nil, err
135 }
136
137 fmt.Print(moreConfigText)
138 return conf, nil
139}
140
141// ValidateConfig returns true if all required keys are present
142func (*Jira) ValidateConfig(conf core.Configuration) error {
143 if v, ok := conf[core.ConfigKeyTarget]; !ok {
144 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
145 } else if v != target {
146 return fmt.Errorf("unexpected target name: %v", v)
147 }
148 if _, ok := conf[confKeyBaseUrl]; !ok {
149 return fmt.Errorf("missing %s key", confKeyBaseUrl)
150 }
151 if _, ok := conf[confKeyProject]; !ok {
152 return fmt.Errorf("missing %s key", confKeyProject)
153 }
154 if _, ok := conf[confKeyCredentialType]; !ok {
155 return fmt.Errorf("missing %s key", confKeyCredentialType)
156 }
157 if _, ok := conf[confKeyDefaultLogin]; !ok {
158 return fmt.Errorf("missing %s key", confKeyDefaultLogin)
159 }
160
161 return nil
162}
163
164func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
165 creds, err := auth.List(repo,
166 auth.WithTarget(target),
167 auth.WithKind(auth.KindToken),
168 auth.WithMeta(auth.MetaKeyLogin, login),
169 auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
170 )
171 if err != nil {
172 return nil, err
173 }
174
175 cred, index, err := input.PromptCredential(target, "password", creds, []string{
176 "enter my password",
177 "ask my password each time",
178 })
179 switch {
180 case err != nil:
181 return nil, err
182 case cred != nil:
183 return cred, nil
184 case index == 0:
185 password, err := input.PromptPassword("Password", "password", input.Required)
186 if err != nil {
187 return nil, err
188 }
189 lp := auth.NewLoginPassword(target, login, password)
190 lp.SetMetadata(auth.MetaKeyLogin, login)
191 lp.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
192 return lp, nil
193 case index == 1:
194 l := auth.NewLogin(target, login)
195 l.SetMetadata(auth.MetaKeyLogin, login)
196 l.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
197 return l, nil
198 default:
199 panic("missed case")
200 }
201}