This is an automated email from the ASF dual-hosted git repository.

FrankYang0529 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 2904f5c4928 KAFKA-18998: Move AuthHelper to server module (#22307)
2904f5c4928 is described below

commit 2904f5c4928cba71b2eba9135bee9e31458a920c
Author: PoAn Yang <[email protected]>
AuthorDate: Wed Jun 24 19:13:50 2026 +0900

    KAFKA-18998: Move AuthHelper to server module (#22307)
    
    Move AuthHelper from core to server module and rewrite in Java.
    
    Reviewers: Mickael Maison <[email protected]>, Sushant Mahajan
     <[email protected]>, Chia-Ping Tsai <[email protected]>
    
    ---------
    
    Signed-off-by: PoAn Yang <[email protected]>
---
 .../DescribeTopicPartitionsRequestHandler.java     |   2 +-
 core/src/main/scala/kafka/server/AclApis.scala     |   1 +
 core/src/main/scala/kafka/server/AuthHelper.scala  | 176 -----------
 .../src/main/scala/kafka/server/ConfigHelper.scala |   1 +
 .../main/scala/kafka/server/ControllerApis.scala   |  44 +--
 core/src/main/scala/kafka/server/KafkaApis.scala   | 116 ++++----
 .../DescribeTopicPartitionsRequestHandlerTest.java |   6 +-
 .../scala/unit/kafka/server/AuthHelperTest.scala   | 261 -----------------
 .../unit/kafka/server/ControllerApisTest.scala     |  38 +--
 .../java/org/apache/kafka/server/AuthHelper.java   | 266 +++++++++++++++++
 .../org/apache/kafka/server/AuthHelperTest.java    | 325 +++++++++++++++++++++
 11 files changed, 705 insertions(+), 531 deletions(-)

diff --git 
a/core/src/main/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandler.java
 
b/core/src/main/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandler.java
index 101f09c2a71..70f7f408378 100644
--- 
a/core/src/main/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandler.java
+++ 
b/core/src/main/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandler.java
@@ -17,7 +17,6 @@
 
 package kafka.server.handlers;
 
-import kafka.server.AuthHelper;
 import kafka.server.KafkaConfig;
 
 import org.apache.kafka.common.Uuid;
@@ -31,6 +30,7 @@ import 
org.apache.kafka.common.requests.DescribeTopicPartitionsRequest;
 import org.apache.kafka.common.resource.Resource;
 import org.apache.kafka.metadata.MetadataCache;
 import org.apache.kafka.network.Request;
+import org.apache.kafka.server.AuthHelper;
 
 import java.util.HashSet;
 import java.util.List;
diff --git a/core/src/main/scala/kafka/server/AclApis.scala 
b/core/src/main/scala/kafka/server/AclApis.scala
index 10c7f6bb4d7..9c8113c8ffd 100644
--- a/core/src/main/scala/kafka/server/AclApis.scala
+++ b/core/src/main/scala/kafka/server/AclApis.scala
@@ -31,6 +31,7 @@ import org.apache.kafka.common.resource.Resource.CLUSTER_NAME
 import org.apache.kafka.common.resource.ResourceType
 import org.apache.kafka.network.Request
 import org.apache.kafka.security.authorizer.AuthorizerUtils
+import org.apache.kafka.server.AuthHelper
 import org.apache.kafka.server.ProcessRole
 import org.apache.kafka.server.authorizer._
 import org.apache.kafka.server.purgatory.DelayedFuturePurgatory
diff --git a/core/src/main/scala/kafka/server/AuthHelper.scala 
b/core/src/main/scala/kafka/server/AuthHelper.scala
deleted file mode 100644
index ea76650da68..00000000000
--- a/core/src/main/scala/kafka/server/AuthHelper.scala
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * 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 kafka.server
-
-import java.lang.{Byte => JByte}
-import java.util.Collections
-import org.apache.kafka.clients.admin.EndpointType
-import org.apache.kafka.common.acl.AclOperation
-import org.apache.kafka.common.acl.AclOperation.DESCRIBE
-import org.apache.kafka.common.errors.ClusterAuthorizationException
-import org.apache.kafka.common.internals.Plugin
-import org.apache.kafka.common.message.DescribeClusterResponseData
-import 
org.apache.kafka.common.message.DescribeClusterResponseData.DescribeClusterBrokerCollection
-import org.apache.kafka.network.Request
-import org.apache.kafka.common.protocol.Errors
-import org.apache.kafka.common.requests.{DescribeClusterRequest, 
RequestContext}
-import org.apache.kafka.common.resource.Resource.CLUSTER_NAME
-import org.apache.kafka.common.resource.ResourceType.CLUSTER
-import org.apache.kafka.common.resource.{PatternType, Resource, 
ResourcePattern, ResourceType}
-import org.apache.kafka.common.utils.Utils
-import org.apache.kafka.security.authorizer.AclEntry
-import org.apache.kafka.server.authorizer.{Action, AuthorizationResult, 
Authorizer}
-
-import scala.collection.Seq
-import scala.jdk.CollectionConverters._
-
-class AuthHelper(authorizer: Option[Plugin[Authorizer]]) {
-  def authorize(requestContext: RequestContext,
-                operation: AclOperation,
-                resourceType: ResourceType,
-                resourceName: String,
-                logIfAllowed: Boolean = true,
-                logIfDenied: Boolean = true,
-                refCount: Int = 1): Boolean = {
-    authorizer.forall { authZ =>
-      val resource = new ResourcePattern(resourceType, resourceName, 
PatternType.LITERAL)
-      val actions = Collections.singletonList(new Action(operation, resource, 
refCount, logIfAllowed, logIfDenied))
-      authZ.get.authorize(requestContext, actions).get(0) == 
AuthorizationResult.ALLOWED
-    }
-  }
-
-  def authorizeClusterOperation(request: Request, operation: AclOperation): 
Unit = {
-    if (!authorize(request.context, operation, CLUSTER, CLUSTER_NAME))
-      throw new ClusterAuthorizationException(s"Request $request needs 
$operation permission.")
-  }
-
-  def authorizedOperations(request: Request, resource: Resource): Int = {
-    val supportedOps = 
AclEntry.supportedOperations(resource.resourceType).asScala.toList
-    val authorizedOps = authorizer match {
-      case Some(authZ) =>
-        val resourcePattern = new ResourcePattern(resource.resourceType, 
resource.name, PatternType.LITERAL)
-        val actions = supportedOps.map { op => new Action(op, resourcePattern, 
1, false, false) }
-        authZ.get.authorize(request.context, actions.asJava).asScala
-          .zip(supportedOps)
-          .filter(_._1 == AuthorizationResult.ALLOWED)
-          .map(_._2).toSet
-      case None =>
-        supportedOps.toSet
-    }
-    Utils.to32BitField(authorizedOps.map(operation => 
operation.code.asInstanceOf[JByte]).asJava)
-  }
-
-  def authorizeByResourceType(requestContext: RequestContext, operation: 
AclOperation,
-                              resourceType: ResourceType): Boolean = {
-    authorizer.forall { authZ =>
-      authZ.get.authorizeByResourceType(requestContext, operation, 
resourceType) == AuthorizationResult.ALLOWED
-    }
-  }
-
-  def partitionSeqByAuthorized[T](requestContext: RequestContext,
-                                  operation: AclOperation,
-                                  resourceType: ResourceType,
-                                  resources: Seq[T],
-                                  logIfAllowed: Boolean = true,
-                                  logIfDenied: Boolean = true)(resourceName: T 
=> String): (Seq[T], Seq[T]) = {
-    authorizer match {
-      case Some(_) =>
-        val authorizedResourceNames = filterByAuthorized(requestContext, 
operation, resourceType,
-          resources, logIfAllowed, logIfDenied)(resourceName)
-        resources.partition(resource => 
authorizedResourceNames.contains(resourceName(resource)))
-      case None => (resources, Seq.empty)
-    }
-  }
-
-  def filterByAuthorized[T](requestContext: RequestContext,
-                            operation: AclOperation,
-                            resourceType: ResourceType,
-                            resources: Iterable[T],
-                            logIfAllowed: Boolean = true,
-                            logIfDenied: Boolean = true)(resourceName: T => 
String): Set[String] = {
-    authorizer match {
-      case Some(authZ) =>
-        val resourceNameToCount = resources.groupMapReduce(resourceName)(_ => 
1)(_ + _)
-        val actions = resourceNameToCount.iterator.map { case (resourceName, 
count) =>
-          val resource = new ResourcePattern(resourceType, resourceName, 
PatternType.LITERAL)
-          new Action(operation, resource, count, logIfAllowed, logIfDenied)
-        }.toBuffer
-        authZ.get.authorize(requestContext, actions.asJava).asScala
-          .zip(resourceNameToCount.keySet)
-          .collect { case (authzResult, resourceName) if authzResult == 
AuthorizationResult.ALLOWED =>
-            resourceName
-          }.toSet
-      case None => resources.iterator.map(resourceName).toSet
-    }
-  }
-
-  def computeDescribeClusterResponse(
-    request: Request,
-    expectedEndpointType: EndpointType,
-    clusterId: String,
-    getNodes: () => DescribeClusterBrokerCollection,
-    getControllerId: () => Int
-  ): DescribeClusterResponseData = {
-    val describeClusterRequest = request.body(classOf[DescribeClusterRequest])
-    val requestEndpointType = 
EndpointType.fromId(describeClusterRequest.data().endpointType())
-    if (requestEndpointType.equals(EndpointType.UNKNOWN)) {
-      return new DescribeClusterResponseData().
-        setErrorCode(if (request.header.data().requestApiVersion() == 0) {
-          Errors.INVALID_REQUEST.code()
-        } else {
-          Errors.UNSUPPORTED_ENDPOINT_TYPE.code()
-        }).
-        setErrorMessage("Unsupported endpoint type " + 
describeClusterRequest.data().endpointType().toInt)
-    } else if (!expectedEndpointType.equals(requestEndpointType)) {
-      return new DescribeClusterResponseData().
-        setErrorCode(if (request.header.data().requestApiVersion() == 0) {
-          Errors.INVALID_REQUEST.code()
-        } else {
-          Errors.MISMATCHED_ENDPOINT_TYPE.code()
-        }).
-        setErrorMessage("The request was sent to an endpoint of type " + 
expectedEndpointType +
-          ", but we wanted an endpoint of type " + requestEndpointType)
-    }
-    var clusterAuthorizedOperations = Int.MinValue // Default value in the 
schema
-    // get cluster authorized operations
-    if (describeClusterRequest.data.includeClusterAuthorizedOperations) {
-      if (authorize(request.context, DESCRIBE, CLUSTER, CLUSTER_NAME))
-        clusterAuthorizedOperations = authorizedOperations(request, 
Resource.CLUSTER)
-      else
-        clusterAuthorizedOperations = 0
-    }
-    // Get the node list and the controller ID.
-    val nodes = getNodes()
-    val controllerId = getControllerId()
-    // If the provided controller ID is not in the node list, return -1 instead
-    // to avoid confusing the client. This could happen in a case where we know
-    // the controller ID, but we don't yet have KIP-919 information about that
-    // controller.
-    val effectiveControllerId = if (nodes.find(controllerId) == null) {
-      -1
-    } else {
-      controllerId
-    }
-    new DescribeClusterResponseData().
-      setClusterId(clusterId).
-      setControllerId(effectiveControllerId).
-      setClusterAuthorizedOperations(clusterAuthorizedOperations).
-      setBrokers(nodes).
-      setEndpointType(expectedEndpointType.id())
-  }
-}
diff --git a/core/src/main/scala/kafka/server/ConfigHelper.scala 
b/core/src/main/scala/kafka/server/ConfigHelper.scala
index 304d8c15541..7e4de8eb6fc 100644
--- a/core/src/main/scala/kafka/server/ConfigHelper.scala
+++ b/core/src/main/scala/kafka/server/ConfigHelper.scala
@@ -33,6 +33,7 @@ import 
org.apache.kafka.common.resource.ResourceType.{CLUSTER, GROUP, TOPIC}
 import org.apache.kafka.coordinator.group.GroupConfig
 import org.apache.kafka.metadata.{ConfigRepository, MetadataCache}
 import org.apache.kafka.network.Request
+import org.apache.kafka.server.AuthHelper
 import org.apache.kafka.server.ConfigHelperUtils.createResponseConfig
 import org.apache.kafka.server.config.{DynamicBrokerConfig, 
ServerTopicConfigSynonyms}
 import org.apache.kafka.server.logger.LoggingController
diff --git a/core/src/main/scala/kafka/server/ControllerApis.scala 
b/core/src/main/scala/kafka/server/ControllerApis.scala
index 77ca2559866..2957d5770fb 100644
--- a/core/src/main/scala/kafka/server/ControllerApis.scala
+++ b/core/src/main/scala/kafka/server/ControllerApis.scala
@@ -55,12 +55,12 @@ import org.apache.kafka.metadata.{BrokerHeartbeatReply, 
BrokerRegistrationReply,
 import org.apache.kafka.network.Request
 import org.apache.kafka.raft.RaftManager
 import org.apache.kafka.security.DelegationTokenManager
-import org.apache.kafka.server.{ApiVersionManager, EnvelopeUtils, ProcessRole}
+import org.apache.kafka.server.{ApiVersionManager, AuthHelper, EnvelopeUtils, 
ProcessRole}
 import org.apache.kafka.server.authorizer.Authorizer
 import org.apache.kafka.server.common.{ApiMessageAndVersion, RequestLocal}
 import org.apache.kafka.server.quota.ControllerMutationQuota
 
-import scala.jdk.CollectionConverters._
+import scala.jdk.javaapi.OptionConverters
 
 
 /**
@@ -81,7 +81,7 @@ class ControllerApis(
 ) extends ApiRequestHandler with Logging {
 
   this.logIdent = s"[ControllerApis nodeId=${config.nodeId}] "
-  val authHelper = new AuthHelper(authorizerPlugin)
+  val authHelper = new AuthHelper(OptionConverters.toJava(authorizerPlugin))
   val configHelper = new ConfigHelper(metadataCache, config, metadataCache)
   val requestHelper = new RequestHandlerHelper(requestChannel, quotas, time)
   val runtimeLoggerManager = new RuntimeLoggerManager(config.nodeId, 
logger.underlying)
@@ -204,9 +204,9 @@ class ControllerApis(
     val future = deleteTopics(context,
       deleteTopicsRequest.data,
       request.context.apiVersion,
-      authHelper.authorize(request.context, DELETE, CLUSTER, CLUSTER_NAME, 
logIfDenied = false),
-      names => authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC, 
names)(n => n),
-      names => authHelper.filterByAuthorized(request.context, DELETE, TOPIC, 
names)(n => n))
+      authHelper.authorize(request.context, DELETE, CLUSTER, CLUSTER_NAME, 
true, false, 1),
+      names => authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC, 
names, (n: String) => n),
+      names => authHelper.filterByAuthorized(request.context, DELETE, TOPIC, 
names, (n: String) => n))
     future.handle[Unit] { (results, exception) =>
       val response = if (exception != null) {
         deleteTopicsRequest.getErrorResponse(exception)
@@ -224,8 +224,8 @@ class ControllerApis(
     request: DeleteTopicsRequestData,
     apiVersion: Int,
     hasClusterAuth: Boolean,
-    getDescribableTopics: Iterable[String] => Set[String],
-    getDeletableTopics: Iterable[String] => Set[String]
+    getDescribableTopics: lang.Iterable[String] => util.Set[String],
+    getDeletableTopics: lang.Iterable[String] => util.Set[String]
   ): CompletableFuture[util.List[DeletableTopicResult]] = {
     // Check if topic deletion is enabled at all.
     if (!config.deleteTopicEnable) {
@@ -298,11 +298,10 @@ class ControllerApis(
       }
       // Get the list of deletable topics (those we can delete) and the list 
of describable
       // topics.
-      val topicsToAuthenticate = toAuthenticate.asScala
       val (describable, deletable) = if (hasClusterAuth) {
-        (topicsToAuthenticate.toSet, topicsToAuthenticate.toSet)
+        (toAuthenticate, toAuthenticate)
       } else {
-        (getDescribableTopics(topicsToAuthenticate), 
getDeletableTopics(topicsToAuthenticate))
+        (getDescribableTopics(toAuthenticate), 
getDeletableTopics(toAuthenticate))
       }
       // For each topic that was provided by ID, check if authentication 
failed.
       // If so, remove it from the idToName map and create an error response 
for it.
@@ -367,10 +366,10 @@ class ControllerApis(
       controllerMutationQuotaRecorderFor(controllerMutationQuota))
     val future = createTopics(context,
         createTopicsRequest.data,
-        authHelper.authorize(request.context, CREATE, CLUSTER, CLUSTER_NAME, 
logIfDenied = false),
-        names => authHelper.filterByAuthorized(request.context, CREATE, TOPIC, 
names)(identity),
+        authHelper.authorize(request.context, CREATE, CLUSTER, CLUSTER_NAME, 
true, false, 1),
+        names => authHelper.filterByAuthorized(request.context, CREATE, TOPIC, 
names, (n: String) => n),
         names => authHelper.filterByAuthorized(request.context, 
DESCRIBE_CONFIGS, TOPIC,
-            names, logIfDenied = false)(identity),
+            names, true, false, (n: String) => n),
         request.isForwarded)
     future.handle[Unit] { (result, exception) =>
       val response = if (exception != null) {
@@ -392,8 +391,8 @@ class ControllerApis(
     context: ControllerRequestContext,
     request: CreateTopicsRequestData,
     hasClusterAuth: Boolean,
-    getCreatableTopics: Iterable[String] => Set[String],
-    getDescribableTopics: Iterable[String] => Set[String],
+    getCreatableTopics: lang.Iterable[String] => util.Set[String],
+    getDescribableTopics: lang.Iterable[String] => util.Set[String],
     forwarded: Boolean
   ): CompletableFuture[CreateTopicsResponseData] = {
     val topicNames = new util.HashSet[String]()
@@ -407,14 +406,15 @@ class ControllerApis(
       }
     }
 
-    val allowedTopicNames = 
topicNames.asScala.diff(Set(Topic.CLUSTER_METADATA_TOPIC_NAME))
+    val allowedTopicNames = new util.HashSet[String](topicNames)
+    allowedTopicNames.remove(Topic.CLUSTER_METADATA_TOPIC_NAME)
 
     val authorizedTopicNames = if (hasClusterAuth) {
       allowedTopicNames
     } else {
       getCreatableTopics.apply(allowedTopicNames)
     }
-    val describableTopicNames = 
getDescribableTopics.apply(allowedTopicNames).asJava
+    val describableTopicNames = getDescribableTopics.apply(allowedTopicNames)
     val effectiveRequest = request.duplicate()
     val iterator = effectiveRequest.topics().iterator()
     while (iterator.hasNext) {
@@ -798,8 +798,8 @@ class ControllerApis(
   }
 
   private def handleCreatePartitions(request: Request): 
CompletableFuture[Unit] = {
-    def filterAlterAuthorizedTopics(topics: Iterable[String]): Set[String] = {
-      authHelper.filterByAuthorized(request.context, ALTER, TOPIC, topics)(n 
=> n)
+    def filterAlterAuthorizedTopics(topics: lang.Iterable[String]): 
util.Set[String] = {
+      authHelper.filterByAuthorized(request.context, ALTER, TOPIC, topics, (n: 
String) => n)
     }
     val createPartitionsRequest = 
request.body(classOf[CreatePartitionsRequest])
     val controllerMutationQuota = 
quotas.controllerMutation.newQuotaFor(request.session, request.header, 3)
@@ -830,7 +830,7 @@ class ControllerApis(
   def createPartitions(
     context: ControllerRequestContext,
     request: CreatePartitionsRequestData,
-    getAlterAuthorizedTopics: Iterable[String] => Set[String]
+    getAlterAuthorizedTopics: lang.Iterable[String] => util.Set[String]
   ): CompletableFuture[util.List[CreatePartitionsTopicResult]] = {
     val responses = new util.ArrayList[CreatePartitionsTopicResult]()
     val duplicateTopicNames = new util.HashSet[String]()
@@ -848,7 +848,7 @@ class ControllerApis(
         setErrorMessage("Duplicate topic name."))
         topicNames.remove(topicName)
     }
-    val authorizedTopicNames = getAlterAuthorizedTopics(topicNames.asScala)
+    val authorizedTopicNames = getAlterAuthorizedTopics(topicNames)
     val topics = new util.ArrayList[CreatePartitionsTopic]
     topicNames.forEach { topicName =>
       if (authorizedTopicNames.contains(topicName)) {
diff --git a/core/src/main/scala/kafka/server/KafkaApis.scala 
b/core/src/main/scala/kafka/server/KafkaApis.scala
index 3bf0861d847..36948c34fb8 100644
--- a/core/src/main/scala/kafka/server/KafkaApis.scala
+++ b/core/src/main/scala/kafka/server/KafkaApis.scala
@@ -64,7 +64,7 @@ import 
org.apache.kafka.coordinator.transaction.InitProducerIdResult
 import org.apache.kafka.metadata.{ConfigRepository, MetadataCache}
 import org.apache.kafka.network.Request
 import org.apache.kafka.security.DelegationTokenManager
-import org.apache.kafka.server.{ApiVersionManager, AutoTopicCreationManager, 
ClientMetricsManager, FetchManager, ForwardingManager, ProcessRole}
+import org.apache.kafka.server.{ApiVersionManager, AuthHelper, 
AutoTopicCreationManager, ClientMetricsManager, FetchManager, 
ForwardingManager, ProcessRole}
 import org.apache.kafka.server.authorizer._
 import org.apache.kafka.server.common.{GroupVersion, RequestLocal, 
ShareVersion, StreamsVersion, TransactionVersion}
 import org.apache.kafka.server.quota.{ReplicaQuota, ReplicationQuotaManager}
@@ -119,7 +119,7 @@ class KafkaApis(val requestChannel: RequestChannel,
   type ProduceResponseStats = Map[TopicIdPartition, RecordValidationStats]
   this.logIdent = "[KafkaApi-%d] ".format(brokerId)
   val configHelper = new ConfigHelper(metadataCache, config, configRepository)
-  val authHelper = new AuthHelper(authorizerPlugin)
+  val authHelper = new AuthHelper(OptionConverters.toJava(authorizerPlugin))
   val requestHelper = new RequestHandlerHelper(requestChannel, quotas, time)
   val aclApis = new AclApis(authHelper, authorizerPlugin, requestHelper, 
ProcessRole.BrokerRole, config)
   val configManager = new ConfigAdminManager(brokerId, config, 
configRepository)
@@ -298,8 +298,9 @@ class KafkaApis(val requestChannel: RequestChannel,
         request.context,
         READ,
         TOPIC,
-        offsetCommitRequest.data.topics.asScala
-      )(_.name)
+        offsetCommitRequest.data.topics,
+        (t: OffsetCommitRequestData.OffsetCommitRequestTopic) => t.name
+      )
 
       val responseBuilder = OffsetCommitResponse.newBuilder(useTopicIds)
       val authorizedTopicsRequest = new 
mutable.ArrayBuffer[OffsetCommitRequestData.OffsetCommitRequestTopic]()
@@ -430,7 +431,7 @@ class KafkaApis(val requestChannel: RequestChannel,
       }
     }
     // cache the result to avoid redundant authorization calls
-    val authorizedTopics = authHelper.filterByAuthorized(request.context, 
WRITE, TOPIC, topicIdToPartitionData)(_._1.topic)
+    val authorizedTopics = authHelper.filterByAuthorized(request.context, 
WRITE, TOPIC, topicIdToPartitionData.asJava, (t: (TopicIdPartition, 
ProduceRequestData.PartitionProduceData)) => t._1.topic)
 
     topicIdToPartitionData.foreach { case (topicIdPartition, partition) =>
       // This caller assumes the type is MemoryRecords and that is true on 
current serialization
@@ -610,7 +611,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         else
           partitionDatas += topicIdPartition -> partitionData
       }
-      val authorizedTopics = authHelper.filterByAuthorized(request.context, 
READ, TOPIC, partitionDatas)(_._1.topicPartition.topic)
+      val authorizedTopics = authHelper.filterByAuthorized(request.context, 
READ, TOPIC, partitionDatas.asJava, (t: (TopicIdPartition, 
FetchRequest.PartitionData)) => t._1.topicPartition.topic)
       partitionDatas.foreach { case (topicIdPartition, data) =>
         if (!authorizedTopics.contains(topicIdPartition.topic))
           erroneous += topicIdPartition -> 
FetchResponse.partitionResponse(topicIdPartition, 
Errors.TOPIC_AUTHORIZATION_FAILED)
@@ -794,8 +795,10 @@ class KafkaApis(val requestChannel: RequestChannel,
         .setOffset(ListOffsetsResponse.UNKNOWN_OFFSET)
     }
 
-    val (authorizedRequestInfo, unauthorizedRequestInfo) = 
authHelper.partitionSeqByAuthorized(request.context,
-        DESCRIBE, TOPIC, offsetRequest.topics.asScala.toSeq)(_.name)
+    val partitionResult = authHelper.partitionByAuthorized(request.context,
+        DESCRIBE, TOPIC, offsetRequest.topics, (t: 
ListOffsetsRequestData.ListOffsetsTopic) => t.name)
+    val authorizedRequestInfo = partitionResult.authorized.asScala
+    val unauthorizedRequestInfo = partitionResult.unauthorized.asScala
 
     val unauthorizedResponseStatus = unauthorizedRequestInfo.map(topic =>
       new ListOffsetsTopicResponse()
@@ -917,16 +920,16 @@ class KafkaApis(val requestChannel: RequestChannel,
       metadataRequest.topics.asScala.toSet
 
     val authorizedForDescribeTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC,
-      topics, logIfDenied = !metadataRequest.isAllTopics)(identity)
+      topics.asJava, true, !metadataRequest.isAllTopics, (t: String) => t)
     var (authorizedTopics, unauthorizedForDescribeTopics) = 
topics.partition(authorizedForDescribeTopics.contains)
     var unauthorizedForCreateTopics = Set[String]()
 
     if (authorizedTopics.nonEmpty) {
       val nonExistingTopics = 
authorizedTopics.filterNot(metadataCache.contains)
       if (metadataRequest.allowAutoTopicCreation && 
config.autoCreateTopicsEnable && nonExistingTopics.nonEmpty) {
-        if (!authHelper.authorize(request.context, CREATE, CLUSTER, 
CLUSTER_NAME, logIfDenied = false)) {
+        if (!authHelper.authorize(request.context, CREATE, CLUSTER, 
CLUSTER_NAME, true, false, 1)) {
           val authorizedForCreateTopics = 
authHelper.filterByAuthorized(request.context, CREATE, TOPIC,
-            nonExistingTopics)(identity)
+            nonExistingTopics.asJava, (t: String) => t).asScala
           unauthorizedForCreateTopics = 
nonExistingTopics.diff(authorizedForCreateTopics)
           authorizedTopics = authorizedTopics.diff(unauthorizedForCreateTopics)
         }
@@ -1083,8 +1086,9 @@ class KafkaApis(val requestChannel: RequestChannel,
           requestContext,
           DESCRIBE,
           TOPIC,
-          groupFetchResponse.topics.asScala
-        )(_.name)
+          groupFetchResponse.topics,
+          (t: OffsetFetchResponseData.OffsetFetchResponseTopics) => t.name
+        )
 
         val topics = new 
mutable.ArrayBuffer[OffsetFetchResponseData.OffsetFetchResponseTopics]
         groupFetchResponse.topics.forEach { topic =>
@@ -1130,8 +1134,9 @@ class KafkaApis(val requestChannel: RequestChannel,
       requestContext,
       DESCRIBE,
       TOPIC,
-      groupFetchRequest.topics.asScala
-    )(_.name)
+      groupFetchRequest.topics,
+      (t: OffsetFetchRequestData.OffsetFetchRequestTopics) => t.name
+    )
 
     val authorizedTopics = new 
mutable.ArrayBuffer[OffsetFetchRequestData.OffsetFetchRequestTopics]
     val errorTopics = new 
mutable.ArrayBuffer[OffsetFetchResponseData.OffsetFetchResponseTopics]
@@ -1356,7 +1361,7 @@ class KafkaApis(val requestChannel: RequestChannel,
 
   def handleListGroupsRequest(request: Request): CompletableFuture[Unit] = {
     val listGroupsRequest = request.body(classOf[ListGroupsRequest])
-    val hasClusterDescribe = authHelper.authorize(request.context, DESCRIBE, 
CLUSTER, CLUSTER_NAME, logIfDenied = false)
+    val hasClusterDescribe = authHelper.authorize(request.context, DESCRIBE, 
CLUSTER, CLUSTER_NAME, true, false, 1)
 
     groupCoordinator.listGroups(
       request.context,
@@ -1371,7 +1376,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         } else {
           // Otherwise, only groups with described group are returned.
           val authorizedGroups = response.groups.asScala.filter { group =>
-            authHelper.authorize(request.context, DESCRIBE, GROUP, 
group.groupId, logIfDenied = false)
+            authHelper.authorize(request.context, DESCRIBE, GROUP, 
group.groupId, true, false, 1)
           }
           new ListGroupsResponse(response.setGroups(authorizedGroups.asJava))
         }
@@ -1439,8 +1444,9 @@ class KafkaApis(val requestChannel: RequestChannel,
     val deleteGroupsRequest = request.body(classOf[DeleteGroupsRequest])
     val groups = deleteGroupsRequest.data.groupsNames.asScala.distinct
 
-    val (authorizedGroups, unauthorizedGroups) =
-      authHelper.partitionSeqByAuthorized(request.context, DELETE, GROUP, 
groups)(identity)
+    val deleteGroupsPartition = 
authHelper.partitionByAuthorized(request.context, DELETE, GROUP, groups.asJava, 
(g: String) => g)
+    val authorizedGroups = deleteGroupsPartition.authorized.asScala
+    val unauthorizedGroups = deleteGroupsPartition.unauthorized.asScala
 
     groupCoordinator.deleteGroups(
       request.context,
@@ -1573,7 +1579,7 @@ class KafkaApis(val requestChannel: RequestChannel,
     val authorizedForDeleteTopicOffsets = mutable.Map[TopicPartition, Long]()
 
     val topics = deleteRecordsRequest.data.topics.asScala
-    val authorizedTopics = authHelper.filterByAuthorized(request.context, 
DELETE, TOPIC, topics)(_.name)
+    val authorizedTopics = authHelper.filterByAuthorized(request.context, 
DELETE, TOPIC, deleteRecordsRequest.data.topics, (t: 
DeleteRecordsRequestData.DeleteRecordsTopic) => t.name)
     val deleteTopicPartitions = topics.flatMap { deleteTopic =>
       deleteTopic.partitions.asScala.map { deletePartition =>
         new TopicPartition(deleteTopic.name, deletePartition.partitionIndex) 
-> deletePartition.offset
@@ -1643,7 +1649,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         requestHelper.sendErrorResponseMaybeThrottle(request, 
Errors.TRANSACTIONAL_ID_AUTHORIZATION_FAILED.exception)
         return
       }
-    } else if (!authHelper.authorize(request.context, IDEMPOTENT_WRITE, 
CLUSTER, CLUSTER_NAME, true, false)
+    } else if (!authHelper.authorize(request.context, IDEMPOTENT_WRITE, 
CLUSTER, CLUSTER_NAME, true, false, 1)
         && !authHelper.authorizeByResourceType(request.context, 
AclOperation.WRITE, ResourceType.TOPIC)) {
       requestHelper.sendErrorResponseMaybeThrottle(request, 
Errors.CLUSTER_AUTHORIZATION_FAILED.exception)
       return
@@ -1741,7 +1747,7 @@ class KafkaApis(val requestChannel: RequestChannel,
   def handleWriteTxnMarkersRequest(request: Request, requestLocal: 
RequestLocal): Unit = {
     // We are checking for AlterCluster permissions first. If it is not 
present, we are authorizing cluster operation
     // The latter will throw an exception if it is denied.
-    if (!authHelper.authorize(request.context, ALTER, CLUSTER, CLUSTER_NAME, 
logIfDenied = false)) {
+    if (!authHelper.authorize(request.context, ALTER, CLUSTER, CLUSTER_NAME, 
true, false, 1)) {
       authHelper.authorizeClusterOperation(request, CLUSTER_ACTION)
     }
     val writeTxnMarkersRequest = request.body(classOf[WriteTxnMarkersRequest])
@@ -1942,7 +1948,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         // Only request versions less than 4 need write authorization since 
they come from clients.
         val authorizedTopics =
           if (version < 4)
-            authHelper.filterByAuthorized(request.context, WRITE, TOPIC, 
partitionsToAdd.filterNot(tp => Topic.isInternal(tp.topic)))(_.topic)
+            authHelper.filterByAuthorized(request.context, WRITE, TOPIC, 
partitionsToAdd.filterNot(tp => Topic.isInternal(tp.topic)).asJava, (tp: 
TopicPartition) => tp.topic).asScala
           else
             partitionsToAdd.map(_.topic).toSet
         for (topicPartition <- partitionsToAdd) {
@@ -2095,8 +2101,9 @@ class KafkaApis(val requestChannel: RequestChannel,
         request.context,
         READ,
         TOPIC,
-        txnOffsetCommitRequest.data.topics.asScala
-      )(_.name)
+        txnOffsetCommitRequest.data.topics,
+        (t: TxnOffsetCommitRequestData.TxnOffsetCommitRequestTopic) => t.name
+      )
 
       val responseBuilder = TxnOffsetCommitResponse.newBuilder(useTopicIds)
       val authorizedTopicCommittedOffsets = new 
mutable.ArrayBuffer[TxnOffsetCommitRequestData.TxnOffsetCommitRequestTopic]()
@@ -2186,9 +2193,12 @@ class KafkaApis(val requestChannel: RequestChannel,
     // cluster permission. With KIP-320, the consumer now also uses this API 
to check for log truncation
     // following a leader change, so we also allow topic describe permission.
     val (authorizedTopics, unauthorizedTopics) =
-      if (authHelper.authorize(request.context, CLUSTER_ACTION, CLUSTER, 
CLUSTER_NAME, logIfDenied = false))
+      if (authHelper.authorize(request.context, CLUSTER_ACTION, CLUSTER, 
CLUSTER_NAME, true, false, 1))
         (topics, Seq.empty[OffsetForLeaderTopic])
-      else authHelper.partitionSeqByAuthorized(request.context, DESCRIBE, 
TOPIC, topics)(_.topic)
+      else {
+        val partitionResult = 
authHelper.partitionByAuthorized(request.context, DESCRIBE, TOPIC, 
topics.asJava, (t: OffsetForLeaderTopic) => t.topic)
+        (partitionResult.authorized.asScala.toSeq, 
partitionResult.unauthorized.asScala.toSeq)
+      }
 
     val endOffsetsForAuthorizedPartitions = 
replicaManager.lastOffsetForLeaderEpoch(authorizedTopics)
     val endOffsetsForUnauthorizedPartitions = unauthorizedTopics.map { 
offsetForLeaderTopic =>
@@ -2432,8 +2442,9 @@ class KafkaApis(val requestChannel: RequestChannel,
         request.context,
         READ,
         TOPIC,
-        offsetDeleteRequest.data.topics.asScala
-      )(_.name)
+        offsetDeleteRequest.data.topics,
+        (t: OffsetDeleteRequestData.OffsetDeleteRequestTopic) => t.name
+      )
 
       val responseBuilder = new OffsetDeleteResponse.Builder
       val authorizedTopicPartitions = new 
OffsetDeleteRequestData.OffsetDeleteRequestTopicCollection()
@@ -2675,7 +2686,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         // Clients are not allowed to see topics that are not authorized for 
Describe.
         val subscribedTopicSet = 
consumerGroupHeartbeatRequest.data.subscribedTopicNames.asScala.toSet
         val authorizedTopics = authHelper.filterByAuthorized(request.context, 
DESCRIBE, TOPIC,
-          subscribedTopicSet)(identity)
+          subscribedTopicSet.asJava, (t: String) => t)
         if (authorizedTopics.size < subscribedTopicSet.size) {
           val responseData = new ConsumerGroupHeartbeatResponseData()
             .setErrorCode(Errors.TOPIC_AUTHORIZATION_FAILED.code)
@@ -2757,7 +2768,7 @@ class KafkaApis(val requestChannel: RequestChannel,
               .collect(Collectors.toSet[String])
               .asScala
             val authorizedTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC,
-              topicsToCheck)(identity)
+              topicsToCheck.asJava, (t: String) => t)
             val updatedGroups = response.groups.stream().map { group =>
               val hasUnauthorizedTopic = group.members.stream()
                 .flatMap(member => util.stream.Stream.of(member.assignment, 
member.targetAssignment))
@@ -2838,7 +2849,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         }
 
         if (requiredTopics.nonEmpty) {
-          val authorizedTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC, 
requiredTopics)(identity)
+          val authorizedTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC, 
requiredTopics.asJava, (t: String) => t)
           if (authorizedTopics.size < requiredTopics.size) {
             val responseData = new 
StreamsGroupHeartbeatResponseData().setErrorCode(Errors.TOPIC_AUTHORIZATION_FAILED.code)
             requestHelper.sendMaybeThrottle(request, new 
StreamsGroupHeartbeatResponse(responseData))
@@ -2859,8 +2870,8 @@ class KafkaApis(val requestChannel: RequestChannel,
           if (!topicsToCreate.isEmpty) {
 
             val createTopicUnauthorized =
-              if(!authHelper.authorize(request.context, CREATE, CLUSTER, 
CLUSTER_NAME, logIfDenied = false))
-                authHelper.partitionSeqByAuthorized(request.context, CREATE, 
TOPIC, topicsToCreate.keySet.asScala.toSeq)(identity[String])._2
+              if (!authHelper.authorize(request.context, CREATE, CLUSTER, 
CLUSTER_NAME, true, false, 1))
+                authHelper.partitionByAuthorized(request.context, CREATE, 
TOPIC, topicsToCreate.keySet.stream().toList, (t: String) => 
t).unauthorized.asScala
               else Set.empty
 
             if (createTopicUnauthorized.nonEmpty) {
@@ -3017,7 +3028,7 @@ class KafkaApis(val requestChannel: RequestChannel,
               .asScala
 
             val authorizedTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC,
-              topicsToCheck)(identity)
+              topicsToCheck.asJava, (t: String) => t)
 
               val updatedGroups = response.groups.stream.map { group =>
                 val hasUnauthorizedTopic = if (group.topology == null) false 
else
@@ -3149,7 +3160,7 @@ class KafkaApis(val requestChannel: RequestChannel,
         // Clients are not allowed to see topics that are not authorized for 
Describe.
         val subscribedTopicSet = 
shareGroupHeartbeatRequest.data.subscribedTopicNames.asScala.toSet
         val authorizedTopics = authHelper.filterByAuthorized(request.context, 
DESCRIBE, TOPIC,
-          subscribedTopicSet)(identity)
+          subscribedTopicSet.asJava, (t: String) => t)
         if (authorizedTopics.size < subscribedTopicSet.size) {
           val responseData = new ShareGroupHeartbeatResponseData()
             .setErrorCode(Errors.TOPIC_AUTHORIZATION_FAILED.code)
@@ -3229,7 +3240,7 @@ class KafkaApis(val requestChannel: RequestChannel,
               .collect(Collectors.toSet[String])
               .asScala
             val authorizedTopics = 
authHelper.filterByAuthorized(request.context, DESCRIBE, TOPIC,
-              topicsToCheck)(identity)
+              topicsToCheck.asJava, (t: String) => t)
             val updatedGroups = response.groups.stream().map { group =>
               val hasUnauthorizedTopic = group.members.stream()
                 .flatMap(member => member.assignment.topicPartitions.stream)
@@ -3363,8 +3374,9 @@ class KafkaApis(val requestChannel: RequestChannel,
       request.context,
       READ,
       TOPIC,
-      topicIdPartitionSeq
-    )(_.topicPartition.topic)
+      topicIdPartitionSeq.asJava,
+      (tp: TopicIdPartition) => tp.topicPartition.topic
+    ).asScala
 
     // Variable to store the topic partition wise result of piggybacked 
acknowledgements.
     var acknowledgeResult: CompletableFuture[Map[TopicIdPartition, 
ShareAcknowledgeResponseData.PartitionData]] =
@@ -3688,8 +3700,9 @@ class KafkaApis(val requestChannel: RequestChannel,
       request.context,
       READ,
       TOPIC,
-      topicIdPartitionSeq
-    )(_.topicPartition.topic)
+      topicIdPartitionSeq.asJava,
+      (tp: TopicIdPartition) => tp.topicPartition.topic
+    ).asScala
 
     val erroneous = mutable.Map[TopicIdPartition, 
ShareAcknowledgeResponseData.PartitionData]()
     val acknowledgementDataFromRequest = 
getAcknowledgeBatchesFromShareAcknowledgeRequest(shareAcknowledgeRequest, 
topicIdNames, erroneous)
@@ -3888,13 +3901,15 @@ class KafkaApis(val requestChannel: RequestChannel,
           .setErrorMessage(error.message)
       } else {
         // Clients are not allowed to see offsets for topics that are not 
authorized for Describe.
-        val (authorizedOffsets, _) = authHelper.partitionSeqByAuthorized(
+        val authorizedOffsetsPartition = authHelper.partitionByAuthorized(
           requestContext,
           DESCRIBE,
           TOPIC,
-          groupDescribeOffsetsResponse.topics.asScala
-        )(_.topicName)
-        groupDescribeOffsetsResponse.setTopics(authorizedOffsets.asJava)
+          groupDescribeOffsetsResponse.topics,
+          (t: 
DescribeShareGroupOffsetsResponseData.DescribeShareGroupOffsetsResponseTopic) 
=> t.topicName
+        )
+        val authorizedOffsets = authorizedOffsetsPartition.authorized
+        groupDescribeOffsetsResponse.setTopics(authorizedOffsets)
       }
     }
   }
@@ -3903,18 +3918,21 @@ class KafkaApis(val requestChannel: RequestChannel,
     groupDescribeOffsetsRequest: 
DescribeShareGroupOffsetsRequestData.DescribeShareGroupOffsetsRequestGroup
   ): 
CompletableFuture[DescribeShareGroupOffsetsResponseData.DescribeShareGroupOffsetsResponseGroup]
 = {
     // Clients are not allowed to see offsets for topics that are not 
authorized for Describe.
-    val (authorizedTopics, unauthorizedTopics) = 
authHelper.partitionSeqByAuthorized(
+    val partitionResult = authHelper.partitionByAuthorized(
       requestContext,
       DESCRIBE,
       TOPIC,
-      groupDescribeOffsetsRequest.topics.asScala
-    )(_.topicName)
+      groupDescribeOffsetsRequest.topics,
+      (t: 
DescribeShareGroupOffsetsRequestData.DescribeShareGroupOffsetsRequestTopic) => 
t.topicName
+    )
+    val authorizedTopics = partitionResult.authorized
+    val unauthorizedTopics = partitionResult.unauthorized.asScala
 
     groupCoordinator.describeShareGroupOffsets(
       requestContext,
       new 
DescribeShareGroupOffsetsRequestData.DescribeShareGroupOffsetsRequestGroup()
         .setGroupId(groupDescribeOffsetsRequest.groupId)
-        .setTopics(authorizedTopics.asJava)
+        .setTopics(authorizedTopics)
     
).handle[DescribeShareGroupOffsetsResponseData.DescribeShareGroupOffsetsResponseGroup]
 { (groupDescribeOffsetsResponse, exception) =>
       if (exception != null) {
         val error = Errors.forException(exception)
diff --git 
a/core/src/test/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandlerTest.java
 
b/core/src/test/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandlerTest.java
index 3f20bcae6c3..8f62356eb7e 100644
--- 
a/core/src/test/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandlerTest.java
+++ 
b/core/src/test/java/kafka/server/handlers/DescribeTopicPartitionsRequestHandlerTest.java
@@ -17,7 +17,6 @@
 
 package kafka.server.handlers;
 
-import kafka.server.AuthHelper;
 import kafka.server.KafkaConfig;
 import kafka.utils.TestUtils;
 
@@ -62,6 +61,7 @@ import org.apache.kafka.network.SocketServerConfigs;
 import org.apache.kafka.network.metrics.RequestChannelMetrics;
 import org.apache.kafka.raft.KRaftConfigs;
 import org.apache.kafka.raft.QuorumConfig;
+import org.apache.kafka.server.AuthHelper;
 import org.apache.kafka.server.authorizer.Action;
 import org.apache.kafka.server.authorizer.AuthorizationResult;
 import org.apache.kafka.server.authorizer.Authorizer;
@@ -192,7 +192,7 @@ class DescribeTopicPartitionsRequestHandlerTest {
         KRaftMetadataCache metadataCache = new KRaftMetadataCache(0, () -> 
KRaftVersion.KRAFT_VERSION_1);
         updateKraftMetadataCache(metadataCache, records);
         DescribeTopicPartitionsRequestHandler handler =
-            new DescribeTopicPartitionsRequestHandler(metadataCache, new 
AuthHelper(scala.Option.apply(authorizerPlugin)), createKafkaDefaultConfig());
+            new DescribeTopicPartitionsRequestHandler(metadataCache, new 
AuthHelper(Optional.ofNullable(authorizerPlugin)), createKafkaDefaultConfig());
 
         // 3.1 Basic test
         DescribeTopicPartitionsRequest describeTopicPartitionsRequest = new 
DescribeTopicPartitionsRequest(
@@ -390,7 +390,7 @@ class DescribeTopicPartitionsRequestHandlerTest {
         KRaftMetadataCache metadataCache = new KRaftMetadataCache(0, () -> 
KRaftVersion.KRAFT_VERSION_1);
         updateKraftMetadataCache(metadataCache, records);
         DescribeTopicPartitionsRequestHandler handler =
-            new DescribeTopicPartitionsRequestHandler(metadataCache, new 
AuthHelper(scala.Option.apply(authorizerPlugin)), createKafkaDefaultConfig());
+            new DescribeTopicPartitionsRequestHandler(metadataCache, new 
AuthHelper(Optional.ofNullable(authorizerPlugin)), createKafkaDefaultConfig());
 
         // 3.1 With cursor point to the first one
         DescribeTopicPartitionsRequest describeTopicPartitionsRequest = new 
DescribeTopicPartitionsRequest(new DescribeTopicPartitionsRequestData()
diff --git a/core/src/test/scala/unit/kafka/server/AuthHelperTest.scala 
b/core/src/test/scala/unit/kafka/server/AuthHelperTest.scala
deleted file mode 100644
index 1f08046a113..00000000000
--- a/core/src/test/scala/unit/kafka/server/AuthHelperTest.scala
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * 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 kafka.server
-
-import java.net.InetAddress
-import java.util
-import org.apache.kafka.clients.admin.EndpointType
-import org.apache.kafka.common.acl.AclOperation
-import org.apache.kafka.common.internals.Plugin
-import org.apache.kafka.common.message.{DescribeClusterRequestData, 
DescribeClusterResponseData}
-import 
org.apache.kafka.common.message.DescribeClusterResponseData.DescribeClusterBrokerCollection
-import org.apache.kafka.common.network.{ClientInformation, ListenerName}
-import org.apache.kafka.common.protocol.{ApiKeys, Errors}
-import org.apache.kafka.common.requests.{DescribeClusterRequest, 
RequestContext, RequestHeader}
-import org.apache.kafka.common.resource.{PatternType, ResourcePattern, 
ResourceType}
-import org.apache.kafka.common.security.auth.{KafkaPrincipal, SecurityProtocol}
-import org.apache.kafka.network.Request
-import org.apache.kafka.server.authorizer.{Action, AuthorizationResult, 
Authorizer}
-import org.junit.jupiter.api.Assertions._
-import org.junit.jupiter.api.Test
-import org.mockito.ArgumentMatchers.argThat
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito.{mock, verify, when}
-
-import scala.collection.Seq
-import scala.jdk.CollectionConverters._
-
-object AuthHelperTest {
-  def newMockDescribeClusterRequest(
-    data: DescribeClusterRequestData,
-    requestVersion: Int
-  ): Request = {
-    val requestContext = new RequestContext(
-      new RequestHeader(ApiKeys.DESCRIBE_CLUSTER, requestVersion.toShort, "", 
0),
-      "",
-      InetAddress.getLocalHost,
-      KafkaPrincipal.ANONYMOUS,
-      new ListenerName("PLAINTEXT"),
-      SecurityProtocol.PLAINTEXT,
-      ClientInformation.EMPTY,
-      false)
-    val request: Request = mock(classOf[Request])
-    when(request.body(classOf[DescribeClusterRequest])).thenReturn(
-      new DescribeClusterRequest(data, requestVersion.toShort))
-    when(request.context).thenReturn(requestContext)
-    when(request.header).thenReturn(requestContext.header)
-    request
-  }
-}
-
-class AuthHelperTest {
-  import AuthHelperTest.newMockDescribeClusterRequest
-
-  private val clientId = ""
-
-  @Test
-  def testAuthorize(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-
-    val operation = AclOperation.WRITE
-    val resourceType = ResourceType.TOPIC
-    val resourceName = "topic-1"
-    val requestHeader = new RequestHeader(ApiKeys.PRODUCE, 
ApiKeys.PRODUCE.latestVersion, clientId, 0)
-    val requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost,
-      KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
-      SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false)
-
-    val expectedActions = Seq(
-      new Action(operation, new ResourcePattern(resourceType, resourceName, 
PatternType.LITERAL),
-        1, true, true)
-    )
-
-    when(authorizer.authorize(requestContext, expectedActions.asJava))
-      .thenReturn(Seq(AuthorizationResult.ALLOWED).asJava)
-
-    val result = new AuthHelper(Some(authorizerPlugin)).authorize(
-      requestContext, operation, resourceType, resourceName)
-
-    verify(authorizer).authorize(requestContext, expectedActions.asJava)
-
-    assertEquals(true, result)
-  }
-
-  @Test
-  def testFilterByAuthorized(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-
-    val operation = AclOperation.WRITE
-    val resourceType = ResourceType.TOPIC
-    val resourceName1 = "topic-1"
-    val resourceName2 = "topic-2"
-    val resourceName3 = "topic-3"
-    val requestHeader = new RequestHeader(ApiKeys.PRODUCE, 
ApiKeys.PRODUCE.latestVersion,
-      clientId, 0)
-    val requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost,
-      KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
-      SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false)
-
-    val expectedActions = Seq(
-      new Action(operation, new ResourcePattern(resourceType, resourceName1, 
PatternType.LITERAL),
-        2, true, true),
-      new Action(operation, new ResourcePattern(resourceType, resourceName2, 
PatternType.LITERAL),
-        1, true, true),
-      new Action(operation, new ResourcePattern(resourceType, resourceName3, 
PatternType.LITERAL),
-        1, true, true),
-    )
-
-    when(authorizer.authorize(
-      ArgumentMatchers.eq(requestContext), argThat((t: java.util.List[Action]) 
=> t.containsAll(expectedActions.asJava))
-    )).thenAnswer { invocation =>
-      val actions = 
invocation.getArgument(1).asInstanceOf[util.List[Action]].asScala
-      actions.map { action =>
-        if (Set(resourceName1, 
resourceName3).contains(action.resourcePattern.name))
-          AuthorizationResult.ALLOWED
-        else
-          AuthorizationResult.DENIED
-      }.asJava
-    }
-
-    val result = new AuthHelper(Some(authorizerPlugin)).filterByAuthorized(
-      requestContext,
-      operation,
-      resourceType,
-      // Duplicate resource names should not trigger multiple calls to 
authorize
-      Seq(resourceName1, resourceName2, resourceName1, resourceName3)
-    )(identity)
-
-    verify(authorizer).authorize(
-      ArgumentMatchers.eq(requestContext), argThat((t: java.util.List[Action]) 
=> t.containsAll(expectedActions.asJava))
-    )
-
-    assertEquals(Set(resourceName1, resourceName3), result)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseV1WithUnknownEndpointType(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new DescribeClusterRequestData().setEndpointType(123.toByte), 1)
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.BROKER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => new DescribeClusterBrokerCollection(),
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setErrorCode(Errors.UNSUPPORTED_ENDPOINT_TYPE.code()).
-      setErrorMessage("Unsupported endpoint type 123"), responseData)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseV0WithUnknownEndpointType(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new DescribeClusterRequestData().setEndpointType(123.toByte), 0)
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.BROKER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => new DescribeClusterBrokerCollection(),
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setErrorCode(Errors.INVALID_REQUEST.code()).
-      setErrorMessage("Unsupported endpoint type 123"), responseData)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseV1WithUnexpectedEndpointType(): Unit = 
{
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new 
DescribeClusterRequestData().setEndpointType(EndpointType.BROKER.id()), 1)
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.CONTROLLER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => new DescribeClusterBrokerCollection(),
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setErrorCode(Errors.MISMATCHED_ENDPOINT_TYPE.code()).
-      setErrorMessage("The request was sent to an endpoint of type CONTROLLER, 
but we wanted an endpoint of type BROKER"), responseData)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseV0WithUnexpectedEndpointType(): Unit = 
{
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new 
DescribeClusterRequestData().setEndpointType(EndpointType.BROKER.id()), 0)
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.CONTROLLER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => new DescribeClusterBrokerCollection(),
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setErrorCode(Errors.INVALID_REQUEST.code()).
-      setErrorMessage("The request was sent to an endpoint of type CONTROLLER, 
but we wanted an endpoint of type BROKER"), responseData)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseWhereControllerIsNotFound(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new 
DescribeClusterRequestData().setEndpointType(EndpointType.CONTROLLER.id()), 1)
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.CONTROLLER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => new DescribeClusterBrokerCollection(),
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setClusterId("ltCWoi9wRhmHSQCIgAznEg").
-      setControllerId(-1).
-      setClusterAuthorizedOperations(Int.MinValue).
-      setEndpointType(2.toByte), responseData)
-  }
-
-  @Test
-  def testComputeDescribeClusterResponseSuccess(): Unit = {
-    val authorizer: Authorizer = mock(classOf[Authorizer])
-    val authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name")
-    val authHelper = new AuthHelper(Some(authorizerPlugin))
-    val request = newMockDescribeClusterRequest(
-      new 
DescribeClusterRequestData().setEndpointType(EndpointType.CONTROLLER.id()), 1)
-    val nodes = new DescribeClusterBrokerCollection(
-      
java.util.Arrays.asList[DescribeClusterResponseData.DescribeClusterBroker](
-        new 
DescribeClusterResponseData.DescribeClusterBroker().setBrokerId(1)))
-    val responseData = authHelper.computeDescribeClusterResponse(request,
-      EndpointType.CONTROLLER,
-      "ltCWoi9wRhmHSQCIgAznEg",
-      () => nodes,
-      () => 1)
-    assertEquals(new DescribeClusterResponseData().
-      setClusterId("ltCWoi9wRhmHSQCIgAznEg").
-      setControllerId(1).
-      setClusterAuthorizedOperations(Int.MinValue).
-      setBrokers(nodes).
-      setEndpointType(2.toByte), responseData)
-  }
-}
diff --git a/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala 
b/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
index 027cea189c0..fd189d1c95e 100644
--- a/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
+++ b/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
@@ -738,8 +738,8 @@ class ControllerApisTest {
         setErrorMessage(s"Creation of internal topic 
${Topic.CLUSTER_METADATA_TOPIC_NAME} is prohibited."))
     assertEquals(expectedResponse, 
controllerApis.createTopics(ANONYMOUS_CONTEXT, request,
       hasClusterAuth = false,
-      _ => Set("baz", "indescribable"),
-      _ => Set("baz"),
+      _ => util.Set.of("baz", "indescribable"),
+      _ => util.Set.of("baz"),
       forwarded = false).get().topics().asScala.toSet)
   }
 
@@ -788,8 +788,8 @@ class ControllerApisTest {
     assertEquals(expectedResponse, 
controllerApis.deleteTopics(ANONYMOUS_CONTEXT, request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = true,
-      _ => Set.empty,
-      _ => Set.empty).get().asScala.toSet)
+      _ => util.Set.of[String](),
+      _ => util.Set.of[String]()).get().asScala.toSet)
   }
 
   @Test
@@ -814,8 +814,8 @@ class ControllerApisTest {
     assertEquals(response, controllerApis.deleteTopics(ANONYMOUS_CONTEXT, 
request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = true,
-      _ => Set.empty,
-      _ => Set.empty).get().asScala.toSet)
+      _ => util.Set.of[String](),
+      _ => util.Set.of[String]()).get().asScala.toSet)
   }
 
   @Test
@@ -856,8 +856,8 @@ class ControllerApisTest {
     assertEquals(response, controllerApis.deleteTopics(ANONYMOUS_CONTEXT, 
request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = false,
-      names => names.toSet,
-      names => names.toSet).get().asScala.toSet)
+      names => names.asScala.toSet.asJava,
+      names => names.asScala.toSet.asJava).get().asScala.toSet)
   }
 
   @Test
@@ -892,8 +892,8 @@ class ControllerApisTest {
     assertEquals(response, controllerApis.deleteTopics(ANONYMOUS_CONTEXT, 
request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = false,
-      _ => Set("foo", "baz"),
-      _ => Set.empty).get().asScala.toSet)
+      _ => util.Set.of("foo", "baz"),
+      _ => util.Set.of[String]()).get().asScala.toSet)
   }
 
   @Test
@@ -917,8 +917,8 @@ class ControllerApisTest {
     assertEquals(expectedResponse, 
controllerApis.deleteTopics(ANONYMOUS_CONTEXT, request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = false,
-      _ => Set("foo"),
-      _ => Set.empty).get().asScala.toSet)
+      _ => util.Set.of("foo"),
+      _ => util.Set.of[String]()).get().asScala.toSet)
   }
 
   @Test
@@ -936,8 +936,8 @@ class ControllerApisTest {
       classOf[ExecutionException], () => 
controllerApis.deleteTopics(ANONYMOUS_CONTEXT, request,
         ApiKeys.DELETE_TOPICS.latestVersion().toInt,
         hasClusterAuth = false,
-        _ => Set("foo", "bar"),
-        _ => Set("foo", "bar")).get()).getCause.getClass)
+        _ => util.Set.of("foo", "bar"),
+        _ => util.Set.of("foo", "bar")).get()).getCause.getClass)
   }
 
   @Test
@@ -954,14 +954,14 @@ class ControllerApisTest {
     TestUtils.assertFutureThrows(classOf[TopicDeletionDisabledException], 
controllerApis.deleteTopics(ANONYMOUS_CONTEXT, request,
       ApiKeys.DELETE_TOPICS.latestVersion().toInt,
       hasClusterAuth = false,
-      _ => Set("foo", "bar"),
-      _ => Set("foo", "bar")))
+      _ => util.Set.of("foo", "bar"),
+      _ => util.Set.of("foo", "bar")))
 
     TestUtils.assertFutureThrows(classOf[InvalidRequestException], 
controllerApis.deleteTopics(ANONYMOUS_CONTEXT, request,
       1,
       hasClusterAuth = false,
-      _ => Set("foo", "bar"),
-      _ => Set("foo", "bar")))
+      _ => util.Set.of("foo", "bar"),
+      _ => util.Set.of("foo", "bar")))
   }
 
   @ParameterizedTest
@@ -999,7 +999,7 @@ class ControllerApisTest {
         setErrorCode(TOPIC_AUTHORIZATION_FAILED.code()).
         setErrorMessage(null)),
       controllerApis.createPartitions(ANONYMOUS_CONTEXT, request,
-        _ => Set("foo", "bar")).get().asScala.toSet)
+        _ => util.Set.of("foo", "bar")).get().asScala.toSet)
   }
 
   @Test
diff --git a/server/src/main/java/org/apache/kafka/server/AuthHelper.java 
b/server/src/main/java/org/apache/kafka/server/AuthHelper.java
new file mode 100644
index 00000000000..8a3d2d6983f
--- /dev/null
+++ b/server/src/main/java/org/apache/kafka/server/AuthHelper.java
@@ -0,0 +1,266 @@
+/*
+ * 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.server;
+
+import org.apache.kafka.clients.admin.EndpointType;
+import org.apache.kafka.common.acl.AclOperation;
+import org.apache.kafka.common.errors.ClusterAuthorizationException;
+import org.apache.kafka.common.internals.Plugin;
+import org.apache.kafka.common.message.DescribeClusterResponseData;
+import 
org.apache.kafka.common.message.DescribeClusterResponseData.DescribeClusterBrokerCollection;
+import org.apache.kafka.common.protocol.Errors;
+import org.apache.kafka.common.requests.DescribeClusterRequest;
+import org.apache.kafka.common.requests.RequestContext;
+import org.apache.kafka.common.resource.PatternType;
+import org.apache.kafka.common.resource.Resource;
+import org.apache.kafka.common.resource.ResourcePattern;
+import org.apache.kafka.common.resource.ResourceType;
+import org.apache.kafka.common.utils.Utils;
+import org.apache.kafka.network.Request;
+import org.apache.kafka.security.authorizer.AclEntry;
+import org.apache.kafka.server.authorizer.Action;
+import org.apache.kafka.server.authorizer.AuthorizationResult;
+import org.apache.kafka.server.authorizer.Authorizer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class AuthHelper {
+
+    private final Optional<Plugin<Authorizer>> authorizer;
+
+    public AuthHelper(Optional<Plugin<Authorizer>> authorizer) {
+        this.authorizer = authorizer;
+    }
+
+    public boolean authorize(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType,
+        String resourceName,
+        boolean logIfAllowed,
+        boolean logIfDenied,
+        int refCount
+    ) {
+        if (authorizer.isEmpty()) {
+            return true;
+        }
+        ResourcePattern resource = new ResourcePattern(resourceType, 
resourceName, PatternType.LITERAL);
+        List<Action> actions = List.of(
+            new Action(operation, resource, refCount, logIfAllowed, 
logIfDenied)
+        );
+        return authorizer.get().get().authorize(requestContext, 
actions).get(0) == AuthorizationResult.ALLOWED;
+    }
+
+    public boolean authorize(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType,
+        String resourceName
+    ) {
+        return authorize(requestContext, operation, resourceType, 
resourceName, true, true, 1);
+    }
+
+    public void authorizeClusterOperation(Request request, AclOperation 
operation) {
+        if (!authorize(request.context(), operation, ResourceType.CLUSTER, 
Resource.CLUSTER_NAME)) {
+            throw new ClusterAuthorizationException("Request " + request + " 
needs " + operation + " permission.");
+        }
+    }
+
+    public int authorizedOperations(Request request, Resource resource) {
+        List<AclOperation> supportedOps = new 
ArrayList<>(AclEntry.supportedOperations(resource.resourceType()));
+        Set<AclOperation> authorizedOps;
+        if (authorizer.isPresent()) {
+            ResourcePattern resourcePattern = new 
ResourcePattern(resource.resourceType(), resource.name(), PatternType.LITERAL);
+            List<Action> actions = supportedOps.stream()
+                .map(op -> new Action(op, resourcePattern, 1, false, false))
+                .toList();
+            List<AuthorizationResult> results = 
authorizer.get().get().authorize(request.context(), actions);
+            authorizedOps = new HashSet<>();
+            // Authorizer.authorize returns one result per action in the same 
order, so the i-th
+            // result corresponds to the i-th supported operation. Iterate up 
to the smaller size to
+            // stay within bounds.
+            int count = Math.min(results.size(), supportedOps.size());
+            for (int i = 0; i < count; i++) {
+                if (results.get(i) == AuthorizationResult.ALLOWED) {
+                    authorizedOps.add(supportedOps.get(i));
+                }
+            }
+        } else {
+            authorizedOps = new HashSet<>(supportedOps);
+        }
+        Set<Byte> opCodes = authorizedOps.stream()
+            .map(AclOperation::code)
+            .collect(Collectors.toSet());
+        return Utils.to32BitField(opCodes);
+    }
+
+    public boolean authorizeByResourceType(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType
+    ) {
+        return authorizer.map(authorizerPlugin -> 
authorizerPlugin.get().authorizeByResourceType(requestContext, operation, 
resourceType) == AuthorizationResult.ALLOWED).orElse(true);
+    }
+
+    public <T> Set<String> filterByAuthorized(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType,
+        Iterable<T> resources,
+        boolean logIfAllowed,
+        boolean logIfDenied,
+        Function<T, String> resourceName
+    ) {
+        if (authorizer.isEmpty()) {
+            Set<String> result = new HashSet<>();
+            for (T resource : resources) {
+                result.add(resourceName.apply(resource));
+            }
+            return result;
+        }
+        // Count occurrences of each resource name
+        Map<String, Integer> resourceNameToCount = new HashMap<>();
+        for (T resource : resources) {
+            String name = resourceName.apply(resource);
+            resourceNameToCount.merge(name, 1, Integer::sum);
+        }
+        if (resourceNameToCount.isEmpty()) {
+            return Set.of();
+        }
+
+        List<String> names = new ArrayList<>(resourceNameToCount.keySet());
+        List<Action> actions = names.stream()
+            .map(name -> new Action(
+                operation,
+                new ResourcePattern(resourceType, name, PatternType.LITERAL),
+                resourceNameToCount.get(name),
+                logIfAllowed,
+                logIfDenied
+            ))
+            .toList();
+        List<AuthorizationResult> results = 
authorizer.get().get().authorize(requestContext, actions);
+        Set<String> authorized = new HashSet<>();
+        // Authorizer.authorize returns one result per action in the same 
order, so the i-th
+        // result corresponds to the i-th resource name. Iterate up to the 
smaller size to
+        // stay within bounds.
+        int count = Math.min(results.size(), names.size());
+        for (int i = 0; i < count; i++) {
+            if (results.get(i) == AuthorizationResult.ALLOWED) {
+                authorized.add(names.get(i));
+            }
+        }
+        return authorized;
+    }
+
+    public <T> Set<String> filterByAuthorized(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType,
+        Iterable<T> resources,
+        Function<T, String> resourceName
+    ) {
+        return filterByAuthorized(requestContext, operation, resourceType, 
resources, true, true, resourceName);
+    }
+
+    public record PartitionResult<T>(List<T> authorized, List<T> unauthorized) 
{
+    }
+
+    public <T> PartitionResult<T> partitionByAuthorized(
+        RequestContext requestContext,
+        AclOperation operation,
+        ResourceType resourceType,
+        List<T> resources,
+        Function<T, String> resourceName
+    ) {
+        if (authorizer.isEmpty()) {
+            return new PartitionResult<>(resources, List.of());
+        }
+        Set<String> authorizedResourceNames = filterByAuthorized(
+            requestContext, operation, resourceType, resources, true, true, 
resourceName
+        );
+        List<T> authorized = new ArrayList<>();
+        List<T> unauthorized = new ArrayList<>();
+        for (T resource : resources) {
+            if 
(authorizedResourceNames.contains(resourceName.apply(resource))) {
+                authorized.add(resource);
+            } else {
+                unauthorized.add(resource);
+            }
+        }
+        return new PartitionResult<>(authorized, unauthorized);
+    }
+
+    public DescribeClusterResponseData computeDescribeClusterResponse(
+        Request request,
+        EndpointType expectedEndpointType,
+        String clusterId,
+        Supplier<DescribeClusterBrokerCollection> getNodes,
+        Supplier<Integer> getControllerId
+    ) {
+        DescribeClusterRequest describeClusterRequest = 
request.body(DescribeClusterRequest.class);
+        EndpointType requestEndpointType = 
EndpointType.fromId(describeClusterRequest.data().endpointType());
+        if (requestEndpointType.equals(EndpointType.UNKNOWN)) {
+            return new DescribeClusterResponseData()
+                .setErrorCode(request.header().data().requestApiVersion() == 0
+                    ? Errors.INVALID_REQUEST.code()
+                    : Errors.UNSUPPORTED_ENDPOINT_TYPE.code())
+                .setErrorMessage("Unsupported endpoint type " + (int) 
describeClusterRequest.data().endpointType());
+        } else if (!expectedEndpointType.equals(requestEndpointType)) {
+            return new DescribeClusterResponseData()
+                .setErrorCode(request.header().data().requestApiVersion() == 0
+                    ? Errors.INVALID_REQUEST.code()
+                    : Errors.MISMATCHED_ENDPOINT_TYPE.code())
+                .setErrorMessage("The request was sent to an endpoint of type 
" + expectedEndpointType +
+                    ", but we wanted an endpoint of type " + 
requestEndpointType);
+        }
+
+        int clusterAuthorizedOperations = Integer.MIN_VALUE; // Default value 
in the schema
+        // get cluster authorized operations
+        if 
(describeClusterRequest.data().includeClusterAuthorizedOperations()) {
+            if (authorize(request.context(), AclOperation.DESCRIBE, 
ResourceType.CLUSTER, Resource.CLUSTER_NAME)) {
+                clusterAuthorizedOperations = authorizedOperations(request, 
Resource.CLUSTER);
+            } else {
+                clusterAuthorizedOperations = 0;
+            }
+        }
+
+        // Get the node list and the controller ID.
+        DescribeClusterBrokerCollection nodes = getNodes.get();
+        int controllerId = getControllerId.get();
+        // If the provided controller ID is not in the node list, return -1 
instead
+        // to avoid confusing the client. This could happen in a case where we 
know
+        // the controller ID, but we don't yet have KIP-919 information about 
that controller.
+        int effectiveControllerId = (nodes.find(controllerId) == null) ? -1 : 
controllerId;
+
+        return new DescribeClusterResponseData()
+            .setClusterId(clusterId)
+            .setControllerId(effectiveControllerId)
+            .setClusterAuthorizedOperations(clusterAuthorizedOperations)
+            .setBrokers(nodes)
+            .setEndpointType(expectedEndpointType.id());
+    }
+}
diff --git a/server/src/test/java/org/apache/kafka/server/AuthHelperTest.java 
b/server/src/test/java/org/apache/kafka/server/AuthHelperTest.java
new file mode 100644
index 00000000000..7d947f75d80
--- /dev/null
+++ b/server/src/test/java/org/apache/kafka/server/AuthHelperTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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.server;
+
+import org.apache.kafka.clients.admin.EndpointType;
+import org.apache.kafka.common.acl.AclOperation;
+import org.apache.kafka.common.internals.Plugin;
+import org.apache.kafka.common.message.DescribeClusterRequestData;
+import org.apache.kafka.common.message.DescribeClusterResponseData;
+import 
org.apache.kafka.common.message.DescribeClusterResponseData.DescribeClusterBroker;
+import 
org.apache.kafka.common.message.DescribeClusterResponseData.DescribeClusterBrokerCollection;
+import org.apache.kafka.common.network.ClientInformation;
+import org.apache.kafka.common.network.ListenerName;
+import org.apache.kafka.common.protocol.ApiKeys;
+import org.apache.kafka.common.protocol.Errors;
+import org.apache.kafka.common.requests.DescribeClusterRequest;
+import org.apache.kafka.common.requests.RequestContext;
+import org.apache.kafka.common.requests.RequestHeader;
+import org.apache.kafka.common.resource.PatternType;
+import org.apache.kafka.common.resource.ResourcePattern;
+import org.apache.kafka.common.resource.ResourceType;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+import org.apache.kafka.common.security.auth.SecurityProtocol;
+import org.apache.kafka.network.Request;
+import org.apache.kafka.server.authorizer.Action;
+import org.apache.kafka.server.authorizer.AuthorizationResult;
+import org.apache.kafka.server.authorizer.Authorizer;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AuthHelperTest {
+
+    private Authorizer authorizer;
+    private Plugin<Authorizer> authorizerPlugin;
+
+    @BeforeEach
+    public void setUp() {
+        authorizer = mock(Authorizer.class);
+        authorizerPlugin = Plugin.wrapInstance(authorizer, null, 
"authorizer.class.name");
+    }
+
+    private static Request 
newMockDescribeClusterRequest(DescribeClusterRequestData data, int 
requestVersion)
+        throws UnknownHostException {
+        RequestContext requestContext = new RequestContext(
+            new RequestHeader(ApiKeys.DESCRIBE_CLUSTER, (short) 
requestVersion, "", 0),
+            "",
+            InetAddress.getLocalHost(),
+            KafkaPrincipal.ANONYMOUS,
+            new ListenerName("PLAINTEXT"),
+            SecurityProtocol.PLAINTEXT,
+            ClientInformation.EMPTY,
+            false);
+        Request request = mock(Request.class);
+        when(request.body(DescribeClusterRequest.class)).thenReturn(
+            new DescribeClusterRequest(data, (short) requestVersion));
+        when(request.context()).thenReturn(requestContext);
+        when(request.header()).thenReturn(requestContext.header);
+        return request;
+    }
+
+    @Test
+    public void testAuthorize() throws UnknownHostException {
+        AclOperation operation = AclOperation.WRITE;
+        ResourceType resourceType = ResourceType.TOPIC;
+        String resourceName = "topic-1";
+        RequestHeader requestHeader = new RequestHeader(ApiKeys.PRODUCE, 
ApiKeys.PRODUCE.latestVersion(), "", 0);
+        RequestContext requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost(),
+            KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
+            SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false);
+
+        List<Action> expectedActions = List.of(
+            new Action(operation, new ResourcePattern(resourceType, 
resourceName, PatternType.LITERAL), 1, true, true)
+        );
+
+        when(authorizer.authorize(requestContext, expectedActions))
+            .thenReturn(List.of(AuthorizationResult.ALLOWED));
+
+        boolean result = new 
AuthHelper(Optional.of(authorizerPlugin)).authorize(
+            requestContext, operation, resourceType, resourceName);
+
+        verify(authorizer).authorize(requestContext, expectedActions);
+        assertTrue(result);
+    }
+
+    @Test
+    public void testFilterByAuthorized() throws UnknownHostException {
+        AclOperation operation = AclOperation.WRITE;
+        ResourceType resourceType = ResourceType.TOPIC;
+        String resourceName1 = "topic-1";
+        String resourceName2 = "topic-2";
+        String resourceName3 = "topic-3";
+        RequestHeader requestHeader = new RequestHeader(ApiKeys.PRODUCE, 
ApiKeys.PRODUCE.latestVersion(), "", 0);
+        RequestContext requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost(),
+            KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
+            SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false);
+
+        List<Action> expectedActions = List.of(
+            new Action(operation, new ResourcePattern(resourceType, 
resourceName1, PatternType.LITERAL), 2, true, true),
+            new Action(operation, new ResourcePattern(resourceType, 
resourceName2, PatternType.LITERAL), 1, true, true),
+            new Action(operation, new ResourcePattern(resourceType, 
resourceName3, PatternType.LITERAL), 1, true, true)
+        );
+
+        when(authorizer.authorize(
+            ArgumentMatchers.eq(requestContext),
+            argThat(t -> t.containsAll(expectedActions))
+        )).thenAnswer(invocation -> {
+            List<Action> actions = invocation.getArgument(1);
+            return actions.stream().map(action -> {
+                String name = action.resourcePattern().name();
+                if (name.equals(resourceName1) || name.equals(resourceName3))
+                    return AuthorizationResult.ALLOWED;
+                else
+                    return AuthorizationResult.DENIED;
+            }).toList();
+        });
+
+        // Duplicate resource names should not trigger multiple calls to 
authorize
+        Set<String> result = new 
AuthHelper(Optional.of(authorizerPlugin)).filterByAuthorized(
+            requestContext,
+            operation,
+            resourceType,
+            List.of(resourceName1, resourceName2, resourceName1, 
resourceName3),
+            s -> s
+        );
+
+        verify(authorizer).authorize(
+            ArgumentMatchers.eq(requestContext),
+            argThat(t -> t.containsAll(expectedActions))
+        );
+
+        assertEquals(Set.of(resourceName1, resourceName3), result);
+    }
+
+    @Test
+    public void 
testFilterByAuthorizedIsResilientToMismatchedAuthorizeResults() throws 
UnknownHostException {
+        AclOperation operation = AclOperation.WRITE;
+        ResourceType resourceType = ResourceType.TOPIC;
+        RequestHeader requestHeader = new RequestHeader(ApiKeys.PRODUCE, 
ApiKeys.PRODUCE.latestVersion(), "", 0);
+        RequestContext requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost(),
+            KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
+            SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false);
+
+        // A single distinct resource produces a single action, but a 
misbehaving authorizer may return
+        // a result list of a different size. The original Scala 
implementation used `zip`, which tolerated
+        // this by truncating to the shorter sequence. Verify we keep that 
behavior instead of failing with
+        // IndexOutOfBoundsException.
+        when(authorizer.authorize(ArgumentMatchers.eq(requestContext), 
ArgumentMatchers.any()))
+            .thenReturn(List.of(AuthorizationResult.ALLOWED, 
AuthorizationResult.DENIED));
+
+        Set<String> result = new 
AuthHelper(Optional.of(authorizerPlugin)).filterByAuthorized(
+            requestContext, operation, resourceType, List.of("topic-1"), s -> 
s);
+
+        assertEquals(Set.of("topic-1"), result);
+    }
+
+    @Test
+    public void testPartitionByAuthorized() throws UnknownHostException {
+        AclOperation operation = AclOperation.DESCRIBE;
+        ResourceType resourceType = ResourceType.TOPIC;
+        RequestHeader requestHeader = new RequestHeader(ApiKeys.METADATA, 
ApiKeys.METADATA.latestVersion(), "", 0);
+        RequestContext requestContext = new RequestContext(requestHeader, "1", 
InetAddress.getLocalHost(),
+            KafkaPrincipal.ANONYMOUS, 
ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
+            SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false);
+
+        when(authorizer.authorize(ArgumentMatchers.eq(requestContext), 
ArgumentMatchers.any()))
+            .thenAnswer(invocation -> {
+                List<Action> actions = invocation.getArgument(1);
+                return actions.stream().map(action -> {
+                    String name = action.resourcePattern().name();
+                    if (name.equals("topic-1") || name.equals("topic-3"))
+                        return AuthorizationResult.ALLOWED;
+                    else
+                        return AuthorizationResult.DENIED;
+                }).toList();
+            });
+
+        AuthHelper.PartitionResult<String> result = new 
AuthHelper(Optional.of(authorizerPlugin)).partitionByAuthorized(
+            requestContext, operation, resourceType, List.of("topic-1", 
"topic-2", "topic-3"), s -> s);
+
+        // The original input order should be preserved within each partition.
+        assertEquals(List.of("topic-1", "topic-3"), result.authorized());
+        assertEquals(List.of("topic-2"), result.unauthorized());
+    }
+
+    @Test
+    public void testPartitionByAuthorizedWithoutAuthorizer() {
+        List<String> resources = List.of("topic-1", "topic-2", "topic-3");
+        // The request context is unused when there is no authorizer, so it 
can be null here.
+        AuthHelper.PartitionResult<String> result = new 
AuthHelper(Optional.empty()).partitionByAuthorized(
+            null, AclOperation.DESCRIBE, ResourceType.TOPIC, resources, s -> 
s);
+
+        // Without an authorizer everything is authorized.
+        assertEquals(resources, result.authorized());
+        assertTrue(result.unauthorized().isEmpty());
+    }
+
+    @Test
+    public void testComputeDescribeClusterResponseV1WithUnknownEndpointType() 
throws UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new DescribeClusterRequestData().setEndpointType((byte) 123), 1);
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.BROKER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            DescribeClusterBrokerCollection::new,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+            .setErrorCode(Errors.UNSUPPORTED_ENDPOINT_TYPE.code())
+            .setErrorMessage("Unsupported endpoint type 123"), responseData);
+    }
+
+    @Test
+    public void testComputeDescribeClusterResponseV0WithUnknownEndpointType() 
throws UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new DescribeClusterRequestData().setEndpointType((byte) 123), 0);
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.BROKER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            DescribeClusterBrokerCollection::new,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+            .setErrorCode(Errors.INVALID_REQUEST.code())
+            .setErrorMessage("Unsupported endpoint type 123"), responseData);
+    }
+
+    @Test
+    public void 
testComputeDescribeClusterResponseV1WithUnexpectedEndpointType() throws 
UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new 
DescribeClusterRequestData().setEndpointType(EndpointType.BROKER.id()), 1);
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.CONTROLLER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            DescribeClusterBrokerCollection::new,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+                .setErrorCode(Errors.MISMATCHED_ENDPOINT_TYPE.code())
+                .setErrorMessage("The request was sent to an endpoint of type 
CONTROLLER, but we wanted an endpoint of type BROKER"),
+            responseData);
+    }
+
+    @Test
+    public void 
testComputeDescribeClusterResponseV0WithUnexpectedEndpointType() throws 
UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new 
DescribeClusterRequestData().setEndpointType(EndpointType.BROKER.id()), 0);
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.CONTROLLER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            DescribeClusterBrokerCollection::new,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+                .setErrorCode(Errors.INVALID_REQUEST.code())
+                .setErrorMessage("The request was sent to an endpoint of type 
CONTROLLER, but we wanted an endpoint of type BROKER"),
+            responseData);
+    }
+
+    @Test
+    public void testComputeDescribeClusterResponseWhereControllerIsNotFound() 
throws UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new 
DescribeClusterRequestData().setEndpointType(EndpointType.CONTROLLER.id()), 1);
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.CONTROLLER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            DescribeClusterBrokerCollection::new,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+            .setClusterId("ltCWoi9wRhmHSQCIgAznEg")
+            .setControllerId(-1)
+            .setClusterAuthorizedOperations(Integer.MIN_VALUE)
+            .setEndpointType((byte) 2), responseData);
+    }
+
+    @Test
+    public void testComputeDescribeClusterResponseSuccess() throws 
UnknownHostException {
+        AuthHelper authHelper = new AuthHelper(Optional.of(authorizerPlugin));
+        Request request = newMockDescribeClusterRequest(
+            new 
DescribeClusterRequestData().setEndpointType(EndpointType.CONTROLLER.id()), 1);
+        DescribeClusterBrokerCollection nodes = new 
DescribeClusterBrokerCollection(
+            List.of(new DescribeClusterBroker().setBrokerId(1)).iterator());
+        DescribeClusterResponseData responseData = 
authHelper.computeDescribeClusterResponse(request,
+            EndpointType.CONTROLLER,
+            "ltCWoi9wRhmHSQCIgAznEg",
+            () -> nodes,
+            () -> 1);
+        assertEquals(new DescribeClusterResponseData()
+            .setClusterId("ltCWoi9wRhmHSQCIgAznEg")
+            .setControllerId(1)
+            .setClusterAuthorizedOperations(Integer.MIN_VALUE)
+            .setBrokers(nodes)
+            .setEndpointType((byte) 2), responseData);
+    }
+}


Reply via email to