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 configKeyUserId = "userid"
18 configKeyTarget = "target"
19 configKeyCreateTime = "createtime"
20)
21
22type CredentialKind string
23
24const (
25 KindToken CredentialKind = "token"
26 KindLoginPassword CredentialKind = "login-password"
27)
28
29var ErrCredentialNotExist = errors.New("credential doesn't exist")
30
31func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
32 return entity.NewErrMultipleMatch("credential", matching)
33}
34
35// Special Id to mark a credential as being associated to the default user, whoever it might be.
36// The intended use is for the bridge configuration, to be able to create and store a credential
37// with no identities created yet, and then select one with `git-bug user adopt`
38const DefaultUserId = entity.Id("default-user")
39
40type Credential interface {
41 ID() entity.Id
42 UserId() entity.Id
43 updateUserId(id entity.Id)
44 Target() string
45 Kind() CredentialKind
46 CreateTime() time.Time
47 Validate() error
48
49 // Return all the specific properties of the credential that need to be saved into the configuration.
50 // This does not include Target, User, Kind and CreateTime.
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
123// List load all existing credentials
124func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
125 rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
126 if err != nil {
127 return nil, err
128 }
129
130 re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
131 if err != nil {
132 panic(err)
133 }
134
135 mapped := make(map[string]map[string]string)
136
137 for key, val := range rawConfigs {
138 res := re.FindStringSubmatch(key)
139 if res == nil {
140 continue
141 }
142 if mapped[res[1]] == nil {
143 mapped[res[1]] = make(map[string]string)
144 }
145 mapped[res[1]][res[2]] = val
146 }
147
148 matcher := matcher(opts)
149
150 var credentials []Credential
151 for id, kvs := range mapped {
152 cred, err := loadFromConfig(kvs, entity.Id(id))
153 if err != nil {
154 return nil, err
155 }
156 if matcher.Match(cred) {
157 credentials = append(credentials, cred)
158 }
159 }
160
161 return credentials, nil
162}
163
164// IdExist return whether a credential id exist or not
165func IdExist(repo repository.RepoConfig, id entity.Id) bool {
166 _, err := LoadWithId(repo, id)
167 return err == nil
168}
169
170// PrefixExist return whether a credential id prefix exist or not
171func PrefixExist(repo repository.RepoConfig, prefix string) bool {
172 _, err := LoadWithPrefix(repo, prefix)
173 return err == nil
174}
175
176// Store stores a credential in the global git config
177func Store(repo repository.RepoConfig, cred Credential) error {
178 confs := cred.toConfig()
179
180 prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
181
182 // Kind
183 err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
184 if err != nil {
185 return err
186 }
187
188 // UserId
189 err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
190 if err != nil {
191 return err
192 }
193
194 // Target
195 err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
196 if err != nil {
197 return err
198 }
199
200 // CreateTime
201 err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
202 if err != nil {
203 return err
204 }
205
206 // Custom
207 for key, val := range confs {
208 err := repo.GlobalConfig().StoreString(prefix+key, val)
209 if err != nil {
210 return err
211 }
212 }
213
214 return nil
215}
216
217// Remove removes a credential from the global git config
218func Remove(repo repository.RepoConfig, id entity.Id) error {
219 keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
220 return repo.GlobalConfig().RemoveAll(keyPrefix)
221}
222
223// ReplaceDefaultUser update all the credential attributed to the temporary "default user"
224// with a real user Id
225func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error {
226 list, err := List(repo, WithUserId(DefaultUserId))
227 if err != nil {
228 return err
229 }
230
231 for _, cred := range list {
232 cred.updateUserId(id)
233 err = Store(repo, cred)
234 if err != nil {
235 return err
236 }
237 }
238
239 return nil
240}
241
242/*
243 * Sorting
244 */
245
246type ById []Credential
247
248func (b ById) Len() int {
249 return len(b)
250}
251
252func (b ById) Less(i, j int) bool {
253 return b[i].ID() < b[j].ID()
254}
255
256func (b ById) Swap(i, j int) {
257 b[i], b[j] = b[j], b[i]
258}