diff --git a/server/storage/rethinkdb.go b/server/storage/rethinkdb.go index a354109204..8847ffe672 100644 --- a/server/storage/rethinkdb.go +++ b/server/storage/rethinkdb.go @@ -3,6 +3,7 @@ package storage import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "sort" "time" @@ -43,6 +44,47 @@ 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 diff --git a/server/storage/rethinkdb_models.go b/server/storage/rethinkdb_models.go index 8b91c9f6e6..8e60f8e8d7 100644 --- a/server/storage/rethinkdb_models.go +++ b/server/storage/rethinkdb_models.go @@ -27,6 +27,7 @@ var ( Config: map[string]string{ "write_acks": "majority", }, + JSONUnmarshaller: rdbTUFFileFromJSON, } // PubKeysRethinkTable is the table definition of notary server's public key information for TUF roles @@ -36,5 +37,6 @@ var ( SecondaryIndexes: map[string][]string{ rdbGunRoleIdx: {"gun", "role"}, }, + JSONUnmarshaller: rdbKeyFromJSON, } ) diff --git a/server/storage/rethinkdb_test.go b/server/storage/rethinkdb_test.go new file mode 100644 index 0000000000..ba63de7b4c --- /dev/null +++ b/server/storage/rethinkdb_test.go @@ -0,0 +1,97 @@ +package storage + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/docker/notary/storage/rethinkdb" + "github.com/stretchr/testify/require" +) + +func TestRDBTUFFileMarshalling(t *testing.T) { + created := time.Now().AddDate(-1, -1, -1) + updated := time.Now().AddDate(0, -5, 0) + deleted := time.Time{} + data := []byte("Hello world") + + createdMarshalled, err := json.Marshal(created) + require.NoError(t, err) + updatedMarshalled, err := json.Marshal(updated) + require.NoError(t, err) + deletedMarshalled, err := json.Marshal(deleted) + require.NoError(t, err) + dataMarshalled, err := json.Marshal(data) + require.NoError(t, err) + + jsonBytes := []byte(fmt.Sprintf(` + { + "created_at": %s, + "updated_at": %s, + "deleted_at": %s, + "gun_role_version": ["completely", "invalid", "garbage"], + "gun": "namespaced/name", + "role": "timestamp", + "version": 5, + "sha256": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", + "data": %s, + "timestamp_checksum": "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0" + } + `, createdMarshalled, updatedMarshalled, deletedMarshalled, dataMarshalled)) + + unmarshalledAnon, err := TUFFilesRethinkTable.JSONUnmarshaller(jsonBytes) + require.NoError(t, err) + unmarshalled, ok := unmarshalledAnon.(RDBTUFFile) + require.True(t, ok) + + // There is some weirdness with comparing time.Time due to a location pointer, + // so let's use time.Time's equal function to compare times, and then re-assign + // the timing struct to compare the rest of the RDBTUFFile struct + require.True(t, created.Equal(unmarshalled.CreatedAt)) + require.True(t, updated.Equal(unmarshalled.UpdatedAt)) + require.True(t, deleted.Equal(unmarshalled.DeletedAt)) + + expected := RDBTUFFile{ + Timing: unmarshalled.Timing, + GunRoleVersion: []interface{}{"namespaced/name", "timestamp", 5}, + Gun: "namespaced/name", + Role: "timestamp", + Version: 5, + Sha256: "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", + Data: data, + TSchecksum: "ebe6b6e082c94ef24043f1786a7046432506c3d193a47c299ed48ff4413ad7b0", + } + require.Equal(t, expected, unmarshalled) +} + +func TestRDBTUFKeyMarshalling(t *testing.T) { + rdb := RDBKey{ + Timing: rethinkdb.Timing{ + CreatedAt: time.Now().AddDate(-1, -1, -1), + UpdatedAt: time.Now().AddDate(0, -5, 0), + DeletedAt: time.Time{}, + }, + Gun: "namespaced/name", + Role: "timestamp", + Cipher: "ecdsa", + Public: []byte("Hello world"), + } + jsonBytes, err := json.Marshal(rdb) + require.NoError(t, err) + + unmarshalledAnon, err := PubKeysRethinkTable.JSONUnmarshaller(jsonBytes) + require.NoError(t, err) + unmarshalled, ok := unmarshalledAnon.(RDBKey) + require.True(t, ok) + + // There is some weirdness with comparing time.Time due to a location pointer, + // so let's use time.Time's equal function to compare times, and then re-assign + // the timing struct to compare the rest of the RDBTUFFile struct + require.True(t, rdb.CreatedAt.Equal(unmarshalled.CreatedAt)) + require.True(t, rdb.UpdatedAt.Equal(unmarshalled.UpdatedAt)) + require.True(t, rdb.DeletedAt.Equal(unmarshalled.DeletedAt)) + unmarshalled.Timing = rdb.Timing + + require.Equal(t, rdb, unmarshalled) +} diff --git a/signer/keydbstore/rethink_keydbstore.go b/signer/keydbstore/rethink_keydbstore.go index 2c69fc69fe..0ea12c3463 100644 --- a/signer/keydbstore/rethink_keydbstore.go +++ b/signer/keydbstore/rethink_keydbstore.go @@ -1,6 +1,7 @@ package keydbstore import ( + "encoding/json" "errors" "fmt" "sync" @@ -39,10 +40,46 @@ type RDBPrivateKey struct { Private string `gorethink:"private"` } +// gorethink can't handle an UnmarshalJSON function (see https://github.com/dancannon/gorethink/issues/201), +// so do this here in an anonymous struct +func rdbPrivateKeyFromJSON(data []byte) (interface{}, error) { + a := struct { + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` + KeyID string `json:"key_id"` + EncryptionAlg string `json:"encryption_alg"` + KeywrapAlg string `json:"keywrap_alg"` + Algorithm string `json:"algorithm"` + PassphraseAlias string `json:"passphrase_alias"` + Public string `json:"public"` + Private string `json:"private"` + }{} + if err := json.Unmarshal(data, &a); err != nil { + return RDBPrivateKey{}, err + } + return RDBPrivateKey{ + Timing: rethinkdb.Timing{ + CreatedAt: a.CreatedAt, + UpdatedAt: a.UpdatedAt, + DeletedAt: a.DeletedAt, + }, + KeyID: a.KeyID, + EncryptionAlg: a.EncryptionAlg, + KeywrapAlg: a.KeywrapAlg, + Algorithm: a.Algorithm, + PassphraseAlias: a.PassphraseAlias, + Public: a.Public, + Private: a.Private, + }, nil + +} + // PrivateKeysRethinkTable is the table definition for notary signer's key information var PrivateKeysRethinkTable = rethinkdb.Table{ - Name: RDBPrivateKey{}.TableName(), - PrimaryKey: RDBPrivateKey{}.KeyID, + Name: RDBPrivateKey{}.TableName(), + PrimaryKey: RDBPrivateKey{}.KeyID, + JSONUnmarshaller: rdbPrivateKeyFromJSON, } // TableName sets a specific table name for our RDBPrivateKey diff --git a/signer/keydbstore/rethink_keydbstore_test.go b/signer/keydbstore/rethink_keydbstore_test.go new file mode 100644 index 0000000000..2e854a74ca --- /dev/null +++ b/signer/keydbstore/rethink_keydbstore_test.go @@ -0,0 +1,63 @@ +package keydbstore + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRDBTUFFileMarshalling(t *testing.T) { + created := time.Now().AddDate(-1, -1, -1) + updated := time.Now().AddDate(0, -5, 0) + deleted := time.Time{} + + createdMarshalled, err := json.Marshal(created) + require.NoError(t, err) + updatedMarshalled, err := json.Marshal(updated) + require.NoError(t, err) + deletedMarshalled, err := json.Marshal(deleted) + require.NoError(t, err) + + jsonBytes := []byte(fmt.Sprintf(` + { + "created_at": %s, + "updated_at": %s, + "deleted_at": %s, + "key_id": "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", + "encryption_alg": "A256GCM", + "keywrap_alg": "PBES2-HS256+A128KW", + "algorithm": "ecdsa", + "passphrase_alias": "timestamp_1", + "public": "Hello world public", + "private": "Hello world private" + } + `, createdMarshalled, updatedMarshalled, deletedMarshalled)) + fmt.Println(string(jsonBytes)) + + unmarshalledAnon, err := PrivateKeysRethinkTable.JSONUnmarshaller(jsonBytes) + require.NoError(t, err) + unmarshalled, ok := unmarshalledAnon.(RDBPrivateKey) + require.True(t, ok) + + // There is some weirdness with comparing time.Time due to a location pointer, + // so let's use time.Time's equal function to compare times, and then re-assign + // the timing struct to compare the rest of the RDBTUFFile struct + require.True(t, created.Equal(unmarshalled.CreatedAt)) + require.True(t, updated.Equal(unmarshalled.UpdatedAt)) + require.True(t, deleted.Equal(unmarshalled.DeletedAt)) + + expected := RDBPrivateKey{ + Timing: unmarshalled.Timing, + KeyID: "56ee4a23129fc22c6cb4b4ba5f78d730c91ab6def514e80d807c947bb21f0d63", + EncryptionAlg: "A256GCM", + KeywrapAlg: "PBES2-HS256+A128KW", + Algorithm: "ecdsa", + PassphraseAlias: "timestamp_1", + Public: "Hello world public", + Private: "Hello world private", + } + require.Equal(t, expected, unmarshalled) +} diff --git a/storage/rethinkdb/bootstrap.go b/storage/rethinkdb/bootstrap.go index d7cadcff67..7752a278b4 100644 --- a/storage/rethinkdb/bootstrap.go +++ b/storage/rethinkdb/bootstrap.go @@ -31,6 +31,9 @@ type Table struct { // on the list of fields in the corrensponding slice value. SecondaryIndexes map[string][]string Config map[string]string + //JSONUnmarshaller takes a byte slice representing JSON data and knows how + //to unmarshal them into a model representing this table + JSONUnmarshaller func([]byte) (interface{}, error) } func (t Table) term(dbName string) gorethink.Term {