aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/gitlab
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2019-12-10 00:42:23 +0100
committerGitHub <noreply@github.com>2019-12-10 00:42:23 +0100
commitf1ed857cbd3a253d77b31c0c896fdc4ade40844f (patch)
treed1efe28a1fa666039bf8180bbed0202f0437910f /bridge/gitlab
parent69af7a1e0c2647c354fd9c5b55a254ba677200e1 (diff)
parent58c0e5aac97eabc02fa890123f3845ae6fe632a8 (diff)
downloadgit-bug-f1ed857cbd3a253d77b31c0c896fdc4ade40844f.tar.gz
Merge pull request #271 from MichaelMure/bridge-credentials
bridge: huge refactor to accept multiple kind of credentials
Diffstat (limited to 'bridge/gitlab')
-rw-r--r--bridge/gitlab/config.go138
-rw-r--r--bridge/gitlab/export.go72
-rw-r--r--bridge/gitlab/export_test.go23
-rw-r--r--bridge/gitlab/gitlab.go6
-rw-r--r--bridge/gitlab/import.go36
-rw-r--r--bridge/gitlab/import_test.go15
-rw-r--r--bridge/gitlab/iterator.go4
7 files changed, 164 insertions, 130 deletions
diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go
index 6b85e8cb..7bc2e577 100644
--- a/bridge/gitlab/config.go
+++ b/bridge/gitlab/config.go
@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"regexp"
+ "sort"
"strconv"
"strings"
"time"
@@ -15,6 +16,8 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
@@ -24,7 +27,7 @@ var (
ErrBadProjectURL = errors.New("bad project url")
)
-func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
+func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
if params.Project != "" {
fmt.Println("warning: --project is ineffective for a gitlab bridge")
}
@@ -34,82 +37,77 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams)
conf := make(core.Configuration)
var err error
- var url string
- var token string
- var tokenId entity.Id
- var tokenObj *core.Token
- if (params.Token != "" || params.TokenStdin) && params.URL == "" {
+ if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
}
+ var url string
+
// get project url
- if params.URL != "" {
+ switch {
+ case params.URL != "":
url = params.URL
-
- } else {
- // remote suggestions
- remotes, err := repo.GetRemotes()
- if err != nil {
- return nil, errors.Wrap(err, "getting remotes")
- }
-
+ default:
// terminal prompt
- url, err = promptURL(remotes)
+ url, err = promptURL(repo)
if err != nil {
return nil, errors.Wrap(err, "url prompt")
}
}
- // get user token
- if params.Token != "" {
- token = params.Token
- } else if params.TokenStdin {
- reader := bufio.NewReader(os.Stdin)
- token, err = reader.ReadString('\n')
- if err != nil {
- return nil, fmt.Errorf("reading from stdin: %v", err)
- }
- token = strings.TrimSpace(token)
- } else if params.TokenId != "" {
- tokenId = entity.Id(params.TokenId)
- } else {
- tokenObj, err = promptTokenOptions(repo)
- if err != nil {
- return nil, errors.Wrap(err, "token prompt")
- }
+ user, err := repo.GetUserIdentity()
+ if err != nil {
+ return nil, err
}
- if token != "" {
- tokenObj, err = core.LoadOrCreateToken(repo, target, token)
+ var cred auth.Credential
+
+ switch {
+ case params.CredPrefix != "":
+ cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
if err != nil {
return nil, err
}
- } else if tokenId != "" {
- tokenObj, err = core.LoadToken(repo, entity.Id(tokenId))
+ if cred.UserId() != user.Id() {
+ return nil, fmt.Errorf("selected credential don't match the user")
+ }
+ case params.TokenRaw != "":
+ cred = auth.NewToken(user.Id(), params.TokenRaw, target)
+ default:
+ cred, err = promptTokenOptions(repo, user.Id())
if err != nil {
return nil, err
}
- if tokenObj.Target != target {
- return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target)
- }
+ }
+
+ token, ok := cred.(*auth.Token)
+ if !ok {
+ return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
}
// validate project url and get its ID
- id, err := validateProjectURL(url, tokenObj.Value)
+ id, err := validateProjectURL(url, token)
if err != nil {
return nil, errors.Wrap(err, "project validation")
}
- conf[keyProjectID] = strconv.Itoa(id)
- conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
conf[core.ConfigKeyTarget] = target
+ conf[keyProjectID] = strconv.Itoa(id)
err = g.ValidateConfig(conf)
if err != nil {
return nil, err
}
+ // don't forget to store the now known valid token
+ if !auth.IdExist(repo, cred.ID()) {
+ err = auth.Store(repo, cred)
+ if err != nil {
+ return nil, err
+ }
+ }
+
return conf, nil
}
@@ -120,10 +118,6 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
return fmt.Errorf("unexpected target name: %v", v)
}
- if _, ok := conf[keyToken]; !ok {
- return fmt.Errorf("missing %s key", keyToken)
- }
-
if _, ok := conf[keyProjectID]; !ok {
return fmt.Errorf("missing %s key", keyProjectID)
}
@@ -131,19 +125,20 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
return nil
}
-func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
+func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) {
for {
- tokens, err := core.LoadTokensWithTarget(repo, target)
+ creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return nil, err
}
- if len(tokens) == 0 {
- token, err := promptToken()
+ // if we don't have existing token, fast-track to the token prompt
+ if len(creds) == 0 {
+ value, err := promptToken()
if err != nil {
return nil, err
}
- return core.LoadOrCreateToken(repo, target, token)
+ return auth.NewToken(userId, value, target), nil
}
fmt.Println()
@@ -151,15 +146,16 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
fmt.Println()
fmt.Println("Existing tokens for Gitlab:")
- for i, token := range tokens {
- if token.Target == target {
- fmt.Printf("[%d]: %s => %s (%s)\n",
- i+2,
- colors.Cyan(token.ID().Human()),
- text.TruncateMax(token.Value, 10),
- token.CreateTime.Format(time.RFC822),
- )
- }
+
+ sort.Sort(auth.ById(creds))
+ for i, cred := range creds {
+ token := cred.(*auth.Token)
+ fmt.Printf("[%d]: %s => %s (%s)\n",
+ i+2,
+ colors.Cyan(token.ID().Human()),
+ colors.Red(text.TruncateMax(token.Value, 10)),
+ token.CreateTime().Format(time.RFC822),
+ )
}
fmt.Println()
@@ -173,23 +169,21 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
line = strings.TrimSpace(line)
index, err := strconv.Atoi(line)
- if err != nil || index < 1 || index > len(tokens)+1 {
+ if err != nil || index < 1 || index > len(creds)+1 {
fmt.Println("invalid input")
continue
}
- var token string
switch index {
case 1:
- token, err = promptToken()
+ value, err := promptToken()
if err != nil {
return nil, err
}
+ return auth.NewToken(userId, value, target), nil
default:
- return tokens[index-2], nil
+ return creds[index-2], nil
}
-
- return core.LoadOrCreateToken(repo, target, token)
}
}
@@ -222,7 +216,13 @@ func promptToken() (string, error) {
}
}
-func promptURL(remotes map[string]string) (string, error) {
+func promptURL(repo repository.RepoCommon) (string, error) {
+ // remote suggestions
+ remotes, err := repo.GetRemotes()
+ if err != nil {
+ return "", errors.Wrap(err, "getting remotes")
+ }
+
validRemotes := getValidGitlabRemoteURLs(remotes)
if len(validRemotes) > 0 {
for {
@@ -302,7 +302,7 @@ func getValidGitlabRemoteURLs(remotes map[string]string) []string {
return urls
}
-func validateProjectURL(url, token string) (int, error) {
+func validateProjectURL(url string, token *auth.Token) (int, error) {
projectPath, err := getProjectPath(url)
if err != nil {
return 0, err
diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go
index 092434a5..373cf637 100644
--- a/bridge/gitlab/export.go
+++ b/bridge/gitlab/export.go
@@ -10,9 +10,11 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/repository"
)
var (
@@ -24,10 +26,7 @@ type gitlabExporter struct {
conf core.Configuration
// cache identities clients
- identityClient map[string]*gitlab.Client
-
- // map identities with their tokens
- identityToken map[string]string
+ identityClient map[entity.Id]*gitlab.Client
// gitlab repository ID
repositoryID string
@@ -38,58 +37,59 @@ type gitlabExporter struct {
}
// Init .
-func (ge *gitlabExporter) Init(conf core.Configuration) error {
+func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
ge.conf = conf
- //TODO: initialize with multiple tokens
- ge.identityToken = make(map[string]string)
- ge.identityClient = make(map[string]*gitlab.Client)
+ ge.identityClient = make(map[entity.Id]*gitlab.Client)
ge.cachedOperationIDs = make(map[string]string)
+ // get repository node id
+ ge.repositoryID = ge.conf[keyProjectID]
+
+ // preload all clients
+ err := ge.cacheAllClient(repo)
+ if err != nil {
+ return err
+ }
+
return nil
}
-// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity.
-// if no client were found it will initialize it from the known tokens map and cache it for next use
-func (ge *gitlabExporter) getIdentityClient(id entity.Id) (*gitlab.Client, error) {
- client, ok := ge.identityClient[id.String()]
- if ok {
- return client, nil
+func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error {
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
+ if err != nil {
+ return err
}
- // get token
- token, ok := ge.identityToken[id.String()]
- if !ok {
- return nil, ErrMissingIdentityToken
+ for _, cred := range creds {
+ if _, ok := ge.identityClient[cred.UserId()]; !ok {
+ client := buildClient(creds[0].(*auth.Token))
+ ge.identityClient[cred.UserId()] = client
+ }
}
- // create client
- client = buildClient(token)
- // cache client
- ge.identityClient[id.String()] = client
+ return nil
+}
+
+// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity.
+func (ge *gitlabExporter) getIdentityClient(userId entity.Id) (*gitlab.Client, error) {
+ client, ok := ge.identityClient[userId]
+ if ok {
+ return client, nil
+ }
- return client, nil
+ return nil, ErrMissingIdentityToken
}
// ExportAll export all event made by the current user to Gitlab
func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
out := make(chan core.ExportResult)
- user, err := repo.GetUserIdentity()
- if err != nil {
- return nil, err
- }
-
- ge.identityToken[user.Id().String()] = ge.conf[core.ConfigKeyToken]
-
- // get repository node id
- ge.repositoryID = ge.conf[keyProjectID]
-
go func() {
defer close(out)
- allIdentitiesIds := make([]entity.Id, 0, len(ge.identityToken))
- for id := range ge.identityToken {
- allIdentitiesIds = append(allIdentitiesIds, entity.Id(id))
+ allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient))
+ for id := range ge.identityClient {
+ allIdentitiesIds = append(allIdentitiesIds, id)
}
allBugsIds := repo.AllBugsIds()
diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go
index 26b47bfb..645e2d76 100644
--- a/bridge/gitlab/export_test.go
+++ b/bridge/gitlab/export_test.go
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/repository"
@@ -32,7 +33,7 @@ type testCase struct {
numOpImp int // number of operations after import
}
-func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase {
+func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
// simple bug
simpleBug, _, err := repo.NewBug("simple bug", "new bug")
require.NoError(t, err)
@@ -135,8 +136,8 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach
func TestPushPull(t *testing.T) {
// token must have 'repo' and 'delete_repo' scopes
- token := os.Getenv("GITLAB_API_TOKEN")
- if token == "" {
+ envToken := os.Getenv("GITLAB_API_TOKEN")
+ if envToken == "" {
t.Skip("Env var GITLAB_API_TOKEN missing")
}
@@ -157,7 +158,11 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- tests := testCases(t, backend, author)
+ tests := testCases(t, backend)
+
+ token := auth.NewToken(author.Id(), envToken, target)
+ err = auth.Store(repo, token)
+ require.NoError(t, err)
// generate project name
projectName := generateRepoName()
@@ -182,9 +187,8 @@ func TestPushPull(t *testing.T) {
// initialize exporter
exporter := &gitlabExporter{}
- err = exporter.Init(core.Configuration{
+ err = exporter.Init(backend, core.Configuration{
keyProjectID: strconv.Itoa(projectID),
- keyToken: token,
})
require.NoError(t, err)
@@ -210,9 +214,8 @@ func TestPushPull(t *testing.T) {
require.NoError(t, err)
importer := &gitlabImporter{}
- err = importer.Init(core.Configuration{
+ err = importer.Init(backend, core.Configuration{
keyProjectID: strconv.Itoa(projectID),
- keyToken: token,
})
require.NoError(t, err)
@@ -276,7 +279,7 @@ func generateRepoName() string {
}
// create repository need a token with scope 'repo'
-func createRepository(ctx context.Context, name, token string) (int, error) {
+func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
client := buildClient(token)
project, _, err := client.Projects.CreateProject(
&gitlab.CreateProjectOptions{
@@ -292,7 +295,7 @@ func createRepository(ctx context.Context, name, token string) (int, error) {
}
// delete repository need a token with scope 'delete_repo'
-func deleteRepository(ctx context.Context, project int, token string) error {
+func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
client := buildClient(token)
_, err := client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
return err
diff --git a/bridge/gitlab/gitlab.go b/bridge/gitlab/gitlab.go
index d976d813..bcc50e4c 100644
--- a/bridge/gitlab/gitlab.go
+++ b/bridge/gitlab/gitlab.go
@@ -7,6 +7,7 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
)
const (
@@ -18,7 +19,6 @@ const (
metaKeyGitlabProject = "gitlab-project-id"
keyProjectID = "project-id"
- keyToken = "token"
defaultTimeout = 60 * time.Second
)
@@ -37,10 +37,10 @@ func (*Gitlab) NewExporter() core.Exporter {
return &gitlabExporter{}
}
-func buildClient(token string) *gitlab.Client {
+func buildClient(token *auth.Token) *gitlab.Client {
client := &http.Client{
Timeout: defaultTimeout,
}
- return gitlab.NewClient(client, token)
+ return gitlab.NewClient(client, token.Value)
}
diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go
index 4fcf8568..00dee252 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -9,6 +9,7 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
@@ -19,6 +20,9 @@ import (
type gitlabImporter struct {
conf core.Configuration
+ // default user client
+ client *gitlab.Client
+
// iterator
iterator *iterator
@@ -26,15 +30,37 @@ type gitlabImporter struct {
out chan<- core.ImportResult
}
-func (gi *gitlabImporter) Init(conf core.Configuration) error {
+func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
gi.conf = conf
+
+ opts := []auth.Option{
+ auth.WithTarget(target),
+ auth.WithKind(auth.KindToken),
+ }
+
+ user, err := repo.GetUserIdentity()
+ if err == nil {
+ opts = append(opts, auth.WithUserId(user.Id()))
+ }
+
+ creds, err := auth.List(repo, opts...)
+ if err != nil {
+ return err
+ }
+
+ if len(creds) == 0 {
+ return ErrMissingIdentityToken
+ }
+
+ gi.client = buildClient(creds[0].(*auth.Token))
+
return nil
}
// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
// of the missing issues / comments / label events / title changes ...
func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
- gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since)
+ gi.iterator = NewIterator(ctx, gi.client, 10, gi.conf[keyProjectID], since)
out := make(chan core.ImportResult)
gi.out = out
@@ -357,13 +383,11 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
if err == nil {
return i, nil
}
- if _, ok := err.(entity.ErrMultipleMatch); ok {
+ if entity.IsErrMultipleMatch(err) {
return nil, err
}
- client := buildClient(gi.conf["token"])
-
- user, _, err := client.Users.GetUser(id)
+ user, _, err := gi.client.Users.GetUser(id)
if err != nil {
return nil, err
}
diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go
index 8e596349..1676bdf3 100644
--- a/bridge/gitlab/import_test.go
+++ b/bridge/gitlab/import_test.go
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/identity"
@@ -83,8 +84,8 @@ func TestImport(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- token := os.Getenv("GITLAB_API_TOKEN")
- if token == "" {
+ envToken := os.Getenv("GITLAB_API_TOKEN")
+ if envToken == "" {
t.Skip("Env var GITLAB_API_TOKEN missing")
}
@@ -93,10 +94,16 @@ func TestImport(t *testing.T) {
t.Skip("Env var GITLAB_PROJECT_ID missing")
}
+ err = author.Commit(repo)
+ require.NoError(t, err)
+
+ token := auth.NewToken(author.Id(), envToken, target)
+ err = auth.Store(repo, token)
+ require.NoError(t, err)
+
importer := &gitlabImporter{}
- err = importer.Init(core.Configuration{
+ err = importer.Init(backend, core.Configuration{
keyProjectID: projectID,
- keyToken: token,
})
require.NoError(t, err)
diff --git a/bridge/gitlab/iterator.go b/bridge/gitlab/iterator.go
index 902dc9f1..07f9cce9 100644
--- a/bridge/gitlab/iterator.go
+++ b/bridge/gitlab/iterator.go
@@ -71,9 +71,9 @@ type iterator struct {
}
// NewIterator create a new iterator
-func NewIterator(ctx context.Context, capacity int, projectID, token string, since time.Time) *iterator {
+func NewIterator(ctx context.Context, client *gitlab.Client, capacity int, projectID string, since time.Time) *iterator {
return &iterator{
- gc: buildClient(token),
+ gc: client,
project: projectID,
since: since,
capacity: capacity,