-
Notifications
You must be signed in to change notification settings - Fork 15.2k
KAFKA-2512: Add version check to broker and clients. #200
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 all 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 |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE | ||
| * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file | ||
| * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the | ||
| * License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations under the License. | ||
| */ | ||
| package org.apache.kafka.common.errors; | ||
|
|
||
| /** | ||
| * The version is not supported | ||
| */ | ||
| public class UnsupportedVersionException extends ApiException { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| public UnsupportedVersionException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| public UnsupportedVersionException(String message, Throwable throwable) { | ||
| super(message, throwable); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,7 +90,9 @@ public enum Errors { | |
| new ApiException("The committing offset data size is not valid")), | ||
| AUTHORIZATION_FAILED(29, new ApiException("Request is not authorized.")), | ||
| REBALANCE_IN_PROGRESS(30, | ||
| new ApiException("The group is rebalancing, so a rejoin is needed.")); | ||
| new ApiException("The group is rebalancing, so a rejoin is needed.")), | ||
| UNSUPPORTED_VERSION(31, | ||
|
Contributor
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. Note that 31 and 32 are already taken, you might want to rebase. |
||
| new UnsupportedVersionException("The version is not supported.")); | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(Errors.class); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
|
|
||
| import java.nio.ByteBuffer; | ||
|
|
||
| import org.apache.kafka.common.errors.UnsupportedVersionException; | ||
| import org.apache.kafka.common.protocol.types.Schema; | ||
| import org.apache.kafka.common.protocol.types.Struct; | ||
|
|
||
|
|
@@ -68,4 +69,10 @@ public static Struct parseResponse(int apiKey, int version, ByteBuffer buffer) { | |
| return (Struct) responseSchema(apiKey, version).read(buffer); | ||
| } | ||
|
|
||
| public static void validateApiVersion(int apiKey, int versionId) { | ||
|
Contributor
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. We might want to add a min version check as well, based on #986 changes. |
||
| if (versionId < 0 || latestVersion(apiKey) < versionId) | ||
| throw new UnsupportedVersionException("The version " + versionId + " for " + ApiKeys.forId(apiKey).name + | ||
| " is higher than highest supported version " + ProtoUtils.latestVersion(apiKey)); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import kafka.common.TopicAndPartition | |
| import kafka.message.ByteBufferMessageSet | ||
| import kafka.metrics.KafkaMetricsGroup | ||
| import kafka.utils.{Logging, SystemTime} | ||
| import org.apache.kafka.common.errors.UnsupportedVersionException | ||
| import org.apache.kafka.common.network.Send | ||
| import org.apache.kafka.common.protocol.{ApiKeys, SecurityProtocol} | ||
| import org.apache.kafka.common.requests.{AbstractRequest, RequestHeader} | ||
|
|
@@ -69,8 +70,16 @@ object RequestChannel extends Logging { | |
| } else | ||
| null | ||
| val body: AbstractRequest = | ||
| if (requestObj == null) | ||
| AbstractRequest.getRequest(header.apiKey, header.apiVersion, buffer) | ||
| if (requestObj == null) { | ||
| try { | ||
| // If the request version is higher than supported version, UnsupportedVersionException might be thrown, | ||
|
Contributor
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. If we add check on min version as well based on #986, we should reflect that in the comment here as well. |
||
| // we ignore it here and let KafkaApis to handle it. | ||
| AbstractRequest.getRequest(header.apiKey, header.apiVersion, buffer) | ||
| } catch { | ||
| case e : UnsupportedVersionException => | ||
| null | ||
| } | ||
| } | ||
| else | ||
| null | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,25 +17,39 @@ | |
|
|
||
| package kafka.server | ||
|
|
||
| import kafka.message.MessageSet | ||
| import kafka.security.auth.Topic | ||
| import org.apache.kafka.common.metrics.Metrics | ||
| import org.apache.kafka.common.protocol.SecurityProtocol | ||
| import org.apache.kafka.common.TopicPartition | ||
| import kafka.api._ | ||
| import kafka.admin.AdminUtils | ||
| import kafka.api._ | ||
| import kafka.common._ | ||
| import kafka.controller.KafkaController | ||
| import kafka.coordinator.ConsumerCoordinator | ||
| import kafka.log._ | ||
| import kafka.message.MessageSet | ||
| import kafka.network.RequestChannel.{Response, Session} | ||
| import kafka.network._ | ||
| import kafka.network.RequestChannel.{Session, Response} | ||
| import org.apache.kafka.common.requests.{JoinGroupRequest, JoinGroupResponse, HeartbeatRequest, HeartbeatResponse, ResponseHeader, ResponseSend} | ||
| import kafka.utils.{ZkUtils, ZKGroupTopicDirs, SystemTime, Logging} | ||
| import scala.collection._ | ||
| import kafka.security.auth.{Authorizer, ClusterAction, ConsumerGroup, Create, Describe, Operation, Read, Resource, Topic, Write} | ||
| import kafka.utils.{Logging, SystemTime, ZKGroupTopicDirs, ZkUtils} | ||
| import org.I0Itec.zkclient.ZkClient | ||
| import kafka.security.auth.{Authorizer, Read, Write, Create, ClusterAction, Describe, Resource, Topic, Operation, ConsumerGroup} | ||
| import org.apache.kafka.common.TopicPartition | ||
| import org.apache.kafka.common.metrics.Metrics | ||
| import org.apache.kafka.common.protocol.{ProtoUtils, SecurityProtocol} | ||
| import org.apache.kafka.common.requests.{HeartbeatRequest, HeartbeatResponse, JoinGroupRequest, JoinGroupResponse, ResponseHeader, ResponseSend} | ||
|
|
||
| import scala.collection._ | ||
|
|
||
| object KafkaApis { | ||
| //TODO: this method should only use request.header after all the requests are migrated to use client java request class. | ||
| // For the requests using old scala class, we need to pass in the API version explicitly because the Request.header will | ||
| // be null. For requests using new java class. the API version will be None. | ||
| def validateRequestVersion(request: RequestChannel.Request, apiVersion: Option[Short]) { | ||
|
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 this all be handled in RequestChannel or before the big match statement in KafkaApis so we are guaranteed to have all messages checked?
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. I agree that it would be better if we can do it that way. |
||
| val requestApiVersion = Option(request.header) match { | ||
| //requests using new java classes. | ||
| case Some(header) => header.apiVersion() | ||
| //requests using old scala classes. | ||
| case None => apiVersion.getOrElse(throw new IllegalArgumentException("apiVersion should be defined if request.header is null")) | ||
| } | ||
| ProtoUtils.validateApiVersion(request.requestId, requestApiVersion) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Logic to handle the various Kafka requests | ||
|
|
@@ -103,6 +117,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| // We can't have the ensureTopicExists check here since the controller sends it as an advisory to all brokers so they | ||
| // stop serving data to clients for the topic being deleted | ||
| val leaderAndIsrRequest = request.requestObj.asInstanceOf[LeaderAndIsrRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(leaderAndIsrRequest.versionId)) | ||
|
|
||
| authorizeClusterAction(request) | ||
|
|
||
|
|
@@ -137,6 +152,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| // We can't have the ensureTopicExists check here since the controller sends it as an advisory to all brokers so they | ||
| // stop serving data to clients for the topic being deleted | ||
| val stopReplicaRequest = request.requestObj.asInstanceOf[StopReplicaRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(stopReplicaRequest.versionId)) | ||
|
|
||
| authorizeClusterAction(request) | ||
|
|
||
|
|
@@ -148,6 +164,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
|
|
||
| def handleUpdateMetadataRequest(request: RequestChannel.Request) { | ||
| val updateMetadataRequest = request.requestObj.asInstanceOf[UpdateMetadataRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(updateMetadataRequest.versionId)) | ||
|
|
||
| authorizeClusterAction(request) | ||
|
|
||
|
|
@@ -162,6 +179,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| // We can't have the ensureTopicExists check here since the controller sends it as an advisory to all brokers so they | ||
| // stop serving data to clients for the topic being deleted | ||
| val controlledShutdownRequest = request.requestObj.asInstanceOf[ControlledShutdownRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(controlledShutdownRequest.versionId)) | ||
|
|
||
| authorizeClusterAction(request) | ||
|
|
||
|
|
@@ -177,6 +195,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleOffsetCommitRequest(request: RequestChannel.Request) { | ||
| val offsetCommitRequest = request.requestObj.asInstanceOf[OffsetCommitRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(offsetCommitRequest.versionId)) | ||
|
|
||
| // filter non-exist topics | ||
| val invalidRequestsInfo = offsetCommitRequest.requestInfo.filter { case (topicAndPartition, offsetMetadata) => | ||
|
|
@@ -282,6 +301,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleProducerRequest(request: RequestChannel.Request) { | ||
| val produceRequest = request.requestObj.asInstanceOf[ProducerRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(produceRequest.versionId)) | ||
| val numBytesAppended = produceRequest.sizeInBytes | ||
|
|
||
| val (authorizedRequestInfo, unauthorizedRequestInfo) = produceRequest.data.partition { | ||
|
|
@@ -359,6 +379,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleFetchRequest(request: RequestChannel.Request) { | ||
| val fetchRequest = request.requestObj.asInstanceOf[FetchRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(fetchRequest.versionId)) | ||
|
|
||
| val (authorizedRequestInfo, unauthorizedRequestInfo) = fetchRequest.requestInfo.partition { | ||
| case (topicAndPartition, _) => authorize(request.session, Read, new Resource(Topic, topicAndPartition.topic)) | ||
|
|
@@ -414,6 +435,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleOffsetRequest(request: RequestChannel.Request) { | ||
| val offsetRequest = request.requestObj.asInstanceOf[OffsetRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(offsetRequest.versionId)) | ||
|
|
||
| val (authorizedRequestInfo, unauthorizedRequestInfo) = offsetRequest.requestInfo.partition { | ||
| case (topicAndPartition, _) => authorize(request.session, Describe, new Resource(Topic, topicAndPartition.topic)) | ||
|
|
@@ -566,6 +588,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleTopicMetadataRequest(request: RequestChannel.Request) { | ||
| val metadataRequest = request.requestObj.asInstanceOf[TopicMetadataRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(metadataRequest.versionId)) | ||
|
|
||
| //if topics is empty -> fetch all topics metadata but filter out the topic response that are not authorized | ||
| val topics = if (metadataRequest.topics.isEmpty) { | ||
|
|
@@ -606,6 +629,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
|
|
||
| def handleOffsetFetchRequest(request: RequestChannel.Request) { | ||
| val offsetFetchRequest = request.requestObj.asInstanceOf[OffsetFetchRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(offsetFetchRequest.versionId)) | ||
|
|
||
| val (authorizedTopicPartitions, unauthorizedTopicPartitions) = offsetFetchRequest.requestInfo.partition { topicAndPartition => | ||
| authorize(request.session, Describe, new Resource(Topic, topicAndPartition.topic)) && | ||
|
|
@@ -660,6 +684,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| */ | ||
| def handleConsumerMetadataRequest(request: RequestChannel.Request) { | ||
| val consumerMetadataRequest = request.requestObj.asInstanceOf[ConsumerMetadataRequest] | ||
| KafkaApis.validateRequestVersion(request, Some(consumerMetadataRequest.versionId)) | ||
|
|
||
| if (!authorize(request.session, Read, new Resource(ConsumerGroup, consumerMetadataRequest.group))) { | ||
| val response = ConsumerMetadataResponse(None, ErrorMapping.AuthorizationCode, consumerMetadataRequest.correlationId) | ||
|
|
@@ -686,8 +711,8 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| } | ||
|
|
||
| def handleJoinGroupRequest(request: RequestChannel.Request) { | ||
| import JavaConversions._ | ||
|
|
||
| import scala.collection.JavaConversions._ | ||
| KafkaApis.validateRequestVersion(request, None) | ||
| val joinGroupRequest = request.body.asInstanceOf[JoinGroupRequest] | ||
| val respHeader = new ResponseHeader(request.header.correlationId) | ||
|
|
||
|
|
@@ -723,6 +748,7 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| } | ||
|
|
||
| def handleHeartbeatRequest(request: RequestChannel.Request) { | ||
| KafkaApis.validateRequestVersion(request, None) | ||
| val heartbeatRequest = request.body.asInstanceOf[HeartbeatRequest] | ||
| val respHeader = new ResponseHeader(request.header.correlationId) | ||
|
|
||
|
|
||
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.
This is added in #986 as well. The later patch to go in will have to rebase, or we can extract out the common code as a separate PR. I am fine with either way. FYI @becketqin.