forked from notaryproject/notary
/
database.go
213 lines (185 loc) · 5.67 KB
/
database.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
package storage
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/Sirupsen/logrus"
"github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
// SQLStorage implements a versioned store using a relational database.
// See server/storage/models.go
type SQLStorage struct {
gorm.DB
}
// NewSQLStorage is a convenience method to create a SQLStorage
func NewSQLStorage(dialect string, args ...interface{}) (*SQLStorage, error) {
gormDB, err := gorm.Open(dialect, args...)
if err != nil {
return nil, err
}
return &SQLStorage{
DB: gormDB,
}, nil
}
// translateOldVersionError captures DB errors, and attempts to translate
// duplicate entry - currently only supports MySQL and Sqlite3
func translateOldVersionError(err error) error {
switch err := err.(type) {
case *mysql.MySQLError:
// https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
// 1022 = Can't write; duplicate key in table '%s'
// 1062 = Duplicate entry '%s' for key %d
if err.Number == 1022 || err.Number == 1062 {
return &ErrOldVersion{}
}
}
return err
}
// UpdateCurrent updates a single TUF.
func (db *SQLStorage) UpdateCurrent(gun string, update MetaUpdate) error {
// ensure we're not inserting an immediately old version - can't use the
// struct, because that only works with non-zero values, and Version
// can be 0.
exists := db.Where("gun = ? and role = ? and version >= ?",
gun, update.Role, update.Version).First(&TUFFile{})
if !exists.RecordNotFound() {
return &ErrOldVersion{}
}
checksum := sha256.Sum256(update.Data)
return translateOldVersionError(db.Create(&TUFFile{
Gun: gun,
Role: update.Role,
Version: update.Version,
Sha256: hex.EncodeToString(checksum[:]),
Data: update.Data,
}).Error)
}
// UpdateMany atomically updates many TUF records in a single transaction
func (db *SQLStorage) UpdateMany(gun string, updates []MetaUpdate) error {
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
rollback := func(err error) error {
if rxErr := tx.Rollback().Error; rxErr != nil {
logrus.Error("Failed on Tx rollback with error: ", rxErr.Error())
return rxErr
}
return err
}
var (
query *gorm.DB
added = make(map[uint]bool)
)
for _, update := range updates {
// This looks like the same logic as UpdateCurrent, but if we just
// called, version ordering in the updates list must be enforced
// (you cannot insert the version 2 before version 1). And we do
// not care about monotonic ordering in the updates.
query = db.Where("gun = ? and role = ? and version >= ?",
gun, update.Role, update.Version).First(&TUFFile{})
if !query.RecordNotFound() {
return rollback(&ErrOldVersion{})
}
var row TUFFile
checksum := sha256.Sum256(update.Data)
hexChecksum := hex.EncodeToString(checksum[:])
query = tx.Where(map[string]interface{}{
"gun": gun,
"role": update.Role,
"version": update.Version,
}).Attrs("data", update.Data).Attrs("sha256", hexChecksum).FirstOrCreate(&row)
if query.Error != nil {
return rollback(translateOldVersionError(query.Error))
}
// it's previously been added, which means it's a duplicate entry
// in the same transaction
if _, ok := added[row.ID]; ok {
return rollback(&ErrOldVersion{})
}
added[row.ID] = true
}
return tx.Commit().Error
}
// GetCurrent gets a specific TUF record
func (db *SQLStorage) GetCurrent(gun, tufRole string) (*time.Time, []byte, error) {
var row TUFFile
q := db.Select("updated_at, data").Where(
&TUFFile{Gun: gun, Role: tufRole}).Order("version desc").Limit(1).First(&row)
if err := isReadErr(q, row); err != nil {
return nil, nil, err
}
return &(row.UpdatedAt), row.Data, nil
}
// GetChecksum gets a specific TUF record by its hex checksum
func (db *SQLStorage) GetChecksum(gun, tufRole, checksum string) (*time.Time, []byte, error) {
var row TUFFile
q := db.Select("created_at, data").Where(
&TUFFile{
Gun: gun,
Role: tufRole,
Sha256: checksum,
},
).First(&row)
if err := isReadErr(q, row); err != nil {
return nil, nil, err
}
return &(row.CreatedAt), row.Data, nil
}
func isReadErr(q *gorm.DB, row TUFFile) error {
if q.RecordNotFound() {
return ErrNotFound{}
} else if q.Error != nil {
return q.Error
}
return nil
}
// Delete deletes all the records for a specific GUN
func (db *SQLStorage) Delete(gun string) error {
return db.Where(&TUFFile{Gun: gun}).Delete(TUFFile{}).Error
}
// GetKey returns the Public Key data for a gun+role
func (db *SQLStorage) GetKey(gun, role string) (algorithm string, public []byte, err error) {
logrus.Debugf("retrieving timestamp key for %s:%s", gun, role)
var row Key
query := db.Select("cipher, public").Where(&Key{Gun: gun, Role: role}).Find(&row)
if query.RecordNotFound() {
return "", nil, &ErrNoKey{gun: gun}
} else if query.Error != nil {
return "", nil, query.Error
}
return row.Cipher, row.Public, nil
}
// SetKey attempts to write a key and returns an error if it already exists for the gun and role
func (db *SQLStorage) SetKey(gun, role, algorithm string, public []byte) error {
entry := Key{
Gun: gun,
Role: role,
}
if !db.Where(&entry).First(&Key{}).RecordNotFound() {
return &ErrKeyExists{gun: gun, role: role}
}
entry.Cipher = algorithm
entry.Public = public
return translateOldVersionError(
db.FirstOrCreate(&Key{}, &entry).Error)
}
// CheckHealth asserts that both required tables are present
func (db *SQLStorage) CheckHealth() error {
interfaces := []interface {
TableName() string
}{&TUFFile{}, &Key{}}
for _, model := range interfaces {
tableOk := db.HasTable(model)
if db.Error != nil {
return db.Error
}
if !tableOk {
return fmt.Errorf(
"Cannot access table: %s", model.TableName())
}
}
return nil
}