config.go

  1package gitlab
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"net/url"
  7	"os"
  8	"regexp"
  9	"sort"
 10	"strconv"
 11	"strings"
 12	"time"
 13
 14	text "github.com/MichaelMure/go-term-text"
 15	"github.com/pkg/errors"
 16	"github.com/xanzy/go-gitlab"
 17
 18	"github.com/MichaelMure/git-bug/bridge/core"
 19	"github.com/MichaelMure/git-bug/bridge/core/auth"
 20	"github.com/MichaelMure/git-bug/cache"
 21	"github.com/MichaelMure/git-bug/entity"
 22	"github.com/MichaelMure/git-bug/repository"
 23	"github.com/MichaelMure/git-bug/util/colors"
 24)
 25
 26var (
 27	ErrBadProjectURL = errors.New("bad project url")
 28)
 29
 30func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
 31	if params.Project != "" {
 32		fmt.Println("warning: --project is ineffective for a gitlab bridge")
 33	}
 34	if params.Owner != "" {
 35		fmt.Println("warning: --owner is ineffective for a gitlab bridge")
 36	}
 37
 38	conf := make(core.Configuration)
 39	var err error
 40
 41	if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
 42		return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
 43	}
 44
 45	if params.URL == "" {
 46		params.URL = defaultBaseURL
 47	}
 48
 49	var url string
 50
 51	// get project url
 52	switch {
 53	case params.URL != "":
 54		url = params.URL
 55	default:
 56		// terminal prompt
 57		url, err = promptURL(repo)
 58		if err != nil {
 59			return nil, errors.Wrap(err, "url prompt")
 60		}
 61	}
 62
 63	if !strings.HasPrefix(url, params.BaseURL) {
 64		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
 65	}
 66
 67	user, err := repo.GetUserIdentity()
 68	if err != nil {
 69		return nil, err
 70	}
 71
 72	var cred auth.Credential
 73
 74	switch {
 75	case params.CredPrefix != "":
 76		cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
 77		if err != nil {
 78			return nil, err
 79		}
 80		if cred.UserId() != user.Id() {
 81			return nil, fmt.Errorf("selected credential don't match the user")
 82		}
 83	case params.TokenRaw != "":
 84		cred = auth.NewToken(user.Id(), params.TokenRaw, target)
 85	default:
 86		cred, err = promptTokenOptions(repo, user.Id())
 87		if err != nil {
 88			return nil, err
 89		}
 90	}
 91
 92	token, ok := cred.(*auth.Token)
 93	if !ok {
 94		return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
 95	}
 96
 97	// validate project url and get its ID
 98	id, err := validateProjectURL(params.BaseURL, url, token)
 99	if err != nil {
100		return nil, errors.Wrap(err, "project validation")
101	}
102
103	conf[core.ConfigKeyTarget] = target
104	conf[keyProjectID] = strconv.Itoa(id)
105	conf[keyGitlabBaseUrl] = params.BaseURL
106
107	err = g.ValidateConfig(conf)
108	if err != nil {
109		return nil, err
110	}
111
112	// don't forget to store the now known valid token
113	if !auth.IdExist(repo, cred.ID()) {
114		err = auth.Store(repo, cred)
115		if err != nil {
116			return nil, err
117		}
118	}
119
120	return conf, nil
121}
122
123func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
124	if v, ok := conf[core.ConfigKeyTarget]; !ok {
125		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
126	} else if v != target {
127		return fmt.Errorf("unexpected target name: %v", v)
128	}
129
130	if _, ok := conf[keyProjectID]; !ok {
131		return fmt.Errorf("missing %s key", keyProjectID)
132	}
133
134	return nil
135}
136
137func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) {
138	for {
139		creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
140		if err != nil {
141			return nil, err
142		}
143
144		// if we don't have existing token, fast-track to the token prompt
145		if len(creds) == 0 {
146			value, err := promptToken()
147			if err != nil {
148				return nil, err
149			}
150			return auth.NewToken(userId, value, target), nil
151		}
152
153		fmt.Println()
154		fmt.Println("[1]: enter my token")
155
156		fmt.Println()
157		fmt.Println("Existing tokens for Gitlab:")
158
159		sort.Sort(auth.ById(creds))
160		for i, cred := range creds {
161			token := cred.(*auth.Token)
162			fmt.Printf("[%d]: %s => %s (%s)\n",
163				i+2,
164				colors.Cyan(token.ID().Human()),
165				colors.Red(text.TruncateMax(token.Value, 10)),
166				token.CreateTime().Format(time.RFC822),
167			)
168		}
169
170		fmt.Println()
171		fmt.Print("Select option: ")
172
173		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
174		fmt.Println()
175		if err != nil {
176			return nil, err
177		}
178
179		line = strings.TrimSpace(line)
180		index, err := strconv.Atoi(line)
181		if err != nil || index < 1 || index > len(creds)+1 {
182			fmt.Println("invalid input")
183			continue
184		}
185
186		switch index {
187		case 1:
188			value, err := promptToken()
189			if err != nil {
190				return nil, err
191			}
192			return auth.NewToken(userId, value, target), nil
193		default:
194			return creds[index-2], nil
195		}
196	}
197}
198
199func promptToken() (string, error) {
200	fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
201	fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
202	fmt.Println()
203	fmt.Println("'api' access scope: to be able to make api calls")
204	fmt.Println()
205
206	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
207	if err != nil {
208		panic("regexp compile:" + err.Error())
209	}
210
211	for {
212		fmt.Print("Enter token: ")
213
214		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
215		if err != nil {
216			return "", err
217		}
218
219		token := strings.TrimSpace(line)
220		if re.MatchString(token) {
221			return token, nil
222		}
223
224		fmt.Println("token format is invalid")
225	}
226}
227
228func promptURL(repo repository.RepoCommon) (string, error) {
229	// remote suggestions
230	remotes, err := repo.GetRemotes()
231	if err != nil {
232		return "", errors.Wrap(err, "getting remotes")
233	}
234
235	validRemotes := getValidGitlabRemoteURLs(remotes)
236	if len(validRemotes) > 0 {
237		for {
238			fmt.Println("\nDetected projects:")
239
240			// print valid remote gitlab urls
241			for i, remote := range validRemotes {
242				fmt.Printf("[%d]: %v\n", i+1, remote)
243			}
244
245			fmt.Printf("\n[0]: Another project\n\n")
246			fmt.Printf("Select option: ")
247
248			line, err := bufio.NewReader(os.Stdin).ReadString('\n')
249			if err != nil {
250				return "", err
251			}
252
253			line = strings.TrimSpace(line)
254
255			index, err := strconv.Atoi(line)
256			if err != nil || index < 0 || index > len(validRemotes) {
257				fmt.Println("invalid input")
258				continue
259			}
260
261			// if user want to enter another project url break this loop
262			if index == 0 {
263				break
264			}
265
266			return validRemotes[index-1], nil
267		}
268	}
269
270	// manually enter gitlab url
271	for {
272		fmt.Print("Gitlab project URL: ")
273
274		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
275		if err != nil {
276			return "", err
277		}
278
279		url := strings.TrimSpace(line)
280		if url == "" {
281			fmt.Println("URL is empty")
282			continue
283		}
284
285		return url, nil
286	}
287}
288
289func getProjectPath(projectUrl string) (string, error) {
290	cleanUrl := strings.TrimSuffix(projectUrl, ".git")
291	cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
292	objectUrl, err := url.Parse(cleanUrl)
293	if err != nil {
294		return "", ErrBadProjectURL
295	}
296
297	return objectUrl.Path[1:], nil
298}
299
300func getValidGitlabRemoteURLs(remotes map[string]string) []string {
301	urls := make([]string, 0, len(remotes))
302	for _, u := range remotes {
303		path, err := getProjectPath(u)
304		if err != nil {
305			continue
306		}
307
308		urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
309	}
310
311	return urls
312}
313
314func validateProjectURL(baseURL, url string, token *auth.Token) (int, error) {
315	projectPath, err := getProjectPath(url)
316	if err != nil {
317		return 0, err
318	}
319
320	client, err := buildClient(baseURL, token)
321	if err != nil {
322		return 0, err
323	}
324
325	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
326	if err != nil {
327		return 0, err
328	}
329
330	return project.ID, nil
331}