aboutsummaryrefslogblamecommitdiffstats
path: root/bridge/core/auth/credential.go
blob: d95b23c7357eddf5288c7871596c39e5c3799813 (plain) (tree)
1
2
3
4
5


            

                         












                                                   

                                          
                                    

                                     

                                   




                          
                                                  
                                                  
                                                           









                                                                                   
                             
                       
                              
                     
                        
 
                                    

                                              

                                                                                                             
                                                                            
                                    














































                                                                                    
                                                                                   










                                                                                     
                     
 
                                                       
                       


                                                       
                               
                                                               
                
                                                                                                

         



                                                                     


                        







                                                                          


                             


                     
















                                                                  






                                                                             
                                                                                           











































                                                                  
                                








                                                                                         











                                                                                               









                                                                            







                                                                                           

































                                                                       
package auth

import (
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/MichaelMure/git-bug/entity"
	"github.com/MichaelMure/git-bug/repository"
)

const (
	configKeyPrefix     = "git-bug.auth"
	configKeyKind       = "kind"
	configKeyTarget     = "target"
	configKeyCreateTime = "createtime"
	configKeySalt       = "salt"
	configKeyPrefixMeta = "meta."

	MetaKeyLogin   = "login"
	MetaKeyBaseURL = "base-url"
)

type CredentialKind string

const (
	KindToken         CredentialKind = "token"
	KindLogin         CredentialKind = "login"
	KindLoginPassword CredentialKind = "login-password"
)

var ErrCredentialNotExist = errors.New("credential doesn't exist")

func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
	return entity.NewErrMultipleMatch("credential", matching)
}

type Credential interface {
	ID() entity.Id
	Kind() CredentialKind
	Target() string
	CreateTime() time.Time
	Salt() []byte
	Validate() error

	Metadata() map[string]string
	GetMetadata(key string) (string, bool)
	SetMetadata(key string, value string)

	// Return all the specific properties of the credential that need to be saved into the configuration.
	// This does not include Target, Kind, CreateTime, Metadata or Salt.
	toConfig() map[string]string
}

// Load loads a credential from the repo config
func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)

	// read token config pairs
	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
	if err != nil {
		// Not exactly right due to the limitation of ReadAll()
		return nil, ErrCredentialNotExist
	}

	return loadFromConfig(rawconfigs, id)
}

// LoadWithPrefix load a credential from the repo config with a prefix
func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
	creds, err := List(repo)
	if err != nil {
		return nil, err
	}

	// preallocate but empty
	matching := make([]Credential, 0, 5)

	for _, cred := range creds {
		if cred.ID().HasPrefix(prefix) {
			matching = append(matching, cred)
		}
	}

	if len(matching) > 1 {
		ids := make([]entity.Id, len(matching))
		for i, cred := range matching {
			ids[i] = cred.ID()
		}
		return nil, NewErrMultipleMatchCredential(ids)
	}

	if len(matching) == 0 {
		return nil, ErrCredentialNotExist
	}

	return matching[0], nil
}

// loadFromConfig is a helper to construct a Credential from the set of git configs
func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)

	// trim key prefix
	configs := make(map[string]string)
	for key, value := range rawConfigs {
		newKey := strings.TrimPrefix(key, keyPrefix)
		configs[newKey] = value
	}

	var cred Credential
	var err error

	switch CredentialKind(configs[configKeyKind]) {
	case KindToken:
		cred, err = NewTokenFromConfig(configs)
	case KindLogin:
		cred, err = NewLoginFromConfig(configs)
	case KindLoginPassword:
		cred, err = NewLoginPasswordFromConfig(configs)
	default:
		return nil, fmt.Errorf("unknown credential type \"%s\"", configs[configKeyKind])
	}

	if err != nil {
		return nil, fmt.Errorf("loading credential: %v", 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
		}
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func makeSalt() []byte {
	result := make([]byte, 16)
	_, err := rand.Read(result)
	if err != nil {
		panic(err)
	}
	return result
}

func saltFromConfig(configs map[string]string) ([]byte, error) {
	val, ok := configs[configKeySalt]
	if !ok {
		return nil, fmt.Errorf("no credential salt found")
	}
	return base64.StdEncoding.DecodeString(val)
}

// List load all existing credentials
func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
	if err != nil {
		return nil, err
	}

	re := regexp.MustCompile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)

	mapped := make(map[string]map[string]string)

	for key, val := range rawConfigs {
		res := re.FindStringSubmatch(key)
		if res == nil {
			continue
		}
		if mapped[res[1]] == nil {
			mapped[res[1]] = make(map[string]string)
		}
		mapped[res[1]][res[2]] = val
	}

	matcher := matcher(opts)

	var credentials []Credential
	for id, kvs := range mapped {
		cred, err := loadFromConfig(kvs, entity.Id(id))
		if err != nil {
			return nil, err
		}
		if matcher.Match(cred) {
			credentials = append(credentials, cred)
		}
	}

	return credentials, nil
}

// IdExist return whether a credential id exist or not
func IdExist(repo repository.RepoConfig, id entity.Id) bool {
	_, err := LoadWithId(repo, id)
	return err == nil
}

// PrefixExist return whether a credential id prefix exist or not
func PrefixExist(repo repository.RepoConfig, prefix string) bool {
	_, err := LoadWithPrefix(repo, prefix)
	return err == nil
}

// Store stores a credential in the global git config
func Store(repo repository.RepoConfig, cred Credential) error {
	confs := cred.toConfig()

	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())

	// Kind
	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
	if err != nil {
		return err
	}

	// Target
	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
	if err != nil {
		return err
	}

	// CreateTime
	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
	if err != nil {
		return err
	}

	// Salt
	if len(cred.Salt()) != 16 {
		panic("credentials need to be salted")
	}
	encoded := base64.StdEncoding.EncodeToString(cred.Salt())
	err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
	if err != nil {
		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)
		if err != nil {
			return err
		}
	}

	return nil
}

// Remove removes a credential from the global git config
func Remove(repo repository.RepoConfig, id entity.Id) error {
	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
	return repo.GlobalConfig().RemoveAll(keyPrefix)
}

/*
 * Sorting
 */

type ById []Credential

func (b ById) Len() int {
	return len(b)
}

func (b ById) Less(i, j int) bool {
	return b[i].ID() < b[j].ID()
}

func (b ById) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}