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}