1package gitlab
  2
  3import (
  4	"fmt"
  5	"net/url"
  6	"regexp"
  7	"strconv"
  8	"strings"
  9
 10	"github.com/pkg/errors"
 11	"github.com/xanzy/go-gitlab"
 12
 13	"github.com/MichaelMure/git-bug/bridge/core"
 14	"github.com/MichaelMure/git-bug/bridge/core/auth"
 15	"github.com/MichaelMure/git-bug/cache"
 16	"github.com/MichaelMure/git-bug/commands/input"
 17	"github.com/MichaelMure/git-bug/repository"
 18)
 19
 20var (
 21	ErrBadProjectURL = errors.New("bad project url")
 22)
 23
 24func (g *Gitlab) ValidParams() map[string]interface{} {
 25	return map[string]interface{}{
 26		"URL":        nil,
 27		"BaseURL":    nil,
 28		"Login":      nil,
 29		"CredPrefix": nil,
 30		"TokenRaw":   nil,
 31	}
 32}
 33
 34func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
 35	var err error
 36	var baseUrl string
 37
 38	switch {
 39	case params.BaseURL != "":
 40		baseUrl = params.BaseURL
 41	default:
 42		if !interactive {
 43			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitlab instance URL via the --base-url option.")
 44		}
 45		baseUrl, err = input.PromptDefault("Gitlab server URL", "URL", defaultBaseURL, input.Required, input.IsURL)
 46		if err != nil {
 47			return nil, errors.Wrap(err, "base url prompt")
 48		}
 49	}
 50
 51	var projectURL string
 52
 53	// get project url
 54	switch {
 55	case params.URL != "":
 56		projectURL = params.URL
 57	default:
 58		// terminal prompt
 59		if !interactive {
 60			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitlab project URL via the --url option.")
 61		}
 62		projectURL, err = promptProjectURL(repo, baseUrl)
 63		if err != nil {
 64			return nil, errors.Wrap(err, "url prompt")
 65		}
 66	}
 67
 68	if !strings.HasPrefix(projectURL, params.BaseURL) {
 69		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, projectURL)
 70	}
 71
 72	var login string
 73	var cred auth.Credential
 74
 75	switch {
 76	case params.CredPrefix != "":
 77		cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
 78		if err != nil {
 79			return nil, err
 80		}
 81		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
 82		if !ok {
 83			return nil, fmt.Errorf("credential doesn't have a login")
 84		}
 85		login = l
 86	case params.TokenRaw != "":
 87		token := auth.NewToken(target, params.TokenRaw)
 88		login, err = getLoginFromToken(baseUrl, token)
 89		if err != nil {
 90			return nil, err
 91		}
 92		token.SetMetadata(auth.MetaKeyLogin, login)
 93		token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
 94		cred = token
 95	default:
 96		if params.Login == "" {
 97			if !interactive {
 98				return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
 99			}
100			// TODO: validate username
101			login, err = input.Prompt("Gitlab login", "login", input.Required)
102		} else {
103			// TODO: validate username
104			login = params.Login
105		}
106		if err != nil {
107			return nil, err
108		}
109		if !interactive {
110			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
111		}
112		cred, err = promptTokenOptions(repo, login, baseUrl)
113		if err != nil {
114			return nil, err
115		}
116	}
117
118	token, ok := cred.(*auth.Token)
119	if !ok {
120		return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
121	}
122
123	// validate project url and get its ID
124	id, err := validateProjectURL(baseUrl, projectURL, token)
125	if err != nil {
126		return nil, errors.Wrap(err, "project validation")
127	}
128
129	conf := make(core.Configuration)
130	conf[core.ConfigKeyTarget] = target
131	conf[confKeyProjectID] = strconv.Itoa(id)
132	conf[confKeyGitlabBaseUrl] = baseUrl
133	conf[confKeyDefaultLogin] = login
134
135	err = g.ValidateConfig(conf)
136	if err != nil {
137		return nil, err
138	}
139
140	// don't forget to store the now known valid token
141	if !auth.IdExist(repo, cred.ID()) {
142		err = auth.Store(repo, cred)
143		if err != nil {
144			return nil, err
145		}
146	}
147
148	return conf, core.FinishConfig(repo, metaKeyGitlabLogin, login)
149}
150
151func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
152	if v, ok := conf[core.ConfigKeyTarget]; !ok {
153		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
154	} else if v != target {
155		return fmt.Errorf("unexpected target name: %v", v)
156	}
157	if _, ok := conf[confKeyGitlabBaseUrl]; !ok {
158		return fmt.Errorf("missing %s key", confKeyGitlabBaseUrl)
159	}
160	if _, ok := conf[confKeyProjectID]; !ok {
161		return fmt.Errorf("missing %s key", confKeyProjectID)
162	}
163	if _, ok := conf[confKeyDefaultLogin]; !ok {
164		return fmt.Errorf("missing %s key", confKeyDefaultLogin)
165	}
166
167	return nil
168}
169
170func promptTokenOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) {
171	creds, err := auth.List(repo,
172		auth.WithTarget(target),
173		auth.WithKind(auth.KindToken),
174		auth.WithMeta(auth.MetaKeyLogin, login),
175		auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
176	)
177	if err != nil {
178		return nil, err
179	}
180
181	cred, index, err := input.PromptCredential(target, "token", creds, []string{
182		"enter my token",
183	})
184	switch {
185	case err != nil:
186		return nil, err
187	case cred != nil:
188		return cred, nil
189	case index == 0:
190		return promptToken(baseUrl)
191	default:
192		panic("missed case")
193	}
194}
195
196func promptToken(baseUrl string) (*auth.Token, error) {
197	fmt.Printf("You can generate a new token by visiting %s.\n", strings.TrimSuffix(baseUrl, "/")+"/-/profile/personal_access_tokens")
198	fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
199	fmt.Println()
200	fmt.Println("'api' access scope: to be able to make api calls")
201	fmt.Println()
202
203	re := regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)
204
205	var login string
206
207	validator := func(name string, value string) (complaint string, err error) {
208		if !re.MatchString(value) {
209			return "token has incorrect format", nil
210		}
211		login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
212		if err != nil {
213			return fmt.Sprintf("token is invalid: %v", err), nil
214		}
215		return "", nil
216	}
217
218	rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
219	if err != nil {
220		return nil, err
221	}
222
223	token := auth.NewToken(target, rawToken)
224	token.SetMetadata(auth.MetaKeyLogin, login)
225	token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
226
227	return token, nil
228}
229
230func promptProjectURL(repo repository.RepoCommon, baseUrl string) (string, error) {
231	validRemotes, err := getValidGitlabRemoteURLs(repo, baseUrl)
232	if err != nil {
233		return "", err
234	}
235
236	return input.PromptURLWithRemote("Gitlab project URL", "URL", validRemotes, input.Required)
237}
238
239func getProjectPath(baseUrl, projectUrl string) (string, error) {
240	cleanUrl := strings.TrimSuffix(projectUrl, ".git")
241	cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
242	objectUrl, err := url.Parse(cleanUrl)
243	if err != nil {
244		return "", ErrBadProjectURL
245	}
246
247	objectBaseUrl, err := url.Parse(baseUrl)
248	if err != nil {
249		return "", ErrBadProjectURL
250	}
251
252	if objectUrl.Hostname() != objectBaseUrl.Hostname() {
253		return "", fmt.Errorf("base url and project url hostnames doesn't match")
254	}
255	return objectUrl.Path[1:], nil
256}
257
258func getValidGitlabRemoteURLs(repo repository.RepoCommon, baseUrl string) ([]string, error) {
259	remotes, err := repo.GetRemotes()
260	if err != nil {
261		return nil, err
262	}
263
264	urls := make([]string, 0, len(remotes))
265	for _, u := range remotes {
266		p, err := getProjectPath(baseUrl, u)
267		if err != nil {
268			continue
269		}
270
271		urls = append(urls, fmt.Sprintf("%s/%s", baseUrl, p))
272	}
273
274	return urls, nil
275}
276
277func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) {
278	projectPath, err := getProjectPath(baseUrl, url)
279	if err != nil {
280		return 0, err
281	}
282
283	client, err := buildClient(baseUrl, token)
284	if err != nil {
285		return 0, err
286	}
287
288	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
289	if err != nil {
290		return 0, errors.Wrap(err, "wrong token scope ou non-existent project")
291	}
292
293	return project.ID, nil
294}
295
296func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) {
297	client, err := buildClient(baseUrl, token)
298	if err != nil {
299		return "", err
300	}
301
302	user, _, err := client.Users.CurrentUser()
303	if err != nil {
304		return "", err
305	}
306	if user.Username == "" {
307		return "", fmt.Errorf("gitlab say username is empty")
308	}
309
310	return user.Username, nil
311}