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