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