git-bug/bridge/gitlab/config.go
Amine Hilaly 35a033c0f1
bridge/gitlab: bridge project validation
bridge/gitlab: token generation
2019-07-23 17:18:04 +02:00

379 lines
7.5 KiB
Go

package gitlab
import (
"bufio"
"fmt"
neturl "net/url"
"os"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/xanzy/go-gitlab"
"golang.org/x/crypto/ssh/terminal"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/repository"
)
const (
target = "gitlab"
gitlabV4Url = "https://gitlab.com/api/v4"
keyID = "project-id"
keyTarget = "target"
keyToken = "token"
defaultTimeout = 60 * time.Second
)
var (
ErrBadProjectURL = errors.New("bad project url")
)
func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
if params.Project != "" {
fmt.Println("warning: --project is ineffective for a gitlab bridge")
}
if params.Owner != "" {
fmt.Println("warning: --owner is ineffective for a gitlab bridge")
}
conf := make(core.Configuration)
var err error
var url string
var token string
// get project url
if params.URL != "" {
url = params.URL
} else {
// remote suggestions
remotes, err := repo.GetRemotes()
if err != nil {
return nil, err
}
// terminal prompt
url, err = promptURL(remotes)
if err != nil {
return nil, err
}
}
// get user token
if params.Token != "" {
token = params.Token
} else {
token, err = promptTokenOptions(url)
if err != nil {
return nil, err
}
}
var ok bool
// validate project url and get it ID
ok, id, err := validateProjectURL(url, token)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("invalid project id or wrong token scope")
}
conf[keyID] = strconv.Itoa(id)
conf[keyToken] = token
conf[keyTarget] = target
return conf, nil
}
func (*Gitlab) ValidateConfig(conf core.Configuration) error {
if v, ok := conf[keyTarget]; !ok {
return fmt.Errorf("missing %s key", keyTarget)
} else if v != target {
return fmt.Errorf("unexpected target name: %v", v)
}
if _, ok := conf[keyToken]; !ok {
return fmt.Errorf("missing %s key", keyToken)
}
if _, ok := conf[keyID]; !ok {
return fmt.Errorf("missing %s key", keyID)
}
return nil
}
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
}
return impToken.Token, nil
}
func promptTokenOptions(url string) (string, error) {
for {
fmt.Println()
fmt.Println("[1]: user provided token")
fmt.Println("[2]: interactive token creation")
fmt.Print("Select option: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
fmt.Println()
if err != nil {
return "", err
}
line = strings.TrimRight(line, "\n")
index, err := strconv.Atoi(line)
if err != nil || (index != 1 && index != 2) {
fmt.Println("invalid input")
continue
}
if index == 1 {
return promptToken()
}
return loginAndRequestToken(url)
}
}
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("'api' scope access : access scope: to be able to make api calls")
fmt.Println()
re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
if err != nil {
panic("regexp compile:" + err.Error())
}
for {
fmt.Print("Enter token: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", err
}
token := strings.TrimRight(line, "\n")
if re.MatchString(token) {
return token, nil
}
fmt.Println("token is invalid")
}
}
func loginAndRequestToken(url string) (string, error) {
username, err := promptUsername()
if err != nil {
return "", err
}
password, err := promptPassword()
if err != nil {
return "", err
}
// Attempt to authenticate and create a token
note := fmt.Sprintf("git-bug - %s", url)
ok, id, err := validateUsername(username)
if err != nil {
return "", err
}
if !ok {
return "", fmt.Errorf("invalid username")
}
client, err := buildClientFromUsernameAndPassword(username, password)
if err != nil {
return "", err
}
fmt.Println(username, password)
return requestToken(client, id, note, "api")
}
func promptUsername() (string, error) {
for {
fmt.Print("username: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", err
}
line = strings.TrimRight(line, "\n")
ok, _, err := validateUsername(line)
if err != nil {
return "", err
}
if ok {
return line, nil
}
fmt.Println("invalid username")
}
}
func promptURL(remotes map[string]string) (string, error) {
validRemotes := getValidGitlabRemoteURLs(remotes)
if len(validRemotes) > 0 {
for {
fmt.Println("\nDetected projects:")
// print valid remote gitlab urls
for i, remote := range validRemotes {
fmt.Printf("[%d]: %v\n", i+1, remote)
}
fmt.Printf("\n[0]: Another project\n\n")
fmt.Printf("Select option: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", err
}
line = strings.TrimRight(line, "\n")
index, err := strconv.Atoi(line)
if err != nil || (index < 0 && index >= len(validRemotes)) {
fmt.Println("invalid input")
continue
}
// if user want to enter another project url break this loop
if index == 0 {
break
}
return validRemotes[index-1], nil
}
}
// manually enter gitlab url
for {
fmt.Print("Gitlab project URL: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", err
}
url := strings.TrimRight(line, "\n")
if line == "" {
fmt.Println("URL is empty")
continue
}
return url, nil
}
}
func getProjectPath(url string) (string, error) {
cleanUrl := strings.TrimSuffix(url, ".git")
objectUrl, err := neturl.Parse(cleanUrl)
if err != nil {
return "", nil
}
return objectUrl.Path[1:], nil
}
func getValidGitlabRemoteURLs(remotes map[string]string) []string {
urls := make([]string, 0, len(remotes))
for _, u := range remotes {
path, err := getProjectPath(u)
if err != nil {
continue
}
urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
}
return urls
}
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, 0, err
}
if len(users) == 0 {
return false, 0, fmt.Errorf("username not found")
} else if len(users) > 1 {
return false, 0, fmt.Errorf("found multiple matches")
}
if users[0].Username == username {
return true, users[0].ID, nil
}
return false, 0, nil
}
func validateProjectURL(url, token string) (bool, int, error) {
client := buildClient(token)
projectPath, err := getProjectPath(url)
if err != nil {
return false, 0, err
}
project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
if err != nil {
return false, 0, err
}
return true, project.ID, nil
}
func promptPassword() (string, error) {
for {
fmt.Print("password: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
// new line for coherent formatting, ReadPassword clip the normal new line
// entered by the user
fmt.Println()
if err != nil {
return "", err
}
if len(bytePassword) > 0 {
return string(bytePassword), nil
}
fmt.Println("password is empty")
}
}