1package auth
2
3import (
4 "crypto/rand"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "regexp"
9 "strings"
10 "time"
11
12 "github.com/MichaelMure/git-bug/entity"
13 "github.com/MichaelMure/git-bug/repository"
14)
15
16const (
17 configKeyPrefix = "git-bug.auth"
18 configKeyKind = "kind"
19 configKeyTarget = "target"
20 configKeyCreateTime = "createtime"
21 configKeySalt = "salt"
22 configKeyPrefixMeta = "meta."
23
24 MetaKeyLogin = "login"
25 MetaKeyBaseURL = "base-url"
26)
27
28type CredentialKind string
29
30const (
31 KindToken CredentialKind = "token"
32 KindLogin CredentialKind = "login"
33 KindLoginPassword CredentialKind = "login-password"
34)
35
36var ErrCredentialNotExist = errors.New("credential doesn't exist")
37
38func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
39 return entity.NewErrMultipleMatch("credential", matching)
40}
41
42type Credential interface {
43 ID() entity.Id
44 Kind() CredentialKind
45 Target() string
46 CreateTime() time.Time
47 Salt() []byte
48 Validate() error
49
50 Metadata() map[string]string
51 GetMetadata(key string) (string, bool)
52 SetMetadata(key string, value string)
53
54 // Return all the specific properties of the credential that need to be saved into the configuration.
55 // This does not include Target, Kind, CreateTime, Metadata or Salt.
56 toConfig() map[string]string
57}
58
59// Load loads a credential from the repo config
60func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
61 keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
62
63 // read token config pairs
64 rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
65 if err != nil {
66 // Not exactly right due to the limitation of ReadAll()
67 return nil, ErrCredentialNotExist
68 }
69
70 return loadFromConfig(rawconfigs, id)
71}
72
73// LoadWithPrefix load a credential from the repo config with a prefix
74func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
75 creds, err := List(repo)
76 if err != nil {
77 return nil, err
78 }
79
80 // preallocate but empty
81 matching := make([]Credential, 0, 5)
82
83 for _, cred := range creds {
84 if cred.ID().HasPrefix(prefix) {
85 matching = append(matching, cred)
86 }
87 }
88
89 if len(matching) > 1 {
90 ids := make([]entity.Id, len(matching))
91 for i, cred := range matching {
92 ids[i] = cred.ID()
93 }
94 return nil, NewErrMultipleMatchCredential(ids)
95 }
96
97 if len(matching) == 0 {
98 return nil, ErrCredentialNotExist
99 }
100
101 return matching[0], nil
102}
103
104// loadFromConfig is a helper to construct a Credential from the set of git configs
105func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
106 keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
107
108 // trim key prefix
109 configs := make(map[string]string)
110 for key, value := range rawConfigs {
111 newKey := strings.TrimPrefix(key, keyPrefix)
112 configs[newKey] = value
113 }
114
115 var cred Credential
116 var err error
117
118 switch CredentialKind(configs[configKeyKind]) {
119 case KindToken:
120 cred, err = NewTokenFromConfig(configs)
121 case KindLogin:
122 cred, err = NewLoginFromConfig(configs)
123 case KindLoginPassword:
124 cred, err = NewLoginPasswordFromConfig(configs)
125 default:
126 return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
127 }
128
129 if err != nil {
130 return nil, fmt.Errorf("loading credential: %v", err)
131 }
132
133 return cred, nil
134}
135
136func metaFromConfig(configs map[string]string) map[string]string {
137 result := make(map[string]string)
138 for key, val := range configs {
139 if strings.HasPrefix(key, configKeyPrefixMeta) {
140 key = strings.TrimPrefix(key, configKeyPrefixMeta)
141 result[key] = val
142 }
143 }
144 if len(result) == 0 {
145 return nil
146 }
147 return result
148}
149
150func makeSalt() []byte {
151 result := make([]byte, 16)
152 _, err := rand.Read(result)
153 if err != nil {
154 panic(err)
155 }
156 return result
157}
158
159func saltFromConfig(configs map[string]string) ([]byte, error) {
160 val, ok := configs[configKeySalt]
161 if !ok {
162 return nil, fmt.Errorf("no credential salt found")
163 }
164 return base64.StdEncoding.DecodeString(val)
165}
166
167// List load all existing credentials
168func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
169 rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
170 if err != nil {
171 return nil, err
172 }
173
174 re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
175 if err != nil {
176 panic(err)
177 }
178
179 mapped := make(map[string]map[string]string)
180
181 for key, val := range rawConfigs {
182 res := re.FindStringSubmatch(key)
183 if res == nil {
184 continue
185 }
186 if mapped[res[1]] == nil {
187 mapped[res[1]] = make(map[string]string)
188 }
189 mapped[res[1]][res[2]] = val
190 }
191
192 matcher := matcher(opts)
193
194 var credentials []Credential
195 for id, kvs := range mapped {
196 cred, err := loadFromConfig(kvs, entity.Id(id))
197 if err != nil {
198 return nil, err
199 }
200 if matcher.Match(cred) {
201 credentials = append(credentials, cred)
202 }
203 }
204
205 return credentials, nil
206}
207
208// IdExist return whether a credential id exist or not
209func IdExist(repo repository.RepoConfig, id entity.Id) bool {
210 _, err := LoadWithId(repo, id)
211 return err == nil
212}
213
214// PrefixExist return whether a credential id prefix exist or not
215func PrefixExist(repo repository.RepoConfig, prefix string) bool {
216 _, err := LoadWithPrefix(repo, prefix)
217 return err == nil
218}
219
220// Store stores a credential in the global git config
221func Store(repo repository.RepoConfig, cred Credential) error {
222 confs := cred.toConfig()
223
224 prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
225
226 // Kind
227 err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
228 if err != nil {
229 return err
230 }
231
232 // Target
233 err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
234 if err != nil {
235 return err
236 }
237
238 // CreateTime
239 err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
240 if err != nil {
241 return err
242 }
243
244 // Salt
245 if len(cred.Salt()) != 16 {
246 panic("credentials need to be salted")
247 }
248 encoded := base64.StdEncoding.EncodeToString(cred.Salt())
249 err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
250 if err != nil {
251 return err
252 }
253
254 // Metadata
255 for key, val := range cred.Metadata() {
256 err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
257 if err != nil {
258 return err
259 }
260 }
261
262 // Custom
263 for key, val := range confs {
264 err := repo.GlobalConfig().StoreString(prefix+key, val)
265 if err != nil {
266 return err
267 }
268 }
269
270 return nil
271}
272
273// Remove removes a credential from the global git config
274func Remove(repo repository.RepoConfig, id entity.Id) error {
275 keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
276 return repo.GlobalConfig().RemoveAll(keyPrefix)
277}
278
279/*
280 * Sorting
281 */
282
283type ById []Credential
284
285func (b ById) Len() int {
286 return len(b)
287}
288
289func (b ById) Less(i, j int) bool {
290 return b[i].ID() < b[j].ID()
291}
292
293func (b ById) Swap(i, j int) {
294 b[i], b[j] = b[j], b[i]
295}