token.go

  1package core
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/json"
  6	"fmt"
  7	"regexp"
  8	"strings"
  9
 10	"github.com/MichaelMure/git-bug/repository"
 11)
 12
 13const (
 14	tokenConfigKeyPrefix = "git-bug.token"
 15	tokenValueKey        = "value"
 16	tokenTargetKey       = "target"
 17	tokenScopesKey       = "scopes"
 18)
 19
 20// Token holds an API access token data
 21type Token struct {
 22	ID     string
 23	Value  string
 24	Target string
 25	Global bool
 26	Scopes []string
 27}
 28
 29// NewToken instantiate a new token
 30func NewToken(value, target string, global bool, scopes []string) *Token {
 31	token := &Token{
 32		Value:  value,
 33		Target: target,
 34		Global: global,
 35		Scopes: scopes,
 36	}
 37
 38	token.ID = hashToken(token)
 39	return token
 40}
 41
 42// Id return full token identifier. It will compute the Id if it's empty
 43func (t *Token) Id() string {
 44	if t.ID == "" {
 45		t.ID = hashToken(t)
 46	}
 47
 48	return t.ID
 49}
 50
 51// HumanId return the truncated token id
 52func (t *Token) HumanId() string {
 53	return t.Id()[:6]
 54}
 55
 56func hashToken(token *Token) string {
 57	tokenJson, err := json.Marshal(&token)
 58	if err != nil {
 59		panic(err)
 60	}
 61
 62	sum := sha256.Sum256(tokenJson)
 63	return fmt.Sprintf("%x", sum)
 64}
 65
 66// Validate ensure token important fields are valid
 67func (t *Token) Validate() error {
 68	if t.ID == "" {
 69		return fmt.Errorf("missing id")
 70	}
 71	if t.Value == "" {
 72		return fmt.Errorf("missing value")
 73	}
 74	if t.Target == "" {
 75		return fmt.Errorf("missing target")
 76	}
 77	if _, ok := bridgeImpl[t.Target]; !ok {
 78		return fmt.Errorf("unknown target")
 79	}
 80	return nil
 81}
 82
 83// Kind return the type of the token as string
 84func (t *Token) Kind() string {
 85	if t.Global {
 86		return "global"
 87	}
 88
 89	return "local"
 90}
 91
 92func loadToken(repo repository.RepoConfig, id string, global bool) (*Token, error) {
 93	keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
 94
 95	readerFn := repo.ReadConfigs
 96	if global {
 97		readerFn = repo.ReadGlobalConfigs
 98	}
 99
100	// read token config pairs
101	configs, err := readerFn(keyPrefix)
102	if err != nil {
103		return nil, err
104	}
105
106	// trim key prefix
107	for key, value := range configs {
108		delete(configs, key)
109		newKey := strings.TrimPrefix(key, keyPrefix)
110		configs[newKey] = value
111	}
112
113	var ok bool
114	token := &Token{ID: id, Global: global}
115
116	token.Value, ok = configs[tokenValueKey]
117	if !ok {
118		return nil, fmt.Errorf("empty token value")
119	}
120
121	token.Target, ok = configs[tokenTargetKey]
122	if !ok {
123		return nil, fmt.Errorf("empty token key")
124	}
125
126	scopesString, ok := configs[tokenScopesKey]
127	if !ok {
128		return nil, fmt.Errorf("missing scopes config")
129	}
130
131	token.Scopes = strings.Split(scopesString, ",")
132	return token, nil
133}
134
135// GetToken loads a token from repo config
136func GetToken(repo repository.RepoConfig, id string) (*Token, error) {
137	return loadToken(repo, id, false)
138}
139
140// GetGlobalToken loads a token from the global config
141func GetGlobalToken(repo repository.RepoConfig, id string) (*Token, error) {
142	return loadToken(repo, id, true)
143}
144
145func listTokens(repo repository.RepoConfig, global bool) ([]string, error) {
146	readerFn := repo.ReadConfigs
147	if global {
148		readerFn = repo.ReadGlobalConfigs
149	}
150
151	configs, err := readerFn(tokenConfigKeyPrefix + ".")
152	if err != nil {
153		return nil, err
154	}
155
156	re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
157	if err != nil {
158		panic(err)
159	}
160
161	set := make(map[string]interface{})
162
163	for key := range configs {
164		res := re.FindStringSubmatch(key)
165
166		if res == nil {
167			continue
168		}
169
170		set[res[1]] = nil
171	}
172
173	result := make([]string, len(set))
174	i := 0
175	for key := range set {
176		result[i] = key
177		i++
178	}
179
180	return result, nil
181}
182
183// ListTokens return a map representing the stored tokens in the repo config and global config
184// along with their type (global: true, local:false)
185func ListTokens(repo repository.RepoConfig) (map[string]bool, error) {
186	localTokens, err := listTokens(repo, false)
187	if err != nil {
188		return nil, err
189	}
190
191	globalTokens, err := listTokens(repo, true)
192	if err != nil {
193		return nil, err
194	}
195
196	tokens := map[string]bool{}
197	for _, token := range localTokens {
198		tokens[token] = false
199	}
200
201	for _, token := range globalTokens {
202		tokens[token] = true
203	}
204
205	return tokens, nil
206}
207
208func storeToken(repo repository.RepoConfig, token *Token) error {
209	storeFn := repo.StoreConfig
210	if token.Global {
211		storeFn = repo.StoreGlobalConfig
212	}
213
214	storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.Id(), tokenValueKey)
215	err := storeFn(storeValueKey, token.Value)
216	if err != nil {
217		return err
218	}
219
220	storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.Id(), tokenTargetKey)
221	err = storeFn(storeTargetKey, token.Target)
222	if err != nil {
223		return err
224	}
225
226	storeScopesKey := fmt.Sprintf("git-bug.token.%s.%s", token.Id(), tokenScopesKey)
227	return storeFn(storeScopesKey, strings.Join(token.Scopes, ","))
228}
229
230// StoreToken stores a token in the repo config
231func StoreToken(repo repository.RepoConfig, token *Token) error {
232	return storeToken(repo, token)
233}
234
235// RemoveToken removes a token from the repo config
236func RemoveToken(repo repository.RepoConfig, id string) error {
237	keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
238	return repo.RmConfigs(keyPrefix)
239}
240
241// RemoveGlobalToken removes a token from the repo config
242func RemoveGlobalToken(repo repository.RepoConfig, id string) error {
243	keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
244	return repo.RmGlobalConfigs(keyPrefix)
245}