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