diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index 2b8666a0ea..6de0629d1c 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -3,6 +3,7 @@ package net.corda.coretests.transactions import net.corda.core.contracts.Attachment import net.corda.core.contracts.Contract import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.internal.declaredField import net.corda.core.internal.inputStream @@ -199,4 +200,178 @@ class AttachmentsClassLoaderTests { private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { return jar.use { storage.importAttachment(jar, uploader, filename) } } + + @Test + fun `Allow loading an untrusted contract jar if another attachment exists that was signed with the same keys and uploaded by a trusted uploader`() { + val keyPairA = Crypto.generateKeyPair() + val keyPairB = Crypto.generateKeyPair() + val classJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone trusted" + ).inputStream() + classJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "rpc", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + val untrustedClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted" + ).inputStream() + val untrustedAttachment = untrustedClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! }) + } + + @Test + fun `Allow loading an untrusted contract jar if another attachment exists that was signed by a trusted uploader - intersection of keys match existing attachment`() { + val keyPairA = Crypto.generateKeyPair() + val keyPairB = Crypto.generateKeyPair() + val keyPairC = Crypto.generateKeyPair() + val classJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone trusted" + ).inputStream() + classJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "rpc", + it, + signers = listOf(keyPairA.public, keyPairC.public) + ) + } + + val untrustedClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted" + ).inputStream() + val untrustedAttachment = untrustedClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! }) + } + + @Test + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys`() { + val keyPairA = Crypto.generateKeyPair() + val keyPairB = Crypto.generateKeyPair() + val untrustedClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted" + ).inputStream() + val untrustedAttachment = untrustedClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! }) + } + } + + @Test + fun `Cannot load an untrusted contract jar if no other attachment exists that was signed with the same keys and uploaded by a trusted uploader`() { + val keyPairA = Crypto.generateKeyPair() + val keyPairB = Crypto.generateKeyPair() + val classJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted with the same keys" + ).inputStream() + classJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + val untrustedClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted" + ).inputStream() + val untrustedAttachment = untrustedClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairA.public, keyPairB.public) + ) + } + + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! }) + } + } + + @Test + fun `Attachments with inherited trust do not grant trust to attachments being loaded (no chain of trust)`() { + val keyPairA = Crypto.generateKeyPair() + val keyPairB = Crypto.generateKeyPair() + val keyPairC = Crypto.generateKeyPair() + val classJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted with the same keys" + ).inputStream() + classJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "app", + it, + signers = listOf(keyPairA.public) + ) + } + + val inheritedTrustClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone who inherits trust" + ).inputStream() + val inheritedTrustAttachment = inheritedTrustClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairB.public, keyPairA.public) + ) + } + + val untrustedClassJar = fakeAttachment( + "/com/example/something/UntrustedClass.class", + "Signed by someone untrusted" + ).inputStream() + val untrustedAttachment = untrustedClassJar.use { + storage.importContractAttachment( + listOf("UntrustedClass.class"), + "untrusted", + it, + signers = listOf(keyPairB.public, keyPairC.public) + ) + } + + make(arrayOf(inheritedTrustAttachment).map { storage.openAttachment(it)!! }) + assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) { + make(arrayOf(untrustedAttachment).map { storage.openAttachment(it)!! }) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 857d90c3cb..6a19308d4a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -184,10 +184,9 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) { // For now we don't check whether the attached network parameters match the current ones. } -private data class AttachmentAttributeKey(val signers: List, val contractClasses: List?) - -// A cache for caching whether a particular attachment ID is trusted -private val attachmentTrustedCache: MutableMap = createSimpleCache(100).toSynchronised() +// A cache for caching whether a particular set of signers are trusted +private val trustedKeysCache: MutableMap = + createSimpleCache(100).toSynchronised() /** * Establishes whether an attachment should be trusted. This logic is required in order to verify transactions, as transaction @@ -195,8 +194,8 @@ private val attachmentTrustedCache: MutableMap * * Attachments are trusted if one of the following is true: * - They are uploaded by a trusted uploader - * - There is another attachment in the attachment store signed by the same keys (and with the same contract classes if the attachment is a - * contract attachment) that is trusted + * - There is another attachment in the attachment store, that is trusted and is signed by at least one key that the input + * attachment is also signed with */ fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Boolean { val trustedByUploader = when (attachment) { @@ -208,22 +207,14 @@ fun isAttachmentTrusted(attachment: Attachment, service: AttachmentStorage?): Bo if (trustedByUploader) return true return if (service != null && attachment.signerKeys.isNotEmpty()) { - val signers = attachment.signerKeys - val contractClasses = if (attachment is ContractAttachment) { - attachment.allContracts.toList() - } else { - null - } - val key = AttachmentAttributeKey(signers, contractClasses) - - attachmentTrustedCache.computeIfAbsent(key) { - val contractClassCondition = it.contractClasses?.let { classes -> Builder.equal(classes) } - val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria( - contractClassNamesCondition = contractClassCondition, - signersCondition = Builder.equal(signers), + attachment.signerKeys.any { signer -> + trustedKeysCache.computeIfAbsent(signer) { + val queryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(signer)), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS) - ) - service.queryAttachments(queryCriteria).isNotEmpty() + ) + service.queryAttachments(queryCriteria).isNotEmpty() + } } } else { false diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index a551dee24f..5881c30351 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -14,10 +14,8 @@ import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.AttachmentQueryCriteria.AttachmentsQueryCriteria -import net.corda.core.node.services.vault.AttachmentSort -import net.corda.core.node.services.vault.Builder -import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.getOrThrow import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.exceptions.DuplicateAttachmentException @@ -801,7 +799,7 @@ class NodeAttachmentServiceTest { } @Test - fun `jar not trusted if same key but different contract`() { + fun `jar trusted if same key but different contract`() { SelfCleaningDir().use { file -> val alias = "testAlias" val password = "testPassword" @@ -817,7 +815,114 @@ class NodeAttachmentServiceTest { // Sanity check. assertEquals(key1, key2, "Different public keys used to sign jars") assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted") - assertFalse(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should not be trusted") + assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted") + } + } + + @Test + fun `jar trusted if the signing keys are a subset of an existing trusted jar's signers`() { + SelfCleaningDir().use { file -> + val alias = "testAlias" + val password = "testPassword" + val alias2 = "anotherTestAlias" + file.path.generateKey(alias, password) + file.path.generateKey(alias2, password) + + val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract") + file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password) + file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password) + + val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2) + file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password) + + val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") } + val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") } + + assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted") + assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted") + } + } + + @Test + fun `jar trusted if the signing keys are an intersection of an existing trusted jar's signers`() { + SelfCleaningDir().use { file -> + val alias = "testAlias" + val password = "testPassword" + val alias2 = "anotherTestAlias" + val alias3 = "yetAnotherTestAlias" + file.path.generateKey(alias, password) + file.path.generateKey(alias2, password) + file.path.generateKey(alias3, password) + + val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract") + file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password) + file.path.signJar(jarV1.toAbsolutePath().toString(), alias2, password) + + val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2) + file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password) + file.path.signJar(jarV2.toAbsolutePath().toString(), alias3, password) + + val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") } + val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") } + + assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted") + assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted") + } + } + + @Test + fun `jar trusted if the signing keys are a superset of an existing trusted jar's signers`() { + SelfCleaningDir().use { file -> + val alias = "testAlias" + val password = "testPassword" + val alias2 = "anotherTestAlias" + file.path.generateKey(alias, password) + file.path.generateKey(alias2, password) + + val jarV1 = makeTestContractJar(file.path, "foo.bar.DummyContract") + file.path.signJar(jarV1.toAbsolutePath().toString(), alias, password) + + val jarV2 = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2) + file.path.signJar(jarV2.toAbsolutePath().toString(), alias, password) + file.path.signJar(jarV2.toAbsolutePath().toString(), alias2, password) + + val v1Id = jarV1.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") } + val v2Id = jarV2.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") } + + assertTrue(isAttachmentTrusted(storage.openAttachment(v1Id)!!, storage), "Initial contract $v1Id should be trusted") + assertTrue(isAttachmentTrusted(storage.openAttachment(v2Id)!!, storage), "Upgraded contract $v2Id should be trusted") + } + } + + @Test + fun `jar with inherited trust does not grant trust to other jars (no chain of trust)`() { + SelfCleaningDir().use { file -> + val aliasA = "Daredevil" + val aliasB = "The Punisher" + val aliasC = "Jessica Jones" + val password = "i am a netflix series" + file.path.generateKey(aliasA, password) + file.path.generateKey(aliasB, password) + file.path.generateKey(aliasC, password) + + val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract") + file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password) + + val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract", version = 2) + file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password) + file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password) + + val jarSignedByBC = makeTestContractJar(file.path, "foo.bar.AnotherContract", version = 2) + file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password) + file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password) + + val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "dummy-contract.jar") } + val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") } + val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "untrusted", "dummy-contract.jar") } + + assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentA)!!, storage), "Contract $attachmentA should be trusted") + assertTrue(isAttachmentTrusted(storage.openAttachment(attachmentB)!!, storage), "Contract $attachmentB should inherit trust") + assertFalse(isAttachmentTrusted(storage.openAttachment(attachmentC)!!, storage), "Contract $attachmentC should not be trusted (no chain of trust)") } } @@ -940,6 +1045,147 @@ class NodeAttachmentServiceTest { } } + @Test + fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() { + SelfCleaningDir().use { file -> + val aliasA = "Luke Skywalker" + val aliasB = "Han Solo" + val aliasC = "Chewbacca" + val aliasD = "Princess Leia" + val password = "may the force be with you" + file.path.generateKey(aliasA, password) + file.path.generateKey(aliasB, password) + file.path.generateKey(aliasC, password) + file.path.generateKey(aliasD, password) + + val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract") + val keyA = file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password) + + val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract") + file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password) + val keyB = file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password) + + val jarSignedByABC = makeTestContractJar(file.path, "foo.bar.ExpensiveContract") + file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasA, password) + file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasB, password) + val keyC = file.path.signJar(jarSignedByABC.toAbsolutePath().toString(), aliasC, password) + + val jarSignedByABCD = makeTestContractJar(file.path, "foo.bar.DidNotReadThisContract") + file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasA, password) + file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasB, password) + file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasC, password) + val keyD = file.path.signJar(jarSignedByABCD.toAbsolutePath().toString(), aliasD, password) + + val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") } + val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "app", "B.jar") } + val attachmentC = jarSignedByABC.read { storage.privilegedImportAttachment(it, "app", "C.jar") } + val attachmentD = jarSignedByABCD.read { storage.privilegedImportAttachment(it, "app", "D.jar") } + + assertEquals( + listOf(attachmentA, attachmentB, attachmentC, attachmentD), + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyA)) + ) + ) + ) + + assertEquals( + listOf(attachmentB, attachmentC, attachmentD), + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyB)) + ) + ) + ) + + assertEquals( + listOf(attachmentC, attachmentD), + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyC)) + ) + ) + ) + + assertEquals( + listOf(attachmentD), + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyD)) + ) + ) + ) + } + } + + @Test + fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing multiple public keys`() { + SelfCleaningDir().use { file -> + val aliasA = "Ironman" + val aliasB = "Captain America" + val aliasC = "Blackwidow" + val aliasD = "Thor" + val password = "avengers, assemble!" + file.path.generateKey(aliasA, password) + file.path.generateKey(aliasB, password) + file.path.generateKey(aliasC, password) + file.path.generateKey(aliasD, password) + + val jarSignedByA = makeTestContractJar(file.path, "foo.bar.DummyContract") + val keyA = file.path.signJar(jarSignedByA.toAbsolutePath().toString(), aliasA, password) + + val jarSignedByAB = makeTestContractJar(file.path, "foo.bar.DifferentContract") + file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasA, password) + val keyB = file.path.signJar(jarSignedByAB.toAbsolutePath().toString(), aliasB, password) + + val jarSignedByBC = makeTestContractJar(file.path, "foo.bar.ExpensiveContract") + file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasB, password) + val keyC = file.path.signJar(jarSignedByBC.toAbsolutePath().toString(), aliasC, password) + + val jarSignedByCD = makeTestContractJar(file.path, "foo.bar.DidNotReadThisContract") + file.path.signJar(jarSignedByCD.toAbsolutePath().toString(), aliasC, password) + val keyD = file.path.signJar(jarSignedByCD.toAbsolutePath().toString(), aliasD, password) + + val attachmentA = jarSignedByA.read { storage.privilegedImportAttachment(it, "app", "A.jar") } + val attachmentB = jarSignedByAB.read { storage.privilegedImportAttachment(it, "app", "B.jar") } + val attachmentC = jarSignedByBC.read { storage.privilegedImportAttachment(it, "app", "C.jar") } + val attachmentD = jarSignedByCD.read { storage.privilegedImportAttachment(it, "app", "D.jar") } + + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyA, keyC)) + ) + ).let { result -> + assertEquals(4, result.size) + assertEquals( + listOf(attachmentA, attachmentB, attachmentC, attachmentD), + result + ) + } + + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyA, keyB)) + ) + ).let { result -> + // made a [Set] due to [NodeAttachmentService.queryAttachments] not returning distinct results + assertEquals(3, result.toSet().size) + assertEquals(setOf(attachmentA, attachmentB, attachmentC), result.toSet()) + } + + storage.queryAttachments( + AttachmentsQueryCriteria( + signersCondition = Builder.equal(listOf(keyB, keyC, keyD)) + ) + ).let { result -> + // made a [Set] due to [NodeAttachmentService.queryAttachments] not returning distinct results + assertEquals(3, result.toSet().size) + assertEquals(setOf(attachmentB, attachmentC, attachmentD), result.toSet()) + } + } + } + // Not the real FetchAttachmentsFlow! private class FetchAttachmentsFlow : FlowLogic() { @Suspendable diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 466cb33766..ccda6d00b7 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -47,7 +47,16 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { // and not all predicate types are covered here. private fun criteriaFilter(metadata: C, predicate: ColumnPredicate?): Boolean { return when (predicate) { - is ColumnPredicate.EqualityComparison -> predicate.rightLiteral == metadata + // real implementation allows an intersection of signers to return results from the query + is ColumnPredicate.EqualityComparison -> { + val rightLiteral = predicate.rightLiteral + when (rightLiteral) { + is Collection<*> -> rightLiteral.any { value -> + (metadata as Collection<*>).contains(value) + } + else -> rightLiteral == metadata + } + } is ColumnPredicate.CollectionExpression -> predicate.rightLiteral.contains(metadata) else -> true }