config.go

  1package gitlab
  2
  3import (
  4	"fmt"
  5	"net/url"
  6	"path"
  7	"regexp"
  8	"strconv"
  9	"strings"
 10
 11	"github.com/pkg/errors"
 12	"github.com/xanzy/go-gitlab"
 13
 14	"github.com/MichaelMure/git-bug/bridge/core"
 15	"github.com/MichaelMure/git-bug/bridge/core/auth"
 16	"github.com/MichaelMure/git-bug/cache"
 17	"github.com/MichaelMure/git-bug/input"
 18	"github.com/MichaelMure/git-bug/repository"
 19)
 20
 21var (
 22	ErrBadProjectURL = errors.New("bad project url")
 23)
 24
 25func (g *Gitlab) ValidParams() map[string]interface{} {
 26	return map[string]interface{}{
 27		"URL":        nil,
 28		"BaseURL":    nil,
 29		"Login":      nil,
 30		"CredPrefix": nil,
 31		"TokenRaw":   nil,
 32	}
 33}
 34
 35func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
 36	var err error
 37	var baseUrl string
 38
 39	switch {
 40	case params.BaseURL != "":
 41		baseUrl = params.BaseURL
 42	default:
 43		baseUrl, err = input.PromptDefault("Gitlab server URL", "URL", defaultBaseURL, input.Required, input.IsURL)
 44		if err != nil {
 45			return nil, errors.Wrap(err, "base url prompt")
 46		}
 47	}
 48
 49	var projectURL string
 50
 51	// get project url
 52	switch {
 53	case params.URL != "":
 54		projectURL = params.URL
 55	default:
 56		// terminal prompt
 57		projectURL, err = promptProjectURL(repo, baseUrl)
 58		if err != nil {
 59			return nil, errors.Wrap(err, "url prompt")
 60		}
 61	}
 62
 63	if !strings.HasPrefix(projectURL, params.BaseURL) {
 64		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, projectURL)
 65	}
 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	case params.TokenRaw != "":
 82		token := auth.NewToken(target, params.TokenRaw)
 83		login, err = getLoginFromToken(baseUrl, token)
 84		if err != nil {
 85			return nil, err
 86		}
 87		token.SetMetadata(auth.MetaKeyLogin, login)
 88		token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
 89		cred = token
 90	default:
 91		login := params.Login
 92		if login == "" {
 93			// TODO: validate username
 94			login, err = input.Prompt("Gitlab login", "login", input.Required)
 95			if err != nil {
 96				return nil, err
 97			}
 98		}
 99		cred, err = promptTokenOptions(repo, login, baseUrl)
100		if err != nil {
101			return nil, err
102		}
103	}
104
105	token, ok := cred.(*auth.Token)
106	if !ok {
107		return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
108	}
109
110	// validate project url and get its ID
111	id, err := validateProjectURL(baseUrl, projectURL, token)
112	if err != nil {
113		return nil, errors.Wrap(err, "project validation")
114	}
115
116	conf := make(core.Configuration)
117	conf[core.ConfigKeyTarget] = target
118	conf[confKeyProjectID] = strconv.Itoa(id)
119	conf[confKeyGitlabBaseUrl] = baseUrl
120	conf[confKeyDefaultLogin] = login
121
122	err = g.ValidateConfig(conf)
123	if err != nil {
124		return nil, err
125	}
126
127	// don't forget to store the now known valid token
128	if !auth.IdExist(repo, cred.ID()) {
129		err = auth.Store(repo, cred)
130		if err != nil {
131			return nil, err
132		}
133	}
134
135	return conf, core.FinishConfig(repo, metaKeyGitlabLogin, login)
136}
137
138func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
139	if v, ok := conf[core.ConfigKeyTarget]; !ok {
140		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
141	} else if v != target {
142		return fmt.Errorf("unexpected target name: %v", v)
143	}
144	if _, ok := conf[confKeyGitlabBaseUrl]; !ok {
145		return fmt.Errorf("missing %s key", confKeyGitlabBaseUrl)
146	}
147	if _, ok := conf[confKeyProjectID]; !ok {
148		return fmt.Errorf("missing %s key", confKeyProjectID)
149	}
150	if _, ok := conf[confKeyDefaultLogin]; !ok {
151		return fmt.Errorf("missing %s key", confKeyDefaultLogin)
152	}
153
154	return nil
155}
156
157func promptTokenOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
158	creds, err := auth.List(repo,
159		auth.WithTarget(target),
160		auth.WithKind(auth.KindToken),
161		auth.WithMeta(auth.MetaKeyLogin, login),
162		auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
163	)
164	if err != nil {
165		return nil, err
166	}
167
168	cred, index, err := input.PromptCredential(target, "token", creds, []string{
169		"enter my token",
170	})
171	switch {
172	case err != nil:
173		return nil, err
174	case cred != nil:
175		return cred, nil
176	case index == 0:
177		return promptToken(baseUrl)
178	default:
179		panic("missed case")
180	}
181}
182
183func promptToken(baseUrl string) (*auth.Token, error) {
184	fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseUrl, "profile/personal_access_tokens"))
185	fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
186	fmt.Println()
187	fmt.Println("'api' access scope: to be able to make api calls")
188	fmt.Println()
189
190	re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}$`)
191	if err != nil {
192		panic("regexp compile:" + err.Error())
193	}
194
195	var login string
196
197	validator := func(name string, value string) (complaint string, err error) {
198		if !re.MatchString(value) {
199			return "token has incorrect format", nil
200		}
201		login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
202		if err != nil {
203			return fmt.Sprintf("token is invalid: %v", err), nil
204		}
205		return "", nil
206	}
207
208	rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
209	if err != nil {
210		return nil, err
211	}
212
213	token := auth.NewToken(target, rawToken)
214	token.SetMetadata(auth.MetaKeyLogin, login)
215	token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
216
217	return token, nil
218}
219
220func promptProjectURL(repo repository.RepoCommon, baseUrl string) (string, error) {
221	validRemotes, err := getValidGitlabRemoteURLs(repo, baseUrl)
222	if err != nil {
223		return "", err
224	}
225
226	return input.PromptURLWithRemote("Gitlab project URL", "URL", validRemotes, input.Required)
227}
228
229func getProjectPath(baseUrl, projectUrl string) (string, error) {
230	cleanUrl := strings.TrimSuffix(projectUrl, ".git")
231	cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
232	objectUrl, err := url.Parse(cleanUrl)
233	if err != nil {
234		return "", ErrBadProjectURL
235	}
236
237	objectBaseUrl, err := url.Parse(baseUrl)
238	if err != nil {
239		return "", ErrBadProjectURL
240	}
241
242	if objectUrl.Hostname() != objectBaseUrl.Hostname() {
243		return "", fmt.Errorf("base url and project url hostnames doesn't match")
244	}
245	return objectUrl.Path[1:], nil
246}
247
248func getValidGitlabRemoteURLs(repo repository.RepoCommon, baseUrl string) ([]string, error) {
249	remotes, err := repo.GetRemotes()
250	if err != nil {
251		return nil, err
252	}
253
254	urls := make([]string, 0, len(remotes))
255	for _, u := range remotes {
256		p, err := getProjectPath(baseUrl, u)
257		if err != nil {
258			continue
259		}
260
261		urls = append(urls, fmt.Sprintf("%s/%s", baseUrl, p))
262	}
263
264	return urls, nil
265}
266
267func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) {
268	projectPath, err := getProjectPath(baseUrl, url)
269	if err != nil {
270		return 0, err
271	}
272
273	client, err := buildClient(baseUrl, token)
274	if err != nil {
275		return 0, err
276	}
277
278	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
279	if err != nil {
280		return 0, errors.Wrap(err, "wrong token scope ou non-existent project")
281	}
282
283	return project.ID, nil
284}
285
286func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) {
287	client, err := buildClient(baseUrl, token)
288	if err != nil {
289		return "", err
290	}
291
292	user, _, err := client.Users.CurrentUser()
293	if err != nil {
294		return "", err
295	}
296	if user.Username == "" {
297		return "", fmt.Errorf("gitlab say username is empty")
298	}
299
300	return user.Username, nil
301}