config.go

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