mirror of
https://github.com/corda/corda.git
synced 2025-03-11 06:54:04 +00:00
Support fuzzy matching for identities.
Matching can be done with case insensitive substrings in the identity service, RPC and shell. In future cleverer matching should be possible, e.g. using Lucene or RDBMS free text search features.
This commit is contained in:
parent
a86c5ba2dd
commit
ccf43a8e17
@ -40,6 +40,7 @@ object JacksonSupport {
|
||||
fun partyFromName(partyName: String): Party?
|
||||
fun partyFromX500Name(name: X500Name): Party?
|
||||
fun partyFromKey(owningKey: PublicKey): Party?
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@ -47,6 +48,7 @@ object JacksonSupport {
|
||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||
override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean) = rpc.partiesFromName(query, exactMatch)
|
||||
}
|
||||
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@ -54,6 +56,7 @@ object JacksonSupport {
|
||||
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||
override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean) = identityService.partiesFromName(query, exactMatch)
|
||||
}
|
||||
|
||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@ -61,6 +64,7 @@ object JacksonSupport {
|
||||
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
val cordaModule: Module by lazy {
|
||||
@ -169,14 +173,21 @@ object JacksonSupport {
|
||||
// how to parse the content
|
||||
return if (parser.text.contains("=")) {
|
||||
val principal = X500Name(parser.text)
|
||||
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
|
||||
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||
} else {
|
||||
val key = try {
|
||||
parsePublicKeyBase58(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key")
|
||||
val nameMatches = mapper.partiesFromName(parser.text, false)
|
||||
if (nameMatches.isEmpty()) {
|
||||
val key = try {
|
||||
parsePublicKeyBase58(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key")
|
||||
}
|
||||
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
|
||||
} else if (nameMatches.size == 1) {
|
||||
nameMatches.first()
|
||||
} else {
|
||||
throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ..."))
|
||||
}
|
||||
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +244,16 @@ interface CordaRPCOps : RPCOps {
|
||||
*/
|
||||
fun partyFromX500Name(x500Name: X500Name): Party?
|
||||
|
||||
/**
|
||||
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
||||
* get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results
|
||||
* but rather show them via a user interface and let the user pick the one they wanted.
|
||||
*
|
||||
* @param query The string to check against the X.500 name components
|
||||
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
|
||||
*/
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
|
||||
/** Enumerates the class names of the flows that this node knows about. */
|
||||
fun registeredFlows(): List<String>
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ interface IdentityService {
|
||||
// but for now this is not supported.
|
||||
|
||||
fun partyFromKey(key: PublicKey): Party?
|
||||
@Deprecated("Use partyFromX500Name")
|
||||
@Deprecated("Use partyFromX500Name or partiesFromName")
|
||||
fun partyFromName(name: String): Party?
|
||||
fun partyFromX500Name(principal: X500Name): Party?
|
||||
|
||||
@ -98,5 +98,15 @@ interface IdentityService {
|
||||
*/
|
||||
fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath?
|
||||
|
||||
/**
|
||||
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
||||
* get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results
|
||||
* but rather show them via a user interface and let the user pick the one they wanted.
|
||||
*
|
||||
* @param query The string to check against the X.500 name components
|
||||
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
|
||||
*/
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
}
|
||||
|
@ -7,8 +7,11 @@ from the previous milestone release.
|
||||
UNRELEASED
|
||||
----------
|
||||
|
||||
Milestone 12.0 - First Public Beta
|
||||
----------------------------------
|
||||
* A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to
|
||||
an unambiguous identity by searching the network map.
|
||||
|
||||
Milestone 12
|
||||
------------
|
||||
|
||||
* Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps:
|
||||
|
||||
@ -97,6 +100,7 @@ Milestone 12.0 - First Public Beta
|
||||
* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain.
|
||||
* The corda node will now be issued a restricted client CA for identity/transaction key signing.
|
||||
* TLS certificate are now stored in `sslkeystore.jks` and identity keys are stored in `nodekeystore.jks`
|
||||
|
||||
.. warning:: The old keystore will need to be removed when upgrading to this version.
|
||||
|
||||
Milestone 11.1
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
@ -174,7 +175,8 @@ class CordaRPCOpsImpl(
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use partyFromX500Name instead")
|
||||
override fun partyFromName(name: String) = services.identityService.partyFromName(name)
|
||||
override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name)
|
||||
override fun partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name)
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> = services.identityService.partiesFromName(query, exactMatch)
|
||||
|
||||
override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()
|
||||
|
||||
|
@ -4,7 +4,10 @@ import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.subject
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.loggerFor
|
||||
@ -18,6 +21,7 @@ import java.security.cert.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Simple identity service which caches parties and provides functionality for efficient lookup.
|
||||
@ -95,6 +99,24 @@ class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
|
||||
return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}")
|
||||
}
|
||||
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
||||
val results = HashSet<Party>()
|
||||
for ((x500name, partyAndCertificate) in principalToParties) {
|
||||
val party = partyAndCertificate.party
|
||||
for (rdn in x500name.rdNs) {
|
||||
val component = rdn.first.value.toString()
|
||||
if (exactMatch && component == query) {
|
||||
results += party
|
||||
} else if (!exactMatch) {
|
||||
// We can imagine this being a query over a lucene index in future.
|
||||
if (component.contains(query, ignoreCase = true))
|
||||
results += party
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
@Throws(IdentityService.UnknownAnonymousPartyException::class)
|
||||
override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) {
|
||||
val path = partyToPath[anonymousParty] ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}")
|
||||
|
@ -11,6 +11,8 @@ import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
@ -53,6 +55,18 @@ class InMemoryIdentityServiceTests {
|
||||
assertNull(service.partyFromX500Name(ALICE.name))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get identity by substring match`() {
|
||||
val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate)
|
||||
service.registerIdentity(ALICE_IDENTITY)
|
||||
service.registerIdentity(BOB_IDENTITY)
|
||||
val (_, _, alicente) = createParty(X500Name("O=Alicente Worldwide,L=London,C=UK"))
|
||||
service.registerIdentity(alicente)
|
||||
assertEquals(setOf(ALICE, alicente.party), service.partiesFromName("Alice", false))
|
||||
assertEquals(setOf(ALICE), service.partiesFromName("Alice Corp", true))
|
||||
assertEquals(setOf(BOB), service.partiesFromName("Bob Plc", true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get identity by name`() {
|
||||
val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate)
|
||||
@ -87,12 +101,7 @@ class InMemoryIdentityServiceTests {
|
||||
*/
|
||||
@Test
|
||||
fun `assert ownership`() {
|
||||
val aliceRootKey = Crypto.generateKeyPair()
|
||||
val aliceRootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, aliceRootKey)
|
||||
val aliceTxKey = Crypto.generateKeyPair()
|
||||
val aliceTxCert = X509Utilities.createCertificate(CertificateType.IDENTITY, aliceRootCert, aliceRootKey, ALICE.name, aliceTxKey.public)
|
||||
val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCert, aliceTxCert, revocationEnabled = false)
|
||||
val alice = PartyAndCertificate(ALICE.name, aliceRootKey.public, aliceRootCert, aliceCertPath)
|
||||
val (aliceTxKey, aliceCertPath, alice) = createParty(ALICE.name)
|
||||
|
||||
val bobRootKey = Crypto.generateKeyPair()
|
||||
val bobRootCert = X509Utilities.createSelfSignedCACertificate(BOB.name, bobRootKey)
|
||||
@ -120,6 +129,15 @@ class InMemoryIdentityServiceTests {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createParty(x500Name: X500Name): Triple<KeyPair, CertPath, PartyAndCertificate> {
|
||||
val rootKey = Crypto.generateKeyPair()
|
||||
val rootCert = X509Utilities.createSelfSignedCACertificate(x500Name, rootKey)
|
||||
val txKey = Crypto.generateKeyPair()
|
||||
val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, rootCert, rootKey, x500Name, txKey.public)
|
||||
val certPath = X509Utilities.createCertificatePath(rootCert, txCert, revocationEnabled = false)
|
||||
return Triple(txKey, certPath, PartyAndCertificate(x500Name, rootKey.public, rootCert, certPath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure if we feed in a full identity, we get the same identity back.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user