From 54cf46952c9d13072f831a5ca38965e9c4d46e31 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 10 Aug 2017 17:17:12 +0100 Subject: [PATCH] JPA Hibernate AbstractParty converter (#1205) * Added JPA AbstractParty converter (using IdentityService to resolve anonymous parties). * Use partyFromX500Name. Add meaningful exception messages. * AutoApply the JPA AbstractParty converter. * Entity attribute still needs the Convert annotation. * Fix incorrect registration of custom attribute converter. * Deal with non-resolvable anonymous parties (eg. store as null and ignore) * Updates following PR review feedback. * Added documentation. * Added entry to changelog. * Added code documentation as per RN PR feedback request. * Updates required following rebase from master. * Renamed converter for clarity. --- ...bstractPartyToX500NameAsStringConverter.kt | 30 +++++++++++++++++++ docs/source/api-persistence.rst | 7 +++++ docs/source/changelog.rst | 3 ++ .../net/corda/contracts/asset/CashTests.kt | 6 ++++ .../net/corda/node/internal/AbstractNode.kt | 4 +-- .../database/HibernateConfiguration.kt | 8 ++++- .../corda/node/services/vault/VaultSchema.kt | 6 ++-- .../services/vault/VaultQueryJavaTests.java | 3 ++ .../database/HibernateConfigurationTest.kt | 4 ++- .../services/schema/HibernateObserverTests.kt | 6 +++- .../services/vault/NodeVaultServiceTest.kt | 3 ++ .../node/services/vault/VaultQueryTests.kt | 8 +++++ .../node/services/vault/VaultWithCashTest.kt | 3 ++ .../net/corda/testing/node/MockServices.kt | 5 ++-- 14 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt diff --git a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt new file mode 100644 index 0000000000..3d88947dbd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt @@ -0,0 +1,30 @@ +package net.corda.core.schemas.converters + +import net.corda.core.identity.AbstractParty +import net.corda.core.node.services.IdentityService +import org.bouncycastle.asn1.x500.X500Name +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +/** + * Converter to persist a party as its's well known identity (where resolvable) + * Completely anonymous parties are stored as null (to preserve privacy) + */ +@Converter(autoApply = true) +class AbstractPartyToX500NameAsStringConverter(val identitySvc: IdentityService) : AttributeConverter { + + override fun convertToDatabaseColumn(party: AbstractParty?): String? { + party?.let { + return identitySvc.partyFromAnonymous(party)?.toString() + } + return null // non resolvable anonymous parties + } + + override fun convertToEntityAttribute(dbData: String?): AbstractParty? { + dbData?.let { + val party = identitySvc.partyFromX500Name(X500Name(dbData)) + return party as AbstractParty + } + return null // non resolvable anonymous parties are stored as nulls + } +} diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 06609ca828..9fee067b2c 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -93,6 +93,13 @@ Several examples of entities and mappings are provided in the codebase, includin .. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt :language: kotlin +Identity mapping +---------------- +Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically +processed to ensure only the ``X500Name`` of the identity is persisted where an identity is well known, otherwise a null +value is stored in the associated column. To preserve privacy, identity keys are never persisted. Developers should use +the ``IdentityService`` to resolve keys from well know X500 identity names. + JDBC session ------------ Apps may also interact directly with the underlying Node's database by using a standard diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 68e1d3f166..4c0e27f7d6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -91,6 +91,9 @@ UNRELEASED of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) +* Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type + (well known party, resolvable anonymous party, completely anonymous party). + Milestone 13 ------------ diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index cd9aa3d42d..5b51d8fc1b 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -6,11 +6,17 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index bcae6876b1..d29b113465 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -479,7 +479,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeVaultObservers() { VaultSoftLockManager(services.vaultService, smm) ScheduledActivityObserver(services) - HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties())) + HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties(), services.identityService)) } private fun makeInfo(): NodeInfo { @@ -766,7 +766,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) } override val vaultQueryService by lazy { - HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties()), vaultService.updatesPublisher) + HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), identityService), vaultService.updatesPublisher) } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with diff --git a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt index c2076db13d..4ab3dc248f 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt @@ -1,7 +1,9 @@ package net.corda.node.services.database import net.corda.core.internal.castIfPossible +import net.corda.core.node.services.IdentityService import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverter import net.corda.core.utilities.loggerFor import net.corda.node.services.api.SchemaService import net.corda.node.utilities.DatabaseTransactionManager @@ -18,7 +20,7 @@ import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap -class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties) { +class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties, val identitySvc: IdentityService) { companion object { val logger = loggerFor() } @@ -58,6 +60,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") + schemas.forEach { schema -> // TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session schema.mappedTypes.forEach { config.addAnnotatedClass(it) } @@ -76,6 +79,9 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted) } }) + // register custom converters + applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identitySvc)) + build() } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index af52eb645f..abf2e44c99 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -105,8 +105,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var participants: Set, /** [OwnableState] attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var owner: CommonSchemaV1.Party, + @Column(name = "owner_id") + var owner: AbstractParty, /** [FungibleAsset] attributes * @@ -126,7 +126,7 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var issuerRef: ByteArray ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : - this(owner = CommonSchemaV1.Party(_owner), + this(owner = _owner, quantity = _quantity, issuerParty = CommonSchemaV1.Party(_issuerParty), issuerRef = _issuerRef.bytes, diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index b5ed6dc440..72b70bb8a0 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -18,6 +18,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.core.utilities.OpaqueBytes; import net.corda.node.utilities.CordaPersistence; +import net.corda.node.services.identity.*; import net.corda.schemas.CashSchemaV1; import net.corda.testing.TestConstants; import net.corda.testing.TestDependencyInjectionBase; @@ -44,6 +45,7 @@ import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZ import static net.corda.core.utilities.ByteArrays.toHexString; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; +import static net.corda.testing.TestConstants.*; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { @@ -57,6 +59,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { public void setUp() { ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); + InMemoryIdentityService identityService = new InMemoryIdentityService(getMOCK_IDENTITIES(), Collections.emptyMap(), getDUMMY_CA().getCertificate()); Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 0a37f8b04e..e19300c60b 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -14,6 +14,7 @@ import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.HibernateVaultQueryImpl @@ -71,7 +72,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { database = configureDatabase(dataSourceProps, defaultDatabaseProperties) val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) database.transaction { - hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties()) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), identityService) services = object : MockServices(BOB_KEY) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 64a5b9d791..ba470599c6 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -10,9 +10,12 @@ import net.corda.core.schemas.QueryableState import net.corda.testing.LogHelper import net.corda.node.services.api.SchemaService import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase +import net.corda.testing.DUMMY_CA import net.corda.testing.MEGA_CORP +import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import org.hibernate.annotations.Cascade @@ -102,7 +105,8 @@ class HibernateObserverTests { } @Suppress("UNUSED_VARIABLE") - val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties())) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties(), identityService)) database.transaction { rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 893d8eff3d..a55801327b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -23,6 +23,9 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 84cc1b8742..5f145243ca 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -13,6 +13,14 @@ import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* +import net.corda.core.utilities.seconds +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.core.utilities.* import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index fe564712b5..ad16be499f 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -12,6 +12,9 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.* diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 689d578e6e..001007f968 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -88,7 +88,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())): VaultService { + fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), identityService)): VaultService { val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties()) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService @@ -221,7 +221,8 @@ fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(Com val databaseProperties = makeTestDatabaseProperties() val database = configureDatabase(dataSourceProps, databaseProperties) val mockService = database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identityService) object : MockServices(*(keys.toTypedArray())) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)