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