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 }