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 := regexp.MustCompile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
175
176	mapped := make(map[string]map[string]string)
177
178	for key, val := range rawConfigs {
179		res := re.FindStringSubmatch(key)
180		if res == nil {
181			continue
182		}
183		if mapped[res[1]] == nil {
184			mapped[res[1]] = make(map[string]string)
185		}
186		mapped[res[1]][res[2]] = val
187	}
188
189	matcher := matcher(opts)
190
191	var credentials []Credential
192	for id, kvs := range mapped {
193		cred, err := loadFromConfig(kvs, entity.Id(id))
194		if err != nil {
195			return nil, err
196		}
197		if matcher.Match(cred) {
198			credentials = append(credentials, cred)
199		}
200	}
201
202	return credentials, nil
203}
204
205// IdExist return whether a credential id exist or not
206func IdExist(repo repository.RepoConfig, id entity.Id) bool {
207	_, err := LoadWithId(repo, id)
208	return err == nil
209}
210
211// PrefixExist return whether a credential id prefix exist or not
212func PrefixExist(repo repository.RepoConfig, prefix string) bool {
213	_, err := LoadWithPrefix(repo, prefix)
214	return err == nil
215}
216
217// Store stores a credential in the global git config
218func Store(repo repository.RepoConfig, cred Credential) error {
219	confs := cred.toConfig()
220
221	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
222
223	// Kind
224	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
225	if err != nil {
226		return err
227	}
228
229	// Target
230	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
231	if err != nil {
232		return err
233	}
234
235	// CreateTime
236	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
237	if err != nil {
238		return err
239	}
240
241	// Salt
242	if len(cred.Salt()) != 16 {
243		panic("credentials need to be salted")
244	}
245	encoded := base64.StdEncoding.EncodeToString(cred.Salt())
246	err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
247	if err != nil {
248		return err
249	}
250
251	// Metadata
252	for key, val := range cred.Metadata() {
253		err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
254		if err != nil {
255			return err
256		}
257	}
258
259	// Custom
260	for key, val := range confs {
261		err := repo.GlobalConfig().StoreString(prefix+key, val)
262		if err != nil {
263			return err
264		}
265	}
266
267	return nil
268}
269
270// Remove removes a credential from the global git config
271func Remove(repo repository.RepoConfig, id entity.Id) error {
272	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
273	return repo.GlobalConfig().RemoveAll(keyPrefix)
274}
275
276/*
277 * Sorting
278 */
279
280type ById []Credential
281
282func (b ById) Len() int {
283	return len(b)
284}
285
286func (b ById) Less(i, j int) bool {
287	return b[i].ID() < b[j].ID()
288}
289
290func (b ById) Swap(i, j int) {
291	b[i], b[j] = b[j], b[i]
292}