docs/server/storage/database.go

214 lines
5.7 KiB
Go

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
}