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