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/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		"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}