1package gitlab
2
3import (
4 "bufio"
5 "fmt"
6 neturl "net/url"
7 "os"
8 "regexp"
9 "strconv"
10 "strings"
11 "syscall"
12 "time"
13
14 "github.com/pkg/errors"
15 "github.com/xanzy/go-gitlab"
16 "golang.org/x/crypto/ssh/terminal"
17
18 "github.com/MichaelMure/git-bug/bridge/core"
19 "github.com/MichaelMure/git-bug/repository"
20)
21
22const (
23 target = "gitlab"
24 gitlabV4Url = "https://gitlab.com/api/v4"
25 keyID = "project-id"
26 keyTarget = "target"
27 keyToken = "token"
28
29 defaultTimeout = 60 * time.Second
30)
31
32var (
33 ErrBadProjectURL = errors.New("bad project url")
34)
35
36func (*Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
37 if params.Project != "" {
38 fmt.Println("warning: --project is ineffective for a gitlab bridge")
39 }
40 if params.Owner != "" {
41 fmt.Println("warning: --owner is ineffective for a gitlab bridge")
42 }
43
44 conf := make(core.Configuration)
45 var err error
46 var url string
47 var token string
48
49 // get project url
50 if params.URL != "" {
51 url = params.URL
52
53 } else {
54 // remote suggestions
55 remotes, err := repo.GetRemotes()
56 if err != nil {
57 return nil, err
58 }
59
60 // terminal prompt
61 url, err = promptURL(remotes)
62 if err != nil {
63 return nil, err
64 }
65 }
66
67 // get user token
68 if params.Token != "" {
69 token = params.Token
70 } else {
71 token, err = promptTokenOptions(url)
72 if err != nil {
73 return nil, err
74 }
75 }
76
77 var ok bool
78 // validate project url and get it ID
79 ok, id, err := validateProjectURL(url, token)
80 if err != nil {
81 return nil, err
82 }
83 if !ok {
84 return nil, fmt.Errorf("invalid project id or wrong token scope")
85 }
86
87 conf[keyID] = strconv.Itoa(id)
88 conf[keyToken] = token
89 conf[keyTarget] = target
90
91 return conf, nil
92}
93
94func (*Gitlab) ValidateConfig(conf core.Configuration) error {
95 if v, ok := conf[keyTarget]; !ok {
96 return fmt.Errorf("missing %s key", keyTarget)
97 } else if v != target {
98 return fmt.Errorf("unexpected target name: %v", v)
99 }
100
101 if _, ok := conf[keyToken]; !ok {
102 return fmt.Errorf("missing %s key", keyToken)
103 }
104
105 if _, ok := conf[keyID]; !ok {
106 return fmt.Errorf("missing %s key", keyID)
107 }
108
109 return nil
110}
111
112func requestToken(client *gitlab.Client, userID int, name string, scopes ...string) (string, error) {
113 impToken, _, err := client.Users.CreateImpersonationToken(
114 userID,
115 &gitlab.CreateImpersonationTokenOptions{
116 Name: &name,
117 Scopes: &scopes,
118 },
119 )
120 if err != nil {
121 return "", err
122 }
123
124 return impToken.Token, nil
125}
126
127func promptTokenOptions(url string) (string, error) {
128 for {
129 fmt.Println()
130 fmt.Println("[1]: user provided token")
131 fmt.Println("[2]: interactive token creation")
132 fmt.Print("Select option: ")
133
134 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
135 fmt.Println()
136 if err != nil {
137 return "", err
138 }
139
140 line = strings.TrimRight(line, "\n")
141
142 index, err := strconv.Atoi(line)
143 if err != nil || (index != 1 && index != 2) {
144 fmt.Println("invalid input")
145 continue
146 }
147
148 if index == 1 {
149 return promptToken()
150 }
151
152 return loginAndRequestToken(url)
153 }
154}
155
156func promptToken() (string, error) {
157 fmt.Println("You can generate a new token by visiting https://gitlab.com/settings/tokens.")
158 fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
159 fmt.Println()
160 fmt.Println("'api' scope access : access scope: to be able to make api calls")
161 fmt.Println()
162
163 re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
164 if err != nil {
165 panic("regexp compile:" + err.Error())
166 }
167
168 for {
169 fmt.Print("Enter token: ")
170
171 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
172 if err != nil {
173 return "", err
174 }
175
176 token := strings.TrimRight(line, "\n")
177 if re.MatchString(token) {
178 return token, nil
179 }
180
181 fmt.Println("token is invalid")
182 }
183}
184
185func loginAndRequestToken(url string) (string, error) {
186 username, err := promptUsername()
187 if err != nil {
188 return "", err
189 }
190
191 password, err := promptPassword()
192 if err != nil {
193 return "", err
194 }
195
196 // Attempt to authenticate and create a token
197
198 note := fmt.Sprintf("git-bug - %s", url)
199
200 ok, id, err := validateUsername(username)
201 if err != nil {
202 return "", err
203 }
204 if !ok {
205 return "", fmt.Errorf("invalid username")
206 }
207
208 client, err := buildClientFromUsernameAndPassword(username, password)
209 if err != nil {
210 return "", err
211 }
212
213 fmt.Println(username, password)
214
215 return requestToken(client, id, note, "api")
216}
217
218func promptUsername() (string, error) {
219 for {
220 fmt.Print("username: ")
221
222 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
223 if err != nil {
224 return "", err
225 }
226
227 line = strings.TrimRight(line, "\n")
228
229 ok, _, err := validateUsername(line)
230 if err != nil {
231 return "", err
232 }
233 if ok {
234 return line, nil
235 }
236
237 fmt.Println("invalid username")
238 }
239}
240
241func promptURL(remotes map[string]string) (string, error) {
242 validRemotes := getValidGitlabRemoteURLs(remotes)
243 if len(validRemotes) > 0 {
244 for {
245 fmt.Println("\nDetected projects:")
246
247 // print valid remote gitlab urls
248 for i, remote := range validRemotes {
249 fmt.Printf("[%d]: %v\n", i+1, remote)
250 }
251
252 fmt.Printf("\n[0]: Another project\n\n")
253 fmt.Printf("Select option: ")
254
255 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
256 if err != nil {
257 return "", err
258 }
259
260 line = strings.TrimRight(line, "\n")
261
262 index, err := strconv.Atoi(line)
263 if err != nil || (index < 0 && index >= len(validRemotes)) {
264 fmt.Println("invalid input")
265 continue
266 }
267
268 // if user want to enter another project url break this loop
269 if index == 0 {
270 break
271 }
272
273 return validRemotes[index-1], nil
274 }
275 }
276
277 // manually enter gitlab url
278 for {
279 fmt.Print("Gitlab project URL: ")
280
281 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
282 if err != nil {
283 return "", err
284 }
285
286 url := strings.TrimRight(line, "\n")
287 if line == "" {
288 fmt.Println("URL is empty")
289 continue
290 }
291
292 return url, nil
293 }
294}
295
296func getProjectPath(url string) (string, error) {
297
298 cleanUrl := strings.TrimSuffix(url, ".git")
299 objectUrl, err := neturl.Parse(cleanUrl)
300 if err != nil {
301 return "", nil
302 }
303
304 return objectUrl.Path[1:], nil
305}
306
307func getValidGitlabRemoteURLs(remotes map[string]string) []string {
308 urls := make([]string, 0, len(remotes))
309 for _, u := range remotes {
310 path, err := getProjectPath(u)
311 if err != nil {
312 continue
313 }
314
315 urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
316 }
317
318 return urls
319}
320
321func validateUsername(username string) (bool, int, error) {
322 // no need for a token for this action
323 client := buildClient("")
324
325 users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &username})
326 if err != nil {
327 return false, 0, err
328 }
329
330 if len(users) == 0 {
331 return false, 0, fmt.Errorf("username not found")
332 } else if len(users) > 1 {
333 return false, 0, fmt.Errorf("found multiple matches")
334 }
335
336 if users[0].Username == username {
337 return true, users[0].ID, nil
338 }
339
340 return false, 0, nil
341}
342
343func validateProjectURL(url, token string) (bool, int, error) {
344 client := buildClient(token)
345
346 projectPath, err := getProjectPath(url)
347 if err != nil {
348 return false, 0, err
349 }
350
351 project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
352 if err != nil {
353 return false, 0, err
354 }
355
356 return true, project.ID, nil
357}
358
359func promptPassword() (string, error) {
360 for {
361 fmt.Print("password: ")
362
363 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
364 // new line for coherent formatting, ReadPassword clip the normal new line
365 // entered by the user
366 fmt.Println()
367
368 if err != nil {
369 return "", err
370 }
371
372 if len(bytePassword) > 0 {
373 return string(bytePassword), nil
374 }
375
376 fmt.Println("password is empty")
377 }
378}