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	configKeyUserId     = "userid"
 18	configKeyTarget     = "target"
 19	configKeyCreateTime = "createtime"
 20)
 21
 22type CredentialKind string
 23
 24const (
 25	KindToken         CredentialKind = "token"
 26	KindLoginPassword CredentialKind = "login-password"
 27)
 28
 29var ErrCredentialNotExist = errors.New("credential doesn't exist")
 30
 31func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
 32	return entity.NewErrMultipleMatch("credential", matching)
 33}
 34
 35// Special Id to mark a credential as being associated to the default user, whoever it might be.
 36// The intended use is for the bridge configuration, to be able to create and store a credential
 37// with no identities created yet, and then select one with `git-bug user adopt`
 38const DefaultUserId = entity.Id("default-user")
 39
 40type Credential interface {
 41	ID() entity.Id
 42	UserId() entity.Id
 43	updateUserId(id entity.Id)
 44	Target() string
 45	Kind() CredentialKind
 46	CreateTime() time.Time
 47	Validate() error
 48
 49	// Return all the specific properties of the credential that need to be saved into the configuration.
 50	// This does not include Target, User, Kind and CreateTime.
 51	toConfig() map[string]string
 52}
 53
 54// Load loads a credential from the repo config
 55func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
 56	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 57
 58	// read token config pairs
 59	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
 60	if err != nil {
 61		// Not exactly right due to the limitation of ReadAll()
 62		return nil, ErrCredentialNotExist
 63	}
 64
 65	return loadFromConfig(rawconfigs, id)
 66}
 67
 68// LoadWithPrefix load a credential from the repo config with a prefix
 69func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
 70	creds, err := List(repo)
 71	if err != nil {
 72		return nil, err
 73	}
 74
 75	// preallocate but empty
 76	matching := make([]Credential, 0, 5)
 77
 78	for _, cred := range creds {
 79		if cred.ID().HasPrefix(prefix) {
 80			matching = append(matching, cred)
 81		}
 82	}
 83
 84	if len(matching) > 1 {
 85		ids := make([]entity.Id, len(matching))
 86		for i, cred := range matching {
 87			ids[i] = cred.ID()
 88		}
 89		return nil, NewErrMultipleMatchCredential(ids)
 90	}
 91
 92	if len(matching) == 0 {
 93		return nil, ErrCredentialNotExist
 94	}
 95
 96	return matching[0], nil
 97}
 98
 99// loadFromConfig is a helper to construct a Credential from the set of git configs
100func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
101	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
102
103	// trim key prefix
104	configs := make(map[string]string)
105	for key, value := range rawConfigs {
106		newKey := strings.TrimPrefix(key, keyPrefix)
107		configs[newKey] = value
108	}
109
110	var cred Credential
111
112	switch CredentialKind(configs[configKeyKind]) {
113	case KindToken:
114		cred = NewTokenFromConfig(configs)
115	case KindLoginPassword:
116	default:
117		return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
118	}
119
120	return cred, nil
121}
122
123// List load all existing credentials
124func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
125	rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
126	if err != nil {
127		return nil, err
128	}
129
130	re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
131	if err != nil {
132		panic(err)
133	}
134
135	mapped := make(map[string]map[string]string)
136
137	for key, val := range rawConfigs {
138		res := re.FindStringSubmatch(key)
139		if res == nil {
140			continue
141		}
142		if mapped[res[1]] == nil {
143			mapped[res[1]] = make(map[string]string)
144		}
145		mapped[res[1]][res[2]] = val
146	}
147
148	matcher := matcher(opts)
149
150	var credentials []Credential
151	for id, kvs := range mapped {
152		cred, err := loadFromConfig(kvs, entity.Id(id))
153		if err != nil {
154			return nil, err
155		}
156		if matcher.Match(cred) {
157			credentials = append(credentials, cred)
158		}
159	}
160
161	return credentials, nil
162}
163
164// IdExist return whether a credential id exist or not
165func IdExist(repo repository.RepoConfig, id entity.Id) bool {
166	_, err := LoadWithId(repo, id)
167	return err == nil
168}
169
170// PrefixExist return whether a credential id prefix exist or not
171func PrefixExist(repo repository.RepoConfig, prefix string) bool {
172	_, err := LoadWithPrefix(repo, prefix)
173	return err == nil
174}
175
176// Store stores a credential in the global git config
177func Store(repo repository.RepoConfig, cred Credential) error {
178	confs := cred.toConfig()
179
180	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
181
182	// Kind
183	err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
184	if err != nil {
185		return err
186	}
187
188	// UserId
189	err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
190	if err != nil {
191		return err
192	}
193
194	// Target
195	err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
196	if err != nil {
197		return err
198	}
199
200	// CreateTime
201	err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
202	if err != nil {
203		return err
204	}
205
206	// Custom
207	for key, val := range confs {
208		err := repo.GlobalConfig().StoreString(prefix+key, val)
209		if err != nil {
210			return err
211		}
212	}
213
214	return nil
215}
216
217// Remove removes a credential from the global git config
218func Remove(repo repository.RepoConfig, id entity.Id) error {
219	keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
220	return repo.GlobalConfig().RemoveAll(keyPrefix)
221}
222
223// ReplaceDefaultUser update all the credential attributed to the temporary "default user"
224// with a real user Id
225func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error {
226	list, err := List(repo, WithUserId(DefaultUserId))
227	if err != nil {
228		return err
229	}
230
231	for _, cred := range list {
232		cred.updateUserId(id)
233		err = Store(repo, cred)
234		if err != nil {
235			return err
236		}
237	}
238
239	return nil
240}
241
242/*
243 * Sorting
244 */
245
246type ById []Credential
247
248func (b ById) Len() int {
249	return len(b)
250}
251
252func (b ById) Less(i, j int) bool {
253	return b[i].ID() < b[j].ID()
254}
255
256func (b ById) Swap(i, j int) {
257	b[i], b[j] = b[j], b[i]
258}