Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions clients/src/test/java/org/apache/kafka/test/TestSslUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ public static Map<String, Object> createSslConfig(boolean useClientCert, boolea
}

public static Map<String, Object> createSslConfig(boolean useClientCert, boolean trustStore,
Mode mode, File trustStoreFile, String certAlias, String hostName)
Mode mode, File trustStoreFile, String certAlias, String cn)
throws IOException, GeneralSecurityException {
return createSslConfig(useClientCert, trustStore, mode, trustStoreFile, certAlias, hostName, new CertificateBuilder());
return createSslConfig(useClientCert, trustStore, mode, trustStoreFile, certAlias, cn, new CertificateBuilder());
}

public static Map<String, Object> createSslConfig(boolean useClientCert, boolean trustStore,
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/scala/kafka/utils/Json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ object Json {
*/
def parseFull(input: String): Option[JsonValue] =
try Option(mapper.readTree(input)).map(JsonValue(_))
catch { case _: JsonProcessingException => None }
catch {
case _: JsonProcessingException =>
// Before 1.0.1, Json#encode did not escape backslash or any other special characters. SSL principals
// stored in ACLs may contain backslash as an escape char, making the JSON generated in 1.0 invalid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to replace 1.0 as well.

// Escape backslash and retry to handle these strings which may have been persisted in ZK.
// Note that this does not handle all special characters (e.g. non-escaped double quotes are not supported)
val escapedInput = input.replaceAll("\\\\", "\\\\\\\\")
try Option(mapper.readTree(escapedInput)).map(JsonValue(_))
catch { case _: JsonProcessingException => None }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we file a JIRA to move this so that it only applies to ACLs in trunk (for 1.1)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/**
* Parse a JSON byte array into a JsonValue if possible. `None` is returned if `input` is not valid JSON.
Expand All @@ -56,7 +65,7 @@ object Json {
obj match {
case null => "null"
case b: Boolean => b.toString
case s: String => "\"" + s + "\""
case s: String => mapper.writeValueAsString(s)
case n: Number => n.toString
case m: Map[_, _] => "{" +
m.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import java.util.Properties
import org.apache.kafka.clients.producer.KafkaProducer
import kafka.server.KafkaConfig
import kafka.integration.KafkaServerTestHarness
import org.apache.kafka.common.network.ListenerName
import org.apache.kafka.common.network.{ListenerName, Mode}
import org.junit.{After, Before}

import scala.collection.mutable.Buffer
Expand Down Expand Up @@ -69,8 +69,8 @@ abstract class IntegrationTestHarness extends KafkaServerTestHarness {

@Before
override def setUp() {
val producerSecurityProps = TestUtils.producerSecurityConfigs(securityProtocol, trustStoreFile, clientSaslProperties)
val consumerSecurityProps = TestUtils.consumerSecurityConfigs(securityProtocol, trustStoreFile, clientSaslProperties)
val producerSecurityProps = clientSecurityProps("producer")
val consumerSecurityProps = clientSecurityProps("consumer")
super.setUp()
producerConfig.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[org.apache.kafka.common.serialization.ByteArraySerializer])
producerConfig.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[org.apache.kafka.common.serialization.ByteArraySerializer])
Expand All @@ -87,6 +87,11 @@ abstract class IntegrationTestHarness extends KafkaServerTestHarness {
TestUtils.createOffsetsTopic(zkUtils, servers)
}

def clientSecurityProps(certAlias: String): Properties = {
TestUtils.securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, certAlias, TestUtils.SslCertificateCn,
clientSaslProperties)
}

def createNewProducer: KafkaProducer[Array[Byte], Array[Byte]] = {
TestUtils.createNewProducer(brokerList,
securityProtocol = this.securityProtocol,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,29 @@

package kafka.api

import java.util.Properties

import kafka.utils.TestUtils
import org.apache.kafka.common.config.SslConfigs
import org.apache.kafka.common.config.internals.BrokerSecurityConfigs
import org.apache.kafka.common.network.Mode
import org.apache.kafka.common.security.auth._
import org.junit.Before

object SslEndToEndAuthorizationTest {
class TestPrincipalBuilder extends KafkaPrincipalBuilder {
private val Pattern = "O=A (.*?),CN=localhost".r
private val Pattern = "O=A (.*?),CN=(.*?)".r

// Use full DN as client principal to test special characters in principal
// Use field from DN as server principal to test custom PrincipalBuilder
override def build(context: AuthenticationContext): KafkaPrincipal = {
context match {
case ctx: SslAuthenticationContext =>
ctx.session.getPeerPrincipal.getName match {
case Pattern(name) =>
new KafkaPrincipal(KafkaPrincipal.USER_TYPE, name)
val peerPrincipal = ctx.session.getPeerPrincipal.getName
peerPrincipal match {
case Pattern(name, _) =>
val principal = if (name == "server") name else peerPrincipal
new KafkaPrincipal(KafkaPrincipal.USER_TYPE, principal)
case _ =>
KafkaPrincipal.ANONYMOUS
}
Expand All @@ -40,18 +49,32 @@ object SslEndToEndAuthorizationTest {
}

class SslEndToEndAuthorizationTest extends EndToEndAuthorizationTest {

import kafka.api.SslEndToEndAuthorizationTest.TestPrincipalBuilder

override protected def securityProtocol = SecurityProtocol.SSL

this.serverConfig.setProperty(BrokerSecurityConfigs.SSL_CLIENT_AUTH_CONFIG, "required")
this.serverConfig.setProperty(BrokerSecurityConfigs.PRINCIPAL_BUILDER_CLASS_CONFIG, classOf[TestPrincipalBuilder].getName)
override val clientPrincipal = "client"
// Escaped characters in DN attribute values: from http://www.ietf.org/rfc/rfc2253.txt
// - a space or "#" character occurring at the beginning of the string
// - a space character occurring at the end of the string
// - one of the characters ",", "+", """, "\", "<", ">" or ";"
//
// Leading and trailing spaces in Kafka principal dont work with ACLs, but we can workaround by using
// a PrincipalBuilder that removes/replaces them.
private val clientCn = """\#A client with special chars in CN : (\, \+ \" \\ \< \> \; ')"""
override val clientPrincipal = s"O=A client,CN=$clientCn"
override val kafkaPrincipal = "server"

@Before
override def setUp() {
startSasl(jaasSections(List.empty, None, ZkSasl))
super.setUp()
}

override def clientSecurityProps(certAlias: String): Properties = {
val props = TestUtils.securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, certAlias, clientCn, clientSaslProperties)
props.remove(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have endpoint identification enabled anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but since we may decide to make HTTPS the default value and this test would fail with endpoint validation enabled, I thought it was better to explicitly remove it.

props
}
}
4 changes: 3 additions & 1 deletion core/src/test/scala/unit/kafka/admin/AclCommandTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import org.junit.Test

class AclCommandTest extends ZooKeeperTestHarness with Logging {

private val Users = Set(KafkaPrincipal.fromString("User:CN=writeuser,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown"), KafkaPrincipal.fromString("User:test2"))
private val Users = Set(KafkaPrincipal.fromString("User:CN=writeuser,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown"),
KafkaPrincipal.fromString("User:test2"),
KafkaPrincipal.fromString("""User:CN=\#User with special chars in CN : (\, \+ \" \\ \< \> \; ')"""))
private val Hosts = Set("host1", "host2")
private val AllowHostCommand = Array("--allow-host", "host1", "--allow-host", "host2")
private val DenyHostCommand = Array("--deny-host", "host1", "--deny-host", "host2")
Expand Down
14 changes: 14 additions & 0 deletions core/src/test/scala/unit/kafka/utils/JsonTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import org.junit.Test
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node._
import kafka.utils.json.JsonValue

import scala.collection.JavaConverters._
import scala.collection.Map

class JsonTest {

Expand All @@ -42,6 +44,16 @@ class JsonTest {
val arrayNode = new ArrayNode(jnf)
Vector(1, 2, 3).map(new IntNode(_)).foreach(arrayNode.add)
assertEquals(Json.parseFull("[1, 2, 3]"), Some(JsonValue(arrayNode)))

// Test with encoder that properly escapes backslash and quotes
val map = Map("foo1" -> """bar1\,bar2""", "foo2" -> """\bar""")
val encoded = Json.encode(map)
val decoded = Json.parseFull(encoded)
assertEquals(Json.parseFull("""{"foo1":"bar1\\,bar2", "foo2":"\\bar"}"""), decoded)

// Test strings with non-escaped backslash and quotes. This is to verify that ACLs
// containing non-escaped chars persisted using 1.0 can be parsed.
assertEquals(decoded, Json.parseFull("""{"foo1":"bar1\,bar2", "foo2":"\bar"}"""))
}

@Test
Expand All @@ -61,6 +73,8 @@ class JsonTest {
assertEquals("{}", Json.encode(Map()))
assertEquals("{\"a\":1,\"b\":2}", Json.encode(Map("a" -> 1, "b" -> 2)))
assertEquals("{\"a\":[1,2],\"c\":[3,4]}", Json.encode(Map("a" -> Seq(1,2), "c" -> Seq(3,4))))
assertEquals(""""str1\\,str2"""", Json.encode("""str1\,str2"""))
assertEquals(""""\"quoted\""""", Json.encode(""""quoted""""))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, maybe my suggestion was bad in this line.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit unclear to be honest. Both options seem hard to read. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall I leave it as is or do you want me to change it back - I don't mind either way.

}

}
18 changes: 11 additions & 7 deletions core/src/test/scala/unit/kafka/utils/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ object TestUtils extends Logging {
val MockZkPort = 1
/** ZooKeeper connection string to use for unit tests that mock/don't require a real ZK server. */
val MockZkConnect = "127.0.0.1:" + MockZkPort
// CN in SSL certificates - this is used for endpoint validation when enabled
val SslCertificateCn = "localhost"

private val transactionStatusKey = "transactionStatus"
private val committedValue : Array[Byte] = "committed".getBytes(StandardCharsets.UTF_8)
Expand Down Expand Up @@ -519,14 +521,15 @@ object TestUtils extends Logging {
new Producer[K, V](new kafka.producer.ProducerConfig(props))
}

private def securityConfigs(mode: Mode,
def securityConfigs(mode: Mode,
securityProtocol: SecurityProtocol,
trustStoreFile: Option[File],
certAlias: String,
certCn: String,
saslProperties: Option[Properties]): Properties = {
val props = new Properties
if (usesSslTransportLayer(securityProtocol))
props ++= sslConfigs(mode, securityProtocol == SecurityProtocol.SSL, trustStoreFile, certAlias)
props ++= sslConfigs(mode, securityProtocol == SecurityProtocol.SSL, trustStoreFile, certAlias, certCn)

if (usesSaslAuthentication(securityProtocol))
props ++= JaasTestUtils.saslConfigs(saslProperties)
Expand All @@ -535,7 +538,7 @@ object TestUtils extends Logging {
}

def producerSecurityConfigs(securityProtocol: SecurityProtocol, trustStoreFile: Option[File], saslProperties: Option[Properties]): Properties =
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "producer", saslProperties)
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "producer", SslCertificateCn, saslProperties)

/**
* Create a (new) producer with a few pre-configured properties.
Expand Down Expand Up @@ -596,10 +599,10 @@ object TestUtils extends Logging {
}

def consumerSecurityConfigs(securityProtocol: SecurityProtocol, trustStoreFile: Option[File], saslProperties: Option[Properties]): Properties =
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "consumer", saslProperties)
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "consumer", SslCertificateCn, saslProperties)

def adminClientSecurityConfigs(securityProtocol: SecurityProtocol, trustStoreFile: Option[File], saslProperties: Option[Properties]): Properties =
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "admin-client", saslProperties)
securityConfigs(Mode.CLIENT, securityProtocol, trustStoreFile, "admin-client", SslCertificateCn, saslProperties)

/**
* Create a new consumer with a few pre-configured properties.
Expand Down Expand Up @@ -1220,12 +1223,13 @@ object TestUtils extends Logging {
copy
}

def sslConfigs(mode: Mode, clientCert: Boolean, trustStoreFile: Option[File], certAlias: String): Properties = {
def sslConfigs(mode: Mode, clientCert: Boolean, trustStoreFile: Option[File], certAlias: String,
certCn: String = SslCertificateCn): Properties = {
val trustStore = trustStoreFile.getOrElse {
throw new Exception("SSL enabled but no trustStoreFile provided")
}

val sslConfigs = TestSslUtils.createSslConfig(clientCert, true, mode, trustStore, certAlias)
val sslConfigs = TestSslUtils.createSslConfig(clientCert, true, mode, trustStore, certAlias, certCn)

val sslProps = new Properties()
sslConfigs.asScala.foreach { case (k, v) => sslProps.put(k, v) }
Expand Down