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	Metadata() map[string]string
 44
 45	// Return all the specific properties of the credential that need to be saved into the configuration.
 46	// This does not include Target, Kind, CreateTime and Metadata.
 47	toConfig() map[string]string
 48}
 49
 50// Load loads a credential from the repo config
 51func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
 52	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 53
 54	// read token config pairs
 55	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
 56	if err != nil {
 57		// Not exactly right due to the limitation of ReadAll()
 58		return nil, ErrCredentialNotExist
 59	}
 60
 61	return loadFromConfig(rawconfigs, id)
 62}
 63
 64// LoadWithPrefix load a credential from the repo config with a prefix
 65func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
 66	creds, err := List(repo)
 67	if err != nil {
 68		return nil, err
 69	}
 70
 71	// preallocate but empty
 72	matching := make([]Credential, 0, 5)
 73
 74	for _, cred := range creds {
 75		if cred.ID().HasPrefix(prefix) {
 76			matching = append(matching, cred)
 77		}
 78	}
 79
 80	if len(matching) > 1 {
 81		ids := make([]entity.Id, len(matching))
 82		for i, cred := range matching {
 83			ids[i] = cred.ID()
 84		}
 85		return nil, NewErrMultipleMatchCredential(ids)
 86	}
 87
 88	if len(matching) == 0 {
 89		return nil, ErrCredentialNotExist
 90	}
 91
 92	return matching[0], nil
 93}
 94
 95// loadFromConfig is a helper to construct a Credential from the set of git configs
 96func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
 97	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 98
 99	// trim key prefix
100	configs := make(map[string]string)
101	for key, value := range rawConfigs {
102		newKey := strings.TrimPrefix(key, keyPrefix)
103		configs[newKey] = value
104	}
105
106	var cred Credential
107
108	switch CredentialKind(configs[configKeyKind]) {
109	case KindToken:
110		cred = NewTokenFromConfig(configs)
111	case KindLoginPassword:
112	default:
113		return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
114	}
115
116	return cred, nil
117}
118
119func metaFromConfig(configs map[string]string) map[string]string {
120	result := make(map[string]string)
121	for key, val := range configs {
122		if strings.HasPrefix(key, configKeyPrefixMeta) {
123			key = strings.TrimPrefix(key, configKeyPrefixMeta)
124			result[key] = val
125		}
126	}
127	return result
128}
129
130// List load all existing credentials
131func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
132	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
133	if err != nil {
134		return nil, err
135	}
136
137	re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
138	if err != nil {
139		panic(err)
140	}
141
142	mapped := make(map[string]map[string]string)
143
144	for key, val := range rawConfigs {
145		res := re.FindStringSubmatch(key)
146		if res == nil {
147			continue
148		}
149		if mapped[res[1]] == nil {
150			mapped[res[1]] = make(map[string]string)
151		}
152		mapped[res[1]][res[2]] = val
153	}
154
155	matcher := matcher(opts)
156
157	var credentials []Credential
158	for id, kvs := range mapped {
159		cred, err := loadFromConfig(kvs, entity.Id(id))
160		if err != nil {
161			return nil, err
162		}
163		if matcher.Match(cred) {
164			credentials = append(credentials, cred)
165		}
166	}
167
168	return credentials, nil
169}
170
171// IdExist return whether a credential id exist or not
172func IdExist(repo repository.RepoConfig, id entity.Id) bool {
173	_, err := LoadWithId(repo, id)
174	return err == nil
175}
176
177// PrefixExist return whether a credential id prefix exist or not
178func PrefixExist(repo repository.RepoConfig, prefix string) bool {
179	_, err := LoadWithPrefix(repo, prefix)
180	return err == nil
181}
182
183// Store stores a credential in the global git config
184func Store(repo repository.RepoConfig, cred Credential) error {
185	confs := cred.toConfig()
186
187	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
188
189	// Kind
190	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
191	if err != nil {
192		return err
193	}
194
195	// Target
196	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
197	if err != nil {
198		return err
199	}
200
201	// CreateTime
202	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
203	if err != nil {
204		return err
205	}
206
207	// Metadata
208	for key, val := range cred.Metadata() {
209		err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
210		if err != nil {
211			return err
212		}
213	}
214
215	// Custom
216	for key, val := range confs {
217		err := repo.GlobalConfig().StoreString(prefix+key, val)
218		if err != nil {
219			return err
220		}
221	}
222
223	return nil
224}
225
226// Remove removes a credential from the global git config
227func Remove(repo repository.RepoConfig, id entity.Id) error {
228	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
229	return repo.GlobalConfig().RemoveAll(keyPrefix)
230}
231
232/*
233 * Sorting
234 */
235
236type ById []Credential
237
238func (b ById) Len() int {
239	return len(b)
240}
241
242func (b ById) Less(i, j int) bool {
243	return b[i].ID() < b[j].ID()
244}
245
246func (b ById) Swap(i, j int) {
247	b[i], b[j] = b[j], b[i]
248}