From b92adfcb5f79f2b32c3dafb0fc3e7f1b753b6197 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 8 Dec 2019 21:15:06 +0100 Subject: bridge: huge refactor to accept multiple kind of credentials --- bridge/gitlab/config.go | 138 +++++++++++++++++++++---------------------- bridge/gitlab/export.go | 72 +++++++++++----------- bridge/gitlab/export_test.go | 23 ++++---- bridge/gitlab/gitlab.go | 6 +- bridge/gitlab/import.go | 36 +++++++++-- bridge/gitlab/import_test.go | 15 +++-- bridge/gitlab/iterator.go | 4 +- 7 files changed, 164 insertions(+), 130 deletions(-) (limited to 'bridge/gitlab') 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, -- cgit