package auth
import (
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
const (
configKeyPrefix = "git-bug.auth"
configKeyKind = "kind"
configKeyUserId = "userid"
configKeyTarget = "target"
configKeyCreateTime = "createtime"
)
type CredentialKind string
const (
KindToken CredentialKind = "token"
KindLoginPassword CredentialKind = "login-password"
)
var ErrCredentialNotExist = errors.New("credential doesn't exist")
func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
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
// 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.
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
switch CredentialKind(configs[configKeyKind]) {
case KindToken:
cred = NewTokenFromConfig(configs)
case KindLoginPassword:
default:
return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
}
return cred, nil
}
// 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, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
if err != nil {
panic(err)
}
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
}
// 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 {
return err
}
// CreateTime
err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
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)
}
// 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
*/
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]
}