-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-6726: Fine Grained ACL for CreateTopics (KIP-277) #4795
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 14 commits
cb242d4
1a1584c
099c539
b2e67b1
ea58fc3
afd55d7
c3bf9fe
b86c3c2
59d1822
8226bfc
d4e5fc4
d82f725
ce3db5a
b89ff7a
dc2727e
c37272f
0276cf0
1346289
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 |
|---|---|---|
|
|
@@ -62,6 +62,7 @@ import scala.collection.JavaConverters._ | |
| import scala.collection._ | ||
| import scala.collection.mutable.ArrayBuffer | ||
| import scala.util.{Failure, Success, Try} | ||
| import org.apache.kafka.common.requests.CreateTopicsRequest.TopicDetails | ||
|
|
||
| /** | ||
| * Logic to handle the various Kafka requests | ||
|
|
@@ -1034,19 +1035,21 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| var (authorizedTopics, unauthorizedForDescribeTopics) = | ||
| topics.partition(topic => authorize(request.session, Describe, new Resource(Topic, topic))) | ||
|
|
||
| var unauthorizedForCreateTopics = Set[String]() | ||
| var authorizedForDescribeNotCreateTopics = Set[String]() | ||
|
|
||
| if (authorizedTopics.nonEmpty) { | ||
| val nonExistingTopics = metadataCache.getNonExistingTopics(authorizedTopics) | ||
| if (metadataRequest.allowAutoTopicCreation && config.autoCreateTopicsEnable && nonExistingTopics.nonEmpty) { | ||
| if (!authorize(request.session, Create, Resource.ClusterResource)) { | ||
| authorizedTopics --= nonExistingTopics | ||
| unauthorizedForCreateTopics ++= nonExistingTopics | ||
| val unauthorizedForCreateTopics = authorizedTopics.filter( | ||
|
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. This seems like a bug, we should only be doing this check for |
||
| topic => !authorize(request.session, Create, new Resource(Topic, topic))) | ||
| authorizedTopics --= unauthorizedForCreateTopics | ||
| authorizedForDescribeNotCreateTopics ++= unauthorizedForCreateTopics | ||
| } | ||
| } | ||
| } | ||
|
|
||
| val unauthorizedForCreateTopicMetadata = unauthorizedForCreateTopics.map(topic => | ||
| val unauthorizedForCreateTopicMetadata = authorizedForDescribeNotCreateTopics.map(topic => | ||
| new MetadataResponse.TopicMetadata(Errors.TOPIC_AUTHORIZATION_FAILED, topic, isInternal(topic), | ||
| java.util.Collections.emptyList())) | ||
|
|
||
|
|
@@ -1424,16 +1427,20 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| (topic, new ApiError(Errors.NOT_CONTROLLER, null)) | ||
| } | ||
| sendResponseCallback(results) | ||
| } else if (!authorize(request.session, Create, Resource.ClusterResource)) { | ||
| val results = createTopicsRequest.topics.asScala.map { case (topic, _) => | ||
| (topic, new ApiError(Errors.CLUSTER_AUTHORIZATION_FAILED, null)) | ||
| } | ||
| sendResponseCallback(results) | ||
| } else { | ||
| val (validTopics, duplicateTopics) = createTopicsRequest.topics.asScala.partition { case (topic, _) => | ||
| !createTopicsRequest.duplicateTopics.contains(topic) | ||
| } | ||
|
|
||
| val (authorizedTopics, unauthorizedTopics) = | ||
| if (authorize(request.session, Create, Resource.ClusterResource)) { | ||
| (validTopics, Map[String, TopicDetails]()) | ||
| } else { | ||
| validTopics.partition { case (topic, _) => | ||
| authorize(request.session, Create, new Resource(Topic, topic)) | ||
| } | ||
| } | ||
|
|
||
| // Special handling to add duplicate topics to the response | ||
| def sendResponseWithDuplicatesCallback(results: Map[String, ApiError]): Unit = { | ||
|
|
||
|
|
@@ -1447,14 +1454,15 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| duplicateTopics.keySet.map((_, new ApiError(Errors.INVALID_REQUEST, errorMessage))).toMap | ||
| } else Map.empty | ||
|
|
||
| val completeResults = results ++ duplicatedTopicsResults | ||
| val unauthorizedTopicsResults = unauthorizedTopics.keySet.map(_ -> new ApiError(Errors.TOPIC_AUTHORIZATION_FAILED, null)) | ||
| val completeResults = results ++ duplicatedTopicsResults ++ unauthorizedTopicsResults | ||
| sendResponseCallback(completeResults) | ||
| } | ||
|
|
||
| adminManager.createTopics( | ||
| createTopicsRequest.timeout, | ||
| createTopicsRequest.validateOnly, | ||
| validTopics, | ||
| authorizedTopics, | ||
| sendResponseWithDuplicatesCallback | ||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import java.util | |
| import java.util.concurrent.ExecutionException | ||
| import java.util.regex.Pattern | ||
| import java.util.{ArrayList, Collections, Properties} | ||
| import java.time.Duration | ||
|
|
||
| import kafka.admin.AdminClient | ||
| import kafka.admin.ConsumerGroupCommand.{ConsumerGroupCommandOptions, KafkaConsumerGroupService} | ||
|
|
@@ -73,6 +74,7 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| val groupResource = new Resource(Group, group) | ||
| val deleteTopicResource = new Resource(Topic, deleteTopic) | ||
| val transactionalIdResource = new Resource(TransactionalId, transactionalId) | ||
| val createTopicResource = new Resource(Topic, createTopic) | ||
|
|
||
| val groupReadAcl = Map(groupResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Read))) | ||
| val groupDescribeAcl = Map(groupResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Describe))) | ||
|
|
@@ -82,6 +84,7 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| val clusterAlterAcl = Map(Resource.ClusterResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Alter))) | ||
| val clusterDescribeAcl = Map(Resource.ClusterResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Describe))) | ||
| val clusterIdempotentWriteAcl = Map(Resource.ClusterResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, IdempotentWrite))) | ||
| val topicCreateAcl = Map(createTopicResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create))) | ||
| val topicReadAcl = Map(topicResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Read))) | ||
| val topicWriteAcl = Map(topicResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Write))) | ||
| val topicDescribeAcl = Map(topicResource -> Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Describe))) | ||
|
|
@@ -207,7 +210,7 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| ApiKeys.LEADER_AND_ISR -> clusterAcl, | ||
| ApiKeys.STOP_REPLICA -> clusterAcl, | ||
| ApiKeys.CONTROLLED_SHUTDOWN -> clusterAcl, | ||
| ApiKeys.CREATE_TOPICS -> clusterCreateAcl, | ||
| ApiKeys.CREATE_TOPICS -> topicCreateAcl, | ||
| ApiKeys.DELETE_TOPICS -> topicDeleteAcl, | ||
| ApiKeys.DELETE_RECORDS -> topicDeleteAcl, | ||
| ApiKeys.OFFSET_FOR_LEADER_EPOCH -> clusterAcl, | ||
|
|
@@ -492,6 +495,18 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| } | ||
| } | ||
|
|
||
| @Test | ||
| def testCreateTopicAuthorizationWithClusterCreate() { | ||
| removeAllAcls() | ||
| val resources = Set[ResourceType](Topic) | ||
|
|
||
| sendRequestAndVerifyResponseError(ApiKeys.CREATE_TOPICS, createTopicsRequest, resources, isAuthorized = false) | ||
|
|
||
| for ((resource, acls) <- clusterCreateAcl) | ||
| addAndVerifyAcls(acls, resource) | ||
| sendRequestAndVerifyResponseError(ApiKeys.CREATE_TOPICS, createTopicsRequest, resources, isAuthorized = true) | ||
| } | ||
|
|
||
| @Test | ||
| def testFetchFollowerRequest() { | ||
| val key = ApiKeys.FETCH | ||
|
|
@@ -551,18 +566,30 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| } | ||
|
|
||
| @Test | ||
| def testCreatePermissionNeededForWritingToNonExistentTopic() { | ||
| val newTopic = "newTopic" | ||
| val topicPartition = new TopicPartition(newTopic, 0) | ||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Write)), new Resource(Topic, newTopic)) | ||
| def testCreatePermissionOnTopicToWriteToNonExistentTopic() { | ||
| testCreatePermissionNeededToWriteToNonExistentTopic(Topic) | ||
| } | ||
|
|
||
| @Test | ||
| def testCreatePermissionOnClusterToWriteToNonExistentTopic() { | ||
| testCreatePermissionNeededToWriteToNonExistentTopic(Cluster) | ||
| } | ||
|
|
||
| private def testCreatePermissionNeededToWriteToNonExistentTopic(resType: ResourceType) { | ||
| val topicPartition = new TopicPartition(createTopic, 0) | ||
| val newTopicResource = new Resource(Topic, createTopic) | ||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Write)), newTopicResource) | ||
| try { | ||
| sendRecords(numRecords, topicPartition) | ||
| Assert.fail("should have thrown exception") | ||
| } catch { | ||
| case e: TopicAuthorizationException => assertEquals(Collections.singleton(newTopic), e.unauthorizedTopics()) | ||
| case e: TopicAuthorizationException => | ||
| assertEquals(Collections.singleton(createTopic), e.unauthorizedTopics()) | ||
| } | ||
|
|
||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create)), Resource.ClusterResource) | ||
| val resource = if (resType == Topic) newTopicResource else Resource.ClusterResource | ||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create)), resource) | ||
|
|
||
| sendRecords(numRecords, topicPartition) | ||
| } | ||
|
|
||
|
|
@@ -800,27 +827,41 @@ class AuthorizerIntegrationTest extends BaseRequestTest { | |
| } | ||
|
|
||
| @Test | ||
| def testCreatePermissionNeededToReadFromNonExistentTopic() { | ||
| val newTopic = "newTopic" | ||
| def testCreatePermissionOnTopicToReadFromNonExistentTopic() { | ||
| testCreatePermissionNeededToReadFromNonExistentTopic("newTopic", | ||
| Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create)), | ||
| Topic) | ||
| } | ||
|
|
||
| @Test | ||
| def testCreatePermissionOnClusterToReadFromNonExistentTopic() { | ||
| testCreatePermissionNeededToReadFromNonExistentTopic("newTopic", | ||
| Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create)), | ||
| Cluster) | ||
| } | ||
|
|
||
| private def testCreatePermissionNeededToReadFromNonExistentTopic(newTopic: String, acls: Set[Acl], resType: ResourceType) { | ||
| val topicPartition = new TopicPartition(newTopic, 0) | ||
| val newTopicResource = new Resource(Topic, newTopic) | ||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Read)), newTopicResource) | ||
| addAndVerifyAcls(groupReadAcl(groupResource), groupResource) | ||
| addAndVerifyAcls(clusterAcl(Resource.ClusterResource), Resource.ClusterResource) | ||
| this.consumers.head.assign(List(topicPartition).asJava) | ||
| try { | ||
| this.consumers.head.assign(List(topicPartition).asJava) | ||
| consumeRecords(this.consumers.head) | ||
| Assert.fail("should have thrown exception") | ||
| this.consumers.head.poll(Duration.ofMillis(50L)); | ||
|
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. Why did we replace
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. We modified the method to avoid producing any records - to avoid adding write permission which made the test more complicated than it needed to be. So we need to pass
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. We want to be generous with the timeout then. Any reason why it's so low?
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. the default timeout was fine for the first call (when we expect it always to throw) it's the second one that caused issues - see https://issues.apache.org/jira/browse/KAFKA-6994 - but that IMHO is unrelated to the changes in this PR. if the second timeout is low - even without adding the required acl the test passed - that's the bug 6994. if the second timeout is large the test takes longer, that's why we kept relatively 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. Did you see my comment in KAFKA-6994?
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. yep - replied there - it's the same with the old
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. Even then,
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. good point - we can use |
||
| Assert.fail("should have thrown Authorization Exception") | ||
| } catch { | ||
| case e: TopicAuthorizationException => | ||
| assertEquals(Collections.singleton(newTopic), e.unauthorizedTopics()) | ||
| } | ||
|
|
||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Write)), newTopicResource) | ||
| addAndVerifyAcls(Set(new Acl(userPrincipal, Allow, Acl.WildCardHost, Create)), Resource.ClusterResource) | ||
| val resource = if (resType == Topic) newTopicResource else Resource.ClusterResource | ||
| addAndVerifyAcls(acls, resource) | ||
|
|
||
| sendRecords(numRecords, topicPartition) | ||
| consumeRecords(this.consumers.head, topic = newTopic, part = 0) | ||
| // need to retry to avoid using a long timeout | ||
| TestUtils.waitUntilTrue(() => { | ||
| this.consumers.head.poll(Duration.ofMillis(50L)) | ||
| this.zkClient.topicExists(newTopic) | ||
| }, "Expected topic was not created") | ||
| } | ||
|
|
||
| @Test(expected = classOf[AuthorizationException]) | ||
|
|
||
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.
Why did we rename this?
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.
the new name though longer and not pretty matches the what the set of topics actually is in terms of authorizations
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.
But then later we still have things like
unauthorizedForCreateTopicMetadata. I think we should just keep this variable as it was since there is no actual change here. I'll push a minor commit.