WIP

Michael Muré created

Change summary

bridge/core/auth/credential.go | 56 ++++++++++++------------------
bridge/core/auth/options.go    | 32 +++++++----------
bridge/core/auth/token.go      | 20 ++++------
bridge/core/bridge.go          | 13 +++---
bridge/github/config.go        | 65 ++++++++++++++---------------------
bridge/gitlab/config.go        |  2 
6 files changed, 78 insertions(+), 110 deletions(-)

Detailed changes

bridge/core/auth/credential.go 🔗

@@ -14,9 +14,11 @@ import (
 const (
 	configKeyPrefix     = "git-bug.auth"
 	configKeyKind       = "kind"
-	configKeyUserId     = "userid"
 	configKeyTarget     = "target"
 	configKeyCreateTime = "createtime"
+	configKeyPrefixMeta = "meta."
+
+	MetaKeyLogin = "login"
 )
 
 type CredentialKind string
@@ -32,19 +34,13 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
 	return entity.NewErrMultipleMatch("credential", matching)
 }
 
-// Special Id to mark a credential as being associated to the default user, whoever it might be.
-// The intended use is for the bridge configuration, to be able to create and store a credential
-// with no identities created yet, and then select one with `git-bug user adopt`
-const DefaultUserId = entity.Id("default-user")
-
 type Credential interface {
 	ID() entity.Id
-	UserId() entity.Id
-	updateUserId(id entity.Id)
 	Target() string
 	Kind() CredentialKind
 	CreateTime() time.Time
 	Validate() error
+	Metadata() map[string]string
 
 	// Return all the specific properties of the credential that need to be saved into the configuration.
 	// This does not include Target, User, Kind and CreateTime.
@@ -120,6 +116,17 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err
 	return cred, nil
 }
 
+func metaFromConfig(configs map[string]string) map[string]string {
+	result := make(map[string]string)
+	for key, val := range configs {
+		if strings.HasPrefix(key, configKeyPrefixMeta) {
+			key = strings.TrimPrefix(key, configKeyPrefixMeta)
+			result[key] = val
+		}
+	}
+	return result
+}
+
 // List load all existing credentials
 func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
 	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
@@ -185,12 +192,6 @@ func Store(repo repository.RepoConfig, cred Credential) error {
 		return err
 	}
 
-	// UserId
-	err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
-	if err != nil {
-		return err
-	}
-
 	// Target
 	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
 	if err != nil {
@@ -203,6 +204,14 @@ func Store(repo repository.RepoConfig, cred Credential) error {
 		return err
 	}
 
+	// Metadata
+	for key, val := range cred.Metadata() {
+		err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
+		if err != nil {
+			return err
+		}
+	}
+
 	// Custom
 	for key, val := range confs {
 		err := repo.GlobalConfig().StoreString(prefix+key, val)
@@ -220,25 +229,6 @@ func Remove(repo repository.RepoConfig, id entity.Id) error {
 	return repo.GlobalConfig().RemoveAll(keyPrefix)
 }
 
-// ReplaceDefaultUser update all the credential attributed to the temporary "default user"
-// with a real user Id
-func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error {
-	list, err := List(repo, WithUserId(DefaultUserId))
-	if err != nil {
-		return err
-	}
-
-	for _, cred := range list {
-		cred.updateUserId(id)
-		err = Store(repo, cred)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
 /*
  * Sorting
  */

bridge/core/auth/options.go 🔗

@@ -1,14 +1,9 @@
 package auth
 
-import (
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/identity"
-)
-
 type options struct {
 	target string
-	userId entity.Id
 	kind   CredentialKind
+	meta   map[string]string
 }
 
 type Option func(opts *options)
@@ -26,12 +21,14 @@ func (opts *options) Match(cred Credential) bool {
 		return false
 	}
 
-	if opts.userId != "" && cred.UserId() != opts.userId {
+	if opts.kind != "" && cred.Kind() != opts.kind {
 		return false
 	}
 
-	if opts.kind != "" && cred.Kind() != opts.kind {
-		return false
+	for key, val := range opts.meta {
+		if v, ok := cred.Metadata()[key]; !ok || v != val {
+			return false
+		}
 	}
 
 	return true
@@ -43,20 +40,17 @@ func WithTarget(target string) Option {
 	}
 }
 
-func WithUser(user identity.Interface) Option {
-	return func(opts *options) {
-		opts.userId = user.Id()
-	}
-}
-
-func WithUserId(userId entity.Id) Option {
+func WithKind(kind CredentialKind) Option {
 	return func(opts *options) {
-		opts.userId = userId
+		opts.kind = kind
 	}
 }
 
-func WithKind(kind CredentialKind) Option {
+func WithMeta(key string, val string) Option {
 	return func(opts *options) {
-		opts.kind = kind
+		if opts.meta == nil {
+			opts.meta = make(map[string]string)
+		}
+		opts.meta[key] = val
 	}
 }

bridge/core/auth/token.go 🔗

@@ -18,26 +18,25 @@ var _ Credential = &Token{}
 
 // Token holds an API access token data
 type Token struct {
-	userId     entity.Id
 	target     string
 	createTime time.Time
 	Value      string
+	meta       map[string]string
 }
 
 // NewToken instantiate a new token
-func NewToken(userId entity.Id, value, target string) *Token {
+func NewToken(value, target string) *Token {
 	return &Token{
-		userId:     userId,
 		target:     target,
 		createTime: time.Now(),
 		Value:      value,
+		meta:       make(map[string]string),
 	}
 }
 
 func NewTokenFromConfig(conf map[string]string) *Token {
 	token := &Token{}
 
-	token.userId = entity.Id(conf[configKeyUserId])
 	token.target = conf[configKeyTarget]
 	if createTime, ok := conf[configKeyCreateTime]; ok {
 		if t, err := repository.ParseTimestamp(createTime); err == nil {
@@ -46,6 +45,7 @@ func NewTokenFromConfig(conf map[string]string) *Token {
 	}
 
 	token.Value = conf[tokenValueKey]
+	token.meta = metaFromConfig(conf)
 
 	return token
 }
@@ -55,14 +55,6 @@ func (t *Token) ID() entity.Id {
 	return entity.Id(fmt.Sprintf("%x", sum))
 }
 
-func (t *Token) UserId() entity.Id {
-	return t.userId
-}
-
-func (t *Token) updateUserId(id entity.Id) {
-	t.userId = id
-}
-
 func (t *Token) Target() string {
 	return t.target
 }
@@ -92,6 +84,10 @@ func (t *Token) Validate() error {
 	return nil
 }
 
+func (t *Token) Metadata() map[string]string {
+	return t.meta
+}
+
 func (t *Token) toConfig() map[string]string {
 	return map[string]string{
 		tokenValueKey: t.Value,

bridge/core/bridge.go 🔗

@@ -32,12 +32,13 @@ var bridgeImpl map[string]reflect.Type
 // BridgeParams holds parameters to simplify the bridge configuration without
 // having to make terminal prompts.
 type BridgeParams struct {
-	Owner      string
-	Project    string
-	URL        string
-	BaseURL    string
-	CredPrefix string
-	TokenRaw   string
+	Owner      string // owner of the repo                    (Github)
+	Project    string // name of the repo                     (Github,         Launchpad)
+	URL        string // complete URL of a repo               (Github, Gitlab, Launchpad)
+	BaseURL    string // base URL for self-hosted instance    (        Gitlab)
+	CredPrefix string // ID prefix of the credential to use   (Github, Gitlab)
+	TokenRaw   string // pre-existing token to use            (Github, Gitlab)
+	Login      string // username for the passed credential   (Github, Gitlab)
 }
 
 // Bridge is a wrapper around a BridgeImpl that will bind low-level

bridge/github/config.go 🔗

@@ -22,8 +22,6 @@ import (
 	"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/identity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/colors"
@@ -49,12 +47,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 
 	conf := make(core.Configuration)
 	var err error
-
-	if (params.CredPrefix != "" || params.TokenRaw != "") &&
-		(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")
-	}
-
 	var owner string
 	var project string
 
@@ -87,15 +79,12 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		return nil, fmt.Errorf("invalid parameter owner: %v", owner)
 	}
 
-	user, err := repo.GetUserIdentity()
-	if err != nil && err != identity.ErrNoIdentitySet {
-		return nil, err
-	}
-
-	// default to a "to be filled" user Id if we don't have a valid one yet
-	userId := auth.DefaultUserId
-	if user != nil {
-		userId = user.Id()
+	login := params.Login
+	if login == "" {
+		login, err = input.Prompt("Github login", "", true, validateUsername)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	var cred auth.Credential
@@ -106,13 +95,11 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		if err != nil {
 			return nil, err
 		}
-		if user != nil && cred.UserId() != user.Id() {
-			return nil, fmt.Errorf("selected credential don't match the user")
-		}
 	case params.TokenRaw != "":
-		cred = auth.NewToken(userId, params.TokenRaw, target)
+		cred = auth.NewToken(params.TokenRaw, target)
+		cred.Metadata()[auth.MetaKeyLogin] = login
 	default:
-		cred, err = promptTokenOptions(repo, userId, owner, project)
+		cred, err = promptTokenOptions(repo, login, owner, project)
 		if err != nil {
 			return nil, err
 		}
@@ -170,11 +157,11 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
 	return nil
 }
 
-func requestToken(note, username, password string, scope string) (*http.Response, error) {
-	return requestTokenWith2FA(note, username, password, "", scope)
+func requestToken(note, login, password string, scope string) (*http.Response, error) {
+	return requestTokenWith2FA(note, login, password, "", scope)
 }
 
-func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) {
+func requestTokenWith2FA(note, login, password, otpCode string, scope string) (*http.Response, error) {
 	url := fmt.Sprintf("%s/authorizations", githubV3Url)
 	params := struct {
 		Scopes      []string `json:"scopes"`
@@ -196,7 +183,7 @@ func requestTokenWith2FA(note, username, password, otpCode string, scope string)
 		return nil, err
 	}
 
-	req.SetBasicAuth(username, password)
+	req.SetBasicAuth(login, password)
 	req.Header.Set("Content-Type", "application/json")
 
 	if otpCode != "" {
@@ -240,9 +227,9 @@ func randomFingerprint() string {
 	return string(b)
 }
 
-func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) {
+func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) {
 	for {
-		creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target))
+		creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login))
 		if err != nil {
 			return nil, err
 		}
@@ -258,10 +245,11 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
 			fmt.Println("Existing tokens for Github:")
 			for i, cred := range creds {
 				token := cred.(*auth.Token)
-				fmt.Printf("[%d]: %s => %s (%s)\n",
+				fmt.Printf("[%d]: %s => %s (login: %s, %s)\n",
 					i+3,
 					colors.Cyan(token.ID().Human()),
 					colors.Red(text.TruncateMax(token.Value, 10)),
+					token.Metadata()[auth.MetaKeyLogin],
 					token.CreateTime().Format(time.RFC822),
 				)
 			}
@@ -289,13 +277,17 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
 			if err != nil {
 				return nil, err
 			}
-			return auth.NewToken(userId, value, target), nil
+			token := auth.NewToken(value, target)
+			token.Metadata()[auth.MetaKeyLogin] = login
+			return token, nil
 		case 2:
-			value, err := loginAndRequestToken(owner, project)
+			value, err := loginAndRequestToken(login, owner, project)
 			if err != nil {
 				return nil, err
 			}
-			return auth.NewToken(userId, value, target), nil
+			token := auth.NewToken(value, target)
+			token.Metadata()[auth.MetaKeyLogin] = login
+			return token, nil
 		default:
 			return creds[index-3], nil
 		}
@@ -328,7 +320,7 @@ func promptToken() (string, error) {
 	return input.Prompt("Enter token", "token", "", input.Required, validator)
 }
 
-func loginAndRequestToken(owner, project string) (string, error) {
+func loginAndRequestToken(login, owner, project string) (string, error) {
 	fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the global git config.")
 	fmt.Println()
 	fmt.Println("The access scope depend on the type of repository.")
@@ -345,11 +337,6 @@ func loginAndRequestToken(owner, project string) (string, error) {
 	}
 	isPublic := i == 0
 
-	username, err := promptUsername()
-	if err != nil {
-		return "", err
-	}
-
 	password, err := input.PromptPassword("Password", "password", input.Required)
 	if err != nil {
 		return "", err
@@ -369,7 +356,7 @@ func loginAndRequestToken(owner, project string) (string, error) {
 
 	note := fmt.Sprintf("git-bug - %s/%s", owner, project)
 
-	resp, err := requestToken(note, username, password, scope)
+	resp, err := requestToken(note, login, password, scope)
 	if err != nil {
 		return "", err
 	}

bridge/gitlab/config.go 🔗

@@ -212,7 +212,7 @@ func validateBaseUrl(baseUrl string) (bool, error) {
 
 func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl string) (auth.Credential, error) {
 	for {
-		creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
+		creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
 		if err != nil {
 			return nil, err
 		}