credential.go

  1package auth
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"regexp"
  7	"strings"
  8	"time"
  9
 10	"github.com/MichaelMure/git-bug/entity"
 11	"github.com/MichaelMure/git-bug/repository"
 12)
 13
 14const (
 15	configKeyPrefix     = "git-bug.auth"
 16	configKeyKind       = "kind"
 17	configKeyTarget     = "target"
 18	configKeyCreateTime = "createtime"
 19	configKeyPrefixMeta = "meta."
 20
 21	MetaKeyLogin = "login"
 22)
 23
 24type CredentialKind string
 25
 26const (
 27	KindToken         CredentialKind = "token"
 28	KindLoginPassword CredentialKind = "login-password"
 29)
 30
 31var ErrCredentialNotExist = errors.New("credential doesn't exist")
 32
 33func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
 34	return entity.NewErrMultipleMatch("credential", matching)
 35}
 36
 37type Credential interface {
 38	ID() entity.Id
 39	Target() string
 40	Kind() CredentialKind
 41	CreateTime() time.Time
 42	Validate() error
 43
 44	Metadata() map[string]string
 45	GetMetadata(key string) (string, bool)
 46	SetMetadata(key string, value string)
 47
 48	// Return all the specific properties of the credential that need to be saved into the configuration.
 49	// This does not include Target, Kind, CreateTime and Metadata.
 50	toConfig() map[string]string
 51}
 52
 53// Load loads a credential from the repo config
 54func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
 55	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 56
 57	// read token config pairs
 58	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
 59	if err != nil {
 60		// Not exactly right due to the limitation of ReadAll()
 61		return nil, ErrCredentialNotExist
 62	}
 63
 64	return loadFromConfig(rawconfigs, id)
 65}
 66
 67// LoadWithPrefix load a credential from the repo config with a prefix
 68func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
 69	creds, err := List(repo)
 70	if err != nil {
 71		return nil, err
 72	}
 73
 74	// preallocate but empty
 75	matching := make([]Credential, 0, 5)
 76
 77	for _, cred := range creds {
 78		if cred.ID().HasPrefix(prefix) {
 79			matching = append(matching, cred)
 80		}
 81	}
 82
 83	if len(matching) > 1 {
 84		ids := make([]entity.Id, len(matching))
 85		for i, cred := range matching {
 86			ids[i] = cred.ID()
 87		}
 88		return nil, NewErrMultipleMatchCredential(ids)
 89	}
 90
 91	if len(matching) == 0 {
 92		return nil, ErrCredentialNotExist
 93	}
 94
 95	return matching[0], nil
 96}
 97
 98// loadFromConfig is a helper to construct a Credential from the set of git configs
 99func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
100	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
101
102	// trim key prefix
103	configs := make(map[string]string)
104	for key, value := range rawConfigs {
105		newKey := strings.TrimPrefix(key, keyPrefix)
106		configs[newKey] = value
107	}
108
109	var cred Credential
110
111	switch CredentialKind(configs[configKeyKind]) {
112	case KindToken:
113		cred = NewTokenFromConfig(configs)
114	case KindLoginPassword:
115	default:
116		return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
117	}
118
119	return cred, nil
120}
121
122func metaFromConfig(configs map[string]string) map[string]string {
123	result := make(map[string]string)
124	for key, val := range configs {
125		if strings.HasPrefix(key, configKeyPrefixMeta) {
126			key = strings.TrimPrefix(key, configKeyPrefixMeta)
127			result[key] = val
128		}
129	}
130	if len(result) == 0 {
131		return nil
132	}
133	return result
134}
135
136// List load all existing credentials
137func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
138	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
139	if err != nil {
140		return nil, err
141	}
142
143	re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
144	if err != nil {
145		panic(err)
146	}
147
148	mapped := make(map[string]map[string]string)
149
150	for key, val := range rawConfigs {
151		res := re.FindStringSubmatch(key)
152		if res == nil {
153			continue
154		}
155		if mapped[res[1]] == nil {
156			mapped[res[1]] = make(map[string]string)
157		}
158		mapped[res[1]][res[2]] = val
159	}
160
161	matcher := matcher(opts)
162
163	var credentials []Credential
164	for id, kvs := range mapped {
165		cred, err := loadFromConfig(kvs, entity.Id(id))
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.RepoConfig, 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.RepoConfig, 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.RepoConfig, cred Credential) error {
191	confs := cred.toConfig()
192
193	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
194
195	// Kind
196	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
197	if err != nil {
198		return err
199	}
200
201	// Target
202	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
203	if err != nil {
204		return err
205	}
206
207	// CreateTime
208	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
209	if err != nil {
210		return err
211	}
212
213	// Metadata
214	for key, val := range cred.Metadata() {
215		err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
216		if err != nil {
217			return err
218		}
219	}
220
221	// Custom
222	for key, val := range confs {
223		err := repo.GlobalConfig().StoreString(prefix+key, val)
224		if err != nil {
225			return err
226		}
227	}
228
229	return nil
230}
231
232// Remove removes a credential from the global git config
233func Remove(repo repository.RepoConfig, id entity.Id) error {
234	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
235	return repo.GlobalConfig().RemoveAll(keyPrefix)
236}
237
238/*
239 * Sorting
240 */
241
242type ById []Credential
243
244func (b ById) Len() int {
245	return len(b)
246}
247
248func (b ById) Less(i, j int) bool {
249	return b[i].ID() < b[j].ID()
250}
251
252func (b ById) Swap(i, j int) {
253	b[i], b[j] = b[j], b[i]
254}