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