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 err = core.FinishConfig(repo, metaKeyJiraLogin, login)
124 if err != nil {
125 return nil, err
126 }
127
128 fmt.Print(moreConfigText)
129 return conf, nil
130}
131
132// ValidateConfig returns true if all required keys are present
133func (*Jira) ValidateConfig(conf core.Configuration) error {
134 if v, ok := conf[core.ConfigKeyTarget]; !ok {
135 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
136 } else if v != target {
137 return fmt.Errorf("unexpected target name: %v", v)
138 }
139
140 if _, ok := conf[confKeyProject]; !ok {
141 return fmt.Errorf("missing %s key", confKeyProject)
142 }
143
144 return nil
145}
146
147func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
148 creds, err := auth.List(repo,
149 auth.WithTarget(target),
150 auth.WithKind(auth.KindToken),
151 auth.WithMeta(auth.MetaKeyLogin, login),
152 auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
153 )
154 if err != nil {
155 return nil, err
156 }
157
158 cred, index, err := input.PromptCredential(target, "password", creds, []string{
159 "enter my password",
160 "ask my password each time",
161 })
162 switch {
163 case err != nil:
164 return nil, err
165 case cred != nil:
166 return cred, nil
167 case index == 0:
168 password, err := input.PromptPassword("Password", "password", input.Required)
169 if err != nil {
170 return nil, err
171 }
172 lp := auth.NewLoginPassword(target, login, password)
173 lp.SetMetadata(auth.MetaKeyLogin, login)
174 return lp, nil
175 case index == 1:
176 l := auth.NewLogin(target, login)
177 l.SetMetadata(auth.MetaKeyLogin, login)
178 return l, nil
179 default:
180 panic("missed case")
181 }
182}