Feature/corda 1947/add package ownership (#4097)

* Upgrade hibernate and fix tests

CORDA-1947 Address code review changes

CORDA-1947 Address code review changes

(cherry picked from commit ab98c03d1a)

* ENT-2506 Changes signers field type

ENT-2506 Clean up some docs

ENT-2506 Fix tests and api

ENT-2506 Fix compilation error

ENT-2506 Fix compilation error

(cherry picked from commit 32f279a243)

* CORDA-1947 added packageOwnership parameter

CORDA-1947 add signers field to DbAttachment. Add check when importing attachments

CORDA-1947 add signers field to DbAttachment. Add check when importing attachments

CORDA-1947 add tests

CORDA-1947 fix comment

CORDA-1947 Fix test

CORDA-1947 fix serialiser

CORDA-1947 fix tests

CORDA-1947 fix tests

CORDA-1947 fix serialiser

CORDA-1947 Address code review changes

CORDA-1947 Address code review changes

CORDA-1947 Revert test fixes

CORDA-1947 address code review comments

CORDA-1947 move verification logic to LedgerTransaction.verify

CORDA-1947 fix test

CORDA-1947 fix tests

CORDA-1947 fix tests

CORDA-1947 address code review comments

CORDA-1947 address code review comments

(cherry picked from commit 86bc0d9606)

CORDA-1947 fix merge
This commit is contained in:
Tudor Malene
2018-10-22 15:00:08 +01:00
committed by GitHub
parent ba7727a4e1
commit 391c6bf66f
35 changed files with 707 additions and 181 deletions

View File

@ -170,7 +170,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage)
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
attachments.servicesForResolution = it
}
@Suppress("LeakingThis")
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory)
@ -1036,7 +1038,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters)
}

View File

@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
output.writeString(obj.contract)
kryo.writeClassAndObject(output, obj.additionalContracts)
output.writeString(obj.uploader)
kryo.writeClassAndObject(output, obj.signers)
}
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
if (kryo.serializationContext() != null) {
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val context = kryo.serializationContext()!!
val attachmentStorage = context.serviceHub.attachments
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
override val id = attachmentHash
}
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
} else {
val attachment = GeneratedAttachment(input.readBytesWithLength())
val contract = input.readString()
@Suppress("UNCHECKED_CAST")
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
return ContractAttachment(attachment, contract, additionalContracts, uploader)
val signers = kryo.readClassAndObject(input) as List<PublicKey>
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
}
}
}

View File

@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream
import net.corda.core.ClientRelevantError
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.sha256
import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
@ -30,6 +33,7 @@ import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.file.Paths
import java.security.PublicKey
import java.time.Instant
import java.util.*
import java.util.jar.JarInputStream
@ -45,6 +49,10 @@ class NodeAttachmentService(
cacheFactory: NamedCacheFactory,
private val database: CordaPersistence
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
// This is to break the circular dependency.
lateinit var servicesForResolution: ServicesForResolution
companion object {
private val log = contextLogger()
@ -94,7 +102,13 @@ class NodeAttachmentService(
@Column(name = "contract_class_name", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null
var contractClassNames: List<ContractClassName>? = null,
@ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER)
@Column(name = "signer", nullable = false)
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
foreignKey = ForeignKey(name = "FK__signers__attachments"))
var signers: List<PublicKey>? = null
)
@VisibleForTesting
@ -212,11 +226,13 @@ class NodeAttachmentService(
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
return database.transaction {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
?: return@transaction null
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader)
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers
?: emptyList())
} else {
it
}
@ -290,14 +306,19 @@ class NodeAttachmentService(
val id = bytes.sha256()
if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream())
val jarSigners = getSigners(bytes)
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(),
content = bytes,
uploader = uploader,
filename = filename,
contractClassNames = contractClassNames
contractClassNames = contractClassNames,
signers = jarSigners
)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
@ -309,6 +330,9 @@ class NodeAttachmentService(
}
}
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
@Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
return try {
@ -339,4 +363,4 @@ class NodeAttachmentService(
query.resultList.map { AttachmentId.parse(it.attId) }
}
}
}
}

View File

@ -0,0 +1,18 @@
package net.corda.node.services.persistence
import net.corda.core.crypto.Crypto
import net.corda.core.utilities.hexToByteArray
import net.corda.core.utilities.toHex
import java.security.PublicKey
import javax.persistence.AttributeConverter
import javax.persistence.Converter
/**
* Converts to and from a Public key into a hex encoded string.
* Used by JPA to automatically map a [PublicKey] to a text column
*/
@Converter(autoApply = true)
class PublicKeyToTextConverter : AttributeConverter<PublicKey, String> {
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex()
override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) }
}

View File

@ -505,7 +505,8 @@ class NodeVaultService(
// Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases.
// So the floor may be necessary anyway.
query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize)
query.maxResults = paging.pageSize + 1 // detection too many results
val pageSize = paging.pageSize + 1
query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow
// execution
val results = query.resultList

View File

@ -14,4 +14,17 @@
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
</changeSet>
<changeSet author="R3.Corda" id="add_signers">
<createTable tableName="node_attachments_signers">
<column name="att_id" type="NVARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="signer" type="NVARCHAR(1024)"/>
</createTable>
<addForeignKeyConstraint baseColumnNames="att_id" baseTableName="node_attachments_signers"
constraintName="FK__signers__attachments"
referencedColumnNames="att_id" referencedTableName="node_attachments"/>
</changeSet>
</databaseChangeLog>

View File

@ -2,6 +2,8 @@ package net.corda.node.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.generateKeyPair
import net.corda.core.node.JavaPackageName
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.core.node.NotaryInfo
import net.corda.core.utilities.days
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -65,7 +68,8 @@ class NetworkParametersTest {
fun `choosing notary not specified in network parameters will fail`() {
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
val notary = NotaryConfig(false)
doReturn(notary).whenever(it).notary}))
doReturn(notary).whenever(it).notary
}))
val fakeNotaryId = fakeNotary.info.singleIdentity()
val alice = mockNet.createPartyNode(ALICE_NAME)
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
@ -87,6 +91,62 @@ class NetworkParametersTest {
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
}
@Test
fun `package ownership checks are correct`() {
val key1 = generateKeyPair().public
val key2 = generateKeyPair().public
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.!example.stuff") to key2
)
)
}.withMessageContaining("Attempting to whitelist illegal java package")
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.example.stuff") to key2
)
)
}.withMessage("multiple packages added to the packageOwnership overlap.")
NetworkParameters(1,
emptyList(),
2001,
2000,
Instant.now(),
1,
emptyMap(),
Int.MAX_VALUE.days,
mapOf(
JavaPackageName("com.example") to key1,
JavaPackageName("com.examplestuff") to key2
)
)
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
}
// Helpers
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
NetworkParametersCopier(params).install(dir)

View File

@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.JarSignatureTestUtils.createJar
import net.corda.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.*
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.node.services.vault.Builder
@ -17,33 +23,40 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.configureDatabase
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.*
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem
import java.nio.file.Path
import java.nio.file.*
import java.security.PublicKey
import java.util.jar.JarEntry
import java.util.jar.JarOutputStream
import javax.tools.JavaFileObject
import javax.tools.SimpleJavaFileObject
import javax.tools.StandardLocation
import javax.tools.ToolProvider
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class NodeAttachmentServiceTest {
// Use an in memory file system for testing attachment storage.
private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private val services = rigorousMock<ServicesForResolution>()
@Before
fun setUp() {
@ -52,18 +65,40 @@ class NodeAttachmentServiceTest {
val dataSourceProperties = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null })
fs = Jimfs.newFileSystem(Configuration.unix())
doReturn(testNetworkParameters()).whenever(services).networkParameters
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
database.transaction {
it.start()
}
}
storage.servicesForResolution = services
}
@After
fun tearDown() {
dir.list { subdir ->
subdir.forEach(Path::deleteRecursively)
}
database.close()
}
@Test
fun `importing a signed jar saves the signers to the storage`() {
val jarAndSigner = makeTestSignedContractJar("com.example.MyContract")
val signedJar = jarAndSigner.first
val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null)
assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash })
}
@Test
fun `importing a non-signed jar will save no signers`() {
val jarName = makeTestContractJar("com.example.MyContract")
val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null)
assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size)
}
@Test
fun `insert and retrieve`() {
val (testJar, expectedHash) = makeTestJar()
@ -289,7 +324,20 @@ class NodeAttachmentServiceTest {
return Pair(file, file.readAll().sha256())
}
private companion object {
companion object {
private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName)
@BeforeClass
@JvmStatic
fun beforeClass() {
}
@AfterClass
@JvmStatic
fun afterClass() {
dir.deleteRecursively()
}
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
output.use {
val jar = JarOutputStream(it)
@ -305,5 +353,48 @@ class NodeAttachmentServiceTest {
jar.closeEntry()
}
}
private fun makeTestSignedContractJar(contractName: String): Pair<Path, PublicKey> {
val alias = "testAlias"
val pwd = "testPassword"
dir.generateKey(alias, pwd, ALICE_NAME.toString())
val jarName = makeTestContractJar(contractName)
val signer = dir.signJar(jarName, alias, pwd)
return dir.resolve(jarName) to signer
}
private fun makeTestContractJar(contractName: String): String {
val packages = contractName.split(".")
val jarName = "testattachment.jar"
val className = packages.last()
createTestClass(className, packages.subList(0, packages.size - 1))
dir.createJar(jarName, "${contractName.replace(".", "/")}.class")
return jarName
}
private fun createTestClass(className: String, packages: List<String>): Path {
val newClass = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*;
import net.corda.core.transactions.*;
public class $className implements Contract {
@Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
}
}
""".trimIndent()
val compiler = ToolProvider.getSystemJavaCompiler()
val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) {
override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence {
return newClass
}
}
val fileManager = compiler.getStandardFileManager(null, null, null)
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile()))
val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call()
return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name)
}
}
}