forked from notaryproject/notary
/
rethinkdb.go
325 lines (300 loc) · 10.1 KB
/
rethinkdb.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package storage
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/docker/notary/storage/rethinkdb"
"github.com/docker/notary/tuf/data"
"gopkg.in/dancannon/gorethink.v2"
)
// RDBTUFFile is a TUF file record
type RDBTUFFile struct {
rethinkdb.Timing
GunRoleVersion []interface{} `gorethink:"gun_role_version"`
Gun string `gorethink:"gun"`
Role string `gorethink:"role"`
Version int `gorethink:"version"`
Sha256 string `gorethink:"sha256"`
Data []byte `gorethink:"data"`
TSchecksum string `gorethink:"timestamp_checksum"`
}
// TableName returns the table name for the record type
func (r RDBTUFFile) TableName() string {
return "tuf_files"
}
// RDBKey is the public key record
type RDBKey struct {
rethinkdb.Timing
Gun string `gorethink:"gun"`
Role string `gorethink:"role"`
Cipher string `gorethink:"cipher"`
Public []byte `gorethink:"public"`
}
// TableName returns the table name for the record type
func (r RDBKey) TableName() string {
return "tuf_keys"
}
// gorethink can't handle an UnmarshalJSON function (see https://github.com/dancannon/gorethink/issues/201),
// so do this here in an anonymous struct
func rdbTUFFileFromJSON(data []byte) (interface{}, error) {
a := struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `json:"deleted_at"`
Gun string `json:"gun"`
Role string `json:"role"`
Version int `json:"version"`
Sha256 string `json:"sha256"`
Data []byte `json:"data"`
TSchecksum string `json:"timestamp_checksum"`
}{}
if err := json.Unmarshal(data, &a); err != nil {
return RDBTUFFile{}, err
}
return RDBTUFFile{
Timing: rethinkdb.Timing{
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
DeletedAt: a.DeletedAt,
},
GunRoleVersion: []interface{}{a.Gun, a.Role, a.Version},
Gun: a.Gun,
Role: a.Role,
Version: a.Version,
Sha256: a.Sha256,
Data: a.Data,
TSchecksum: a.TSchecksum,
}, nil
}
func rdbKeyFromJSON(data []byte) (interface{}, error) {
rdb := RDBKey{}
if err := json.Unmarshal(data, &rdb); err != nil {
return RDBKey{}, err
}
return rdb, nil
}
// RethinkDB implements a MetaStore against the Rethink Database
type RethinkDB struct {
dbName string
sess *gorethink.Session
user string
password string
}
// NewRethinkDBStorage initializes a RethinkDB object
func NewRethinkDBStorage(dbName, user, password string, sess *gorethink.Session) RethinkDB {
return RethinkDB{
dbName: dbName,
sess: sess,
user: user,
password: password,
}
}
// GetKey returns the cipher and public key for the given GUN and role.
// If the GUN+role don't exist, returns an error.
func (rdb RethinkDB) GetKey(gun, role string) (cipher string, public []byte, err error) {
var key RDBKey
res, err := gorethink.DB(rdb.dbName).Table(key.TableName()).GetAllByIndex(
rdbGunRoleIdx, []string{gun, role},
).Run(rdb.sess)
if err != nil {
return "", nil, err
}
defer res.Close()
err = res.One(&key)
if err == gorethink.ErrEmptyResult {
return "", nil, &ErrNoKey{gun: gun}
}
return key.Cipher, key.Public, err
}
// SetKey sets the cipher and public key for the given GUN and role if
// it doesn't already exist. Otherwise an error is returned.
func (rdb RethinkDB) SetKey(gun, role, cipher string, public []byte) error {
now := time.Now()
key := RDBKey{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
Gun: gun,
Role: role,
Cipher: cipher,
Public: public,
}
_, err := gorethink.DB(rdb.dbName).Table(key.TableName()).Insert(key).RunWrite(rdb.sess)
return err
}
// UpdateCurrent adds new metadata version for the given GUN if and only
// if it's a new role, or the version is greater than the current version
// for the role. Otherwise an error is returned.
func (rdb RethinkDB) UpdateCurrent(gun string, update MetaUpdate) error {
now := time.Now()
checksum := sha256.Sum256(update.Data)
file := RDBTUFFile{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
GunRoleVersion: []interface{}{gun, update.Role, update.Version},
Gun: gun,
Role: update.Role,
Version: update.Version,
Sha256: hex.EncodeToString(checksum[:]),
Data: update.Data,
}
_, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Insert(
file,
gorethink.InsertOpts{
Conflict: "error", // default but explicit for clarity of intent
},
).RunWrite(rdb.sess)
if err != nil && gorethink.IsConflictErr(err) {
return &ErrOldVersion{}
}
return err
}
// UpdateCurrentWithTSChecksum adds new metadata version for the given GUN with an associated
// checksum for the timestamp it belongs to, to afford us transaction-like functionality
func (rdb RethinkDB) UpdateCurrentWithTSChecksum(gun, tsChecksum string, update MetaUpdate) error {
now := time.Now()
checksum := sha256.Sum256(update.Data)
file := RDBTUFFile{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
GunRoleVersion: []interface{}{gun, update.Role, update.Version},
Gun: gun,
Role: update.Role,
Version: update.Version,
Sha256: hex.EncodeToString(checksum[:]),
TSchecksum: tsChecksum,
Data: update.Data,
}
_, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Insert(
file,
gorethink.InsertOpts{
Conflict: "error", // default but explicit for clarity of intent
},
).RunWrite(rdb.sess)
if err != nil && gorethink.IsConflictErr(err) {
return &ErrOldVersion{}
}
return err
}
// Used for sorting updates alphabetically by role name, such that timestamp is always last:
// Ordering: root, snapshot, targets, targets/* (delegations), timestamp
type updateSorter []MetaUpdate
func (u updateSorter) Len() int { return len(u) }
func (u updateSorter) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
func (u updateSorter) Less(i, j int) bool {
return u[i].Role < u[j].Role
}
// UpdateMany adds multiple new metadata for the given GUN. RethinkDB does
// not support transactions, therefore we will attempt to insert the timestamp
// last as this represents a published version of the repo. However, we will
// insert all other role data in alphabetical order first, and also include the
// associated timestamp checksum so that we can easily roll back this pseudotransaction
func (rdb RethinkDB) UpdateMany(gun string, updates []MetaUpdate) error {
// find the timestamp first and save its checksum
// then apply the updates in alphabetic role order with the timestamp last
// if there are any failures, we roll back in the same alphabetic order
var tsChecksum string
for _, up := range updates {
if up.Role == data.CanonicalTimestampRole {
tsChecksumBytes := sha256.Sum256(up.Data)
tsChecksum = hex.EncodeToString(tsChecksumBytes[:])
break
}
}
// alphabetize the updates by Role name
sort.Stable(updateSorter(updates))
for _, up := range updates {
if err := rdb.UpdateCurrentWithTSChecksum(gun, tsChecksum, up); err != nil {
// roll back with best-effort deletion, and then error out
rdb.deleteByTSChecksum(tsChecksum)
return err
}
}
return nil
}
// GetCurrent returns the modification date and data part of the metadata for
// the latest version of the given GUN and role. If there is no data for
// the given GUN and role, an error is returned.
func (rdb RethinkDB) GetCurrent(gun, role string) (created *time.Time, data []byte, err error) {
file := RDBTUFFile{}
res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex(
rdbGunRoleIdx, []string{gun, role},
).OrderBy(gorethink.Desc("version")).Run(rdb.sess)
if err != nil {
return nil, nil, err
}
defer res.Close()
if res.IsNil() {
return nil, nil, ErrNotFound{}
}
err = res.One(&file)
if err == gorethink.ErrEmptyResult {
return nil, nil, ErrNotFound{}
}
return &file.CreatedAt, file.Data, err
}
// GetChecksum returns the given TUF role file and creation date for the
// GUN with the provided checksum. If the given (gun, role, checksum) are
// not found, it returns storage.ErrNotFound
func (rdb RethinkDB) GetChecksum(gun, role, checksum string) (created *time.Time, data []byte, err error) {
var file RDBTUFFile
res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex(
rdbGunRoleSha256Idx, []string{gun, role, checksum},
).Run(rdb.sess)
if err != nil {
return nil, nil, err
}
defer res.Close()
if res.IsNil() {
return nil, nil, ErrNotFound{}
}
err = res.One(&file)
if err == gorethink.ErrEmptyResult {
return nil, nil, ErrNotFound{}
}
return &file.CreatedAt, file.Data, err
}
// Delete removes all metadata for a given GUN. It does not return an
// error if no metadata exists for the given GUN.
func (rdb RethinkDB) Delete(gun string) error {
_, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex(
"gun", []string{gun},
).Delete().RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("unable to delete %s from database: %s", gun, err.Error())
}
return nil
}
// deleteByTSChecksum removes all metadata by a timestamp checksum, used for rolling back a "transaction"
// from a call to rethinkdb's UpdateMany
func (rdb RethinkDB) deleteByTSChecksum(tsChecksum string) error {
_, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex(
"timestamp_checksum", []string{tsChecksum},
).Delete().RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("unable to delete timestamp checksum data: %s from database: %s", tsChecksum, err.Error())
}
return nil
}
// Bootstrap sets up the database and tables, also creating the notary server user with appropriate db permission
func (rdb RethinkDB) Bootstrap() error {
if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{
TUFFilesRethinkTable,
PubKeysRethinkTable,
}); err != nil {
return err
}
return rethinkdb.CreateAndGrantDBUser(rdb.sess, rdb.dbName, rdb.user, rdb.password)
}
// CheckHealth is currently a noop
func (rdb RethinkDB) CheckHealth() error {
return nil
}