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