diff options
-rw-r--r-- | bridge/core/bridge.go | 15 | ||||
-rw-r--r-- | bridge/core/token.go | 118 | ||||
-rw-r--r-- | bridge/github/config.go | 77 | ||||
-rw-r--r-- | bridge/github/export.go | 6 | ||||
-rw-r--r-- | bridge/github/import.go | 4 | ||||
-rw-r--r-- | bridge/gitlab/config.go | 74 | ||||
-rw-r--r-- | bridge/gitlab/export.go | 2 | ||||
-rw-r--r-- | bridge/gitlab/import.go | 2 | ||||
-rw-r--r-- | commands/bridge_configure.go | 4 |
9 files changed, 272 insertions, 30 deletions
diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index a3133b9c..3a36dfaa 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -20,8 +21,10 @@ var ErrImportNotSupported = errors.New("import is not supported") var ErrExportNotSupported = errors.New("export is not supported") const ( - ConfigKeyTarget = "target" - MetaKeyOrigin = "origin" + ConfigKeyTarget = "target" + ConfigKeyToken = "token" + ConfigKeyTokenId = "token-id" + MetaKeyOrigin = "origin" bridgeConfigKeyPrefix = "git-bug.bridge" ) @@ -35,6 +38,7 @@ type BridgeParams struct { Project string URL string Token string + TokenId string TokenStdin bool } @@ -276,6 +280,13 @@ func (b *Bridge) ensureInit() error { return nil } + token, err := LoadToken(b.repo, entity.Id(b.conf[ConfigKeyTokenId])) + if err != nil { + return err + } + + b.conf[ConfigKeyToken] = token.Value + importer := b.getImporter() if importer != nil { err := importer.Init(b.conf) diff --git a/bridge/core/token.go b/bridge/core/token.go index 449ebbb5..28c64f5c 100644 --- a/bridge/core/token.go +++ b/bridge/core/token.go @@ -122,8 +122,7 @@ func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) return LoadToken(repo, matching[0]) } -// ListTokens return a map representing the stored tokens in the repo config and global config -// along with their type (global: true, local:false) +// ListTokens list all existing token ids func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) { configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".") if err != nil { @@ -157,6 +156,99 @@ func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) { return result, nil } +// ListTokensWithTarget list all token ids associated with the target +func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) { + var ids []entity.Id + tokensIds, err := ListTokens(repo) + if err != nil { + return nil, err + } + + for _, tokenId := range tokensIds { + token, err := LoadToken(repo, tokenId) + if err != nil { + return nil, err + } + + if token.Target == target { + ids = append(ids, tokenId) + } + } + return ids, nil +} + +// LoadTokens load all existing tokens +func LoadTokens(repo repository.RepoCommon) ([]*Token, error) { + tokensIds, err := ListTokens(repo) + if err != nil { + return nil, err + } + + var tokens []*Token + for _, id := range tokensIds { + token, err := LoadToken(repo, id) + if err != nil { + return nil, err + } + tokens = append(tokens, token) + } + return tokens, nil +} + +// LoadTokensWithTarget load all existing tokens for a given target +func LoadTokensWithTarget(repo repository.RepoCommon, target string) ([]*Token, error) { + tokensIds, err := ListTokens(repo) + if err != nil { + return nil, err + } + + var tokens []*Token + for _, id := range tokensIds { + token, err := LoadToken(repo, id) + if err != nil { + return nil, err + } + if token.Target == target { + tokens = append(tokens, token) + } + } + return tokens, nil +} + +// TokenIdExist return wether token id exist or not +func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool { + _, err := LoadToken(repo, id) + return err == nil +} + +// TokenExist return wether there is a token with a certain value or not +func TokenExist(repo repository.RepoCommon, value string) bool { + tokens, err := LoadTokens(repo) + if err != nil { + return false + } + for _, token := range tokens { + if token.Value == value { + return true + } + } + return false +} + +// TokenExistWithTarget same as TokenExist but restrict search for a given target +func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool { + tokens, err := LoadTokensWithTarget(repo, target) + if err != nil { + return false + } + for _, token := range tokens { + if token.Value == value { + return true + } + } + return false +} + // StoreToken stores a token in the repo config func StoreToken(repo repository.RepoCommon, token *Token) error { storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey) @@ -180,3 +272,25 @@ func RemoveToken(repo repository.RepoCommon, id entity.Id) error { keyPrefix := fmt.Sprintf("git-bug.token.%s", id) return repo.GlobalConfig().RemoveAll(keyPrefix) } + +// LoadOrCreateToken will try to load a token matching the same value or create it +func LoadOrCreateToken(repo repository.RepoCommon, target, tokenValue string) (*Token, error) { + tokens, err := LoadTokensWithTarget(repo, target) + if err != nil { + return nil, err + } + + for _, token := range tokens { + if token.Value == tokenValue { + return token, nil + } + } + + token := NewToken(tokenValue, target) + err = StoreToken(repo, token) + if err != nil { + return nil, err + } + + return token, nil +} diff --git a/bridge/github/config.go b/bridge/github/config.go index e76a14f4..0fbbd5aa 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -21,6 +21,7 @@ import ( "golang.org/x/crypto/ssh/terminal" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/interrupt" ) @@ -43,10 +44,12 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) conf := make(core.Configuration) var err error var token string + var tokenId entity.Id + var tokenObj *core.Token var owner string var project string - if (params.Token != "" || params.TokenStdin) && + if (params.Token != "" || params.TokenId != "" || params.TokenStdin) && (params.URL == "" && (params.Project == "" || params.Owner == "")) { return nil, fmt.Errorf("you must provide a project URL or Owner/Name to configure this bridge with a token") } @@ -87,11 +90,11 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) return nil, fmt.Errorf("invalid parameter owner: %v", owner) } - // try to get token from params if provided, else use terminal prompt to either - // enter a token or login and generate a new one + // try to get token from params if provided, else use terminal prompt + // to either enter a token or login and generate a new one, or choose + // an existing token if params.Token != "" { token = params.Token - } else if params.TokenStdin { reader := bufio.NewReader(os.Stdin) token, err = reader.ReadString('\n') @@ -99,15 +102,33 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) return nil, fmt.Errorf("reading from stdin: %v", err) } token = strings.TrimSuffix(token, "\n") + } else if params.TokenId != "" { + tokenId = entity.Id(params.TokenId) } else { - token, err = promptTokenOptions(owner, project) + tokenObj, err = promptTokenOptions(repo, owner, project) + if err != nil { + return nil, err + } + } + + // at this point, we check if the token already exist or we create a new one + if token != "" { + tokenObj, err = core.LoadOrCreateToken(repo, target, token) if err != nil { return nil, err } + } else if tokenId != "" { + tokenObj, err = core.LoadToken(repo, entity.Id(tokenId)) + if err != nil { + return nil, err + } + if tokenObj.Target != target { + return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target) + } } // verify access to the repository with token - ok, err = validateProject(owner, project, token) + ok, err = validateProject(owner, project, tokenObj.Value) if err != nil { return nil, err } @@ -116,7 +137,7 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) } conf[core.ConfigKeyTarget] = target - conf[keyToken] = token + conf[core.ConfigKeyTokenId] = tokenObj.ID().String() conf[keyOwner] = owner conf[keyProject] = project @@ -135,8 +156,8 @@ func (*Github) 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[core.ConfigKeyTokenId]; !ok { + return fmt.Errorf("missing %s key", core.ConfigKeyTokenId) } if _, ok := conf[keyOwner]; !ok { @@ -220,32 +241,58 @@ func randomFingerprint() string { return string(b) } -func promptTokenOptions(owner, project string) (string, error) { +func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*core.Token, error) { for { + tokens, err := core.LoadTokensWithTarget(repo, target) + if err != nil { + return nil, err + } + fmt.Println() fmt.Println("[1]: user provided token") fmt.Println("[2]: interactive token creation") + + if len(tokens) > 0 { + fmt.Println("known tokens for Github:") + for i, token := range tokens { + if token.Target == target { + fmt.Printf("[%d]: %s\n", i+3, token.ID()) + } + } + } fmt.Print("Select option: ") line, err := bufio.NewReader(os.Stdin).ReadString('\n') fmt.Println() if err != nil { - return "", err + return nil, err } line = strings.TrimRight(line, "\n") index, err := strconv.Atoi(line) - if err != nil || (index != 1 && index != 2) { + if err != nil || index < 1 || index > len(tokens)+2 { fmt.Println("invalid input") continue } - if index == 1 { - return promptToken() + var token string + switch index { + case 1: + token, err = promptToken() + if err != nil { + return nil, err + } + case 2: + token, err = loginAndRequestToken(owner, project) + if err != nil { + return nil, err + } + default: + return tokens[index-3], nil } - return loginAndRequestToken(owner, project) + return core.LoadOrCreateToken(repo, target, token) } } diff --git a/bridge/github/export.go b/bridge/github/export.go index 2fb92636..8d515802 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -87,14 +87,14 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, return nil, err } - ge.identityToken[user.Id()] = ge.conf[keyToken] + ge.identityToken[user.Id()] = ge.conf[core.ConfigKeyToken] // get repository node id ge.repositoryID, err = getRepositoryNodeID( ctx, ge.conf[keyOwner], ge.conf[keyProject], - ge.conf[keyToken], + ge.conf[core.ConfigKeyToken], ) if err != nil { @@ -512,7 +512,7 @@ func (ge *githubExporter) createGithubLabel(ctx context.Context, label, color st req = req.WithContext(ctx) // need the token for private repositories - req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken])) + req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[core.ConfigKeyToken])) resp, err := client.Do(req) if err != nil { diff --git a/bridge/github/import.go b/bridge/github/import.go index 86444057..c0fb3d6c 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -39,7 +39,7 @@ func (gi *githubImporter) Init(conf core.Configuration) error { // ImportAll iterate over all the configured repository issues and ensure the creation of the // missing issues / timeline items / edits / label events ... func (gi *githubImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) { - gi.iterator = NewIterator(ctx, 10, gi.conf[keyOwner], gi.conf[keyProject], gi.conf[keyToken], since) + gi.iterator = NewIterator(ctx, 10, gi.conf[keyOwner], gi.conf[keyProject], gi.conf[core.ConfigKeyToken], since) out := make(chan core.ImportResult) gi.out = out @@ -553,7 +553,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, "login": githubv4.String("ghost"), } - gc := buildClient(gi.conf[keyToken]) + gc := buildClient(gi.conf[core.ConfigKeyToken]) ctx, cancel := context.WithTimeout(gi.iterator.ctx, defaultTimeout) defer cancel() diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index f2e667a8..88ba7db8 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -13,6 +13,7 @@ import ( "github.com/xanzy/go-gitlab" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -32,6 +33,8 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) var err error var url string var token string + var tokenId entity.Id + var tokenObj *core.Token if (params.Token != "" || params.TokenStdin) && params.URL == "" { return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token") @@ -65,21 +68,38 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) return nil, fmt.Errorf("reading from stdin: %v", err) } token = strings.TrimSuffix(token, "\n") + } else if params.TokenId != "" { + tokenId = entity.Id(params.TokenId) } else { - token, err = promptToken() + tokenObj, err = promptTokenOptions(repo) if err != nil { return nil, errors.Wrap(err, "token prompt") } } + if token != "" { + tokenObj, err = core.LoadOrCreateToken(repo, target, token) + if err != nil { + return nil, err + } + } else if tokenId != "" { + tokenObj, err = core.LoadToken(repo, entity.Id(tokenId)) + if err != nil { + return nil, err + } + if tokenObj.Target != target { + return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target) + } + } + // validate project url and get its ID - id, err := validateProjectURL(url, token) + id, err := validateProjectURL(url, tokenObj.Value) if err != nil { return nil, errors.Wrap(err, "project validation") } conf[keyProjectID] = strconv.Itoa(id) - conf[keyToken] = token + conf[core.ConfigKeyTokenId] = tokenObj.ID().String() conf[core.ConfigKeyTarget] = target err = g.ValidateConfig(conf) @@ -108,6 +128,54 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { return nil } +func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) { + for { + tokens, err := core.LoadTokensWithTarget(repo, target) + if err != nil { + return nil, err + } + + fmt.Println() + fmt.Println("[1]: user provided token") + + if len(tokens) > 0 { + fmt.Println("known tokens for Gitlab:") + for i, token := range tokens { + if token.Target == target { + fmt.Printf("[%d]: %s\n", i+2, token.ID()) + } + } + } + fmt.Print("Select option: ") + + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + fmt.Println() + if err != nil { + return nil, err + } + + line = strings.TrimRight(line, "\n") + index, err := strconv.Atoi(line) + if err != nil || index < 1 || index > len(tokens)+1 { + fmt.Println("invalid input") + continue + } + + var token string + switch index { + case 1: + token, err = promptToken() + if err != nil { + return nil, err + } + default: + return tokens[index-2], nil + } + + return core.LoadOrCreateToken(repo, target, token) + } +} + func promptToken() (string, error) { fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.") fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.") diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index 7c00e39d..092434a5 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -79,7 +79,7 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, return nil, err } - ge.identityToken[user.Id().String()] = ge.conf[keyToken] + ge.identityToken[user.Id().String()] = ge.conf[core.ConfigKeyToken] // get repository node id ge.repositoryID = ge.conf[keyProjectID] diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index 92e9952e..4fcf8568 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -34,7 +34,7 @@ func (gi *gitlabImporter) Init(conf core.Configuration) error { // 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[keyToken], since) + gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since) out := make(chan core.ImportResult) gi.out = out diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go index 3562af17..6f314135 100644 --- a/commands/bridge_configure.go +++ b/commands/bridge_configure.go @@ -34,7 +34,8 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - if (bridgeParams.TokenStdin || bridgeParams.Token != "") && (bridgeConfigureName == "" || bridgeConfigureTarget == "") { + if (bridgeParams.TokenStdin || bridgeParams.Token != "" || bridgeParams.TokenId != "") && + (bridgeConfigureName == "" || bridgeConfigureTarget == "") { return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a token") } @@ -195,6 +196,7 @@ func init() { bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.URL, "url", "u", "", "The URL of the target repository") bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Owner, "owner", "o", "", "The owner of the target repository") bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Token, "token", "T", "", "The authentication token for the API") + bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.TokenId, "token-id", "i", "", "The authentication token identifier for the API") bridgeConfigureCmd.Flags().BoolVar(&bridgeParams.TokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token") bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Project, "project", "p", "", "The name of the target repository") bridgeConfigureCmd.Flags().SortFlags = false |