config.go

  1package gitlab
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	neturl "net/url"
  7	"os"
  8	"regexp"
  9	"strconv"
 10	"strings"
 11	"syscall"
 12	"time"
 13
 14	"github.com/pkg/errors"
 15	"github.com/xanzy/go-gitlab"
 16	"golang.org/x/crypto/ssh/terminal"
 17
 18	"github.com/MichaelMure/git-bug/bridge/core"
 19	"github.com/MichaelMure/git-bug/repository"
 20)
 21
 22const (
 23	target      = "gitlab"
 24	gitlabV4Url = "https://gitlab.com/api/v4"
 25	keyID       = "project-id"
 26	keyTarget   = "target"
 27	keyToken    = "token"
 28
 29	defaultTimeout = 60 * time.Second
 30)
 31
 32var (
 33	ErrBadProjectURL = errors.New("bad project url")
 34)
 35
 36func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
 37	if params.Project != "" {
 38		fmt.Println("warning: --project is ineffective for a gitlab bridge")
 39	}
 40	if params.Owner != "" {
 41		fmt.Println("warning: --owner is ineffective for a gitlab bridge")
 42	}
 43
 44	conf := make(core.Configuration)
 45	var err error
 46	var url string
 47	var token string
 48
 49	// get project url
 50	if params.URL != "" {
 51		url = params.URL
 52
 53	} else {
 54		// remote suggestions
 55		remotes, err := repo.GetRemotes()
 56		if err != nil {
 57			return nil, err
 58		}
 59
 60		// terminal prompt
 61		url, err = promptURL(remotes)
 62		if err != nil {
 63			return nil, err
 64		}
 65	}
 66
 67	// get user token
 68	if params.Token != "" {
 69		token = params.Token
 70	} else {
 71		token, err = promptTokenOptions(url)
 72		if err != nil {
 73			return nil, err
 74		}
 75	}
 76
 77	var ok bool
 78	// validate project url and get it ID
 79	ok, id, err := validateProjectURL(url, token)
 80	if err != nil {
 81		return nil, err
 82	}
 83	if !ok {
 84		return nil, fmt.Errorf("invalid project id or wrong token scope")
 85	}
 86
 87	conf[keyID] = strconv.Itoa(id)
 88	conf[keyToken] = token
 89	conf[keyTarget] = target
 90
 91	return conf, nil
 92}
 93
 94func (*Gitlab) ValidateConfig(conf core.Configuration) error {
 95	if v, ok := conf[keyTarget]; !ok {
 96		return fmt.Errorf("missing %s key", keyTarget)
 97	} else if v != target {
 98		return fmt.Errorf("unexpected target name: %v", v)
 99	}
100
101	if _, ok := conf[keyToken]; !ok {
102		return fmt.Errorf("missing %s key", keyToken)
103	}
104
105	if _, ok := conf[keyID]; !ok {
106		return fmt.Errorf("missing %s key", keyID)
107	}
108
109	return nil
110}
111
112func requestToken(client *gitlab.Client, userID int, name string, scopes ...string) (string, error) {
113	impToken, _, err := client.Users.CreateImpersonationToken(
114		userID,
115		&gitlab.CreateImpersonationTokenOptions{
116			Name:   &name,
117			Scopes: &scopes,
118		},
119	)
120	if err != nil {
121		return "", err
122	}
123
124	return impToken.Token, nil
125}
126
127func promptTokenOptions(url string) (string, error) {
128	for {
129		fmt.Println()
130		fmt.Println("[1]: user provided token")
131		fmt.Println("[2]: interactive token creation")
132		fmt.Print("Select option: ")
133
134		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
135		fmt.Println()
136		if err != nil {
137			return "", err
138		}
139
140		line = strings.TrimRight(line, "\n")
141
142		index, err := strconv.Atoi(line)
143		if err != nil || (index != 1 && index != 2) {
144			fmt.Println("invalid input")
145			continue
146		}
147
148		if index == 1 {
149			return promptToken()
150		}
151
152		return loginAndRequestToken(url)
153	}
154}
155
156func promptToken() (string, error) {
157	fmt.Println("You can generate a new token by visiting https://gitlab.com/settings/tokens.")
158	fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
159	fmt.Println()
160	fmt.Println("'api' scope access : access scope: to be able to make api calls")
161	fmt.Println()
162
163	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
164	if err != nil {
165		panic("regexp compile:" + err.Error())
166	}
167
168	for {
169		fmt.Print("Enter token: ")
170
171		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
172		if err != nil {
173			return "", err
174		}
175
176		token := strings.TrimRight(line, "\n")
177		if re.MatchString(token) {
178			return token, nil
179		}
180
181		fmt.Println("token is invalid")
182	}
183}
184
185func loginAndRequestToken(url string) (string, error) {
186	username, err := promptUsername()
187	if err != nil {
188		return "", err
189	}
190
191	password, err := promptPassword()
192	if err != nil {
193		return "", err
194	}
195
196	// Attempt to authenticate and create a token
197
198	note := fmt.Sprintf("git-bug - %s", url)
199
200	ok, id, err := validateUsername(username)
201	if err != nil {
202		return "", err
203	}
204	if !ok {
205		return "", fmt.Errorf("invalid username")
206	}
207
208	client, err := buildClientFromUsernameAndPassword(username, password)
209	if err != nil {
210		return "", err
211	}
212
213	fmt.Println(username, password)
214
215	return requestToken(client, id, note, "api")
216}
217
218func promptUsername() (string, error) {
219	for {
220		fmt.Print("username: ")
221
222		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
223		if err != nil {
224			return "", err
225		}
226
227		line = strings.TrimRight(line, "\n")
228
229		ok, _, err := validateUsername(line)
230		if err != nil {
231			return "", err
232		}
233		if ok {
234			return line, nil
235		}
236
237		fmt.Println("invalid username")
238	}
239}
240
241func promptURL(remotes map[string]string) (string, error) {
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.TrimRight(line, "\n")
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.TrimRight(line, "\n")
287		if line == "" {
288			fmt.Println("URL is empty")
289			continue
290		}
291
292		return url, nil
293	}
294}
295
296func getProjectPath(url string) (string, error) {
297
298	cleanUrl := strings.TrimSuffix(url, ".git")
299	objectUrl, err := neturl.Parse(cleanUrl)
300	if err != nil {
301		return "", nil
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 validateUsername(username string) (bool, int, error) {
322	// no need for a token for this action
323	client := buildClient("")
324
325	users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &username})
326	if err != nil {
327		return false, 0, err
328	}
329
330	if len(users) == 0 {
331		return false, 0, fmt.Errorf("username not found")
332	} else if len(users) > 1 {
333		return false, 0, fmt.Errorf("found multiple matches")
334	}
335
336	if users[0].Username == username {
337		return true, users[0].ID, nil
338	}
339
340	return false, 0, nil
341}
342
343func validateProjectURL(url, token string) (bool, int, error) {
344	client := buildClient(token)
345
346	projectPath, err := getProjectPath(url)
347	if err != nil {
348		return false, 0, err
349	}
350
351	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
352	if err != nil {
353		return false, 0, err
354	}
355
356	return true, project.ID, nil
357}
358
359func promptPassword() (string, error) {
360	for {
361		fmt.Print("password: ")
362
363		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
364		// new line for coherent formatting, ReadPassword clip the normal new line
365		// entered by the user
366		fmt.Println()
367
368		if err != nil {
369			return "", err
370		}
371
372		if len(bytePassword) > 0 {
373			return string(bytePassword), nil
374		}
375
376		fmt.Println("password is empty")
377	}
378}