1package core
  2
  3import (
  4	"crypto/sha256"
  5	"errors"
  6	"fmt"
  7	"regexp"
  8	"sort"
  9	"strings"
 10	"time"
 11
 12	"github.com/MichaelMure/git-bug/entity"
 13	"github.com/MichaelMure/git-bug/repository"
 14)
 15
 16const (
 17	tokenConfigKeyPrefix = "git-bug.token"
 18	tokenValueKey        = "value"
 19	tokenTargetKey       = "target"
 20	tokenCreateTimeKey   = "createtime"
 21)
 22
 23var ErrTokenNotExist = errors.New("token doesn't exist")
 24
 25func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch {
 26	return entity.NewErrMultipleMatch("token", matching)
 27}
 28
 29// Token holds an API access token data
 30type Token struct {
 31	Value      string
 32	Target     string
 33	CreateTime time.Time
 34}
 35
 36// NewToken instantiate a new token
 37func NewToken(value, target string) *Token {
 38	return &Token{
 39		Value:      value,
 40		Target:     target,
 41		CreateTime: time.Now(),
 42	}
 43}
 44
 45func (t *Token) ID() entity.Id {
 46	sum := sha256.Sum256([]byte(t.Target + t.Value))
 47	return entity.Id(fmt.Sprintf("%x", sum))
 48}
 49
 50// Validate ensure token important fields are valid
 51func (t *Token) Validate() error {
 52	if t.Value == "" {
 53		return fmt.Errorf("missing value")
 54	}
 55	if t.Target == "" {
 56		return fmt.Errorf("missing target")
 57	}
 58	if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) {
 59		return fmt.Errorf("missing creation time")
 60	}
 61	if !TargetExist(t.Target) {
 62		return fmt.Errorf("unknown target")
 63	}
 64	return nil
 65}
 66
 67// LoadToken loads a token from the repo config
 68func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) {
 69	keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
 70
 71	// read token config pairs
 72	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
 73	if err != nil {
 74		// Not exactly right due to the limitation of ReadAll()
 75		return nil, ErrTokenNotExist
 76	}
 77
 78	// trim key prefix
 79	configs := make(map[string]string)
 80	for key, value := range rawconfigs {
 81		newKey := strings.TrimPrefix(key, keyPrefix)
 82		configs[newKey] = value
 83	}
 84
 85	token := &Token{}
 86
 87	token.Value = configs[tokenValueKey]
 88	token.Target = configs[tokenTargetKey]
 89	if createTime, ok := configs[tokenCreateTimeKey]; ok {
 90		if t, err := repository.ParseTimestamp(createTime); err == nil {
 91			token.CreateTime = t
 92		}
 93	}
 94
 95	return token, nil
 96}
 97
 98// LoadTokenPrefix load a token from the repo config with a prefix
 99func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) {
100	tokens, err := ListTokens(repo)
101	if err != nil {
102		return nil, err
103	}
104
105	// preallocate but empty
106	matching := make([]entity.Id, 0, 5)
107
108	for _, id := range tokens {
109		if id.HasPrefix(prefix) {
110			matching = append(matching, id)
111		}
112	}
113
114	if len(matching) > 1 {
115		return nil, NewErrMultipleMatchToken(matching)
116	}
117
118	if len(matching) == 0 {
119		return nil, ErrTokenNotExist
120	}
121
122	return LoadToken(repo, matching[0])
123}
124
125// ListTokens list all existing token ids
126func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
127	configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
128	if err != nil {
129		return nil, err
130	}
131
132	re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
133	if err != nil {
134		panic(err)
135	}
136
137	set := make(map[string]interface{})
138
139	for key := range configs {
140		res := re.FindStringSubmatch(key)
141
142		if res == nil {
143			continue
144		}
145
146		set[res[1]] = nil
147	}
148
149	result := make([]entity.Id, 0, len(set))
150	for key := range set {
151		result = append(result, entity.Id(key))
152	}
153
154	sort.Sort(entity.Alphabetical(result))
155
156	return result, nil
157}
158
159// ListTokensWithTarget list all token ids associated with the target
160func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) {
161	var ids []entity.Id
162	tokensIds, err := ListTokens(repo)
163	if err != nil {
164		return nil, err
165	}
166
167	for _, tokenId := range tokensIds {
168		token, err := LoadToken(repo, tokenId)
169		if err != nil {
170			return nil, err
171		}
172
173		if token.Target == target {
174			ids = append(ids, tokenId)
175		}
176	}
177	return ids, nil
178}
179
180// LoadTokens load all existing tokens
181func LoadTokens(repo repository.RepoCommon) ([]*Token, error) {
182	tokensIds, err := ListTokens(repo)
183	if err != nil {
184		return nil, err
185	}
186
187	var tokens []*Token
188	for _, id := range tokensIds {
189		token, err := LoadToken(repo, id)
190		if err != nil {
191			return nil, err
192		}
193		tokens = append(tokens, token)
194	}
195	return tokens, nil
196}
197
198// LoadTokensWithTarget load all existing tokens for a given target
199func LoadTokensWithTarget(repo repository.RepoCommon, target string) ([]*Token, error) {
200	tokensIds, err := ListTokens(repo)
201	if err != nil {
202		return nil, err
203	}
204
205	var tokens []*Token
206	for _, id := range tokensIds {
207		token, err := LoadToken(repo, id)
208		if err != nil {
209			return nil, err
210		}
211		if token.Target == target {
212			tokens = append(tokens, token)
213		}
214	}
215	return tokens, nil
216}
217
218// TokenIdExist return wether token id exist or not
219func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool {
220	_, err := LoadToken(repo, id)
221	return err == nil
222}
223
224// TokenExist return wether there is a token with a certain value or not
225func TokenExist(repo repository.RepoCommon, value string) bool {
226	tokens, err := LoadTokens(repo)
227	if err != nil {
228		return false
229	}
230	for _, token := range tokens {
231		if token.Value == value {
232			return true
233		}
234	}
235	return false
236}
237
238// TokenExistWithTarget same as TokenExist but restrict search for a given target
239func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool {
240	tokens, err := LoadTokens(repo)
241	if err != nil {
242		return false
243	}
244	for _, token := range tokens {
245		if token.Value == value && token.Target == target {
246			return true
247		}
248	}
249	return false
250}
251
252// StoreToken stores a token in the repo config
253func StoreToken(repo repository.RepoCommon, token *Token) error {
254	storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey)
255	err := repo.GlobalConfig().StoreString(storeValueKey, token.Value)
256	if err != nil {
257		return err
258	}
259
260	storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey)
261	err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target)
262	if err != nil {
263		return err
264	}
265
266	createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey)
267	return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime)
268}
269
270// RemoveToken removes a token from the repo config
271func RemoveToken(repo repository.RepoCommon, id entity.Id) error {
272	keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
273	return repo.GlobalConfig().RemoveAll(keyPrefix)
274}
275
276// LoadOrCreateToken will try to load a token matching the same value or create it
277func LoadOrCreateToken(repo repository.RepoCommon, target, tokenValue string) (*Token, error) {
278	tokens, err := LoadTokensWithTarget(repo, target)
279	if err != nil {
280		return nil, err
281	}
282
283	for _, token := range tokens {
284		if token.Value == tokenValue {
285			return token, nil
286		}
287	}
288
289	token := NewToken(tokenValue, target)
290	err = StoreToken(repo, token)
291	if err != nil {
292		return nil, err
293	}
294
295	return token, nil
296}