package auth
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
const (
keyringKeyPrefix = "auth-"
keyringKeyKind = "kind"
keyringKeyTarget = "target"
keyringKeyCreateTime = "createtime"
keyringKeySalt = "salt"
keyringKeyPrefixMeta = "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.RepoKeyring, id entity.Id) (Credential, error) {
key := fmt.Sprintf("%s%s", keyringKeyPrefix, id)
item, err := repo.Keyring().Get(key)
if err == repository.ErrKeyringKeyNotFound {
return nil, ErrCredentialNotExist
}
if err != nil {
return nil, err
}
return decode(item)
}
// LoadWithPrefix load a credential from the repo config with a prefix
func LoadWithPrefix(repo repository.RepoKeyring, prefix string) (Credential, error) {
keys, err := repo.Keyring().Keys()
if err != nil {
return nil, err
}
// preallocate but empty
matching := make([]Credential, 0, 5)
for _, key := range keys {
if !strings.HasPrefix(key, keyringKeyPrefix+prefix) {
continue
}
item, err := repo.Keyring().Get(key)
if err != nil {
return nil, err
}
cred, err := decode(item)
if err != nil {
return nil, err
}
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
}
// decode is a helper to construct a Credential from the keyring Item
func decode(item repository.Item) (Credential, error) {
data := make(map[string]string)
err := json.Unmarshal(item.Data, &data)
if err != nil {
return nil, err
}
var cred Credential
switch CredentialKind(data[keyringKeyKind]) {
case KindToken:
cred, err = NewTokenFromConfig(data)
case KindLogin:
cred, err = NewLoginFromConfig(data)
case KindLoginPassword:
cred, err = NewLoginPasswordFromConfig(data)
default:
return nil, fmt.Errorf("unknown credential type \"%s\"", data[keyringKeyKind])
}
if err != nil {
return nil, fmt.Errorf("loading credential: %v", err)
}
return cred, nil
}
// List load all existing credentials
func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) {
keys, err := repo.Keyring().Keys()
if err != nil {
return nil, err
}
matcher := matcher(opts)
var credentials []Credential
for _, key := range keys {
if !strings.HasPrefix(key, keyringKeyPrefix) {
continue
}
item, err := repo.Keyring().Get(key)
if err != nil {
return nil, err
}
cred, err := decode(item)
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.RepoKeyring, 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.RepoKeyring, prefix string) bool {
_, err := LoadWithPrefix(repo, prefix)
return err == nil
}
// Store stores a credential in the global git config
func Store(repo repository.RepoKeyring, cred Credential) error {
if len(cred.Salt()) != 16 {
panic("credentials need to be salted")
}
confs := cred.toConfig()
confs[keyringKeyKind] = string(cred.Kind())
confs[keyringKeyTarget] = cred.Target()
confs[keyringKeyCreateTime] = strconv.Itoa(int(cred.CreateTime().Unix()))
confs[keyringKeySalt] = base64.StdEncoding.EncodeToString(cred.Salt())
for key, val := range cred.Metadata() {
confs[keyringKeyPrefixMeta+key] = val
}
data, err := json.Marshal(confs)
if err != nil {
return err
}
return repo.Keyring().Set(repository.Item{
Key: keyringKeyPrefix + cred.ID().String(),
Data: data,
})
}
// Remove removes a credential from the global git config
func Remove(repo repository.RepoKeyring, id entity.Id) error {
return repo.Keyring().Remove(keyringKeyPrefix + id.String())
}
/*
* 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]
}