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