token.go

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