1package auth
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/pkg/errors"
12
13 "github.com/MichaelMure/git-bug/entity"
14 "github.com/MichaelMure/git-bug/repository"
15)
16
17const (
18 keyringKeyPrefix = "auth-"
19 keyringKeyKind = "kind"
20 keyringKeyTarget = "target"
21 keyringKeyCreateTime = "createtime"
22 keyringKeySalt = "salt"
23 keyringKeyPrefixMeta = "meta."
24
25 MetaKeyLogin = "login"
26 MetaKeyBaseURL = "base-url"
27)
28
29type CredentialKind string
30
31const (
32 KindToken CredentialKind = "token"
33 KindLogin CredentialKind = "login"
34 KindLoginPassword CredentialKind = "login-password"
35)
36
37var ErrCredentialNotExist = errors.New("credential doesn't exist")
38
39func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
40 return entity.NewErrMultipleMatch("credential", matching)
41}
42
43type Credential interface {
44 ID() entity.Id
45 Kind() CredentialKind
46 Target() string
47 CreateTime() time.Time
48 Salt() []byte
49 Validate() error
50
51 Metadata() map[string]string
52 GetMetadata(key string) (string, bool)
53 SetMetadata(key string, value string)
54
55 // Return all the specific properties of the credential that need to be saved into the configuration.
56 // This does not include Target, Kind, CreateTime, Metadata or Salt.
57 toConfig() map[string]string
58}
59
60// Load loads a credential from the repo config
61func LoadWithId(repo repository.RepoKeyring, id entity.Id) (Credential, error) {
62 key := fmt.Sprintf("%s%s", keyringKeyPrefix, id)
63
64 item, err := repo.Keyring().Get(key)
65 if err == repository.ErrKeyringKeyNotFound {
66 return nil, ErrCredentialNotExist
67 }
68 if err != nil {
69 return nil, err
70 }
71
72 return decode(item)
73}
74
75// LoadWithPrefix load a credential from the repo config with a prefix
76func LoadWithPrefix(repo repository.RepoKeyring, prefix string) (Credential, error) {
77 keys, err := repo.Keyring().Keys()
78 if err != nil {
79 return nil, err
80 }
81
82 // preallocate but empty
83 matching := make([]Credential, 0, 5)
84
85 for _, key := range keys {
86 if !strings.HasPrefix(key, keyringKeyPrefix+prefix) {
87 continue
88 }
89
90 item, err := repo.Keyring().Get(key)
91 if err != nil {
92 return nil, err
93 }
94
95 cred, err := decode(item)
96 if err != nil {
97 return nil, err
98 }
99
100 matching = append(matching, cred)
101 }
102
103 if len(matching) > 1 {
104 ids := make([]entity.Id, len(matching))
105 for i, cred := range matching {
106 ids[i] = cred.ID()
107 }
108 return nil, NewErrMultipleMatchCredential(ids)
109 }
110
111 if len(matching) == 0 {
112 return nil, ErrCredentialNotExist
113 }
114
115 return matching[0], nil
116}
117
118// decode is a helper to construct a Credential from the keyring Item
119func decode(item repository.Item) (Credential, error) {
120 data := make(map[string]string)
121
122 err := json.Unmarshal(item.Data, &data)
123 if err != nil {
124 return nil, err
125 }
126
127 var cred Credential
128 switch CredentialKind(data[keyringKeyKind]) {
129 case KindToken:
130 cred, err = NewTokenFromConfig(data)
131 case KindLogin:
132 cred, err = NewLoginFromConfig(data)
133 case KindLoginPassword:
134 cred, err = NewLoginPasswordFromConfig(data)
135 default:
136 return nil, fmt.Errorf("unknown credential type \"%s\"", data[keyringKeyKind])
137 }
138
139 if err != nil {
140 return nil, fmt.Errorf("loading credential: %v", err)
141 }
142
143 return cred, nil
144}
145
146// List load all existing credentials
147func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) {
148 keys, err := repo.Keyring().Keys()
149 if err != nil {
150 return nil, err
151 }
152
153 matcher := matcher(opts)
154
155 var credentials []Credential
156 for _, key := range keys {
157 if !strings.HasPrefix(key, keyringKeyPrefix) {
158 continue
159 }
160
161 item, err := repo.Keyring().Get(key)
162 if err != nil {
163 // skip unreadable items, nothing much we can do for them anyway
164 continue
165 }
166
167 cred, err := decode(item)
168 if err != nil {
169 return nil, err
170 }
171 if matcher.Match(cred) {
172 credentials = append(credentials, cred)
173 }
174 }
175
176 return credentials, nil
177}
178
179// IdExist return whether a credential id exist or not
180func IdExist(repo repository.RepoKeyring, id entity.Id) bool {
181 _, err := LoadWithId(repo, id)
182 return err == nil
183}
184
185// PrefixExist return whether a credential id prefix exist or not
186func PrefixExist(repo repository.RepoKeyring, prefix string) bool {
187 _, err := LoadWithPrefix(repo, prefix)
188 return err == nil
189}
190
191// Store stores a credential in the global git config
192func Store(repo repository.RepoKeyring, cred Credential) error {
193 if len(cred.Salt()) != 16 {
194 panic("credentials need to be salted")
195 }
196
197 confs := cred.toConfig()
198
199 confs[keyringKeyKind] = string(cred.Kind())
200 confs[keyringKeyTarget] = cred.Target()
201 confs[keyringKeyCreateTime] = strconv.Itoa(int(cred.CreateTime().Unix()))
202 confs[keyringKeySalt] = base64.StdEncoding.EncodeToString(cred.Salt())
203
204 for key, val := range cred.Metadata() {
205 confs[keyringKeyPrefixMeta+key] = val
206 }
207
208 data, err := json.Marshal(confs)
209 if err != nil {
210 return err
211 }
212
213 return repo.Keyring().Set(repository.Item{
214 Key: keyringKeyPrefix + cred.ID().String(),
215 Data: data,
216 })
217}
218
219// Remove removes a credential from the global git config
220func Remove(repo repository.RepoKeyring, id entity.Id) error {
221 return repo.Keyring().Remove(keyringKeyPrefix + id.String())
222}
223
224/*
225 * Sorting
226 */
227
228type ById []Credential
229
230func (b ById) Len() int {
231 return len(b)
232}
233
234func (b ById) Less(i, j int) bool {
235 return b[i].ID() < b[j].ID()
236}
237
238func (b ById) Swap(i, j int) {
239 b[i], b[j] = b[j], b[i]
240}