Merge pull request #260 from MichaelMure/bridge

Support bridge configuration with global tokens
This commit is contained in:
Michael Muré 2019-11-24 23:54:23 +01:00 committed by GitHub
commit 44f648a931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 272 additions and 30 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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()

View File

@ -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.")

View File

@ -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]

View File

@ -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

View File

@ -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