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 := regexp.MustCompile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
175
176 mapped := make(map[string]map[string]string)
177
178 for key, val := range rawConfigs {
179 res := re.FindStringSubmatch(key)
180 if res == nil {
181 continue
182 }
183 if mapped[res[1]] == nil {
184 mapped[res[1]] = make(map[string]string)
185 }
186 mapped[res[1]][res[2]] = val
187 }
188
189 matcher := matcher(opts)
190
191 var credentials []Credential
192 for id, kvs := range mapped {
193 cred, err := loadFromConfig(kvs, entity.Id(id))
194 if err != nil {
195 return nil, err
196 }
197 if matcher.Match(cred) {
198 credentials = append(credentials, cred)
199 }
200 }
201
202 return credentials, nil
203}
204
205// IdExist return whether a credential id exist or not
206func IdExist(repo repository.RepoConfig, id entity.Id) bool {
207 _, err := LoadWithId(repo, id)
208 return err == nil
209}
210
211// PrefixExist return whether a credential id prefix exist or not
212func PrefixExist(repo repository.RepoConfig, prefix string) bool {
213 _, err := LoadWithPrefix(repo, prefix)
214 return err == nil
215}
216
217// Store stores a credential in the global git config
218func Store(repo repository.RepoConfig, cred Credential) error {
219 confs := cred.toConfig()
220
221 prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
222
223 // Kind
224 err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
225 if err != nil {
226 return err
227 }
228
229 // Target
230 err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
231 if err != nil {
232 return err
233 }
234
235 // CreateTime
236 err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
237 if err != nil {
238 return err
239 }
240
241 // Salt
242 if len(cred.Salt()) != 16 {
243 panic("credentials need to be salted")
244 }
245 encoded := base64.StdEncoding.EncodeToString(cred.Salt())
246 err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
247 if err != nil {
248 return err
249 }
250
251 // Metadata
252 for key, val := range cred.Metadata() {
253 err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
254 if err != nil {
255 return err
256 }
257 }
258
259 // Custom
260 for key, val := range confs {
261 err := repo.GlobalConfig().StoreString(prefix+key, val)
262 if err != nil {
263 return err
264 }
265 }
266
267 return nil
268}
269
270// Remove removes a credential from the global git config
271func Remove(repo repository.RepoConfig, id entity.Id) error {
272 keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
273 return repo.GlobalConfig().RemoveAll(keyPrefix)
274}
275
276/*
277 * Sorting
278 */
279
280type ById []Credential
281
282func (b ById) Len() int {
283 return len(b)
284}
285
286func (b ById) Less(i, j int) bool {
287 return b[i].ID() < b[j].ID()
288}
289
290func (b ById) Swap(i, j int) {
291 b[i], b[j] = b[j], b[i]
292}