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