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