credential.go

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