1package auth
  2
  3import (
  4	"encoding/base64"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"strconv"
  9	"strings"
 10	"time"
 11
 12	"github.com/MichaelMure/git-bug/entity"
 13	"github.com/MichaelMure/git-bug/repository"
 14)
 15
 16const (
 17	keyringKeyPrefix     = "auth-"
 18	keyringKeyKind       = "kind"
 19	keyringKeyTarget     = "target"
 20	keyringKeyCreateTime = "createtime"
 21	keyringKeySalt       = "salt"
 22	keyringKeyPrefixMeta = "meta."
 23
 24	MetaKeyLogin   = "login"
 25	MetaKeyBaseURL = "base-url"
 26)
 27
 28type CredentialKind string
 29
 30const (
 31	KindToken         CredentialKind = "token"
 32	KindLogin         CredentialKind = "login"
 33	KindLoginPassword CredentialKind = "login-password"
 34)
 35
 36var ErrCredentialNotExist = errors.New("credential doesn't exist")
 37
 38func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
 39	return entity.NewErrMultipleMatch("credential", matching)
 40}
 41
 42type Credential interface {
 43	ID() entity.Id
 44	Kind() CredentialKind
 45	Target() string
 46	CreateTime() time.Time
 47	Salt() []byte
 48	Validate() error
 49
 50	Metadata() map[string]string
 51	GetMetadata(key string) (string, bool)
 52	SetMetadata(key string, value string)
 53
 54	// Return all the specific properties of the credential that need to be saved into the configuration.
 55	// This does not include Target, Kind, CreateTime, Metadata or Salt.
 56	toConfig() map[string]string
 57}
 58
 59// Load loads a credential from the repo config
 60func LoadWithId(repo repository.RepoKeyring, id entity.Id) (Credential, error) {
 61	key := fmt.Sprintf("%s%s", keyringKeyPrefix, id)
 62
 63	item, err := repo.Keyring().Get(key)
 64	if err == repository.ErrKeyringKeyNotFound {
 65		return nil, ErrCredentialNotExist
 66	}
 67	if err != nil {
 68		return nil, err
 69	}
 70
 71	return decode(item)
 72}
 73
 74// LoadWithPrefix load a credential from the repo config with a prefix
 75func LoadWithPrefix(repo repository.RepoKeyring, prefix string) (Credential, error) {
 76	keys, err := repo.Keyring().Keys()
 77	if err != nil {
 78		return nil, err
 79	}
 80
 81	// preallocate but empty
 82	matching := make([]Credential, 0, 5)
 83
 84	for _, key := range keys {
 85		if !strings.HasPrefix(key, keyringKeyPrefix+prefix) {
 86			continue
 87		}
 88
 89		item, err := repo.Keyring().Get(key)
 90		if err != nil {
 91			return nil, err
 92		}
 93
 94		cred, err := decode(item)
 95		if err != nil {
 96			return nil, err
 97		}
 98
 99		matching = append(matching, cred)
100	}
101
102	if len(matching) > 1 {
103		ids := make([]entity.Id, len(matching))
104		for i, cred := range matching {
105			ids[i] = cred.ID()
106		}
107		return nil, NewErrMultipleMatchCredential(ids)
108	}
109
110	if len(matching) == 0 {
111		return nil, ErrCredentialNotExist
112	}
113
114	return matching[0], nil
115}
116
117// decode is a helper to construct a Credential from the keyring Item
118func decode(item repository.Item) (Credential, error) {
119	data := make(map[string]string)
120
121	err := json.Unmarshal(item.Data, &data)
122	if err != nil {
123		return nil, err
124	}
125
126	var cred Credential
127	switch CredentialKind(data[keyringKeyKind]) {
128	case KindToken:
129		cred, err = NewTokenFromConfig(data)
130	case KindLogin:
131		cred, err = NewLoginFromConfig(data)
132	case KindLoginPassword:
133		cred, err = NewLoginPasswordFromConfig(data)
134	default:
135		return nil, fmt.Errorf("unknown credential type \"%s\"", data[keyringKeyKind])
136	}
137
138	if err != nil {
139		return nil, fmt.Errorf("loading credential: %v", err)
140	}
141
142	return cred, nil
143}
144
145// List load all existing credentials
146func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) {
147	keys, err := repo.Keyring().Keys()
148	if err != nil {
149		return nil, err
150	}
151
152	matcher := matcher(opts)
153
154	var credentials []Credential
155	for _, key := range keys {
156		if !strings.HasPrefix(key, keyringKeyPrefix) {
157			continue
158		}
159
160		item, err := repo.Keyring().Get(key)
161		if err != nil {
162			return nil, err
163		}
164
165		cred, err := decode(item)
166		if err != nil {
167			return nil, err
168		}
169		if matcher.Match(cred) {
170			credentials = append(credentials, cred)
171		}
172	}
173
174	return credentials, nil
175}
176
177// IdExist return whether a credential id exist or not
178func IdExist(repo repository.RepoKeyring, id entity.Id) bool {
179	_, err := LoadWithId(repo, id)
180	return err == nil
181}
182
183// PrefixExist return whether a credential id prefix exist or not
184func PrefixExist(repo repository.RepoKeyring, prefix string) bool {
185	_, err := LoadWithPrefix(repo, prefix)
186	return err == nil
187}
188
189// Store stores a credential in the global git config
190func Store(repo repository.RepoKeyring, cred Credential) error {
191	if len(cred.Salt()) != 16 {
192		panic("credentials need to be salted")
193	}
194
195	confs := cred.toConfig()
196
197	confs[keyringKeyKind] = string(cred.Kind())
198	confs[keyringKeyTarget] = cred.Target()
199	confs[keyringKeyCreateTime] = strconv.Itoa(int(cred.CreateTime().Unix()))
200	confs[keyringKeySalt] = base64.StdEncoding.EncodeToString(cred.Salt())
201
202	for key, val := range cred.Metadata() {
203		confs[keyringKeyPrefixMeta+key] = val
204	}
205
206	data, err := json.Marshal(confs)
207	if err != nil {
208		return err
209	}
210
211	return repo.Keyring().Set(repository.Item{
212		Key:  keyringKeyPrefix + cred.ID().String(),
213		Data: data,
214	})
215}
216
217// Remove removes a credential from the global git config
218func Remove(repo repository.RepoKeyring, id entity.Id) error {
219	return repo.Keyring().Remove(keyringKeyPrefix + id.String())
220}
221
222/*
223 * Sorting
224 */
225
226type ById []Credential
227
228func (b ById) Len() int {
229	return len(b)
230}
231
232func (b ById) Less(i, j int) bool {
233	return b[i].ID() < b[j].ID()
234}
235
236func (b ById) Swap(i, j int) {
237	b[i], b[j] = b[j], b[i]
238}