credential.go

  1package auth
  2
  3import (
  4	"crypto/rand"
  5	"encoding/base64"
  6	"errors"
  7	"fmt"
  8	"regexp"
  9	"strings"
 10	"time"
 11
 12	"github.com/MichaelMure/git-bug/entity"
 13	"github.com/MichaelMure/git-bug/repository"
 14)
 15
 16const (
 17	configKeyPrefix     = "git-bug.auth"
 18	configKeyKind       = "kind"
 19	configKeyTarget     = "target"
 20	configKeyCreateTime = "createtime"
 21	configKeySalt       = "salt"
 22	configKeyPrefixMeta = "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.RepoConfig, id entity.Id) (Credential, error) {
 61	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 62
 63	// read token config pairs
 64	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
 65	if err != nil {
 66		// Not exactly right due to the limitation of ReadAll()
 67		return nil, ErrCredentialNotExist
 68	}
 69
 70	return loadFromConfig(rawconfigs, id)
 71}
 72
 73// LoadWithPrefix load a credential from the repo config with a prefix
 74func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
 75	creds, err := List(repo)
 76	if err != nil {
 77		return nil, err
 78	}
 79
 80	// preallocate but empty
 81	matching := make([]Credential, 0, 5)
 82
 83	for _, cred := range creds {
 84		if cred.ID().HasPrefix(prefix) {
 85			matching = append(matching, cred)
 86		}
 87	}
 88
 89	if len(matching) > 1 {
 90		ids := make([]entity.Id, len(matching))
 91		for i, cred := range matching {
 92			ids[i] = cred.ID()
 93		}
 94		return nil, NewErrMultipleMatchCredential(ids)
 95	}
 96
 97	if len(matching) == 0 {
 98		return nil, ErrCredentialNotExist
 99	}
100
101	return matching[0], nil
102}
103
104// loadFromConfig is a helper to construct a Credential from the set of git configs
105func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
106	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
107
108	// trim key prefix
109	configs := make(map[string]string)
110	for key, value := range rawConfigs {
111		newKey := strings.TrimPrefix(key, keyPrefix)
112		configs[newKey] = value
113	}
114
115	var cred Credential
116	var err error
117
118	switch CredentialKind(configs[configKeyKind]) {
119	case KindToken:
120		cred, err = NewTokenFromConfig(configs)
121	case KindLogin:
122		cred, err = NewLoginFromConfig(configs)
123	case KindLoginPassword:
124		cred, err = NewLoginPasswordFromConfig(configs)
125	default:
126		return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
127	}
128
129	if err != nil {
130		return nil, fmt.Errorf("loading credential: %v", err)
131	}
132
133	return cred, nil
134}
135
136func metaFromConfig(configs map[string]string) map[string]string {
137	result := make(map[string]string)
138	for key, val := range configs {
139		if strings.HasPrefix(key, configKeyPrefixMeta) {
140			key = strings.TrimPrefix(key, configKeyPrefixMeta)
141			result[key] = val
142		}
143	}
144	if len(result) == 0 {
145		return nil
146	}
147	return result
148}
149
150func makeSalt() []byte {
151	result := make([]byte, 16)
152	_, err := rand.Read(result)
153	if err != nil {
154		panic(err)
155	}
156	return result
157}
158
159func saltFromConfig(configs map[string]string) ([]byte, error) {
160	val, ok := configs[configKeySalt]
161	if !ok {
162		return nil, fmt.Errorf("no credential salt found")
163	}
164	return base64.StdEncoding.DecodeString(val)
165}
166
167// List load all existing credentials
168func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
169	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
170	if err != nil {
171		return nil, err
172	}
173
174	re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
175	if err != nil {
176		panic(err)
177	}
178
179	mapped := make(map[string]map[string]string)
180
181	for key, val := range rawConfigs {
182		res := re.FindStringSubmatch(key)
183		if res == nil {
184			continue
185		}
186		if mapped[res[1]] == nil {
187			mapped[res[1]] = make(map[string]string)
188		}
189		mapped[res[1]][res[2]] = val
190	}
191
192	matcher := matcher(opts)
193
194	var credentials []Credential
195	for id, kvs := range mapped {
196		cred, err := loadFromConfig(kvs, entity.Id(id))
197		if err != nil {
198			return nil, err
199		}
200		if matcher.Match(cred) {
201			credentials = append(credentials, cred)
202		}
203	}
204
205	return credentials, nil
206}
207
208// IdExist return whether a credential id exist or not
209func IdExist(repo repository.RepoConfig, id entity.Id) bool {
210	_, err := LoadWithId(repo, id)
211	return err == nil
212}
213
214// PrefixExist return whether a credential id prefix exist or not
215func PrefixExist(repo repository.RepoConfig, prefix string) bool {
216	_, err := LoadWithPrefix(repo, prefix)
217	return err == nil
218}
219
220// Store stores a credential in the global git config
221func Store(repo repository.RepoConfig, cred Credential) error {
222	confs := cred.toConfig()
223
224	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
225
226	// Kind
227	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
228	if err != nil {
229		return err
230	}
231
232	// Target
233	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
234	if err != nil {
235		return err
236	}
237
238	// CreateTime
239	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
240	if err != nil {
241		return err
242	}
243
244	// Salt
245	if len(cred.Salt()) != 16 {
246		panic("credentials need to be salted")
247	}
248	encoded := base64.StdEncoding.EncodeToString(cred.Salt())
249	err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
250	if err != nil {
251		return err
252	}
253
254	// Metadata
255	for key, val := range cred.Metadata() {
256		err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
257		if err != nil {
258			return err
259		}
260	}
261
262	// Custom
263	for key, val := range confs {
264		err := repo.GlobalConfig().StoreString(prefix+key, val)
265		if err != nil {
266			return err
267		}
268	}
269
270	return nil
271}
272
273// Remove removes a credential from the global git config
274func Remove(repo repository.RepoConfig, id entity.Id) error {
275	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
276	return repo.GlobalConfig().RemoveAll(keyPrefix)
277}
278
279/*
280 * Sorting
281 */
282
283type ById []Credential
284
285func (b ById) Len() int {
286	return len(b)
287}
288
289func (b ById) Less(i, j int) bool {
290	return b[i].ID() < b[j].ID()
291}
292
293func (b ById) Swap(i, j int) {
294	b[i], b[j] = b[j], b[i]
295}