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