-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-6319: Quote strings stored in JSON configs #4303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| // 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 } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have endpoint identification enabled anywhere?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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"""")) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch, maybe my suggestion was bad in this line.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit unclear to be honest. Both options seem hard to read. :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
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.