config.go

  1package gitea
  2
  3import (
  4	"context"
  5	"fmt"
  6	"path"
  7	"regexp"
  8	"sort"
  9	"strings"
 10
 11	"github.com/pkg/errors"
 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 *Gitea) ValidParams() map[string]interface{} {
 25	return map[string]interface{}{
 26		"URL":        nil,
 27		"Login":      nil,
 28		"CredPrefix": nil,
 29		"TokenRaw":   nil,
 30	}
 31}
 32
 33func (g *Gitea) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
 34	var err error
 35	var baseURL, owner, project string
 36
 37	// get project url
 38	switch {
 39	case params.URL != "":
 40		baseURL, owner, project, err = splitURL(params.URL)
 41		if err != nil {
 42			return nil, err
 43		}
 44	default:
 45		// terminal prompt
 46		if !interactive {
 47			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitea project URL via the --url option.")
 48		}
 49		baseURL, owner, project, err = promptURL(repo)
 50		if err != nil {
 51			return nil, errors.Wrap(err, "url prompt")
 52		}
 53	}
 54
 55	var login string
 56	var cred auth.Credential
 57
 58	switch {
 59	case params.CredPrefix != "":
 60		cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
 61		if err != nil {
 62			return nil, err
 63		}
 64		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
 65		if !ok {
 66			return nil, fmt.Errorf("credential doesn't have a login")
 67		}
 68		login = l
 69	case params.TokenRaw != "":
 70		token := auth.NewToken(target, params.TokenRaw)
 71		login, err = getLoginFromToken(baseURL, token)
 72		if err != nil {
 73			return nil, err
 74		}
 75		token.SetMetadata(auth.MetaKeyLogin, login)
 76		token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
 77		cred = token
 78	default:
 79		if params.Login == "" {
 80			if !interactive {
 81				return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
 82			}
 83			// TODO: validate username
 84			login, err = input.Prompt("Gitea login", "login", input.Required)
 85		} else {
 86			// TODO: validate username
 87			login = params.Login
 88		}
 89		if err != nil {
 90			return nil, err
 91		}
 92		if !interactive {
 93			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
 94		}
 95		cred, err = promptTokenOptions(repo, login, baseURL)
 96		if err != nil {
 97			return nil, err
 98		}
 99	}
100
101	token, ok := cred.(*auth.Token)
102	if !ok {
103		return nil, fmt.Errorf("the Gitea bridge only handle token credentials")
104	}
105
106	// verify access to the repository with token
107	_, err = validateProject(baseURL, owner, project, token)
108	if err != nil {
109		return nil, errors.Wrap(err, "project validation")
110	}
111
112	conf := make(core.Configuration)
113	conf[core.ConfigKeyTarget] = target
114	conf[confKeyBaseURL] = baseURL
115	conf[confKeyOwner] = owner
116	conf[confKeyProject] = project
117	conf[confKeyDefaultLogin] = login
118
119	err = g.ValidateConfig(conf)
120	if err != nil {
121		return nil, err
122	}
123
124	// don't forget to store the now known valid token
125	if !auth.IdExist(repo, cred.ID()) {
126		err = auth.Store(repo, cred)
127		if err != nil {
128			return nil, err
129		}
130	}
131
132	return conf, core.FinishConfig(repo, metaKeyGiteaLogin, login)
133}
134
135func (g *Gitea) ValidateConfig(conf core.Configuration) error {
136	if v, ok := conf[core.ConfigKeyTarget]; !ok {
137		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
138	} else if v != target {
139		return fmt.Errorf("unexpected target name: %v", v)
140	}
141	if _, ok := conf[confKeyBaseURL]; !ok {
142		return fmt.Errorf("missing %s key", confKeyBaseURL)
143	}
144	if _, ok := conf[confKeyOwner]; !ok {
145		return fmt.Errorf("missing %s key", confKeyOwner)
146	}
147	if _, ok := conf[confKeyProject]; !ok {
148		return fmt.Errorf("missing %s key", confKeyProject)
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.RepoKeyring, 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, "user/settings/applications"))
185	fmt.Println()
186
187	re := regexp.MustCompile(`^[a-z0-9]{40}$`)
188
189	var login string
190
191	validator := func(name string, value string) (complaint string, err error) {
192		if !re.MatchString(value) {
193			return "token has incorrect format", nil
194		}
195		login, err = getLoginFromToken(baseURL, auth.NewToken(target, value))
196		if err != nil {
197			return fmt.Sprintf("token is invalid: %v", err), nil
198		}
199		return "", nil
200	}
201
202	rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
203	if err != nil {
204		return nil, err
205	}
206
207	token := auth.NewToken(target, rawToken)
208	token.SetMetadata(auth.MetaKeyLogin, login)
209	token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
210
211	return token, nil
212}
213
214func promptURL(repo repository.RepoCommon) (string, string, string, error) {
215	validRemotes, err := getRemoteURLs(repo)
216	if err != nil {
217		return "", "", "", err
218	}
219
220	validator := func(name, value string) (string, error) {
221		_, _, _, err := splitURL(value)
222		if err != nil {
223			return err.Error(), nil
224		}
225		return "", nil
226	}
227
228	url, err := input.PromptURLWithRemote("Gitea project URL", "URL", validRemotes, input.Required, input.IsURL, validator)
229	if err != nil {
230		return "", "", "", err
231	}
232
233	return splitURL(url)
234}
235
236func splitURL(url string) (baseURL, owner, project string, err error) {
237	cleanURL := strings.TrimSuffix(url, ".git")
238
239	re := regexp.MustCompile(`(.*)/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)$`)
240
241	res := re.FindStringSubmatch(cleanURL)
242	if res == nil {
243		return "", "", "", ErrBadProjectURL
244	}
245
246	baseURL = res[1]
247	owner = res[2]
248	project = res[3]
249	return
250}
251
252func getRemoteURLs(repo repository.RepoCommon) ([]string, error) {
253	remotes, err := repo.GetRemotes()
254	if err != nil {
255		return nil, err
256	}
257
258	urls := make([]string, 0, len(remotes))
259	for _, url := range remotes {
260		urls = append(urls, url)
261	}
262
263	sort.Strings(urls)
264
265	return urls, nil
266}
267
268func validateProject(baseURL, owner, project string, token *auth.Token) (bool, error) {
269	client, err := buildClient(baseURL, token)
270	if err != nil {
271		return false, err
272	}
273
274	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
275	defer cancel()
276	client.SetContext(ctx)
277
278	_, _, err = client.GetRepo(owner, project)
279	if err != nil {
280		return false, errors.Wrap(err, "wrong token scope or non-existent project")
281	}
282
283	return true, 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	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
293	defer cancel()
294	client.SetContext(ctx)
295
296	user, _, err := client.GetMyUserInfo()
297	if err != nil {
298		return "", err
299	}
300	if user.UserName == "" {
301		return "", fmt.Errorf("gitea say username is empty")
302	}
303
304	return user.UserName, nil
305}