bridge/gitlab: bridge project validation

Amine Hilaly created

bridge/gitlab: token generation

Change summary

bridge/gitlab/config.go | 230 ++++++++----------------------------------
bridge/gitlab/export.go |  11 ++
bridge/gitlab/gitlab.go |   5 
3 files changed, 61 insertions(+), 185 deletions(-)

Detailed changes

bridge/gitlab/config.go 🔗

@@ -2,13 +2,7 @@ package gitlab
 
 import (
 	"bufio"
-	"bytes"
-	"encoding/json"
 	"fmt"
-	"io"
-	"io/ioutil"
-	"math/rand"
-	"net/http"
 	neturl "net/url"
 	"os"
 	"regexp"
@@ -28,15 +22,13 @@ import (
 const (
 	target      = "gitlab"
 	gitlabV4Url = "https://gitlab.com/api/v4"
-	keyID       = "id"
+	keyID       = "project-id"
 	keyTarget   = "target"
 	keyToken    = "token"
 
 	defaultTimeout = 60 * time.Second
 )
 
-//note to my self: bridge configure --target=gitlab --url=$URL
-
 var (
 	ErrBadProjectURL = errors.New("bad project url")
 )
@@ -53,7 +45,6 @@ func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (
 	var err error
 	var url string
 	var token string
-	var projectID string
 
 	// get project url
 	if params.URL != "" {
@@ -85,7 +76,7 @@ func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (
 
 	var ok bool
 	// validate project url and get it ID
-	ok, projectID, err = validateProjectURL(url, token)
+	ok, id, err := validateProjectURL(url, token)
 	if err != nil {
 		return nil, err
 	}
@@ -93,7 +84,7 @@ func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (
 		return nil, fmt.Errorf("invalid project id or wrong token scope")
 	}
 
-	conf[keyID] = projectID
+	conf[keyID] = strconv.Itoa(id)
 	conf[keyToken] = token
 	conf[keyTarget] = target
 
@@ -118,75 +109,19 @@ func (*Gitlab) ValidateConfig(conf core.Configuration) error {
 	return nil
 }
 
-func requestToken(note, username, password string, scope string) (*http.Response, error) {
-	return requestTokenWith2FA(note, username, password, "", scope)
-}
-
-//TODO: FIX THIS ONE
-func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) {
-	url := fmt.Sprintf("%s/authorizations", gitlabV4Url)
-	params := struct {
-		Scopes      []string `json:"scopes"`
-		Note        string   `json:"note"`
-		Fingerprint string   `json:"fingerprint"`
-	}{
-		Scopes:      []string{scope},
-		Note:        note,
-		Fingerprint: randomFingerprint(),
-	}
-
-	data, err := json.Marshal(params)
-	if err != nil {
-		return nil, err
-	}
-
-	req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
-	if err != nil {
-		return nil, err
-	}
-
-	req.SetBasicAuth(username, password)
-	req.Header.Set("Content-Type", "application/json")
-
-	if otpCode != "" {
-		req.Header.Set("X-GitHub-OTP", otpCode)
-	}
-
-	client := &http.Client{
-		Timeout: defaultTimeout,
-	}
-
-	return client.Do(req)
-}
-
-func decodeBody(body io.ReadCloser) (string, error) {
-	data, _ := ioutil.ReadAll(body)
-
-	aux := struct {
-		Token string `json:"token"`
-	}{}
-
-	err := json.Unmarshal(data, &aux)
+func requestToken(client *gitlab.Client, userID int, name string, scopes ...string) (string, error) {
+	impToken, _, err := client.Users.CreateImpersonationToken(
+		userID,
+		&gitlab.CreateImpersonationTokenOptions{
+			Name:   &name,
+			Scopes: &scopes,
+		},
+	)
 	if err != nil {
 		return "", err
 	}
 
-	if aux.Token == "" {
-		return "", fmt.Errorf("no token found in response: %s", string(data))
-	}
-
-	return aux.Token, nil
-}
-
-func randomFingerprint() string {
-	// Doesn't have to be crypto secure, it's just to avoid token collision
-	rand.Seed(time.Now().UnixNano())
-	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
-	b := make([]rune, 32)
-	for i := range b {
-		b[i] = letterRunes[rand.Intn(len(letterRunes))]
-	}
-	return string(b)
+	return impToken.Token, nil
 }
 
 func promptTokenOptions(url string) (string, error) {
@@ -222,11 +157,7 @@ func promptToken() (string, error) {
 	fmt.Println("You can generate a new token by visiting https://gitlab.com/settings/tokens.")
 	fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
 	fmt.Println()
-	fmt.Println("The access scope depend on the type of repository.")
-	fmt.Println("Public:")
-	fmt.Println("  - 'public_repo': to be able to read public repositories")
-	fmt.Println("Private:")
-	fmt.Println("  - 'repo'       : to be able to read private repositories")
+	fmt.Println("'api' scope access : access scope: to be able to make api calls")
 	fmt.Println()
 
 	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
@@ -251,15 +182,7 @@ func promptToken() (string, error) {
 	}
 }
 
-// TODO: FIX THIS ONE TOO
 func loginAndRequestToken(url string) (string, error) {
-
-	// prompt project visibility to know the token scope needed for the repository
-	isPublic, err := promptProjectVisibility()
-	if err != nil {
-		return "", err
-	}
-
 	username, err := promptUsername()
 	if err != nil {
 		return "", err
@@ -270,50 +193,26 @@ func loginAndRequestToken(url string) (string, error) {
 		return "", err
 	}
 
-	var scope string
-	//TODO: Gitlab scopes
-	if isPublic {
-		// public_repo is requested to be able to read public repositories
-		scope = "public_repo"
-	} else {
-		// 'repo' is request to be able to read private repositories
-		// /!\ token will have read/write rights on every private repository you have access to
-		scope = "repo"
-	}
-
 	// Attempt to authenticate and create a token
 
-	note := fmt.Sprintf("git-bug - %s/%s", url)
+	note := fmt.Sprintf("git-bug - %s", url)
 
-	resp, err := requestToken(note, username, password, scope)
+	ok, id, err := validateUsername(username)
 	if err != nil {
 		return "", err
 	}
-
-	defer resp.Body.Close()
-
-	// Handle 2FA is needed
-	OTPHeader := resp.Header.Get("X-GitHub-OTP")
-	if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
-		otpCode, err := prompt2FA()
-		if err != nil {
-			return "", err
-		}
-
-		resp, err = requestTokenWith2FA(note, username, password, otpCode, scope)
-		if err != nil {
-			return "", err
-		}
-
-		defer resp.Body.Close()
+	if !ok {
+		return "", fmt.Errorf("invalid username")
 	}
 
-	if resp.StatusCode == http.StatusCreated {
-		return decodeBody(resp.Body)
+	client, err := buildClientFromUsernameAndPassword(username, password)
+	if err != nil {
+		return "", err
 	}
 
-	b, _ := ioutil.ReadAll(resp.Body)
-	return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b))
+	fmt.Println(username, password)
+
+	return requestToken(client, id, note, "api")
 }
 
 func promptUsername() (string, error) {
@@ -327,7 +226,7 @@ func promptUsername() (string, error) {
 
 		line = strings.TrimRight(line, "\n")
 
-		ok, err := validateUsername(line)
+		ok, _, err := validateUsername(line)
 		if err != nil {
 			return "", err
 		}
@@ -394,63 +293,67 @@ func promptURL(remotes map[string]string) (string, error) {
 	}
 }
 
-func splitURL(url string) (string, string, error) {
+func getProjectPath(url string) (string, error) {
+
 	cleanUrl := strings.TrimSuffix(url, ".git")
 	objectUrl, err := neturl.Parse(cleanUrl)
 	if err != nil {
-		return "", "", nil
+		return "", nil
 	}
 
-	return fmt.Sprintf("%s%s", objectUrl.Host, objectUrl.Path), objectUrl.Path, nil
+	return objectUrl.Path[1:], nil
 }
 
 func getValidGitlabRemoteURLs(remotes map[string]string) []string {
 	urls := make([]string, 0, len(remotes))
 	for _, u := range remotes {
-		url, _, err := splitURL(u)
+		path, err := getProjectPath(u)
 		if err != nil {
 			continue
 		}
 
-		urls = append(urls, url)
+		urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
 	}
 
 	return urls
 }
 
-func validateUsername(username string) (bool, error) {
+func validateUsername(username string) (bool, int, error) {
 	// no need for a token for this action
 	client := buildClient("")
 
 	users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &username})
 	if err != nil {
-		return false, err
+		return false, 0, err
 	}
 
 	if len(users) == 0 {
-		return false, fmt.Errorf("username not found")
+		return false, 0, fmt.Errorf("username not found")
 	} else if len(users) > 1 {
-		return false, fmt.Errorf("found multiple matches")
+		return false, 0, fmt.Errorf("found multiple matches")
+	}
+
+	if users[0].Username == username {
+		return true, users[0].ID, nil
 	}
 
-	return users[0].Username == username, nil
+	return false, 0, nil
 }
 
-func validateProjectURL(url, token string) (bool, string, error) {
+func validateProjectURL(url, token string) (bool, int, error) {
 	client := buildClient(token)
 
-	_, projectPath, err := splitURL(url)
+	projectPath, err := getProjectPath(url)
 	if err != nil {
-		return false, "", err
+		return false, 0, err
 	}
 
-	project, _, err := client.Projects.GetProject(projectPath[1:], &gitlab.GetProjectOptions{})
+	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
 	if err != nil {
-		return false, "", err
+		return false, 0, err
 	}
-	projectID := strconv.Itoa(project.ID)
 
-	return true, projectID, nil
+	return true, project.ID, nil
 }
 
 func promptPassword() (string, error) {
@@ -473,46 +376,3 @@ func promptPassword() (string, error) {
 		fmt.Println("password is empty")
 	}
 }
-
-func prompt2FA() (string, error) {
-	for {
-		fmt.Print("two-factor authentication code: ")
-
-		byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
-		fmt.Println()
-		if err != nil {
-			return "", err
-		}
-
-		if len(byte2fa) > 0 {
-			return string(byte2fa), nil
-		}
-
-		fmt.Println("code is empty")
-	}
-}
-
-func promptProjectVisibility() (bool, error) {
-	for {
-		fmt.Println("[1]: public")
-		fmt.Println("[2]: private")
-		fmt.Print("repository visibility: ")
-
-		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
-		fmt.Println()
-		if err != nil {
-			return false, err
-		}
-
-		line = strings.TrimRight(line, "\n")
-
-		index, err := strconv.Atoi(line)
-		if err != nil || (index != 0 && index != 1) {
-			fmt.Println("invalid input")
-			continue
-		}
-
-		// return true for public repositories, false for private
-		return index == 0, nil
-	}
-}

bridge/gitlab/export.go 🔗

@@ -403,10 +403,21 @@ func markOperationAsExported(b *cache.BugCache, target git.Hash, gitlabID, gitla
 
 // get label from gitlab
 func (ge *gitlabExporter) getGitlabLabelID(gc *gitlab.Client, label string) (string, error) {
+
 	return "", nil
 }
 
 func (ge *gitlabExporter) createGitlabLabel(label, color string) (string, error) {
+	client := buildClient(ge.conf[keyToken])
+	_, _, err := client.Labels.CreateLabel(ge.repositoryID, &gitlab.CreateLabelOptions{
+		Name:  &label,
+		Color: &color,
+	})
+
+	if err != nil {
+		return "", err
+	}
+
 	return "", nil
 }
 

bridge/gitlab/gitlab.go 🔗

@@ -26,3 +26,8 @@ func (*Gitlab) NewExporter() core.Exporter {
 func buildClient(token string) *gitlab.Client {
 	return gitlab.NewClient(nil, token)
 }
+
+func buildClientFromUsernameAndPassword(username, password string) (*gitlab.Client, error) {
+	return gitlab.NewBasicAuthClient(nil, "https://gitlab.com", username, password)
+
+}