legal name validator for doorman node registration (#532)

This commit is contained in:
Patrick Kuo
2017-04-24 21:16:00 +08:00
committed by GitHub
parent 9425b7c927
commit 87dd99d968
3 changed files with 235 additions and 1 deletions

View File

@ -0,0 +1,101 @@
@file:JvmName("LegalNameValidator")
package net.corda.core.utilities
import java.lang.Character.UnicodeScript.*
import java.text.Normalizer
import java.util.regex.Pattern
import javax.security.auth.x500.X500Principal
/**
* The validation function will validate the input string using the following rules:
* - No blacklisted words like "node", "server".
* - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks.
* - Should start with a capital letter.
* - No commas or equals signs.
* - No dollars or quote marks, we might need to relax the quote mark constraint in future to handle Irish company names.
*/
fun validateLegalName(normalizedLegalName: String) {
rules.forEach { it.validate(normalizedLegalName) }
}
/**
* The normalize function will trim the input string, replace any multiple spaces with a single space,
* and normalize the string according to NFKC normalization form.
*/
fun normaliseLegalName(legalName: String): String {
val trimmedLegalName = legalName.trim().replace(Regex("\\s+"), " ")
return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC)
}
private val rules: List<Rule<String>> = listOf(
UnicodeNormalizationRule(),
CharacterRule(',', '=', '$', '"', '\'', '\\'),
WordRule("node", "server"),
LengthRule(maxLength = 255),
// TODO: Implement confusable character detection if we add more scripts.
UnicodeRangeRule(LATIN, COMMON, INHERITED),
CapitalLetterRule(),
X500NameRule()
)
private class UnicodeNormalizationRule : Rule<String> {
override fun validate(legalName: String) {
require(legalName == normaliseLegalName(legalName)) { "Legal name must be normalized. Please use 'normaliseLegalName' to normalize the legal name before validation." }
}
}
private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : Rule<String> {
private val pattern = supportScripts.map { "\\p{Is$it}" }.joinToString(separator = "", prefix = "[", postfix = "]*").let { Pattern.compile(it) }
override fun validate(legalName: String) {
require(pattern.matcher(legalName).matches()) {
val illegalChars = legalName.replace(pattern.toRegex(), "").toSet()
if (illegalChars.size > 1) {
"Illegal characters $illegalChars in \"$legalName\"."
} else {
"Illegal character $illegalChars in \"$legalName\"."
}
}
}
}
private class CharacterRule(vararg val bannedChars: Char) : Rule<String> {
override fun validate(legalName: String) {
bannedChars.forEach {
require(!legalName.contains(it, true)) { "Illegal character: $it" }
}
}
}
private class WordRule(vararg val bannedWords: String) : Rule<String> {
override fun validate(legalName: String) {
bannedWords.forEach {
require(!legalName.contains(it, ignoreCase = true)) { "Illegal word: $it" }
}
}
}
private class LengthRule(val maxLength: Int) : Rule<String> {
override fun validate(legalName: String) {
require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." }
}
}
private class CapitalLetterRule : Rule<String> {
override fun validate(legalName: String) {
val capitalizedLegalName = legalName.split(" ").map(String::capitalize).joinToString(" ")
require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" }
}
}
private class X500NameRule : Rule<String> {
override fun validate(legalName: String) {
// This will throw IllegalArgumentException if the name does not comply with X500 name format.
X500Principal("CN=$legalName")
}
}
private interface Rule<in T> {
fun validate(legalName: T)
}

View File

@ -0,0 +1,107 @@
package net.corda.core.utilities
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class LegalNameValidatorTest {
@Test
fun `no double spaces`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test Legal Name")
}
validateLegalName(normaliseLegalName("Test Legal Name"))
}
@Test
fun `no trailing white space`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test ")
}
}
@Test
fun `no prefixed white space`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName(" Test")
}
}
@Test
fun `blacklisted words`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test Server")
}
}
@Test
fun `blacklisted characters`() {
validateLegalName("Test")
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\$Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\"Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\'Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("=Test")
}
}
@Test
fun `unicode range`() {
validateLegalName("Test A")
assertFailsWith(IllegalArgumentException::class) {
// Greek letter A.
validateLegalName("Test Α")
}
}
@Test
fun `legal name length less then 256 characters`() {
val longLegalName = StringBuilder()
while (longLegalName.length < 255) {
longLegalName.append("A")
}
validateLegalName(longLegalName.toString())
assertFailsWith(IllegalArgumentException::class) {
validateLegalName(longLegalName.append("A").toString())
}
}
@Test
fun `legal name should be capitalized`() {
validateLegalName("Good Legal Name")
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("bad name")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Bad name")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("bad Name")
}
}
@Test
fun `correctly handle whitespaces`() {
assertEquals("Legal Name With Tab", normaliseLegalName("Legal Name With\tTab"))
assertEquals("Legal Name With Unicode Whitespaces", normaliseLegalName("Legal Name\u2004With\u0009Unicode\u0020Whitespaces"))
assertEquals("Legal Name With Line Breaks", normaliseLegalName("Legal Name With\n\rLine\nBreaks"))
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name With\tTab")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name\u2004With\u0009Unicode\u0020Whitespaces")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name With\n\rLine\nBreaks")
}
}
}