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}