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.RepoKeyring, 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}