Create a Delegation Token.
+ *
+ * This is a convenience method for {@link #createDelegationToken(CreateDelegationTokenOptions)} with default options.
+ * See the overload for more details.
+ *
+ * @return The CreateDelegationTokenResult.
+ */
+ public CreateDelegationTokenResult createDelegationToken() {
+ return createDelegationToken(new CreateDelegationTokenOptions());
+ }
+
+
+ /**
+ * Create a Delegation Token.
+ *
+ * This operation is supported by brokers with version 1.1.0 or higher.
+ *
+ * The following exceptions can be anticipated when calling {@code get()} on the futures obtained from the
+ * {@link CreateDelegationTokenResult#delegationToken() delegationToken()} method of the returned {@code CreateDelegationTokenResult}
+ * Renew a Delegation Token.
+ *
+ * This is a convenience method for {@link #renewDelegationToken(byte[], RenewDelegationTokenOptions)} with default options.
+ * See the overload for more details.
+ *
+ *
+ * @param hmac HMAC of the Delegation token
+ * @return The RenewDelegationTokenResult.
+ */
+ public RenewDelegationTokenResult renewDelegationToken(byte[] hmac) {
+ return renewDelegationToken(hmac, new RenewDelegationTokenOptions());
+ }
+
+ /**
+ * Renew a Delegation Token.
+ *
+ * This operation is supported by brokers with version 1.1.0 or higher.
+ *
+ * The following exceptions can be anticipated when calling {@code get()} on the futures obtained from the
+ * {@link RenewDelegationTokenResult#expiryTimestamp() expiryTimestamp()} method of the returned {@code RenewDelegationTokenResult}
+ * Expire a Delegation Token.
+ *
+ * This is a convenience method for {@link #expireDelegationToken(byte[], ExpireDelegationTokenOptions)} with default options.
+ * This will expire the token immediately. See the overload for more details.
+ *
+ * @param hmac HMAC of the Delegation token
+ * @return The ExpireDelegationTokenResult.
+ */
+ public ExpireDelegationTokenResult expireDelegationToken(byte[] hmac) {
+ return expireDelegationToken(hmac, new ExpireDelegationTokenOptions());
+ }
+
+ /**
+ * Expire a Delegation Token.
+ *
+ * This operation is supported by brokers with version 1.1.0 or higher.
+ *
+ * The following exceptions can be anticipated when calling {@code get()} on the futures obtained from the
+ * {@link ExpireDelegationTokenResult#expiryTimestamp() expiryTimestamp()} method of the returned {@code ExpireDelegationTokenResult}
+ * Describe the Delegation Tokens.
+ *
+ * This is a convenience method for {@link #describeDelegationToken(DescribeDelegationTokenOptions)} with default options.
+ * This will return all the user owned tokens and tokens where user have Describe permission. See the overload for more details.
+ *
+ * @return The DescribeDelegationTokenResult.
+ */
+ public DescribeDelegationTokenResult describeDelegationToken() {
+ return describeDelegationToken(new DescribeDelegationTokenOptions());
+ }
+
+ /**
+ * Describe the Delegation Tokens.
+ *
+ * This operation is supported by brokers with version 1.1.0 or higher.
+ *
+ * The following exceptions can be anticipated when calling {@code get()} on the futures obtained from the
+ * {@link DescribeDelegationTokenResult#delegationTokens() delegationTokens()} method of the returned {@code DescribeDelegationTokenResult}
+ * groupIds,
+ DescribeConsumerGroupsOptions options);
+
+ /**
+ * Describe some group IDs in the cluster, with the default options.
+ *
+ * This is a convenience method for
+ * #{@link AdminClient#describeConsumerGroups(Collection, DescribeConsumerGroupsOptions)} with
+ * default options. See the overload for more details.
+ *
+ * @param groupIds The IDs of the groups to describe.
+ * @return The DescribeConsumerGroupResult.
+ */
+ public DescribeConsumerGroupsResult describeConsumerGroups(Collection groupIds) {
+ return describeConsumerGroups(groupIds, new DescribeConsumerGroupsOptions());
+ }
+
+ /**
+ * List the consumer groups available in the cluster.
+ *
+ * @param options The options to use when listing the consumer groups.
+ * @return The ListGroupsResult.
+ */
+ public abstract ListConsumerGroupsResult listConsumerGroups(ListConsumerGroupsOptions options);
+
+ /**
+ * List the consumer groups available in the cluster with the default options.
+ *
+ * This is a convenience method for #{@link AdminClient#listConsumerGroups(ListConsumerGroupsOptions)} with default options.
+ * See the overload for more details.
+ *
+ * @return The ListGroupsResult.
+ */
+ public ListConsumerGroupsResult listConsumerGroups() {
+ return listConsumerGroups(new ListConsumerGroupsOptions());
+ }
+
+ /**
+ * List the consumer group offsets available in the cluster.
+ *
+ * @param options The options to use when listing the consumer group offsets.
+ * @return The ListGroupOffsetsResult
+ */
+ public abstract ListConsumerGroupOffsetsResult listConsumerGroupOffsets(String groupId, ListConsumerGroupOffsetsOptions options);
+
+ /**
+ * List the consumer group offsets available in the cluster with the default options.
+ *
+ * This is a convenience method for #{@link AdminClient#listConsumerGroupOffsets(String, ListConsumerGroupOffsetsOptions)} with default options.
+ *
+ * @return The ListGroupOffsetsResult.
+ */
+ public ListConsumerGroupOffsetsResult listConsumerGroupOffsets(String groupId) {
+ return listConsumerGroupOffsets(groupId, new ListConsumerGroupOffsetsOptions());
+ }
+
+ /**
+ * Delete consumer groups from the cluster.
+ *
+ * @param options The options to use when deleting a consumer group.
+ * @return The DeletConsumerGroupResult.
+ */
+ public abstract DeleteConsumerGroupsResult deleteConsumerGroups(Collection groupIds, DeleteConsumerGroupsOptions options);
+
+ /**
+ * Delete consumer groups from the cluster with the default options.
+ *
+ * @return The DeleteConsumerGroupResult.
+ */
+ public DeleteConsumerGroupsResult deleteConsumerGroups(Collection groupIds) {
+ return deleteConsumerGroups(groupIds, new DeleteConsumerGroupsOptions());
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupDescription.java b/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupDescription.java
new file mode 100644
index 0000000000000..0bfa8a782d5ac
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupDescription.java
@@ -0,0 +1,104 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.utils.Utils;
+
+import java.util.List;
+
+/**
+ * A detailed description of a single consumer group in the cluster.
+ */
+public class ConsumerGroupDescription {
+
+ private final String groupId;
+ private final boolean isSimpleConsumerGroup;
+ private final List members;
+ private final String partitionAssignor;
+
+ /**
+ * Creates an instance with the specified parameters.
+ *
+ * @param groupId The consumer group id
+ * @param isSimpleConsumerGroup If Consumer Group is simple
+ * @param members The consumer group members
+ * @param partitionAssignor The consumer group partition assignor
+ */
+ public ConsumerGroupDescription(String groupId, boolean isSimpleConsumerGroup, List members, String partitionAssignor) {
+ this.groupId = groupId;
+ this.isSimpleConsumerGroup = isSimpleConsumerGroup;
+ this.members = members;
+ this.partitionAssignor = partitionAssignor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ConsumerGroupDescription that = (ConsumerGroupDescription) o;
+
+ if (isSimpleConsumerGroup != that.isSimpleConsumerGroup) return false;
+ if (groupId != null ? !groupId.equals(that.groupId) : that.groupId != null) return false;
+ if (members != null ? !members.equals(that.members) : that.members != null) return false;
+ return partitionAssignor != null ? partitionAssignor.equals(that.partitionAssignor) : that.partitionAssignor == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = groupId != null ? groupId.hashCode() : 0;
+ result = 31 * result + (isSimpleConsumerGroup ? 1 : 0);
+ result = 31 * result + (members != null ? members.hashCode() : 0);
+ result = 31 * result + (partitionAssignor != null ? partitionAssignor.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * The id of the consumer group.
+ */
+ public String groupId() {
+ return groupId;
+ }
+
+ /**
+ * If consumer group is simple or not.
+ */
+ public boolean isSimpleConsumerGroup() {
+ return isSimpleConsumerGroup;
+ }
+
+ /**
+ * A list of the members of the consumer group.
+ */
+ public List members() {
+ return members;
+ }
+
+ /**
+ * The consumer group partition assignor.
+ */
+ public String partitionAssignor() {
+ return partitionAssignor;
+ }
+
+ @Override
+ public String toString() {
+ return "(groupId=" + groupId + ", isSimpleConsumerGroup=" + isSimpleConsumerGroup + ", members=" +
+ Utils.join(members, ",") + ", partitionAssignor=" + partitionAssignor + ")";
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupListing.java b/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupListing.java
new file mode 100644
index 0000000000000..46da9628010b6
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ConsumerGroupListing.java
@@ -0,0 +1,59 @@
+/*
+ * 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.clients.admin;
+
+/**
+ * A listing of a consumer group in the cluster.
+ */
+public class ConsumerGroupListing {
+ private final String groupId;
+ private final boolean isSimpleConsumerGroup;
+
+ /**
+ * Create an instance with the specified parameters.
+ *
+ * @param groupId Group Id
+ * @param isSimpleConsumerGroup If consumer group is simple or not.
+ */
+ public ConsumerGroupListing(String groupId, boolean isSimpleConsumerGroup) {
+ this.groupId = groupId;
+ this.isSimpleConsumerGroup = isSimpleConsumerGroup;
+ }
+
+ /**
+ * Consumer Group Id
+ */
+ public String groupId() {
+ return groupId;
+ }
+
+ /**
+ * If Consumer Group is simple or not.
+ */
+ public boolean isSimpleConsumerGroup() {
+ return isSimpleConsumerGroup;
+ }
+
+ @Override
+ public String toString() {
+ return "(" +
+ "groupId='" + groupId + '\'' +
+ ", isSimpleConsumerGroup=" + isSimpleConsumerGroup +
+ ')';
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenOptions.java
new file mode 100644
index 0000000000000..1b77b943800ec
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenOptions.java
@@ -0,0 +1,53 @@
+/*
+ * 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.clients.admin;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+
+/**
+ * Options for {@link AdminClient#createDelegationToken(CreateDelegationTokenOptions)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class CreateDelegationTokenOptions extends AbstractOptions {
+ private long maxLifeTimeMs = -1;
+ private List renewers = new LinkedList<>();
+
+ public CreateDelegationTokenOptions renewers(List renewers) {
+ this.renewers = renewers;
+ return this;
+ }
+
+ public List renewers() {
+ return renewers;
+ }
+
+ public CreateDelegationTokenOptions maxlifeTimeMs(long maxLifeTimeMs) {
+ this.maxLifeTimeMs = maxLifeTimeMs;
+ return this;
+ }
+
+ public long maxlifeTimeMs() {
+ return maxLifeTimeMs;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenResult.java
new file mode 100644
index 0000000000000..043cbe87fef5c
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/CreateDelegationTokenResult.java
@@ -0,0 +1,43 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+import org.apache.kafka.common.security.token.delegation.DelegationToken;
+
+/**
+ * The result of the {@link KafkaAdminClient#createDelegationToken(CreateDelegationTokenOptions)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class CreateDelegationTokenResult {
+ private final KafkaFuture delegationToken;
+
+ CreateDelegationTokenResult(KafkaFuture delegationToken) {
+ this.delegationToken = delegationToken;
+ }
+
+ /**
+ * Returns a future which yields a delegation token
+ */
+ public KafkaFuture delegationToken() {
+ return delegationToken;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsOptions.java
new file mode 100644
index 0000000000000..cd505f4c11138
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsOptions.java
@@ -0,0 +1,31 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.Collection;
+
+/**
+ * Options for the {@link AdminClient#deleteConsumerGroups(Collection)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class DeleteConsumerGroupsOptions extends AbstractOptions {
+
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/AuthCallbackHandler.java b/clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsResult.java
similarity index 52%
rename from clients/src/main/java/org/apache/kafka/common/security/authenticator/AuthCallbackHandler.java
rename to clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsResult.java
index d517162a1762c..b4bce26440547 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/AuthCallbackHandler.java
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DeleteConsumerGroupsResult.java
@@ -14,32 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.authenticator;
+package org.apache.kafka.clients.admin;
-import java.util.Map;
-
-import org.apache.kafka.common.network.Mode;
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
-import javax.security.auth.Subject;
-import javax.security.auth.callback.CallbackHandler;
+import java.util.Collection;
+import java.util.Map;
-/*
- * Callback handler for SASL-based authentication
+/**
+ * The result of the {@link AdminClient#deleteConsumerGroups(Collection)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
*/
-public interface AuthCallbackHandler extends CallbackHandler {
+@InterfaceStability.Evolving
+public class DeleteConsumerGroupsResult {
+ final KafkaFuture>> futures;
- /**
- * Configures this callback handler.
- *
- * @param configs Configuration
- * @param mode The mode that indicates if this is a client or server connection
- * @param subject Subject from login context
- * @param saslMechanism Negotiated SASL mechanism
- */
- void configure(Map configs, Mode mode, Subject subject, String saslMechanism);
+ DeleteConsumerGroupsResult(KafkaFuture>> futures) {
+ this.futures = futures;
+ }
- /**
- * Closes this instance.
- */
- void close();
+ public KafkaFuture>> deletedGroups() {
+ return futures;
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsOptions.java
new file mode 100644
index 0000000000000..7daff1a483be8
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsOptions.java
@@ -0,0 +1,31 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.Collection;
+
+/**
+ * Options for {@link AdminClient#describeConsumerGroups(Collection, DescribeConsumerGroupsOptions)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class DescribeConsumerGroupsOptions extends AbstractOptions {
+}
\ No newline at end of file
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsResult.java
new file mode 100644
index 0000000000000..adde031b6788d
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeConsumerGroupsResult.java
@@ -0,0 +1,47 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.Collection;
+import java.util.Map;
+
+
+/**
+ * The result of the {@link KafkaAdminClient#describeConsumerGroups(Collection, DescribeConsumerGroupsOptions)}} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class DescribeConsumerGroupsResult {
+
+ private final KafkaFuture>> futures;
+
+ public DescribeConsumerGroupsResult(KafkaFuture>> futures) {
+ this.futures = futures;
+ }
+
+ /**
+ * Return a map from group name to futures which can be used to check the description of a consumer group.
+ */
+ public KafkaFuture>> describedGroups() {
+ return futures;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenOptions.java
new file mode 100644
index 0000000000000..60b99354e3552
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenOptions.java
@@ -0,0 +1,48 @@
+/*
+ * 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.clients.admin;
+
+import java.util.List;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+
+/**
+ * Options for {@link AdminClient#describeDelegationToken(DescribeDelegationTokenOptions)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class DescribeDelegationTokenOptions extends AbstractOptions {
+ private List owners;
+
+ /**
+ * if owners is null, all the user owned tokens and tokens where user have Describe permission
+ * will be returned.
+ * @param owners
+ * @return this instance
+ */
+ public DescribeDelegationTokenOptions owners(List owners) {
+ this.owners = owners;
+ return this;
+ }
+
+ public List owners() {
+ return owners;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenResult.java
new file mode 100644
index 0000000000000..7a9d4b9dd9737
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/DescribeDelegationTokenResult.java
@@ -0,0 +1,45 @@
+/*
+ * 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.clients.admin;
+
+import java.util.List;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+import org.apache.kafka.common.security.token.delegation.DelegationToken;
+
+/**
+ * The result of the {@link KafkaAdminClient#describeDelegationToken(DescribeDelegationTokenOptions)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class DescribeDelegationTokenResult {
+ private final KafkaFuture> delegationTokens;
+
+ DescribeDelegationTokenResult(KafkaFuture> delegationTokens) {
+ this.delegationTokens = delegationTokens;
+ }
+
+ /**
+ * Returns a future which yields list of delegation tokens
+ */
+ public KafkaFuture> delegationTokens() {
+ return delegationTokens;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenOptions.java
new file mode 100644
index 0000000000000..138cd4e69e477
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenOptions.java
@@ -0,0 +1,39 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+/**
+ * Options for {@link AdminClient#expireDelegationToken(byte[], ExpireDelegationTokenOptions)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ExpireDelegationTokenOptions extends AbstractOptions {
+ private long expiryTimePeriodMs = -1L;
+
+ public ExpireDelegationTokenOptions expiryTimePeriodMs(long expiryTimePeriodMs) {
+ this.expiryTimePeriodMs = expiryTimePeriodMs;
+ return this;
+ }
+
+ public long expiryTimePeriodMs() {
+ return expiryTimePeriodMs;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenResult.java
new file mode 100644
index 0000000000000..41782bdcb5ce7
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ExpireDelegationTokenResult.java
@@ -0,0 +1,42 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+/**
+ * The result of the {@link KafkaAdminClient#expireDelegationToken(byte[], ExpireDelegationTokenOptions)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ExpireDelegationTokenResult {
+ private final KafkaFuture expiryTimestamp;
+
+ ExpireDelegationTokenResult(KafkaFuture expiryTimestamp) {
+ this.expiryTimestamp = expiryTimestamp;
+ }
+
+ /**
+ * Returns a future which yields expiry timestamp
+ */
+ public KafkaFuture expiryTimestamp() {
+ return expiryTimestamp;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java b/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
index e455b9ce81c63..50bcfd388569c 100644
--- a/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/KafkaAdminClient.java
@@ -27,6 +27,9 @@
import org.apache.kafka.clients.admin.DeleteAclsResult.FilterResult;
import org.apache.kafka.clients.admin.DeleteAclsResult.FilterResults;
import org.apache.kafka.clients.admin.DescribeReplicaLogDirsResult.ReplicaLogDirInfo;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.clients.consumer.internals.ConsumerProtocol;
+import org.apache.kafka.clients.consumer.internals.PartitionAssignor;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.KafkaFuture;
@@ -69,6 +72,8 @@
import org.apache.kafka.common.requests.CreateAclsRequest.AclCreation;
import org.apache.kafka.common.requests.CreateAclsResponse;
import org.apache.kafka.common.requests.CreateAclsResponse.AclCreationResponse;
+import org.apache.kafka.common.requests.CreateDelegationTokenRequest;
+import org.apache.kafka.common.requests.CreateDelegationTokenResponse;
import org.apache.kafka.common.requests.CreatePartitionsRequest;
import org.apache.kafka.common.requests.CreatePartitionsResponse;
import org.apache.kafka.common.requests.CreateTopicsRequest;
@@ -77,6 +82,8 @@
import org.apache.kafka.common.requests.DeleteAclsResponse;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclDeletionResult;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclFilterResponse;
+import org.apache.kafka.common.requests.DeleteGroupsRequest;
+import org.apache.kafka.common.requests.DeleteGroupsResponse;
import org.apache.kafka.common.requests.DeleteRecordsRequest;
import org.apache.kafka.common.requests.DeleteRecordsResponse;
import org.apache.kafka.common.requests.DeleteTopicsRequest;
@@ -85,19 +92,37 @@
import org.apache.kafka.common.requests.DescribeAclsResponse;
import org.apache.kafka.common.requests.DescribeConfigsRequest;
import org.apache.kafka.common.requests.DescribeConfigsResponse;
+import org.apache.kafka.common.requests.DescribeDelegationTokenRequest;
+import org.apache.kafka.common.requests.DescribeDelegationTokenResponse;
import org.apache.kafka.common.requests.DescribeLogDirsRequest;
import org.apache.kafka.common.requests.DescribeLogDirsResponse;
+import org.apache.kafka.common.requests.ExpireDelegationTokenRequest;
+import org.apache.kafka.common.requests.ExpireDelegationTokenResponse;
import org.apache.kafka.common.requests.MetadataRequest;
import org.apache.kafka.common.requests.MetadataResponse;
+import org.apache.kafka.common.requests.RenewDelegationTokenRequest;
+import org.apache.kafka.common.requests.RenewDelegationTokenResponse;
+import org.apache.kafka.common.requests.DescribeGroupsRequest;
+import org.apache.kafka.common.requests.DescribeGroupsResponse;
+import org.apache.kafka.common.requests.FindCoordinatorRequest;
+import org.apache.kafka.common.requests.FindCoordinatorResponse;
+import org.apache.kafka.common.requests.ListGroupsRequest;
+import org.apache.kafka.common.requests.ListGroupsResponse;
+import org.apache.kafka.common.requests.OffsetFetchRequest;
+import org.apache.kafka.common.requests.OffsetFetchResponse;
import org.apache.kafka.common.requests.Resource;
import org.apache.kafka.common.requests.ResourceType;
+import org.apache.kafka.common.security.token.delegation.DelegationToken;
+import org.apache.kafka.common.security.token.delegation.TokenInformation;
import org.apache.kafka.common.utils.AppInfoParser;
import org.apache.kafka.common.utils.KafkaThread;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -1150,9 +1175,10 @@ void handleFailure(Throwable throwable) {
}
@Override
- public DeleteTopicsResult deleteTopics(final Collection topicNames,
+ public DeleteTopicsResult deleteTopics(Collection topicNames,
DeleteTopicsOptions options) {
final Map> topicFutures = new HashMap<>(topicNames.size());
+ final List validTopicNames = new ArrayList<>(topicNames.size());
for (String topicName : topicNames) {
if (topicNameIsUnrepresentable(topicName)) {
KafkaFutureImpl future = new KafkaFutureImpl<>();
@@ -1161,6 +1187,7 @@ public DeleteTopicsResult deleteTopics(final Collection topicNames,
topicFutures.put(topicName, future);
} else if (!topicFutures.containsKey(topicName)) {
topicFutures.put(topicName, new KafkaFutureImpl());
+ validTopicNames.add(topicName);
}
}
final long now = time.milliseconds();
@@ -1169,7 +1196,7 @@ public DeleteTopicsResult deleteTopics(final Collection topicNames,
@Override
AbstractRequest.Builder createRequest(int timeoutMs) {
- return new DeleteTopicsRequest.Builder(new HashSet<>(topicNames), timeoutMs);
+ return new DeleteTopicsRequest.Builder(new HashSet<>(validTopicNames), timeoutMs);
}
@Override
@@ -1204,7 +1231,7 @@ void handleFailure(Throwable throwable) {
completeAllExceptionally(topicFutures.values(), throwable);
}
};
- if (!topicNames.isEmpty()) {
+ if (!validTopicNames.isEmpty()) {
runnable.call(call, now);
}
return new DeleteTopicsResult(new HashMap>(topicFutures));
@@ -2070,4 +2097,452 @@ void handleFailure(Throwable throwable) {
return new DeleteRecordsResult(new HashMap>(futures));
}
+
+ @Override
+ public CreateDelegationTokenResult createDelegationToken(final CreateDelegationTokenOptions options) {
+ final KafkaFutureImpl delegationTokenFuture = new KafkaFutureImpl<>();
+ final long now = time.milliseconds();
+ runnable.call(new Call("createDelegationToken", calcDeadlineMs(now, options.timeoutMs()),
+ new LeastLoadedNodeProvider()) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new CreateDelegationTokenRequest.Builder(options.renewers(), options.maxlifeTimeMs());
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ CreateDelegationTokenResponse response = (CreateDelegationTokenResponse) abstractResponse;
+ if (response.hasError()) {
+ delegationTokenFuture.completeExceptionally(response.error().exception());
+ } else {
+ TokenInformation tokenInfo = new TokenInformation(response.tokenId(), response.owner(),
+ options.renewers(), response.issueTimestamp(), response.maxTimestamp(), response.expiryTimestamp());
+ DelegationToken token = new DelegationToken(tokenInfo, response.hmacBytes());
+ delegationTokenFuture.complete(token);
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ delegationTokenFuture.completeExceptionally(throwable);
+ }
+ }, now);
+
+ return new CreateDelegationTokenResult(delegationTokenFuture);
+ }
+
+ @Override
+ public RenewDelegationTokenResult renewDelegationToken(final byte[] hmac, final RenewDelegationTokenOptions options) {
+ final KafkaFutureImpl expiryTimeFuture = new KafkaFutureImpl<>();
+ final long now = time.milliseconds();
+ runnable.call(new Call("renewDelegationToken", calcDeadlineMs(now, options.timeoutMs()),
+ new LeastLoadedNodeProvider()) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new RenewDelegationTokenRequest.Builder(hmac, options.renewTimePeriodMs());
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ RenewDelegationTokenResponse response = (RenewDelegationTokenResponse) abstractResponse;
+ if (response.hasError()) {
+ expiryTimeFuture.completeExceptionally(response.error().exception());
+ } else {
+ expiryTimeFuture.complete(response.expiryTimestamp());
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ expiryTimeFuture.completeExceptionally(throwable);
+ }
+ }, now);
+
+ return new RenewDelegationTokenResult(expiryTimeFuture);
+ }
+
+ @Override
+ public ExpireDelegationTokenResult expireDelegationToken(final byte[] hmac, final ExpireDelegationTokenOptions options) {
+ final KafkaFutureImpl expiryTimeFuture = new KafkaFutureImpl<>();
+ final long now = time.milliseconds();
+ runnable.call(new Call("expireDelegationToken", calcDeadlineMs(now, options.timeoutMs()),
+ new LeastLoadedNodeProvider()) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new ExpireDelegationTokenRequest.Builder(hmac, options.expiryTimePeriodMs());
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ ExpireDelegationTokenResponse response = (ExpireDelegationTokenResponse) abstractResponse;
+ if (response.hasError()) {
+ expiryTimeFuture.completeExceptionally(response.error().exception());
+ } else {
+ expiryTimeFuture.complete(response.expiryTimestamp());
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ expiryTimeFuture.completeExceptionally(throwable);
+ }
+ }, now);
+
+ return new ExpireDelegationTokenResult(expiryTimeFuture);
+ }
+
+ @Override
+ public DescribeDelegationTokenResult describeDelegationToken(final DescribeDelegationTokenOptions options) {
+ final KafkaFutureImpl> tokensFuture = new KafkaFutureImpl<>();
+ final long now = time.milliseconds();
+ runnable.call(new Call("describeDelegationToken", calcDeadlineMs(now, options.timeoutMs()),
+ new LeastLoadedNodeProvider()) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new DescribeDelegationTokenRequest.Builder(options.owners());
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ DescribeDelegationTokenResponse response = (DescribeDelegationTokenResponse) abstractResponse;
+ if (response.hasError()) {
+ tokensFuture.completeExceptionally(response.error().exception());
+ } else {
+ tokensFuture.complete(response.tokens());
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ tokensFuture.completeExceptionally(throwable);
+ }
+ }, now);
+
+ return new DescribeDelegationTokenResult(tokensFuture);
+ }
+
+ @Override
+ public DescribeConsumerGroupsResult describeConsumerGroups(final Collection groupIds,
+ final DescribeConsumerGroupsOptions options) {
+ final KafkaFutureImpl>> resultFutures = new KafkaFutureImpl<>();
+ final Map> consumerGroupFutures = new HashMap<>(groupIds.size());
+ final ArrayList groupIdList = new ArrayList<>();
+ for (String groupId : groupIds) {
+ if (!consumerGroupFutures.containsKey(groupId)) {
+ consumerGroupFutures.put(groupId, new KafkaFutureImpl());
+ groupIdList.add(groupId);
+ }
+ }
+
+ for (final String groupId : groupIdList) {
+
+ final long nowFindCoordinator = time.milliseconds();
+ final long deadline = calcDeadlineMs(nowFindCoordinator, options.timeoutMs());
+
+ runnable.call(new Call("findCoordinator", deadline, new LeastLoadedNodeProvider()) {
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new FindCoordinatorRequest.Builder(FindCoordinatorRequest.CoordinatorType.GROUP, groupId);
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final FindCoordinatorResponse response = (FindCoordinatorResponse) abstractResponse;
+
+ final long nowDescribeConsumerGroups = time.milliseconds();
+
+ final int nodeId = response.node().id();
+
+ runnable.call(new Call("describeConsumerGroups", deadline, new ConstantNodeIdProvider(nodeId)) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new DescribeGroupsRequest.Builder(groupIdList);
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final DescribeGroupsResponse response = (DescribeGroupsResponse) abstractResponse;
+ // Handle server responses for particular groupId.
+ for (Map.Entry> entry : consumerGroupFutures.entrySet()) {
+ final String groupId = entry.getKey();
+ final KafkaFutureImpl future = entry.getValue();
+ final DescribeGroupsResponse.GroupMetadata groupMetadata = response.groups().get(groupId);
+ final Errors groupError = groupMetadata.error();
+ if (groupError != Errors.NONE) {
+ future.completeExceptionally(groupError.exception());
+ continue;
+ }
+
+ final String protocolType = groupMetadata.protocolType();
+ if (protocolType.equals(ConsumerProtocol.PROTOCOL_TYPE) || protocolType.isEmpty()) {
+ final List members = groupMetadata.members();
+ final List consumers = new ArrayList<>(members.size());
+
+ for (DescribeGroupsResponse.GroupMember groupMember : members) {
+ final PartitionAssignor.Assignment assignment =
+ ConsumerProtocol.deserializeAssignment(
+ ByteBuffer.wrap(Utils.readBytes(groupMember.memberAssignment())));
+
+ final MemberDescription memberDescription =
+ new MemberDescription(
+ groupMember.memberId(),
+ groupMember.clientId(),
+ groupMember.clientHost(),
+ new MemberAssignment(assignment.partitions()));
+ consumers.add(memberDescription);
+ }
+ final String protocol = groupMetadata.protocol();
+ final ConsumerGroupDescription consumerGroupDescription =
+ new ConsumerGroupDescription(groupId, protocolType.isEmpty(), consumers, protocol);
+ future.complete(consumerGroupDescription);
+ }
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ completeAllExceptionally(consumerGroupFutures.values(), throwable);
+ }
+ }, nowDescribeConsumerGroups);
+
+ resultFutures.complete(new HashMap>(consumerGroupFutures));
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ resultFutures.completeExceptionally(throwable);
+ }
+ }, nowFindCoordinator);
+ }
+
+ return new DescribeConsumerGroupsResult(resultFutures);
+ }
+
+ @Override
+ public ListConsumerGroupsResult listConsumerGroups(ListConsumerGroupsOptions options) {
+ //final KafkaFutureImpl>>> nodeAndConsumerGroupListing = new KafkaFutureImpl<>();
+ final KafkaFutureImpl> future = new KafkaFutureImpl>();
+
+ final long nowMetadata = time.milliseconds();
+ final long deadline = calcDeadlineMs(nowMetadata, options.timeoutMs());
+
+ runnable.call(new Call("listNodes", deadline, new LeastLoadedNodeProvider()) {
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new MetadataRequest.Builder(Collections.emptyList(), true);
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ MetadataResponse metadataResponse = (MetadataResponse) abstractResponse;
+
+ final Map>> futures = new HashMap<>();
+
+ for (final Node node : metadataResponse.brokers()) {
+ futures.put(node, new KafkaFutureImpl>());
+ }
+
+ future.combine(futures.values().toArray(new KafkaFuture[0])).thenApply(
+ new KafkaFuture.BaseFunction, Collection>() {
+ @Override
+ public Collection apply(Collection v) {
+ List listings = new ArrayList<>();
+ for (Map.Entry>> entry : futures.entrySet()) {
+ Collection results;
+ try {
+ results = entry.getValue().get();
+ } catch (Throwable e) {
+ // This should be unreachable, since the future returned by KafkaFuture#allOf should
+ // have failed if any Future failed.
+ throw new KafkaException("ListConsumerGroupsResult#listings(): internal error", e);
+ }
+ listings.addAll(results);
+ }
+ return listings;
+ }
+ });
+
+
+ for (final Map.Entry>> entry : futures.entrySet()) {
+ final long nowList = time.milliseconds();
+
+ final int brokerId = entry.getKey().id();
+
+ runnable.call(new Call("listConsumerGroups", deadline, new ConstantNodeIdProvider(brokerId)) {
+
+ private final KafkaFutureImpl> future = entry.getValue();
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new ListGroupsRequest.Builder();
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final ListGroupsResponse response = (ListGroupsResponse) abstractResponse;
+ final List groupsListing = new ArrayList<>();
+ for (ListGroupsResponse.Group group : response.groups()) {
+ if (group.protocolType().equals(ConsumerProtocol.PROTOCOL_TYPE) || group.protocolType().isEmpty()) {
+ final String groupId = group.groupId();
+ final String protocolType = group.protocolType();
+ final ConsumerGroupListing groupListing = new ConsumerGroupListing(groupId, protocolType.isEmpty());
+ groupsListing.add(groupListing);
+ }
+ }
+ future.complete(groupsListing);
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ completeAllExceptionally(futures.values(), throwable);
+ }
+ }, nowList);
+
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ future.completeExceptionally(throwable);
+ }
+ }, nowMetadata);
+
+ return new ListConsumerGroupsResult(future);
+ }
+
+ @Override
+ public ListConsumerGroupOffsetsResult listConsumerGroupOffsets(final String groupId, final ListConsumerGroupOffsetsOptions options) {
+ final KafkaFutureImpl> groupOffsetListingFuture = new KafkaFutureImpl<>();
+
+ final long nowFindCoordinator = time.milliseconds();
+ final long deadline = calcDeadlineMs(nowFindCoordinator, options.timeoutMs());
+
+ runnable.call(new Call("findCoordinator", deadline, new LeastLoadedNodeProvider()) {
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new FindCoordinatorRequest.Builder(FindCoordinatorRequest.CoordinatorType.GROUP, groupId);
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final FindCoordinatorResponse response = (FindCoordinatorResponse) abstractResponse;
+
+ final long nowListConsumerGroupOffsets = time.milliseconds();
+
+ final int nodeId = response.node().id();
+
+ runnable.call(new Call("listConsumerGroupOffsets", deadline, new ConstantNodeIdProvider(nodeId)) {
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new OffsetFetchRequest.Builder(groupId, options.topicPartitions());
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final OffsetFetchResponse response = (OffsetFetchResponse) abstractResponse;
+ final Map groupOffsetsListing = new HashMap<>();
+ for (Map.Entry entry :
+ response.responseData().entrySet()) {
+ final TopicPartition topicPartition = entry.getKey();
+ final Long offset = entry.getValue().offset;
+ final String metadata = entry.getValue().metadata;
+ groupOffsetsListing.put(topicPartition, new OffsetAndMetadata(offset, metadata));
+ }
+ groupOffsetListingFuture.complete(groupOffsetsListing);
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ groupOffsetListingFuture.completeExceptionally(throwable);
+ }
+ }, nowListConsumerGroupOffsets);
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ groupOffsetListingFuture.completeExceptionally(throwable);
+ }
+ }, nowFindCoordinator);
+
+ return new ListConsumerGroupOffsetsResult(groupOffsetListingFuture);
+ }
+
+ @Override
+ public DeleteConsumerGroupsResult deleteConsumerGroups(Collection groupIds, DeleteConsumerGroupsOptions options) {
+ final KafkaFutureImpl>> deleteConsumerGroupsFuture = new KafkaFutureImpl<>();
+ final Map> deleteConsumerGroupFutures = new HashMap<>(groupIds.size());
+ final Set groupIdList = new HashSet<>();
+ for (String groupId : groupIds) {
+ if (!deleteConsumerGroupFutures.containsKey(groupId)) {
+ deleteConsumerGroupFutures.put(groupId, new KafkaFutureImpl());
+ groupIdList.add(groupId);
+ }
+ }
+
+ for (final String groupId : groupIdList) {
+
+ final long nowFindCoordinator = time.milliseconds();
+ final long deadline = calcDeadlineMs(nowFindCoordinator, options.timeoutMs());
+
+ runnable.call(new Call("findCoordinator", deadline, new LeastLoadedNodeProvider()) {
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new FindCoordinatorRequest.Builder(FindCoordinatorRequest.CoordinatorType.GROUP, groupId);
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final FindCoordinatorResponse response = (FindCoordinatorResponse) abstractResponse;
+
+ final long nowDeleteConsumerGroups = time.milliseconds();
+
+ final int nodeId = response.node().id();
+
+ runnable.call(new Call("deleteConsumerGroups", deadline, new ConstantNodeIdProvider(nodeId)) {
+
+ @Override
+ AbstractRequest.Builder createRequest(int timeoutMs) {
+ return new DeleteGroupsRequest.Builder(Collections.singleton(groupId));
+ }
+
+ @Override
+ void handleResponse(AbstractResponse abstractResponse) {
+ final DeleteGroupsResponse response = (DeleteGroupsResponse) abstractResponse;
+ // Handle server responses for particular groupId.
+ for (Map.Entry> entry : deleteConsumerGroupFutures.entrySet()) {
+ final String groupId = entry.getKey();
+ final KafkaFutureImpl future = entry.getValue();
+ final Errors groupError = response.get(groupId);
+ if (groupError != Errors.NONE) {
+ future.completeExceptionally(groupError.exception());
+ continue;
+ }
+
+ future.complete(null);
+ }
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ completeAllExceptionally(deleteConsumerGroupFutures.values(), throwable);
+ }
+ }, nowDeleteConsumerGroups);
+
+ deleteConsumerGroupsFuture.complete(new HashMap>(deleteConsumerGroupFutures));
+ }
+
+ @Override
+ void handleFailure(Throwable throwable) {
+ deleteConsumerGroupsFuture.completeExceptionally(throwable);
+ }
+ }, nowFindCoordinator);
+ }
+
+ return new DeleteConsumerGroupsResult(deleteConsumerGroupsFuture);
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsOptions.java
new file mode 100644
index 0000000000000..c6434ebb15c13
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsOptions.java
@@ -0,0 +1,53 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.List;
+
+/**
+ * Options for {@link AdminClient#listConsumerGroupOffsets(String)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ListConsumerGroupOffsetsOptions extends AbstractOptions {
+
+ private List topicPartitions = null;
+
+ /**
+ * Set the topic partitions to list as part of the result.
+ * {@code null} includes all topic partitions.
+ *
+ * @param topicPartitions List of topic partitions to include
+ * @return This ListGroupOffsetsOptions
+ */
+ public ListConsumerGroupOffsetsOptions topicPartitions(List topicPartitions) {
+ this.topicPartitions = topicPartitions;
+ return this;
+ }
+
+ /**
+ * Returns a list of topic partitions to add as part of the result.
+ */
+ public List topicPartitions() {
+ return topicPartitions;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsResult.java
new file mode 100644
index 0000000000000..23657b5ad3cf7
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupOffsetsResult.java
@@ -0,0 +1,48 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.Map;
+
+/**
+ * The result of the {@link AdminClient#listConsumerGroupOffsets(String)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ListConsumerGroupOffsetsResult {
+
+ final KafkaFuture> future;
+
+ ListConsumerGroupOffsetsResult(KafkaFuture> future) {
+ this.future = future;
+ }
+
+ /**
+ * Return a future which yields a map of topic partitions to OffsetAndMetadata objects.
+ */
+ public KafkaFuture> partitionsToOffsetAndMetadata() {
+ return future;
+ }
+
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsOptions.java
new file mode 100644
index 0000000000000..86ca171b726c0
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsOptions.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.clients.admin;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+/**
+ * Options for {@link AdminClient#listConsumerGroups()}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ListConsumerGroupsOptions extends AbstractOptions {
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsResult.java
new file mode 100644
index 0000000000000..c72537104409d
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/ListConsumerGroupsResult.java
@@ -0,0 +1,44 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+import java.util.Collection;
+
+/**
+ * The result of the {@link AdminClient#listConsumerGroups()} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class ListConsumerGroupsResult {
+ private final KafkaFuture> future;
+
+ ListConsumerGroupsResult(KafkaFuture> future) {
+ this.future = future;
+ }
+
+ /**
+ * Return a future which yields a collection of ConsumerGroupListing objects.
+ */
+ public KafkaFuture> listings() {
+ return future;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/MemberAssignment.java b/clients/src/main/java/org/apache/kafka/clients/admin/MemberAssignment.java
new file mode 100644
index 0000000000000..bd958132b7c89
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/MemberAssignment.java
@@ -0,0 +1,65 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.utils.Utils;
+
+import java.util.List;
+
+/**
+ * A description of the assignments of a specific group member.
+ */
+public class MemberAssignment {
+ private final List topicPartitions;
+
+ /**
+ * Creates an instance with the specified parameters.
+ *
+ * @param topicPartitions List of topic partitions
+ */
+ public MemberAssignment(List topicPartitions) {
+ this.topicPartitions = topicPartitions;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MemberAssignment that = (MemberAssignment) o;
+
+ return topicPartitions != null ? topicPartitions.equals(that.topicPartitions) : that.topicPartitions == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return topicPartitions != null ? topicPartitions.hashCode() : 0;
+ }
+
+ /**
+ * The topic partitions assigned to a group member.
+ */
+ public List topicPartitions() {
+ return topicPartitions;
+ }
+
+ @Override
+ public String toString() {
+ return "(topicPartitions=" + Utils.join(topicPartitions, ",") + ")";
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/MemberDescription.java b/clients/src/main/java/org/apache/kafka/clients/admin/MemberDescription.java
new file mode 100644
index 0000000000000..2ba19634208e1
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/MemberDescription.java
@@ -0,0 +1,98 @@
+/*
+ * 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.clients.admin;
+
+/**
+ * A detailed description of a single group instance in the cluster.
+ */
+public class MemberDescription {
+
+ private final String memberId;
+ private final String clientId;
+ private final String host;
+ private final MemberAssignment assignment;
+
+ /**
+ * Creates an instance with the specified parameters.
+ *
+ * @param memberId The consumer id
+ * @param clientId The client id
+ * @param host The host
+ * @param assignment The assignment
+ */
+ public MemberDescription(String memberId, String clientId, String host, MemberAssignment assignment) {
+ this.memberId = memberId;
+ this.clientId = clientId;
+ this.host = host;
+ this.assignment = assignment;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MemberDescription that = (MemberDescription) o;
+
+ if (memberId != null ? !memberId.equals(that.memberId) : that.memberId != null) return false;
+ if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null) return false;
+ return assignment != null ? assignment.equals(that.assignment) : that.assignment == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = memberId != null ? memberId.hashCode() : 0;
+ result = 31 * result + (clientId != null ? clientId.hashCode() : 0);
+ result = 31 * result + (assignment != null ? assignment.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * The consumer id of the group member.
+ */
+ public String consumerId() {
+ return memberId;
+ }
+
+ /**
+ * The client id of the group member.
+ */
+ public String clientId() {
+ return clientId;
+ }
+
+ /**
+ * The host where the group member is running.
+ */
+ public String host() {
+ return host;
+ }
+
+ /**
+ * The assignment of the group member.
+ */
+ public MemberAssignment assignment() {
+ return assignment;
+ }
+
+ @Override
+ public String toString() {
+ return "(memberId=" + memberId + ", clientId=" + clientId + ", host=" + host + ", assignment=" +
+ assignment + ")";
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenOptions.java b/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenOptions.java
new file mode 100644
index 0000000000000..238dc4a3494a8
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenOptions.java
@@ -0,0 +1,39 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+/**
+ * Options for {@link AdminClient#renewDelegationToken(byte[], RenewDelegationTokenOptions)}.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class RenewDelegationTokenOptions extends AbstractOptions {
+ private long renewTimePeriodMs = -1;
+
+ public RenewDelegationTokenOptions renewTimePeriodMs(long renewTimePeriodMs) {
+ this.renewTimePeriodMs = renewTimePeriodMs;
+ return this;
+ }
+
+ public long renewTimePeriodMs() {
+ return renewTimePeriodMs;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenResult.java b/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenResult.java
new file mode 100644
index 0000000000000..38cdf1ae1b241
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/clients/admin/RenewDelegationTokenResult.java
@@ -0,0 +1,42 @@
+/*
+ * 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.clients.admin;
+
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.annotation.InterfaceStability;
+
+/**
+ * The result of the {@link KafkaAdminClient#expireDelegationToken(byte[], ExpireDelegationTokenOptions)} call.
+ *
+ * The API of this class is evolving, see {@link AdminClient} for details.
+ */
+@InterfaceStability.Evolving
+public class RenewDelegationTokenResult {
+ private final KafkaFuture expiryTimestamp;
+
+ RenewDelegationTokenResult(KafkaFuture expiryTimestamp) {
+ this.expiryTimestamp = expiryTimestamp;
+ }
+
+ /**
+ * Returns a future which yields expiry timestamp
+ */
+ public KafkaFuture expiryTimestamp() {
+ return expiryTimestamp;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java b/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
index 426b273b88538..0514c99563529 100644
--- a/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
+++ b/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
@@ -329,7 +329,7 @@ private boolean maybeSendTransactionalRequest(long now) {
return false;
AbstractRequest.Builder> requestBuilder = nextRequestHandler.requestBuilder();
- while (running) {
+ while (!forceClose) {
Node targetNode = null;
try {
if (nextRequestHandler.needsCoordinator()) {
diff --git a/clients/src/main/java/org/apache/kafka/common/KafkaFuture.java b/clients/src/main/java/org/apache/kafka/common/KafkaFuture.java
index 9cd2e01dc42f6..4af996cb6b8c7 100644
--- a/clients/src/main/java/org/apache/kafka/common/KafkaFuture.java
+++ b/clients/src/main/java/org/apache/kafka/common/KafkaFuture.java
@@ -106,6 +106,15 @@ public static KafkaFuture allOf(KafkaFuture>... futures) {
return allOfFuture;
}
+ public KafkaFuture combine(KafkaFuture>... futures) {
+ AllOfAdapter allOfWaiter = new AllOfAdapter<>(futures.length, this);
+ for (KafkaFuture> future : futures) {
+ future.addWaiter(allOfWaiter);
+ }
+
+ return this;
+ }
+
/**
* Returns a new KafkaFuture that, when this future completes normally, is executed with this
* futures's result as the argument to the supplied function.
diff --git a/clients/src/main/java/org/apache/kafka/common/config/ConfigResource.java b/clients/src/main/java/org/apache/kafka/common/config/ConfigResource.java
index 4402c260202e4..da718f54d547e 100644
--- a/clients/src/main/java/org/apache/kafka/common/config/ConfigResource.java
+++ b/clients/src/main/java/org/apache/kafka/common/config/ConfigResource.java
@@ -90,6 +90,6 @@ public int hashCode() {
@Override
public String toString() {
- return "ConfigResource{type=" + type + ", name='" + name + "'}";
+ return "ConfigResource(type=" + type + ", name='" + name + "')";
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java b/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
index f61b7dd5e6c66..148ab15dfadcb 100644
--- a/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
+++ b/clients/src/main/java/org/apache/kafka/common/config/SaslConfigs.java
@@ -49,7 +49,24 @@ public class SaslConfigs {
public static final String SASL_JAAS_CONFIG = "sasl.jaas.config";
public static final String SASL_JAAS_CONFIG_DOC = "JAAS login context parameters for SASL connections in the format used by JAAS configuration files. "
+ "JAAS configuration file format is described here . "
- + "The format for the value is: ' (=)*;'";
+ + "The format for the value is: ' (=)*;'. For brokers, "
+ + "the config must be prefixed with listener prefix and SASL mechanism name in lower-case. For example, "
+ + "listener.name.sasl_ssl.scram-sha-256.sasl.jaas.config=com.example.ScramLoginModule required;";
+
+ public static final String SASL_CLIENT_CALLBACK_HANDLER_CLASS = "sasl.client.callback.handler.class";
+ public static final String SASL_CLIENT_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL client callback handler class "
+ + "that implements the AuthenticateCallbackHandler interface.";
+
+ public static final String SASL_LOGIN_CALLBACK_HANDLER_CLASS = "sasl.login.callback.handler.class";
+ public static final String SASL_LOGIN_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL login callback handler class "
+ + "that implements the AuthenticateCallbackHandler interface. For brokers, login callback handler config must be prefixed with "
+ + "listener prefix and SASL mechanism name in lower-case. For example, "
+ + "listener.name.sasl_ssl.scram-sha-256.sasl.login.callback.handler.class=com.example.CustomScramLoginCallbackHandler";
+
+ public static final String SASL_LOGIN_CLASS = "sasl.login.class";
+ public static final String SASL_LOGIN_CLASS_DOC = "The fully qualified name of a class that implements the Login interface. "
+ + "For brokers, login config must be prefixed with listener prefix and SASL mechanism name in lower-case. For example, "
+ + "listener.name.sasl_ssl.scram-sha-256.sasl.login.class=com.example.CustomScramLogin";
public static final String SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name";
public static final String SASL_KERBEROS_SERVICE_NAME_DOC = "The Kerberos principal name that Kafka runs as. "
@@ -95,6 +112,9 @@ public static void addClientSaslSupport(ConfigDef config) {
.define(SaslConfigs.SASL_KERBEROS_TICKET_RENEW_JITTER, ConfigDef.Type.DOUBLE, SaslConfigs.DEFAULT_KERBEROS_TICKET_RENEW_JITTER, ConfigDef.Importance.LOW, SaslConfigs.SASL_KERBEROS_TICKET_RENEW_JITTER_DOC)
.define(SaslConfigs.SASL_KERBEROS_MIN_TIME_BEFORE_RELOGIN, ConfigDef.Type.LONG, SaslConfigs.DEFAULT_KERBEROS_MIN_TIME_BEFORE_RELOGIN, ConfigDef.Importance.LOW, SaslConfigs.SASL_KERBEROS_MIN_TIME_BEFORE_RELOGIN_DOC)
.define(SaslConfigs.SASL_MECHANISM, ConfigDef.Type.STRING, SaslConfigs.DEFAULT_SASL_MECHANISM, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_MECHANISM_DOC)
- .define(SaslConfigs.SASL_JAAS_CONFIG, ConfigDef.Type.PASSWORD, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_JAAS_CONFIG_DOC);
+ .define(SaslConfigs.SASL_JAAS_CONFIG, ConfigDef.Type.PASSWORD, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_JAAS_CONFIG_DOC)
+ .define(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS_DOC)
+ .define(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS_DOC)
+ .define(SaslConfigs.SASL_LOGIN_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_LOGIN_CLASS_DOC);
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java b/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
index 18616ec74e57e..a29d8069b9999 100644
--- a/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
+++ b/clients/src/main/java/org/apache/kafka/common/config/internals/BrokerSecurityConfigs.java
@@ -33,6 +33,7 @@ public class BrokerSecurityConfigs {
public static final String SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES_CONFIG = "sasl.kerberos.principal.to.local.rules";
public static final String SSL_CLIENT_AUTH_CONFIG = "ssl.client.auth";
public static final String SASL_ENABLED_MECHANISMS_CONFIG = "sasl.enabled.mechanisms";
+ public static final String SASL_SERVER_CALLBACK_HANDLER_CLASS = "sasl.server.callback.handler.class";
public static final String PRINCIPAL_BUILDER_CLASS_DOC = "The fully qualified name of a class that implements the " +
"KafkaPrincipalBuilder interface, which is used to build the KafkaPrincipal object used during " +
@@ -67,4 +68,9 @@ public class BrokerSecurityConfigs {
+ "Only GSSAPI is enabled by default.";
public static final List DEFAULT_SASL_ENABLED_MECHANISMS = Collections.singletonList(SaslConfigs.GSSAPI_MECHANISM);
+ public static final String SASL_SERVER_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL server callback handler "
+ + "class that implements the AuthenticateCallbackHandler interface. Server callback handlers must be prefixed with "
+ + "listener prefix and SASL mechanism name in lower-case. For example, "
+ + "listener.name.sasl_ssl.plain.sasl.server.callback.handler.class=com.example.CustomPlainCallbackHandler.";
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/network/ChannelBuilders.java b/clients/src/main/java/org/apache/kafka/common/network/ChannelBuilders.java
index 80ccb7e1382a2..078d844e85fce 100644
--- a/clients/src/main/java/org/apache/kafka/common/network/ChannelBuilders.java
+++ b/clients/src/main/java/org/apache/kafka/common/network/ChannelBuilders.java
@@ -26,7 +26,7 @@
import org.apache.kafka.common.security.auth.KafkaPrincipalBuilder;
import org.apache.kafka.common.security.authenticator.CredentialCache;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
+import org.apache.kafka.common.security.token.delegation.internal.DelegationTokenCache;
import org.apache.kafka.common.utils.Utils;
import java.util.Collections;
diff --git a/clients/src/main/java/org/apache/kafka/common/network/ListenerName.java b/clients/src/main/java/org/apache/kafka/common/network/ListenerName.java
index fc0cb14925b01..2decccbc50649 100644
--- a/clients/src/main/java/org/apache/kafka/common/network/ListenerName.java
+++ b/clients/src/main/java/org/apache/kafka/common/network/ListenerName.java
@@ -73,6 +73,10 @@ public String configPrefix() {
}
public String saslMechanismConfigPrefix(String saslMechanism) {
- return configPrefix() + saslMechanism.toLowerCase(Locale.ROOT) + ".";
+ return configPrefix() + saslMechanismPrefix(saslMechanism);
+ }
+
+ public static String saslMechanismPrefix(String saslMechanism) {
+ return saslMechanism.toLowerCase(Locale.ROOT) + ".";
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/network/SaslChannelBuilder.java b/clients/src/main/java/org/apache/kafka/common/network/SaslChannelBuilder.java
index 095f826ccba5a..3985c7e97fe56 100644
--- a/clients/src/main/java/org/apache/kafka/common/network/SaslChannelBuilder.java
+++ b/clients/src/main/java/org/apache/kafka/common/network/SaslChannelBuilder.java
@@ -21,22 +21,35 @@
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.config.internals.BrokerSecurityConfigs;
import org.apache.kafka.common.memory.MemoryPool;
-import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.security.JaasContext;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
-import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+import org.apache.kafka.common.security.auth.Login;
+import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.security.authenticator.CredentialCache;
+import org.apache.kafka.common.security.authenticator.DefaultLogin;
import org.apache.kafka.common.security.authenticator.LoginManager;
import org.apache.kafka.common.security.authenticator.SaslClientAuthenticator;
+import org.apache.kafka.common.security.authenticator.SaslClientCallbackHandler;
import org.apache.kafka.common.security.authenticator.SaslServerAuthenticator;
+import org.apache.kafka.common.security.authenticator.SaslServerCallbackHandler;
+import org.apache.kafka.common.security.kerberos.KerberosClientCallbackHandler;
+import org.apache.kafka.common.security.kerberos.KerberosLogin;
+import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
+import org.apache.kafka.common.security.plain.internal.PlainSaslServer;
+import org.apache.kafka.common.security.plain.internal.PlainServerCallbackHandler;
+import org.apache.kafka.common.security.scram.ScramCredential;
+import org.apache.kafka.common.security.scram.internal.ScramMechanism;
+import org.apache.kafka.common.security.scram.internal.ScramServerCallbackHandler;
import org.apache.kafka.common.security.ssl.SslFactory;
+import org.apache.kafka.common.security.token.delegation.internal.DelegationTokenCache;
import org.apache.kafka.common.utils.Java;
+import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.io.IOException;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
@@ -66,6 +79,7 @@ public class SaslChannelBuilder implements ChannelBuilder, ListenerReconfigurabl
private SslFactory sslFactory;
private Map configs;
private KerberosShortNamer kerberosShortNamer;
+ private Map saslCallbackHandlers;
public SaslChannelBuilder(Mode mode,
Map jaasContexts,
@@ -87,15 +101,25 @@ public SaslChannelBuilder(Mode mode,
this.clientSaslMechanism = clientSaslMechanism;
this.credentialCache = credentialCache;
this.tokenCache = tokenCache;
+ this.saslCallbackHandlers = new HashMap<>();
}
+ @SuppressWarnings("unchecked")
@Override
public void configure(Map configs) throws KafkaException {
try {
this.configs = configs;
- boolean hasKerberos = jaasContexts.containsKey(SaslConfigs.GSSAPI_MECHANISM);
+ if (mode == Mode.SERVER)
+ createServerCallbackHandlers(configs);
+ else
+ createClientCallbackHandler(configs);
+ for (Map.Entry entry : saslCallbackHandlers.entrySet()) {
+ String mechanism = entry.getKey();
+ entry.getValue().configure(configs, mechanism, jaasContexts.get(mechanism).configurationEntries());
+ }
- if (hasKerberos) {
+ Class extends Login> defaultLoginClass = DefaultLogin.class;
+ if (jaasContexts.containsKey(SaslConfigs.GSSAPI_MECHANISM)) {
String defaultRealm;
try {
defaultRealm = defaultKerberosRealm();
@@ -106,12 +130,13 @@ public void configure(Map configs) throws KafkaException {
List principalToLocalRules = (List) configs.get(BrokerSecurityConfigs.SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES_CONFIG);
if (principalToLocalRules != null)
kerberosShortNamer = KerberosShortNamer.fromUnparsedRules(defaultRealm, principalToLocalRules);
+ defaultLoginClass = KerberosLogin.class;
}
for (Map.Entry entry : jaasContexts.entrySet()) {
String mechanism = entry.getKey();
// With static JAAS configuration, use KerberosLogin if Kerberos is enabled. With dynamic JAAS configuration,
// use KerberosLogin only for the LoginContext corresponding to GSSAPI
- LoginManager loginManager = LoginManager.acquireLoginManager(entry.getValue(), mechanism, hasKerberos, configs);
+ LoginManager loginManager = LoginManager.acquireLoginManager(entry.getValue(), mechanism, defaultLoginClass, configs);
loginManagers.put(mechanism, loginManager);
subjects.put(mechanism, loginManager.subject());
}
@@ -120,7 +145,7 @@ public void configure(Map configs) throws KafkaException {
this.sslFactory = new SslFactory(mode, "none", isInterBrokerListener);
this.sslFactory.configure(configs);
}
- } catch (Exception e) {
+ } catch (Throwable e) {
close();
throw new KafkaException(e);
}
@@ -156,11 +181,20 @@ public KafkaChannel buildChannel(String id, SelectionKey key, int maxReceiveSize
TransportLayer transportLayer = buildTransportLayer(id, key, socketChannel);
Authenticator authenticator;
if (mode == Mode.SERVER) {
- authenticator = buildServerAuthenticator(configs, id, transportLayer, subjects);
+ authenticator = buildServerAuthenticator(configs,
+ saslCallbackHandlers,
+ id,
+ transportLayer,
+ subjects);
} else {
LoginManager loginManager = loginManagers.get(clientSaslMechanism);
- authenticator = buildClientAuthenticator(configs, id, socket.getInetAddress().getHostName(),
- loginManager.serviceName(), transportLayer, loginManager.subject());
+ authenticator = buildClientAuthenticator(configs,
+ saslCallbackHandlers.get(clientSaslMechanism),
+ id,
+ socket.getInetAddress().getHostName(),
+ loginManager.serviceName(),
+ transportLayer,
+ subjects.get(clientSaslMechanism));
}
return new KafkaChannel(id, transportLayer, authenticator, maxReceiveSize, memoryPool != null ? memoryPool : MemoryPool.NONE);
} catch (Exception e) {
@@ -174,6 +208,8 @@ public void close() {
for (LoginManager loginManager : loginManagers.values())
loginManager.release();
loginManagers.clear();
+ for (AuthenticateCallbackHandler handler : saslCallbackHandlers.values())
+ handler.close();
}
private TransportLayer buildTransportLayer(String id, SelectionKey key, SocketChannel socketChannel) throws IOException {
@@ -186,16 +222,23 @@ private TransportLayer buildTransportLayer(String id, SelectionKey key, SocketCh
}
// Visible to override for testing
- protected SaslServerAuthenticator buildServerAuthenticator(Map configs, String id,
- TransportLayer transportLayer, Map subjects) throws IOException {
- return new SaslServerAuthenticator(configs, id, jaasContexts, subjects,
- kerberosShortNamer, credentialCache, listenerName, securityProtocol, transportLayer, tokenCache);
+ protected SaslServerAuthenticator buildServerAuthenticator(Map configs,
+ Map callbackHandlers,
+ String id,
+ TransportLayer transportLayer,
+ Map subjects) throws IOException {
+ return new SaslServerAuthenticator(configs, callbackHandlers, id, subjects,
+ kerberosShortNamer, listenerName, securityProtocol, transportLayer);
}
// Visible to override for testing
- protected SaslClientAuthenticator buildClientAuthenticator(Map configs, String id,
- String serverHost, String servicePrincipal, TransportLayer transportLayer, Subject subject) throws IOException {
- return new SaslClientAuthenticator(configs, id, subject, servicePrincipal,
+ protected SaslClientAuthenticator buildClientAuthenticator(Map configs,
+ AuthenticateCallbackHandler callbackHandler,
+ String id,
+ String serverHost,
+ String servicePrincipal,
+ TransportLayer transportLayer, Subject subject) throws IOException {
+ return new SaslClientAuthenticator(configs, callbackHandler, id, subject, servicePrincipal,
serverHost, clientSaslMechanism, handshakeRequestEnable, transportLayer);
}
@@ -224,4 +267,30 @@ private static String defaultKerberosRealm() throws ClassNotFoundException, NoSu
getDefaultRealmMethod = classRef.getDeclaredMethod("getDefaultRealm", new Class[0]);
return (String) getDefaultRealmMethod.invoke(kerbConf, new Object[0]);
}
+
+ private void createClientCallbackHandler(Map configs) {
+ Class extends AuthenticateCallbackHandler> clazz = (Class extends AuthenticateCallbackHandler>) configs.get(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS);
+ if (clazz == null)
+ clazz = clientSaslMechanism.equals(SaslConfigs.GSSAPI_MECHANISM) ? KerberosClientCallbackHandler.class : SaslClientCallbackHandler.class;
+ AuthenticateCallbackHandler callbackHandler = Utils.newInstance(clazz);
+ saslCallbackHandlers.put(clientSaslMechanism, callbackHandler);
+ }
+
+ private void createServerCallbackHandlers(Map configs) throws ClassNotFoundException {
+ for (String mechanism : jaasContexts.keySet()) {
+ AuthenticateCallbackHandler callbackHandler;
+ String prefix = ListenerName.saslMechanismPrefix(mechanism);
+ Class extends AuthenticateCallbackHandler> clazz =
+ (Class extends AuthenticateCallbackHandler>) configs.get(prefix + BrokerSecurityConfigs.SASL_SERVER_CALLBACK_HANDLER_CLASS);
+ if (clazz != null)
+ callbackHandler = Utils.newInstance(clazz);
+ else if (mechanism.equals(PlainSaslServer.PLAIN_MECHANISM))
+ callbackHandler = new PlainServerCallbackHandler();
+ else if (ScramMechanism.isScram(mechanism))
+ callbackHandler = new ScramServerCallbackHandler(credentialCache.cache(mechanism, ScramCredential.class), tokenCache);
+ else
+ callbackHandler = new SaslServerCallbackHandler();
+ saslCallbackHandlers.put(mechanism, callbackHandler);
+ }
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/common/requests/DescribeDelegationTokenResponse.java b/clients/src/main/java/org/apache/kafka/common/requests/DescribeDelegationTokenResponse.java
index dba29eafe9937..7ba270a61530b 100644
--- a/clients/src/main/java/org/apache/kafka/common/requests/DescribeDelegationTokenResponse.java
+++ b/clients/src/main/java/org/apache/kafka/common/requests/DescribeDelegationTokenResponse.java
@@ -184,4 +184,8 @@ public Errors error() {
public List tokens() {
return tokens;
}
+
+ public boolean hasError() {
+ return this.error != Errors.NONE;
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/common/requests/ExpireDelegationTokenRequest.java b/clients/src/main/java/org/apache/kafka/common/requests/ExpireDelegationTokenRequest.java
index 0d43440d32986..40f0aadc0bb85 100644
--- a/clients/src/main/java/org/apache/kafka/common/requests/ExpireDelegationTokenRequest.java
+++ b/clients/src/main/java/org/apache/kafka/common/requests/ExpireDelegationTokenRequest.java
@@ -88,9 +88,9 @@ public static class Builder extends AbstractRequest.Builderkey from this context.
* If login module name is specified, return option value only from that module.
*/
- public String configEntryOption(String key, String loginModuleName) {
+ public static String configEntryOption(List configurationEntries, String key, String loginModuleName) {
for (AppConfigurationEntry entry : configurationEntries) {
if (loginModuleName != null && !loginModuleName.equals(entry.getLoginModuleName()))
continue;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/auth/AuthenticateCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/auth/AuthenticateCallbackHandler.java
new file mode 100644
index 0000000000000..8951d3a589367
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/security/auth/AuthenticateCallbackHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.common.security.auth;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.AppConfigurationEntry;
+
+/*
+ * Callback handler for SASL-based authentication
+ */
+public interface AuthenticateCallbackHandler extends CallbackHandler {
+
+ /**
+ * Configures this callback handler for the specified SASL mechanism.
+ *
+ * @param configs Key-value pairs containing the parsed configuration options of
+ * the client or broker. Note that these are the Kafka configuration options
+ * and not the JAAS configuration options. JAAS config options may be obtained
+ * from `jaasConfigEntries` for callbacks which obtain some configs from the
+ * JAAS configuration. For configs that may be specified as both Kafka config
+ * as well as JAAS config (e.g. sasl.kerberos.service.name), the configuration
+ * is treated as invalid if conflicting values are provided.
+ * @param saslMechanism Negotiated SASL mechanism. For clients, this is the SASL
+ * mechanism configured for the client. For brokers, this is the mechanism
+ * negotiated with the client and is one of the mechanisms enabled on the broker.
+ * @param jaasConfigEntries JAAS configuration entries from the JAAS login context.
+ * This list contains a single entry for clients and may contain more than
+ * one entry for brokers if multiple mechanisms are enabled on a listener using
+ * static JAAS configuration where there is no mapping between mechanisms and
+ * login module entries. In this case, callback handlers can use the login module in
+ * `jaasConfigEntries` to identify the entry corresponding to `saslMechanism`.
+ * Alternatively, dynamic JAAS configuration option
+ * {@link org.apache.kafka.common.config.SaslConfigs#SASL_JAAS_CONFIG} may be
+ * configured on brokers with listener and mechanism prefix, in which case
+ * only the configuration entry corresponding to `saslMechanism` will be provided
+ * in `jaasConfigEntries`.
+ */
+ void configure(Map configs, String saslMechanism, List jaasConfigEntries);
+
+ /**
+ * Closes this instance.
+ */
+ void close();
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/Login.java b/clients/src/main/java/org/apache/kafka/common/security/auth/Login.java
similarity index 55%
rename from clients/src/main/java/org/apache/kafka/common/security/authenticator/Login.java
rename to clients/src/main/java/org/apache/kafka/common/security/auth/Login.java
index b41d1b2572fcc..eda5e7a225af1 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/Login.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/auth/Login.java
@@ -14,13 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.authenticator;
-
-import org.apache.kafka.common.security.JaasContext;
+package org.apache.kafka.common.security.auth;
import java.util.Map;
import javax.security.auth.Subject;
+import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
@@ -31,8 +30,21 @@ public interface Login {
/**
* Configures this login instance.
+ * @param configs Key-value pairs containing the parsed configuration options of
+ * the client or broker. Note that these are the Kafka configuration options
+ * and not the JAAS configuration options. The JAAS options may be obtained
+ * from `jaasConfiguration`.
+ * @param contextName JAAS context name for this login which may be used to obtain
+ * the login context from `jaasConfiguration`.
+ * @param jaasConfiguration JAAS configuration containing the login context named
+ * `contextName`. If static JAAS configuration is used, this `Configuration`
+ * may also contain other login contexts.
+ * @param loginCallbackHandler Login callback handler instance to use for this Login.
+ * Login callback handler class may be configured using
+ * {@link org.apache.kafka.common.config.SaslConfigs#SASL_LOGIN_CALLBACK_HANDLER_CLASS}.
*/
- void configure(Map configs, JaasContext jaasContext);
+ void configure(Map configs, String contextName, Configuration jaasConfiguration,
+ AuthenticateCallbackHandler loginCallbackHandler);
/**
* Performs login for each login module specified for the login context of this instance.
@@ -54,4 +66,3 @@ public interface Login {
*/
void close();
}
-
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/AbstractLogin.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/AbstractLogin.java
index 643f859e8298c..7e1350864e438 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/AbstractLogin.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/AbstractLogin.java
@@ -16,20 +16,23 @@
*/
package org.apache.kafka.common.security.authenticator;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.sasl.RealmCallback;
import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.Subject;
-import org.apache.kafka.common.security.JaasContext;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+import org.apache.kafka.common.security.auth.Login;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.List;
import java.util.Map;
/**
@@ -38,17 +41,22 @@
public abstract class AbstractLogin implements Login {
private static final Logger log = LoggerFactory.getLogger(AbstractLogin.class);
- private JaasContext jaasContext;
+ private String contextName;
+ private Configuration configuration;
private LoginContext loginContext;
+ private AuthenticateCallbackHandler loginCallbackHandler;
@Override
- public void configure(Map configs, JaasContext jaasContext) {
- this.jaasContext = jaasContext;
+ public void configure(Map configs, String contextName, Configuration configuration,
+ AuthenticateCallbackHandler loginCallbackHandler) {
+ this.contextName = contextName;
+ this.configuration = configuration;
+ this.loginCallbackHandler = loginCallbackHandler;
}
@Override
public LoginContext login() throws LoginException {
- loginContext = new LoginContext(jaasContext.name(), null, new LoginCallbackHandler(), jaasContext.configuration());
+ loginContext = new LoginContext(contextName, null, loginCallbackHandler, configuration);
loginContext.login();
log.info("Successfully logged in.");
return loginContext;
@@ -59,8 +67,12 @@ public Subject subject() {
return loginContext.getSubject();
}
- protected JaasContext jaasContext() {
- return jaasContext;
+ protected String contextName() {
+ return contextName;
+ }
+
+ protected Configuration configuration() {
+ return configuration;
}
/**
@@ -70,7 +82,11 @@ protected JaasContext jaasContext() {
* callback handlers which require additional user input.
*
*/
- public static class LoginCallbackHandler implements CallbackHandler {
+ public static class DefaultLoginCallbackHandler implements AuthenticateCallbackHandler {
+
+ @Override
+ public void configure(Map configs, String saslMechanism, List jaasConfigEntries) {
+ }
@Override
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
@@ -90,6 +106,10 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
}
}
}
+
+ @Override
+ public void close() {
+ }
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/LoginManager.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/LoginManager.java
index 81dc063c7433f..4ae798d7d293b 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/LoginManager.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/LoginManager.java
@@ -23,11 +23,17 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
+
+import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.types.Password;
+import org.apache.kafka.common.network.ListenerName;
import org.apache.kafka.common.security.JaasContext;
-import org.apache.kafka.common.security.kerberos.KerberosLogin;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+import org.apache.kafka.common.security.auth.Login;
+import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,20 +42,23 @@ public class LoginManager {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginManager.class);
// static configs (broker or client)
- private static final Map STATIC_INSTANCES = new HashMap<>();
+ private static final Map, LoginManager> STATIC_INSTANCES = new HashMap<>();
- // dynamic configs (client-only)
- private static final Map DYNAMIC_INSTANCES = new HashMap<>();
+ // dynamic configs (broker or client)
+ private static final Map, LoginManager> DYNAMIC_INSTANCES = new HashMap<>();
private final Login login;
- private final Object cacheKey;
+ private final LoginMetadata> loginMetadata;
+ private final AuthenticateCallbackHandler loginCallbackHandler;
private int refCount;
- private LoginManager(JaasContext jaasContext, boolean hasKerberos, Map configs,
- Object cacheKey) throws IOException, LoginException {
- this.cacheKey = cacheKey;
- login = hasKerberos ? new KerberosLogin() : new DefaultLogin();
- login.configure(configs, jaasContext);
+ private LoginManager(JaasContext jaasContext, String saslMechanism, Map configs,
+ LoginMetadata> loginMetadata) throws IOException, LoginException {
+ this.loginMetadata = loginMetadata;
+ this.login = Utils.newInstance(loginMetadata.loginClass);
+ loginCallbackHandler = Utils.newInstance(loginMetadata.loginCallbackClass);
+ loginCallbackHandler.configure(configs, saslMechanism, jaasContext.configurationEntries());
+ login.configure(configs, jaasContext.name(), jaasContext.configuration(), loginCallbackHandler);
login.login();
}
@@ -72,28 +81,34 @@ private LoginManager(JaasContext jaasContext, boolean hasKerberos, Map defaultLoginClass,
Map configs) throws IOException, LoginException {
+ Class extends Login> loginClass = configuredClassOrDefault(configs, jaasContext,
+ saslMechanism, SaslConfigs.SASL_LOGIN_CLASS, defaultLoginClass);
+ Class extends AuthenticateCallbackHandler> loginCallbackClass = configuredClassOrDefault(configs,
+ jaasContext, saslMechanism, SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
+ AbstractLogin.DefaultLoginCallbackHandler.class);
synchronized (LoginManager.class) {
LoginManager loginManager;
Password jaasConfigValue = jaasContext.dynamicJaasConfig();
if (jaasConfigValue != null) {
- loginManager = DYNAMIC_INSTANCES.get(jaasConfigValue);
+ LoginMetadata loginMetadata = new LoginMetadata<>(jaasConfigValue, loginClass, loginCallbackClass);
+ loginManager = DYNAMIC_INSTANCES.get(loginMetadata);
if (loginManager == null) {
- loginManager = new LoginManager(jaasContext, saslMechanism.equals(SaslConfigs.GSSAPI_MECHANISM), configs, jaasConfigValue);
- DYNAMIC_INSTANCES.put(jaasConfigValue, loginManager);
+ loginManager = new LoginManager(jaasContext, saslMechanism, configs, loginMetadata);
+ DYNAMIC_INSTANCES.put(loginMetadata, loginManager);
}
} else {
- loginManager = STATIC_INSTANCES.get(jaasContext.name());
+ LoginMetadata loginMetadata = new LoginMetadata<>(jaasContext.name(), loginClass, loginCallbackClass);
+ loginManager = STATIC_INSTANCES.get(loginMetadata);
if (loginManager == null) {
- loginManager = new LoginManager(jaasContext, hasKerberos, configs, jaasContext.name());
- STATIC_INSTANCES.put(jaasContext.name(), loginManager);
+ loginManager = new LoginManager(jaasContext, saslMechanism, configs, loginMetadata);
+ STATIC_INSTANCES.put(loginMetadata, loginManager);
}
}
return loginManager.acquire();
@@ -110,7 +125,7 @@ public String serviceName() {
// Only for testing
Object cacheKey() {
- return cacheKey;
+ return loginMetadata.configInfo;
}
private LoginManager acquire() {
@@ -127,12 +142,13 @@ public void release() {
if (refCount == 0)
throw new IllegalStateException("release() called on disposed " + this);
else if (refCount == 1) {
- if (cacheKey instanceof Password) {
- DYNAMIC_INSTANCES.remove(cacheKey);
+ if (loginMetadata.configInfo instanceof Password) {
+ DYNAMIC_INSTANCES.remove(loginMetadata);
} else {
- STATIC_INSTANCES.remove(cacheKey);
+ STATIC_INSTANCES.remove(loginMetadata);
}
login.close();
+ loginCallbackHandler.close();
}
--refCount;
LOGGER.trace("{} released", this);
@@ -150,10 +166,56 @@ public String toString() {
/* Should only be used in tests. */
public static void closeAll() {
synchronized (LoginManager.class) {
- for (String key : new ArrayList<>(STATIC_INSTANCES.keySet()))
+ for (LoginMetadata key : new ArrayList<>(STATIC_INSTANCES.keySet()))
STATIC_INSTANCES.remove(key).login.close();
- for (Password key : new ArrayList<>(DYNAMIC_INSTANCES.keySet()))
+ for (LoginMetadata key : new ArrayList<>(DYNAMIC_INSTANCES.keySet()))
DYNAMIC_INSTANCES.remove(key).login.close();
}
}
+
+ private static Class extends T> configuredClassOrDefault(Map configs,
+ JaasContext jaasContext,
+ String saslMechanism,
+ String configName,
+ Class extends T> defaultClass) {
+ String prefix = jaasContext.type() == JaasContext.Type.SERVER ? ListenerName.saslMechanismPrefix(saslMechanism) : "";
+ Class extends T> clazz = (Class extends T>) configs.get(prefix + configName);
+ if (clazz != null && jaasContext.configurationEntries().size() != 1) {
+ String errorMessage = configName + " cannot be specified with multiple login modules in the JAAS context. " +
+ SaslConfigs.SASL_JAAS_CONFIG + " must be configured to override mechanism-specific configs.";
+ throw new ConfigException(errorMessage);
+ }
+ if (clazz == null)
+ clazz = defaultClass;
+ return clazz;
+ }
+
+ private static class LoginMetadata {
+ final T configInfo;
+ final Class extends Login> loginClass;
+ final Class extends AuthenticateCallbackHandler> loginCallbackClass;
+
+ LoginMetadata(T configInfo, Class extends Login> loginClass,
+ Class extends AuthenticateCallbackHandler> loginCallbackClass) {
+ this.configInfo = configInfo;
+ this.loginClass = loginClass;
+ this.loginCallbackClass = loginCallbackClass;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(configInfo, loginClass, loginCallbackClass);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LoginMetadata> loginMetadata = (LoginMetadata>) o;
+ return Objects.equals(configInfo, loginMetadata.configInfo) &&
+ Objects.equals(loginClass, loginMetadata.loginClass) &&
+ Objects.equals(loginCallbackClass, loginMetadata.loginCallbackClass);
+ }
+ }
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientAuthenticator.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientAuthenticator.java
index 8b0116563d8be..2ef6d77f13f36 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientAuthenticator.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientAuthenticator.java
@@ -24,9 +24,8 @@
import org.apache.kafka.common.errors.SaslAuthenticationException;
import org.apache.kafka.common.errors.UnsupportedSaslMechanismException;
import org.apache.kafka.common.network.Authenticator;
-import org.apache.kafka.common.network.Mode;
-import org.apache.kafka.common.network.NetworkReceive;
import org.apache.kafka.common.network.NetworkSend;
+import org.apache.kafka.common.network.NetworkReceive;
import org.apache.kafka.common.network.Send;
import org.apache.kafka.common.network.TransportLayer;
import org.apache.kafka.common.protocol.ApiKeys;
@@ -41,6 +40,7 @@
import org.apache.kafka.common.requests.SaslAuthenticateResponse;
import org.apache.kafka.common.requests.SaslHandshakeRequest;
import org.apache.kafka.common.requests.SaslHandshakeResponse;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.auth.KafkaPrincipal;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
@@ -87,7 +87,7 @@ public enum SaslState {
private final SaslClient saslClient;
private final Map configs;
private final String clientPrincipalName;
- private final AuthCallbackHandler callbackHandler;
+ private final AuthenticateCallbackHandler callbackHandler;
// buffers used in `authenticate`
private NetworkReceive netInBuffer;
@@ -105,6 +105,7 @@ public enum SaslState {
private short saslAuthenticateVersion;
public SaslClientAuthenticator(Map configs,
+ AuthenticateCallbackHandler callbackHandler,
String node,
Subject subject,
String servicePrincipal,
@@ -114,6 +115,7 @@ public SaslClientAuthenticator(Map configs,
TransportLayer transportLayer) throws IOException {
this.node = node;
this.subject = subject;
+ this.callbackHandler = callbackHandler;
this.host = host;
this.servicePrincipal = servicePrincipal;
this.mechanism = mechanism;
@@ -133,9 +135,6 @@ public SaslClientAuthenticator(Map configs,
else
this.clientPrincipalName = null;
- callbackHandler = new SaslClientCallbackHandler();
- callbackHandler.configure(configs, Mode.CLIENT, subject, mechanism);
-
saslClient = createSaslClient();
} catch (Exception e) {
throw new SaslAuthenticationException("Failed to configure SaslClientAuthenticator", e);
@@ -325,8 +324,6 @@ public boolean complete() {
public void close() throws IOException {
if (saslClient != null)
saslClient.dispose();
- if (callbackHandler != null)
- callbackHandler.close();
}
private byte[] receiveToken() throws IOException {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientCallbackHandler.java
index 31c51c22ca38c..5b2a28181cd58 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientCallbackHandler.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslClientCallbackHandler.java
@@ -16,6 +16,8 @@
*/
package org.apache.kafka.common.security.authenticator;
+import java.security.AccessController;
+import java.util.List;
import java.util.Map;
import javax.security.auth.Subject;
@@ -23,52 +25,46 @@
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import org.apache.kafka.common.config.SaslConfigs;
-import org.apache.kafka.common.network.Mode;
import org.apache.kafka.common.security.scram.ScramExtensionsCallback;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
/**
- * Callback handler for Sasl clients. The callbacks required for the SASL mechanism
+ * Default callback handler for Sasl clients. The callbacks required for the SASL mechanism
* configured for the client should be supported by this callback handler. See
* Java SASL API
* for the list of SASL callback handlers required for each SASL mechanism.
*/
-public class SaslClientCallbackHandler implements AuthCallbackHandler {
+public class SaslClientCallbackHandler implements AuthenticateCallbackHandler {
- private boolean isKerberos;
- private Subject subject;
+ private String mechanism;
@Override
- public void configure(Map configs, Mode mode, Subject subject, String mechanism) {
- this.isKerberos = mechanism.equals(SaslConfigs.GSSAPI_MECHANISM);
- this.subject = subject;
+ public void configure(Map configs, String saslMechanism, List jaasConfigEntries) {
+ this.mechanism = saslMechanism;
}
@Override
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
+ Subject subject = Subject.getSubject(AccessController.getContext());
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nc = (NameCallback) callback;
- if (!isKerberos && subject != null && !subject.getPublicCredentials(String.class).isEmpty()) {
+ if (subject != null && !subject.getPublicCredentials(String.class).isEmpty()) {
nc.setName(subject.getPublicCredentials(String.class).iterator().next());
} else
nc.setName(nc.getDefaultName());
} else if (callback instanceof PasswordCallback) {
- if (!isKerberos && subject != null && !subject.getPrivateCredentials(String.class).isEmpty()) {
+ if (subject != null && !subject.getPrivateCredentials(String.class).isEmpty()) {
char[] password = subject.getPrivateCredentials(String.class).iterator().next().toCharArray();
((PasswordCallback) callback).setPassword(password);
} else {
String errorMessage = "Could not login: the client is being asked for a password, but the Kafka" +
" client code does not currently support obtaining a password from the user.";
- if (isKerberos) {
- errorMessage += " Make sure -Djava.security.auth.login.config property passed to JVM and" +
- " the client is configured to use a ticket cache (using" +
- " the JAAS configuration setting 'useTicketCache=true)'. Make sure you are using" +
- " FQDN of the Kafka broker you are trying to connect to.";
- }
throw new UnsupportedCallbackException(callback, errorMessage);
}
} else if (callback instanceof RealmCallback) {
@@ -83,7 +79,7 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
ac.setAuthorizedID(authzId);
} else if (callback instanceof ScramExtensionsCallback) {
ScramExtensionsCallback sc = (ScramExtensionsCallback) callback;
- if (!isKerberos && subject != null && !subject.getPublicCredentials(Map.class).isEmpty()) {
+ if (!SaslConfigs.GSSAPI_MECHANISM.equals(mechanism) && subject != null && !subject.getPublicCredentials(Map.class).isEmpty()) {
sc.extensions((Map) subject.getPublicCredentials(Map.class).iterator().next());
}
} else {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
index 2a80e5bc0e4c0..5140afb196d2c 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java
@@ -28,7 +28,6 @@
import org.apache.kafka.common.network.Authenticator;
import org.apache.kafka.common.network.ChannelBuilders;
import org.apache.kafka.common.network.ListenerName;
-import org.apache.kafka.common.network.Mode;
import org.apache.kafka.common.network.NetworkReceive;
import org.apache.kafka.common.network.NetworkSend;
import org.apache.kafka.common.network.Send;
@@ -46,18 +45,15 @@
import org.apache.kafka.common.requests.SaslAuthenticateResponse;
import org.apache.kafka.common.requests.SaslHandshakeRequest;
import org.apache.kafka.common.requests.SaslHandshakeResponse;
-import org.apache.kafka.common.security.JaasContext;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.auth.KafkaPrincipal;
import org.apache.kafka.common.security.auth.KafkaPrincipalBuilder;
import org.apache.kafka.common.security.auth.SaslAuthenticationContext;
import org.apache.kafka.common.security.kerberos.KerberosName;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
-import org.apache.kafka.common.security.scram.ScramCredential;
import org.apache.kafka.common.security.scram.ScramLoginModule;
-import org.apache.kafka.common.security.scram.ScramMechanism;
-import org.apache.kafka.common.security.scram.ScramServerCallbackHandler;
+import org.apache.kafka.common.security.scram.internal.ScramMechanism;
import org.apache.kafka.common.utils.Utils;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
@@ -102,14 +98,12 @@ private enum SaslState {
private final SecurityProtocol securityProtocol;
private final ListenerName listenerName;
private final String connectionId;
- private final Map jaasContexts;
private final Map subjects;
- private final CredentialCache credentialCache;
private final TransportLayer transportLayer;
private final Set enabledMechanisms;
private final Map configs;
private final KafkaPrincipalBuilder principalBuilder;
- private final DelegationTokenCache tokenCache;
+ private final Map callbackHandlers;
// Current SASL state
private SaslState saslState = SaslState.INITIAL_REQUEST;
@@ -119,7 +113,6 @@ private enum SaslState {
private AuthenticationException pendingException = null;
private SaslServer saslServer;
private String saslMechanism;
- private AuthCallbackHandler callbackHandler;
// buffers used in `authenticate`
private NetworkReceive netInBuffer;
@@ -128,23 +121,19 @@ private enum SaslState {
private boolean enableKafkaSaslAuthenticateHeaders;
public SaslServerAuthenticator(Map configs,
+ Map callbackHandlers,
String connectionId,
- Map jaasContexts,
Map subjects,
KerberosShortNamer kerberosNameParser,
- CredentialCache credentialCache,
ListenerName listenerName,
SecurityProtocol securityProtocol,
- TransportLayer transportLayer,
- DelegationTokenCache tokenCache) throws IOException {
+ TransportLayer transportLayer) throws IOException {
+ this.callbackHandlers = callbackHandlers;
this.connectionId = connectionId;
- this.jaasContexts = jaasContexts;
this.subjects = subjects;
- this.credentialCache = credentialCache;
this.listenerName = listenerName;
this.securityProtocol = securityProtocol;
this.enableKafkaSaslAuthenticateHeaders = false;
- this.tokenCache = tokenCache;
this.transportLayer = transportLayer;
this.configs = configs;
@@ -154,8 +143,8 @@ public SaslServerAuthenticator(Map configs,
throw new IllegalArgumentException("No SASL mechanisms are enabled");
this.enabledMechanisms = new HashSet<>(enabledMechanisms);
for (String mechanism : enabledMechanisms) {
- if (!jaasContexts.containsKey(mechanism))
- throw new IllegalArgumentException("Jaas context not specified for SASL mechanism " + mechanism);
+ if (!callbackHandlers.containsKey(mechanism))
+ throw new IllegalArgumentException("Callback handler not specified for SASL mechanism " + mechanism);
if (!subjects.containsKey(mechanism))
throw new IllegalArgumentException("Subject cannot be null for SASL mechanism " + mechanism);
}
@@ -168,11 +157,7 @@ public SaslServerAuthenticator(Map configs,
private void createSaslServer(String mechanism) throws IOException {
this.saslMechanism = mechanism;
Subject subject = subjects.get(mechanism);
- if (!ScramMechanism.isScram(mechanism))
- callbackHandler = new SaslServerCallbackHandler(jaasContexts.get(mechanism));
- else
- callbackHandler = new ScramServerCallbackHandler(credentialCache.cache(mechanism, ScramCredential.class), tokenCache);
- callbackHandler.configure(configs, Mode.SERVER, subject, saslMechanism);
+ final AuthenticateCallbackHandler callbackHandler = callbackHandlers.get(mechanism);
if (mechanism.equals(SaslConfigs.GSSAPI_MECHANISM)) {
saslServer = createSaslKerberosServer(callbackHandler, configs, subject);
} else {
@@ -189,7 +174,7 @@ public SaslServer run() throws SaslException {
}
}
- private SaslServer createSaslKerberosServer(final AuthCallbackHandler saslServerCallbackHandler, final Map configs, Subject subject) throws IOException {
+ private SaslServer createSaslKerberosServer(final AuthenticateCallbackHandler saslServerCallbackHandler, final Map configs, Subject subject) throws IOException {
// server is using a JAAS-authenticated subject: determine service principal name and hostname from kafka server's subject.
final String servicePrincipal = SaslClientAuthenticator.firstPrincipal(subject);
KerberosName kerberosName;
@@ -316,8 +301,6 @@ public void close() throws IOException {
Utils.closeQuietly((Closeable) principalBuilder, "principal builder");
if (saslServer != null)
saslServer.dispose();
- if (callbackHandler != null)
- callbackHandler.close();
}
private void setSaslState(SaslState saslState) throws IOException {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerCallbackHandler.java
index 7d5372db9771d..d3d43cbfa26d2 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerCallbackHandler.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerCallbackHandler.java
@@ -16,51 +16,46 @@
*/
package org.apache.kafka.common.security.authenticator;
-import java.io.IOException;
+import java.util.List;
import java.util.Map;
-import org.apache.kafka.common.security.JaasContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
-import org.apache.kafka.common.network.Mode;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
- * Callback handler for Sasl servers. The callbacks required for all the SASL
+ * Default callback handler for Sasl servers. The callbacks required for all the SASL
* mechanisms enabled in the server should be supported by this callback handler. See
* Java SASL API
* for the list of SASL callback handlers required for each SASL mechanism.
*/
-public class SaslServerCallbackHandler implements AuthCallbackHandler {
+public class SaslServerCallbackHandler implements AuthenticateCallbackHandler {
private static final Logger LOG = LoggerFactory.getLogger(SaslServerCallbackHandler.class);
- private final JaasContext jaasContext;
- public SaslServerCallbackHandler(JaasContext jaasContext) throws IOException {
- this.jaasContext = jaasContext;
- }
+ private String mechanism;
@Override
- public void configure(Map configs, Mode mode, Subject subject, String saslMechanism) {
- }
-
- public JaasContext jaasContext() {
- return jaasContext;
+ public void configure(Map configs, String mechanism, List jaasConfigEntries) {
+ this.mechanism = mechanism;
}
@Override
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
for (Callback callback : callbacks) {
- if (callback instanceof RealmCallback) {
+ if (callback instanceof RealmCallback)
handleRealmCallback((RealmCallback) callback);
- } else if (callback instanceof AuthorizeCallback) {
+ else if (callback instanceof AuthorizeCallback && mechanism.equals(SaslConfigs.GSSAPI_MECHANISM))
handleAuthorizeCallback((AuthorizeCallback) callback);
- }
+ else
+ throw new UnsupportedCallbackException(callback);
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosClientCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosClientCallbackHandler.java
new file mode 100644
index 0000000000000..fa9cad261c589
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosClientCallbackHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.kafka.common.security.kerberos;
+
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.sasl.AuthorizeCallback;
+import javax.security.sasl.RealmCallback;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Callback handler for SASL/GSSAPI clients.
+ */
+public class KerberosClientCallbackHandler implements AuthenticateCallbackHandler {
+
+ @Override
+ public void configure(Map configs, String saslMechanism, List jaasConfigEntries) {
+ if (!saslMechanism.equals(SaslConfigs.GSSAPI_MECHANISM))
+ throw new IllegalStateException("Kerberos callback handler should only be used with GSSAPI");
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback) {
+ NameCallback nc = (NameCallback) callback;
+ nc.setName(nc.getDefaultName());
+ } else if (callback instanceof PasswordCallback) {
+ String errorMessage = "Could not login: the client is being asked for a password, but the Kafka" +
+ " client code does not currently support obtaining a password from the user.";
+ errorMessage += " Make sure -Djava.security.auth.login.config property passed to JVM and" +
+ " the client is configured to use a ticket cache (using" +
+ " the JAAS configuration setting 'useTicketCache=true)'. Make sure you are using" +
+ " FQDN of the Kafka broker you are trying to connect to.";
+ throw new UnsupportedCallbackException(callback, errorMessage);
+ } else if (callback instanceof RealmCallback) {
+ RealmCallback rc = (RealmCallback) callback;
+ rc.setText(rc.getDefaultText());
+ } else if (callback instanceof AuthorizeCallback) {
+ AuthorizeCallback ac = (AuthorizeCallback) callback;
+ String authId = ac.getAuthenticationID();
+ String authzId = ac.getAuthorizationID();
+ ac.setAuthorized(authId.equals(authzId));
+ if (ac.isAuthorized())
+ ac.setAuthorizedID(authzId);
+ } else {
+ throw new UnsupportedCallbackException(callback, "Unrecognized SASL ClientCallback");
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosLogin.java b/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosLogin.java
index 65c3b1cbe444c..ec996a8ccc6c0 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosLogin.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/kerberos/KerberosLogin.java
@@ -18,6 +18,7 @@
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.auth.kerberos.KerberosTicket;
@@ -25,6 +26,7 @@
import org.apache.kafka.common.security.JaasContext;
import org.apache.kafka.common.security.JaasUtils;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.authenticator.AbstractLogin;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.utils.KafkaThread;
@@ -33,11 +35,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.Set;
-import java.util.Map;
/**
* This class is responsible for refreshing Kerberos credentials for
@@ -78,13 +81,15 @@ public class KerberosLogin extends AbstractLogin {
private String serviceName;
private long lastLogin;
- public void configure(Map configs, JaasContext jaasContext) {
- super.configure(configs, jaasContext);
+ @Override
+ public void configure(Map configs, String contextName, Configuration configuration,
+ AuthenticateCallbackHandler callbackHandler) {
+ super.configure(configs, contextName, configuration, callbackHandler);
this.ticketRenewWindowFactor = (Double) configs.get(SaslConfigs.SASL_KERBEROS_TICKET_RENEW_WINDOW_FACTOR);
this.ticketRenewJitter = (Double) configs.get(SaslConfigs.SASL_KERBEROS_TICKET_RENEW_JITTER);
this.minTimeBeforeRelogin = (Long) configs.get(SaslConfigs.SASL_KERBEROS_MIN_TIME_BEFORE_RELOGIN);
this.kinitCmd = (String) configs.get(SaslConfigs.SASL_KERBEROS_KINIT_CMD);
- this.serviceName = getServiceName(configs, jaasContext);
+ this.serviceName = getServiceName(configs, contextName, configuration);
}
/**
@@ -99,13 +104,13 @@ public LoginContext login() throws LoginException {
subject = loginContext.getSubject();
isKrbTicket = !subject.getPrivateCredentials(KerberosTicket.class).isEmpty();
- List entries = jaasContext().configurationEntries();
- if (entries.isEmpty()) {
+ AppConfigurationEntry[] entries = configuration().getAppConfigurationEntry(contextName());
+ if (entries.length == 0) {
isUsingTicketCache = false;
principal = null;
} else {
// there will only be a single entry
- AppConfigurationEntry entry = entries.get(0);
+ AppConfigurationEntry entry = entries[0];
if (entry.getOptions().get("useTicketCache") != null) {
String val = (String) entry.getOptions().get("useTicketCache");
isUsingTicketCache = val.equals("true");
@@ -280,8 +285,9 @@ public String serviceName() {
return serviceName;
}
- private static String getServiceName(Map configs, JaasContext jaasContext) {
- String jaasServiceName = jaasContext.configEntryOption(JaasUtils.SERVICE_NAME, null);
+ private static String getServiceName(Map configs, String contextName, Configuration configuration) {
+ List configEntries = Arrays.asList(configuration.getAppConfigurationEntry(contextName));
+ String jaasServiceName = JaasContext.configEntryOption(configEntries, JaasUtils.SERVICE_NAME, null);
String configServiceName = (String) configs.get(SaslConfigs.SASL_KERBEROS_SERVICE_NAME);
if (jaasServiceName != null && configServiceName != null && !jaasServiceName.equals(configServiceName)) {
String message = String.format("Conflicting serviceName values found in JAAS and Kafka configs " +
@@ -360,7 +366,7 @@ private void reLogin() throws LoginException {
loginContext.logout();
//login and also update the subject field of this instance to
//have the new credentials (pass it to the LoginContext constructor)
- loginContext = new LoginContext(jaasContext().name(), subject, null, jaasContext().configuration());
+ loginContext = new LoginContext(contextName(), subject, null, configuration());
log.info("Initiating re-login for {}", principal);
loginContext.login();
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainAuthenticateCallback.java b/clients/src/main/java/org/apache/kafka/common/security/plain/PlainAuthenticateCallback.java
new file mode 100644
index 0000000000000..7f42645e487fb
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/security/plain/PlainAuthenticateCallback.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.common.security.plain;
+
+import javax.security.auth.callback.Callback;
+
+/*
+ * Authentication callback for SASL/PLAIN authentication. Callback handler must
+ * set authenticated flag to true if the client provided password in the callback
+ * matches the expected password.
+ */
+public class PlainAuthenticateCallback implements Callback {
+ private final char[] password;
+ private boolean authenticated;
+
+ /**
+ * Creates a callback with the password provided by the client
+ * @param password The password provided by the client during SASL/PLAIN authentication
+ */
+ public PlainAuthenticateCallback(char[] password) {
+ this.password = password;
+ }
+
+ /**
+ * Returns the password provided by the client during SASL/PLAIN authentication
+ */
+ public char[] password() {
+ return password;
+ }
+
+ /**
+ * Returns true if client password matches expected password, false otherwise.
+ * This state is set the server-side callback handler.
+ */
+ public boolean authenticated() {
+ return this.authenticated;
+ }
+
+ /**
+ * Sets the authenticated state. This is set by the server-side callback handler
+ * by matching the client provided password with the expected password.
+ *
+ * @param authenticated true indicates successful authentication
+ */
+ public void authenticated(boolean authenticated) {
+ this.authenticated = authenticated;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainLoginModule.java b/clients/src/main/java/org/apache/kafka/common/security/plain/PlainLoginModule.java
index c8b29fc69a9cc..f0a5971ebebbf 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainLoginModule.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/plain/PlainLoginModule.java
@@ -16,6 +16,8 @@
*/
package org.apache.kafka.common.security.plain;
+import org.apache.kafka.common.security.plain.internal.PlainSaslServerProvider;
+
import java.util.Map;
import javax.security.auth.Subject;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServer.java b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServer.java
similarity index 87%
rename from clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServer.java
rename to clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServer.java
index e54887f652ffb..811d9e94acafe 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServer.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServer.java
@@ -14,21 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.plain;
+package org.apache.kafka.common.security.plain.internal;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
+import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;
import org.apache.kafka.common.errors.SaslAuthenticationException;
-import org.apache.kafka.common.security.JaasContext;
-import org.apache.kafka.common.security.authenticator.SaslServerCallbackHandler;
+import org.apache.kafka.common.security.plain.PlainAuthenticateCallback;
/**
* Simple SaslServer implementation for SASL/PLAIN. In order to make this implementation
@@ -46,15 +47,13 @@
public class PlainSaslServer implements SaslServer {
public static final String PLAIN_MECHANISM = "PLAIN";
- private static final String JAAS_USER_PREFIX = "user_";
-
- private final JaasContext jaasContext;
+ private final CallbackHandler callbackHandler;
private boolean complete;
private String authorizationId;
- public PlainSaslServer(JaasContext jaasContext) {
- this.jaasContext = jaasContext;
+ public PlainSaslServer(CallbackHandler callbackHandler) {
+ this.callbackHandler = callbackHandler;
}
/**
@@ -101,12 +100,15 @@ public byte[] evaluateResponse(byte[] response) throws SaslException, SaslAuthen
throw new SaslException("Authentication failed: password not specified");
}
- String expectedPassword = jaasContext.configEntryOption(JAAS_USER_PREFIX + username,
- PlainLoginModule.class.getName());
- if (!password.equals(expectedPassword)) {
- throw new SaslAuthenticationException("Authentication failed: Invalid username or password");
+ NameCallback nameCallback = new NameCallback("username", username);
+ PlainAuthenticateCallback authenticateCallback = new PlainAuthenticateCallback(password.toCharArray());
+ try {
+ callbackHandler.handle(new Callback[]{nameCallback, authenticateCallback});
+ } catch (Throwable e) {
+ throw new SaslAuthenticationException("Authentication failed: credentials for user could not be verified", e);
}
-
+ if (!authenticateCallback.authenticated())
+ throw new SaslAuthenticationException("Authentication failed: Invalid username or password");
if (!authorizationIdFromClient.isEmpty() && !authorizationIdFromClient.equals(username))
throw new SaslAuthenticationException("Authentication failed: Client requested an authorization id that is different from username");
@@ -167,10 +169,7 @@ public SaslServer createSaslServer(String mechanism, String protocol, String ser
if (!PLAIN_MECHANISM.equals(mechanism))
throw new SaslException(String.format("Mechanism \'%s\' is not supported. Only PLAIN is supported.", mechanism));
- if (!(cbh instanceof SaslServerCallbackHandler))
- throw new SaslException("CallbackHandler must be of type SaslServerCallbackHandler, but it is: " + cbh.getClass());
-
- return new PlainSaslServer(((SaslServerCallbackHandler) cbh).jaasContext());
+ return new PlainSaslServer(cbh);
}
@Override
diff --git a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServerProvider.java b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServerProvider.java
similarity index 90%
rename from clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServerProvider.java
rename to clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServerProvider.java
index ae1424417f8fb..c2229532449e5 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/plain/PlainSaslServerProvider.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainSaslServerProvider.java
@@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.plain;
+package org.apache.kafka.common.security.plain.internal;
import java.security.Provider;
import java.security.Security;
-import org.apache.kafka.common.security.plain.PlainSaslServer.PlainSaslServerFactory;
+import org.apache.kafka.common.security.plain.internal.PlainSaslServer.PlainSaslServerFactory;
public class PlainSaslServerProvider extends Provider {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainServerCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainServerCallbackHandler.java
new file mode 100644
index 0000000000000..84fbdfd4c62f5
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/security/plain/internal/PlainServerCallbackHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.common.security.plain.internal;
+
+import org.apache.kafka.common.security.JaasContext;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+import org.apache.kafka.common.KafkaException;
+import org.apache.kafka.common.security.plain.PlainAuthenticateCallback;
+import org.apache.kafka.common.security.plain.PlainLoginModule;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+
+public class PlainServerCallbackHandler implements AuthenticateCallbackHandler {
+
+ private static final String JAAS_USER_PREFIX = "user_";
+ private List jaasConfigEntries;
+
+ @Override
+ public void configure(Map configs, String mechanism, List jaasConfigEntries) {
+ this.jaasConfigEntries = jaasConfigEntries;
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ String username = null;
+ for (Callback callback: callbacks) {
+ if (callback instanceof NameCallback)
+ username = ((NameCallback) callback).getDefaultName();
+ else if (callback instanceof PlainAuthenticateCallback) {
+ PlainAuthenticateCallback plainCallback = (PlainAuthenticateCallback) callback;
+ boolean authenticated = authenticate(username, plainCallback.password());
+ plainCallback.authenticated(authenticated);
+ } else
+ throw new UnsupportedCallbackException(callback);
+ }
+ }
+
+ protected boolean authenticate(String username, char[] password) throws IOException {
+ if (username == null)
+ return false;
+ else {
+ String expectedPassword = JaasContext.configEntryOption(jaasConfigEntries,
+ JAAS_USER_PREFIX + username,
+ PlainLoginModule.class.getName());
+ return expectedPassword != null && Arrays.equals(password, expectedPassword.toCharArray());
+ }
+ }
+
+ @Override
+ public void close() throws KafkaException {
+ }
+
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredential.java b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredential.java
index 09ff0aacd3c84..dfbfef15b4f74 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredential.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredential.java
@@ -16,6 +16,11 @@
*/
package org.apache.kafka.common.security.scram;
+/**
+ * SCRAM credential class that encapsulates the credential data persisted for each user that is
+ * accessible to the server. See RFC rfc5802
+ * for details.
+ */
public class ScramCredential {
private final byte[] salt;
@@ -23,6 +28,9 @@ public class ScramCredential {
private final byte[] storedKey;
private final int iterations;
+ /**
+ * Constructs a new credential.
+ */
public ScramCredential(byte[] salt, byte[] storedKey, byte[] serverKey, int iterations) {
this.salt = salt;
this.serverKey = serverKey;
@@ -30,18 +38,30 @@ public ScramCredential(byte[] salt, byte[] storedKey, byte[] serverKey, int iter
this.iterations = iterations;
}
+ /**
+ * Returns the salt used to process this credential using the SCRAM algorithm.
+ */
public byte[] salt() {
return salt;
}
+ /**
+ * Server key computed from the client password using the SCRAM algorithm.
+ */
public byte[] serverKey() {
return serverKey;
}
+ /**
+ * Stored key computed from the client password using the SCRAM algorithm.
+ */
public byte[] storedKey() {
return storedKey;
}
+ /**
+ * Number of iterations used to process this credential using the SCRAM algorithm.
+ */
public int iterations() {
return iterations;
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialCallback.java b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialCallback.java
index 931210a0e0702..d5988cbeb8905 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialCallback.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialCallback.java
@@ -18,14 +18,23 @@
import javax.security.auth.callback.Callback;
+/**
+ * Callback used for SCRAM mechanisms.
+ */
public class ScramCredentialCallback implements Callback {
private ScramCredential scramCredential;
- public ScramCredential scramCredential() {
- return scramCredential;
- }
-
+ /**
+ * Sets the SCRAM credential for this instance.
+ */
public void scramCredential(ScramCredential scramCredential) {
this.scramCredential = scramCredential;
}
-}
\ No newline at end of file
+
+ /**
+ * Returns the SCRAM credential if set on this instance.
+ */
+ public ScramCredential scramCredential() {
+ return scramCredential;
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensionsCallback.java b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensionsCallback.java
index b40468bd650ef..debe163e36b2c 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensionsCallback.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensionsCallback.java
@@ -21,13 +21,25 @@
import java.util.Collections;
import java.util.Map;
+/**
+ * Optional callback used for SCRAM mechanisms if any extensions need to be set
+ * in the SASL/SCRAM exchange.
+ */
public class ScramExtensionsCallback implements Callback {
private Map extensions = Collections.emptyMap();
+ /**
+ * Returns the extension names and values that are sent by the client to
+ * the server in the initial client SCRAM authentication message.
+ * Default is an empty map.
+ */
public Map extensions() {
return extensions;
}
+ /**
+ * Sets the SCRAM extensions on this callback.
+ */
public void extensions(Map extensions) {
this.extensions = extensions;
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramLoginModule.java b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramLoginModule.java
index 43df515256fc9..20d1f221b1c05 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramLoginModule.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/ScramLoginModule.java
@@ -16,6 +16,9 @@
*/
package org.apache.kafka.common.security.scram;
+import org.apache.kafka.common.security.scram.internal.ScramSaslClientProvider;
+import org.apache.kafka.common.security.scram.internal.ScramSaslServerProvider;
+
import java.util.Collections;
import java.util.Map;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialUtils.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramCredentialUtils.java
similarity index 96%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialUtils.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramCredentialUtils.java
index b4875d6f57ee4..91e28a62e1d34 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramCredentialUtils.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramCredentialUtils.java
@@ -14,12 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.util.Collection;
import java.util.Properties;
import org.apache.kafka.common.security.authenticator.CredentialCache;
+import org.apache.kafka.common.security.scram.ScramCredential;
import org.apache.kafka.common.utils.Base64;
/**
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensions.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramExtensions.java
similarity index 95%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensions.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramExtensions.java
index 0f461c0e3c2df..66d9362a3e6cc 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramExtensions.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramExtensions.java
@@ -14,7 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
+
+import org.apache.kafka.common.security.scram.ScramLoginModule;
import java.util.Collections;
import java.util.HashMap;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramFormatter.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramFormatter.java
similarity index 94%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramFormatter.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramFormatter.java
index 406c28599b3bb..6fcb7a152dbaa 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramFormatter.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramFormatter.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
@@ -27,9 +27,10 @@
import javax.crypto.spec.SecretKeySpec;
import org.apache.kafka.common.KafkaException;
-import org.apache.kafka.common.security.scram.ScramMessages.ClientFinalMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ClientFirstMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ServerFirstMessage;
+import org.apache.kafka.common.security.scram.ScramCredential;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ClientFinalMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ClientFirstMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ServerFirstMessage;
/**
* Scram message salt and hash functions defined in RFC 5802 .
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramMechanism.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMechanism.java
similarity index 97%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramMechanism.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMechanism.java
index d8c0c6d3d603e..73be4cf24fb81 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramMechanism.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMechanism.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.util.Collection;
import java.util.Collections;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramMessages.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMessages.java
similarity index 99%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramMessages.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMessages.java
index 05b3d7755404e..439b274f561d2 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramMessages.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramMessages.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import org.apache.kafka.common.utils.Base64;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClient.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClient.java
similarity index 90%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClient.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClient.java
index 71109df2ddd1f..a98a86d61cb1a 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClient.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClient.java
@@ -14,9 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -34,9 +33,10 @@
import javax.security.sasl.SaslException;
import org.apache.kafka.common.errors.IllegalSaslStateException;
-import org.apache.kafka.common.security.scram.ScramMessages.ClientFinalMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ServerFinalMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ServerFirstMessage;
+import org.apache.kafka.common.security.scram.ScramExtensionsCallback;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ClientFinalMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ServerFinalMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ServerFirstMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -100,9 +100,15 @@ public byte[] evaluateChallenge(byte[] challenge) throws SaslException {
ScramExtensionsCallback extensionsCallback = new ScramExtensionsCallback();
try {
- callbackHandler.handle(new Callback[]{nameCallback, extensionsCallback});
- } catch (IOException | UnsupportedCallbackException e) {
- throw new SaslException("User name could not be obtained", e);
+ callbackHandler.handle(new Callback[]{nameCallback});
+ try {
+ callbackHandler.handle(new Callback[]{extensionsCallback});
+ } catch (UnsupportedCallbackException e) {
+ log.debug("Extensions callback is not supported by client callback handler {}, no extensions will be added",
+ callbackHandler);
+ }
+ } catch (Throwable e) {
+ throw new SaslException("User name or extensions could not be obtained", e);
}
String username = nameCallback.getName();
@@ -121,7 +127,7 @@ public byte[] evaluateChallenge(byte[] challenge) throws SaslException {
PasswordCallback passwordCallback = new PasswordCallback("Password:", false);
try {
callbackHandler.handle(new Callback[]{passwordCallback});
- } catch (IOException | UnsupportedCallbackException e) {
+ } catch (Throwable e) {
throw new SaslException("User name could not be obtained", e);
}
this.clientFinalMessage = handleServerFirstMessage(passwordCallback.getPassword());
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClientProvider.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClientProvider.java
similarity index 90%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClientProvider.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClientProvider.java
index d389f044a9967..4d9ff81309b79 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslClientProvider.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslClientProvider.java
@@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.security.Provider;
import java.security.Security;
-import org.apache.kafka.common.security.scram.ScramSaslClient.ScramSaslClientFactory;
+import org.apache.kafka.common.security.scram.internal.ScramSaslClient.ScramSaslClientFactory;
public class ScramSaslClientProvider extends Provider {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServer.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServer.java
similarity index 92%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServer.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServer.java
index 314c1d413ba52..60857279f59ad 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServer.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServer.java
@@ -14,9 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
-import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -27,18 +26,21 @@
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
-import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;
+import org.apache.kafka.common.errors.AuthenticationException;
import org.apache.kafka.common.errors.IllegalSaslStateException;
import org.apache.kafka.common.errors.SaslAuthenticationException;
-import org.apache.kafka.common.security.scram.ScramMessages.ClientFinalMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ClientFirstMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ServerFinalMessage;
-import org.apache.kafka.common.security.scram.ScramMessages.ServerFirstMessage;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCredentialCallback;
+import org.apache.kafka.common.security.scram.ScramCredential;
+import org.apache.kafka.common.security.scram.ScramCredentialCallback;
+import org.apache.kafka.common.security.scram.ScramLoginModule;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ClientFinalMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ClientFirstMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ServerFinalMessage;
+import org.apache.kafka.common.security.scram.internal.ScramMessages.ServerFirstMessage;
+import org.apache.kafka.common.security.token.delegation.internal.DelegationTokenCredentialCallback;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -133,7 +135,9 @@ public byte[] evaluateResponse(byte[] response) throws SaslException, SaslAuthen
scramCredential.iterations());
setState(State.RECEIVE_CLIENT_FINAL_MESSAGE);
return serverFirstMessage.toBytes();
- } catch (IOException | NumberFormatException | UnsupportedCallbackException e) {
+ } catch (SaslException | AuthenticationException e) {
+ throw e;
+ } catch (Throwable e) {
throw new SaslException("Authentication failed: Credentials could not be obtained", e);
}
@@ -154,7 +158,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException, SaslAuthen
default:
throw new IllegalSaslStateException("Unexpected challenge in Sasl server state " + state);
}
- } catch (SaslException e) {
+ } catch (SaslException | AuthenticationException e) {
clearCredentials();
setState(State.FAILED);
throw e;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServerProvider.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServerProvider.java
similarity index 90%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServerProvider.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServerProvider.java
index 9f2a6b3d25621..099e50e19ddab 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramSaslServerProvider.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramSaslServerProvider.java
@@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.security.Provider;
import java.security.Security;
-import org.apache.kafka.common.security.scram.ScramSaslServer.ScramSaslServerFactory;
+import org.apache.kafka.common.security.scram.internal.ScramSaslServer.ScramSaslServerFactory;
public class ScramSaslServerProvider extends Provider {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramServerCallbackHandler.java b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramServerCallbackHandler.java
similarity index 76%
rename from clients/src/main/java/org/apache/kafka/common/security/scram/ScramServerCallbackHandler.java
rename to clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramServerCallbackHandler.java
index 5e37eae9d4fb6..9a3f0dc66b764 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/scram/ScramServerCallbackHandler.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/scram/internal/ScramServerCallbackHandler.java
@@ -14,23 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.scram;
+package org.apache.kafka.common.security.scram.internal;
import java.io.IOException;
+import java.util.List;
import java.util.Map;
-import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
-import org.apache.kafka.common.network.Mode;
-import org.apache.kafka.common.security.authenticator.AuthCallbackHandler;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.authenticator.CredentialCache;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
-import org.apache.kafka.common.security.token.delegation.DelegationTokenCredentialCallback;
+import org.apache.kafka.common.security.scram.ScramCredential;
+import org.apache.kafka.common.security.scram.ScramCredentialCallback;
+import org.apache.kafka.common.security.token.delegation.internal.DelegationTokenCache;
+import org.apache.kafka.common.security.token.delegation.internal.DelegationTokenCredentialCallback;
-public class ScramServerCallbackHandler implements AuthCallbackHandler {
+public class ScramServerCallbackHandler implements AuthenticateCallbackHandler {
private final CredentialCache.Cache credentialCache;
private final DelegationTokenCache tokenCache;
@@ -42,6 +44,11 @@ public ScramServerCallbackHandler(CredentialCache.Cache credent
this.tokenCache = tokenCache;
}
+ @Override
+ public void configure(Map configs, String mechanism, List jaasConfigEntries) {
+ this.saslMechanism = mechanism;
+ }
+
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
String username = null;
@@ -60,11 +67,6 @@ else if (callback instanceof DelegationTokenCredentialCallback) {
}
}
- @Override
- public void configure(Map configs, Mode mode, Subject subject, String saslMechanism) {
- this.saslMechanism = saslMechanism;
- }
-
@Override
public void close() {
}
diff --git a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationToken.java b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationToken.java
index 05ccbda2fe6ee..e1f97c1b72d77 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationToken.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationToken.java
@@ -16,11 +16,16 @@
*/
package org.apache.kafka.common.security.token.delegation;
+import org.apache.kafka.common.annotation.InterfaceStability;
import org.apache.kafka.common.utils.Base64;
-import java.nio.ByteBuffer;
import java.util.Arrays;
+/**
+ * A class representing a delegation token.
+ *
+ */
+@InterfaceStability.Evolving
public class DelegationToken {
private TokenInformation tokenInformation;
private byte[] hmac;
@@ -42,10 +47,6 @@ public String hmacAsBase64String() {
return Base64.encoder().encodeToString(hmac);
}
- public ByteBuffer hmacBuffer() {
- return ByteBuffer.wrap(hmac);
- }
-
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/TokenInformation.java b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/TokenInformation.java
index 1d500d21eeff6..ffd2af3f1c231 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/TokenInformation.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/TokenInformation.java
@@ -16,11 +16,17 @@
*/
package org.apache.kafka.common.security.token.delegation;
+import org.apache.kafka.common.annotation.InterfaceStability;
import org.apache.kafka.common.security.auth.KafkaPrincipal;
import java.util.ArrayList;
import java.util.Collection;
+/**
+ * A class representing a delegation token details.
+ *
+ */
+@InterfaceStability.Evolving
public class TokenInformation {
private KafkaPrincipal owner;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCache.java b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCache.java
similarity index 91%
rename from clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCache.java
rename to clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCache.java
index 78575b81bb6e3..c05b7350b4174 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCache.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCache.java
@@ -15,12 +15,14 @@
* limitations under the License.
*/
-package org.apache.kafka.common.security.token.delegation;
+package org.apache.kafka.common.security.token.delegation.internal;
import org.apache.kafka.common.security.authenticator.CredentialCache;
import org.apache.kafka.common.security.scram.ScramCredential;
-import org.apache.kafka.common.security.scram.ScramCredentialUtils;
-import org.apache.kafka.common.security.scram.ScramMechanism;
+import org.apache.kafka.common.security.scram.internal.ScramCredentialUtils;
+import org.apache.kafka.common.security.scram.internal.ScramMechanism;
+import org.apache.kafka.common.security.token.delegation.DelegationToken;
+import org.apache.kafka.common.security.token.delegation.TokenInformation;
import java.util.Collection;
import java.util.HashMap;
diff --git a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCredentialCallback.java b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCredentialCallback.java
similarity index 94%
rename from clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCredentialCallback.java
rename to clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCredentialCallback.java
index 7490a3e91b1ca..294d7b1d3a015 100644
--- a/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationTokenCredentialCallback.java
+++ b/clients/src/main/java/org/apache/kafka/common/security/token/delegation/internal/DelegationTokenCredentialCallback.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.kafka.common.security.token.delegation;
+package org.apache.kafka.common.security.token.delegation.internal;
import org.apache.kafka.common.security.scram.ScramCredentialCallback;
diff --git a/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaCallback.java b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaCallback.java
new file mode 100644
index 0000000000000..210e9f45840e2
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaCallback.java
@@ -0,0 +1,106 @@
+/*
+ * 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.quota;
+
+import org.apache.kafka.common.Cluster;
+import org.apache.kafka.common.Configurable;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+
+import java.util.Map;
+
+/**
+ * Quota callback interface for brokers that enables customization of client quota computation.
+ */
+public interface ClientQuotaCallback extends Configurable {
+
+ /**
+ * Quota callback invoked to determine the quota metric tags to be applied for a request.
+ * Quota limits are associated with quota metrics and all clients which use the same
+ * metric tags share the quota limit.
+ *
+ * @param quotaType Type of quota requested
+ * @param principal The user principal of the connection for which quota is requested
+ * @param clientId The client id associated with the request
+ * @return quota metric tags that indicate which other clients share this quota
+ */
+ Map quotaMetricTags(ClientQuotaType quotaType, KafkaPrincipal principal, String clientId);
+
+ /**
+ * Returns the quota limit associated with the provided metric tags. These tags were returned from
+ * a previous call to {@link #quotaMetricTags(ClientQuotaType, KafkaPrincipal, String)}. This method is
+ * invoked by quota managers to obtain the current quota limit applied to a metric when the first request
+ * using these tags is processed. It is also invoked after a quota update or cluster metadata change.
+ * If the tags are no longer in use after the update, (e.g. this is a {user, client-id} quota metric
+ * and the quota now in use is a {user} quota), null is returned.
+ *
+ * @param quotaType Type of quota requested
+ * @param metricTags Metric tags for a quota metric of type `quotaType`
+ * @return the quota limit for the provided metric tags or null if the metric tags are no longer in use
+ */
+ Double quotaLimit(ClientQuotaType quotaType, Map metricTags);
+
+ /**
+ * Quota configuration update callback that is invoked when quota configuration for an entity is
+ * updated in ZooKeeper. This is useful to track configured quotas if built-in quota configuration
+ * tools are used for quota management.
+ *
+ * @param quotaType Type of quota being updated
+ * @param quotaEntity The quota entity for which quota is being updated
+ * @param newValue The new quota value
+ */
+ void updateQuota(ClientQuotaType quotaType, ClientQuotaEntity quotaEntity, double newValue);
+
+ /**
+ * Quota configuration removal callback that is invoked when quota configuration for an entity is
+ * removed in ZooKeeper. This is useful to track configured quotas if built-in quota configuration
+ * tools are used for quota management.
+ *
+ * @param quotaType Type of quota being updated
+ * @param quotaEntity The quota entity for which quota is being updated
+ */
+ void removeQuota(ClientQuotaType quotaType, ClientQuotaEntity quotaEntity);
+
+ /**
+ * Returns true if any of the existing quota configs may have been updated since the last call
+ * to this method for the provided quota type. Quota updates as a result of calls to
+ * {@link #updateClusterMetadata(Cluster)}, {@link #updateQuota(ClientQuotaType, ClientQuotaEntity, double)}
+ * and {@link #removeQuota(ClientQuotaType, ClientQuotaEntity)} are automatically processed.
+ * So callbacks that rely only on built-in quota configuration tools always return false. Quota callbacks
+ * with external quota configuration or custom reconfigurable quota configs that affect quota limits must
+ * return true if existing metric configs may need to be updated. This method is invoked on every request
+ * and hence is expected to be handled by callbacks as a simple flag that is updated when quotas change.
+ *
+ * @param quotaType Type of quota
+ */
+ boolean quotaResetRequired(ClientQuotaType quotaType);
+
+ /**
+ * Metadata update callback that is invoked whenever UpdateMetadata request is received from
+ * the controller. This is useful if quota computation takes partitions into account.
+ * Topics that are being deleted will not be included in `cluster`.
+ *
+ * @param cluster Cluster metadata including partitions and their leaders if known
+ * @return true if quotas have changed and metric configs may need to be updated
+ */
+ boolean updateClusterMetadata(Cluster cluster);
+
+ /**
+ * Closes this instance.
+ */
+ void close();
+}
+
diff --git a/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java
new file mode 100644
index 0000000000000..a5ff082dfef7e
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java
@@ -0,0 +1,62 @@
+/*
+ * 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.quota;
+
+import java.util.List;
+
+/**
+ * The metadata for an entity for which quota is configured. Quotas may be defined at
+ * different levels and `configEntities` gives the list of config entities that define
+ * the level of this quota entity.
+ */
+public interface ClientQuotaEntity {
+
+ /**
+ * Entity type of a {@link ConfigEntity}
+ */
+ public enum ConfigEntityType {
+ USER,
+ CLIENT_ID,
+ DEFAULT_USER,
+ DEFAULT_CLIENT_ID
+ }
+
+ /**
+ * Interface representing a quota configuration entity. Quota may be
+ * configured at levels that include one or more configuration entities.
+ * For example, {user, client-id} quota is represented using two
+ * instances of ConfigEntity with entity types USER and CLIENT_ID.
+ */
+ public interface ConfigEntity {
+ /**
+ * Returns the name of this entity. For default quotas, an empty string is returned.
+ */
+ String name();
+
+ /**
+ * Returns the type of this entity.
+ */
+ ConfigEntityType entityType();
+ }
+
+ /**
+ * Returns the list of configuration entities that this quota entity is comprised of.
+ * For {user} or {clientId} quota, this is a single entity and for {user, clientId}
+ * quota, this is a list of two entities.
+ */
+ List configEntities();
+}
diff --git a/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaType.java b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaType.java
new file mode 100644
index 0000000000000..4dd67d3125d37
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaType.java
@@ -0,0 +1,26 @@
+/*
+ * 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.quota;
+
+/**
+ * Types of quotas that may be configured on brokers for client requests.
+ */
+public enum ClientQuotaType {
+ PRODUCE,
+ FETCH,
+ REQUEST
+}
diff --git a/clients/src/test/java/org/apache/kafka/clients/MockClient.java b/clients/src/test/java/org/apache/kafka/clients/MockClient.java
index 60af9bcc84937..a73175c995430 100644
--- a/clients/src/test/java/org/apache/kafka/clients/MockClient.java
+++ b/clients/src/test/java/org/apache/kafka/clients/MockClient.java
@@ -16,6 +16,7 @@
*/
package org.apache.kafka.clients;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.errors.AuthenticationException;
@@ -88,7 +89,7 @@ public FutureResponse(Node node,
private final Queue metadataUpdates = new ArrayDeque<>();
private volatile NodeApiVersions nodeApiVersions = NodeApiVersions.create();
private volatile int numBlockingWakeups = 0;
-
+ private final AtomicInteger totalRequestCount = new AtomicInteger(0);
public MockClient(Time time) {
this(time, null);
}
@@ -394,6 +395,7 @@ public void reset() {
futureResponses.clear();
metadataUpdates.clear();
authenticationErrors.clear();
+ totalRequestCount.set(0);
}
public boolean hasPendingMetadataUpdates() {
@@ -461,6 +463,7 @@ public ClientRequest newClientRequest(String nodeId, AbstractRequest.Builder>
@Override
public ClientRequest newClientRequest(String nodeId, AbstractRequest.Builder> requestBuilder, long createdTimeMs,
boolean expectResponse, RequestCompletionHandler callback) {
+ totalRequestCount.incrementAndGet();
return new ClientRequest(nodeId, requestBuilder, 0, "mockClientId", createdTimeMs,
expectResponse, callback);
}
@@ -503,4 +506,9 @@ private static class MetadataUpdate {
this.expectMatchRefreshTopics = expectMatchRefreshTopics;
}
}
+
+ // visible for testing
+ public int totalRequestCount() {
+ return totalRequestCount.get();
+ }
}
diff --git a/clients/src/test/java/org/apache/kafka/clients/admin/KafkaAdminClientTest.java b/clients/src/test/java/org/apache/kafka/clients/admin/KafkaAdminClientTest.java
index f08a99b6ddc4b..a242ed660609b 100644
--- a/clients/src/test/java/org/apache/kafka/clients/admin/KafkaAdminClientTest.java
+++ b/clients/src/test/java/org/apache/kafka/clients/admin/KafkaAdminClientTest.java
@@ -18,6 +18,9 @@
import org.apache.kafka.clients.NodeApiVersions;
import org.apache.kafka.clients.admin.DeleteAclsResult.FilterResults;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.clients.consumer.internals.ConsumerProtocol;
+import org.apache.kafka.clients.consumer.internals.PartitionAssignor;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.Node;
@@ -47,10 +50,15 @@
import org.apache.kafka.common.requests.DeleteAclsResponse;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclDeletionResult;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclFilterResponse;
+import org.apache.kafka.common.requests.DeleteGroupsResponse;
import org.apache.kafka.common.requests.DeleteRecordsResponse;
import org.apache.kafka.common.requests.DescribeAclsResponse;
import org.apache.kafka.common.requests.DescribeConfigsResponse;
+import org.apache.kafka.common.requests.DescribeGroupsResponse;
+import org.apache.kafka.common.requests.FindCoordinatorResponse;
+import org.apache.kafka.common.requests.ListGroupsResponse;
import org.apache.kafka.common.requests.MetadataResponse;
+import org.apache.kafka.common.requests.OffsetFetchResponse;
import org.apache.kafka.common.resource.Resource;
import org.apache.kafka.common.resource.ResourceFilter;
import org.apache.kafka.common.resource.ResourceType;
@@ -64,6 +72,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -81,6 +90,7 @@
import static org.apache.kafka.common.requests.ResourceType.TOPIC;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -230,22 +240,26 @@ public void testInvalidTopicNames() throws Exception {
for (String sillyTopicName : sillyTopicNames) {
assertFutureError(deleteFutures.get(sillyTopicName), InvalidTopicException.class);
}
+ assertEquals(0, env.kafkaClient().totalRequestCount());
Map> describeFutures =
env.adminClient().describeTopics(sillyTopicNames).values();
for (String sillyTopicName : sillyTopicNames) {
assertFutureError(describeFutures.get(sillyTopicName), InvalidTopicException.class);
}
+ assertEquals(0, env.kafkaClient().totalRequestCount());
List newTopics = new ArrayList<>();
for (String sillyTopicName : sillyTopicNames) {
newTopics.add(new NewTopic(sillyTopicName, 1, (short) 1));
}
+
Map> createFutures =
env.adminClient().createTopics(newTopics).values();
for (String sillyTopicName : sillyTopicNames) {
assertFutureError(createFutures .get(sillyTopicName), InvalidTopicException.class);
}
+ assertEquals(0, env.kafkaClient().totalRequestCount());
}
}
@@ -632,6 +646,188 @@ public void testDeleteRecords() throws Exception {
}
}
+ //Ignoring test to be fixed on follow-up PR
+ @Ignore
+ @Test
+ public void testListConsumerGroups() throws Exception {
+ final HashMap nodes = new HashMap<>();
+ nodes.put(0, new Node(0, "localhost", 8121));
+
+ final Cluster cluster =
+ new Cluster(
+ "mockClusterId",
+ nodes.values(),
+ Collections.emptyList(),
+ Collections.emptySet(),
+ Collections.emptySet(), nodes.get(0));
+
+ try (AdminClientUnitTestEnv env = new AdminClientUnitTestEnv(cluster)) {
+ env.kafkaClient().setNodeApiVersions(NodeApiVersions.create());
+ env.kafkaClient().prepareMetadataUpdate(env.cluster(), Collections.emptySet());
+ env.kafkaClient().setNode(env.cluster().controller());
+
+ env.kafkaClient().prepareResponse(
+ new MetadataResponse(
+ env.cluster().nodes(),
+ env.cluster().clusterResource().clusterId(),
+ env.cluster().controller().id(),
+ new ArrayList()));
+
+ env.kafkaClient().prepareResponse(
+ new ListGroupsResponse(
+ Errors.NONE,
+ Arrays.asList(
+ new ListGroupsResponse.Group("group-1", ConsumerProtocol.PROTOCOL_TYPE),
+ new ListGroupsResponse.Group("group-connect-1", "connector")
+ )));
+
+ final ListConsumerGroupsResult result = env.adminClient().listConsumerGroups();
+ final List consumerGroups = new ArrayList<>();
+
+ final KafkaFuture> listings = result.listings();
+ consumerGroups.addAll(listings.get());
+ assertEquals(1, consumerGroups.size());
+ }
+ }
+
+ @Test
+ public void testDescribeConsumerGroups() throws Exception {
+ final HashMap nodes = new HashMap<>();
+ nodes.put(0, new Node(0, "localhost", 8121));
+
+ final Cluster cluster =
+ new Cluster(
+ "mockClusterId",
+ nodes.values(),
+ Collections.emptyList(),
+ Collections.emptySet(),
+ Collections.emptySet(), nodes.get(0));
+
+ try (AdminClientUnitTestEnv env = new AdminClientUnitTestEnv(cluster)) {
+ env.kafkaClient().setNodeApiVersions(NodeApiVersions.create());
+ env.kafkaClient().prepareMetadataUpdate(env.cluster(), Collections.emptySet());
+ env.kafkaClient().setNode(env.cluster().controller());
+
+ env.kafkaClient().prepareResponse(new FindCoordinatorResponse(Errors.NONE, env.cluster().controller()));
+
+ final Map groupMetadataMap = new HashMap<>();
+ TopicPartition myTopicPartition0 = new TopicPartition("my_topic", 0);
+ TopicPartition myTopicPartition1 = new TopicPartition("my_topic", 1);
+ TopicPartition myTopicPartition2 = new TopicPartition("my_topic", 2);
+
+ final List topicPartitions = new ArrayList<>();
+ topicPartitions.add(0, myTopicPartition0);
+ topicPartitions.add(1, myTopicPartition1);
+ topicPartitions.add(2, myTopicPartition2);
+
+ final ByteBuffer memberAssignment = ConsumerProtocol.serializeAssignment(new PartitionAssignor.Assignment(topicPartitions));
+
+ groupMetadataMap.put(
+ "group-0",
+ new DescribeGroupsResponse.GroupMetadata(
+ Errors.NONE,
+ "",
+ ConsumerProtocol.PROTOCOL_TYPE,
+ "",
+ Arrays.asList(
+ new DescribeGroupsResponse.GroupMember("0", "clientId0", "clientHost", null, memberAssignment),
+ new DescribeGroupsResponse.GroupMember("1", "clientId1", "clientHost", null, memberAssignment))));
+ groupMetadataMap.put(
+ "group-connect-0",
+ new DescribeGroupsResponse.GroupMetadata(
+ Errors.NONE,
+ "",
+ "connect",
+ "",
+ Arrays.asList(
+ new DescribeGroupsResponse.GroupMember("0", "clientId0", "clientHost", null, memberAssignment),
+ new DescribeGroupsResponse.GroupMember("1", "clientId1", "clientHost", null, memberAssignment))));
+
+ env.kafkaClient().prepareResponse(new DescribeGroupsResponse(groupMetadataMap));
+
+ final DescribeConsumerGroupsResult result = env.adminClient().describeConsumerGroups(Collections.singletonList("group-0"));
+ final KafkaFuture groupDescriptionFuture = result.describedGroups().get().get("group-0");
+ final ConsumerGroupDescription groupDescription = groupDescriptionFuture.get();
+
+ assertEquals(1, result.describedGroups().get().size());
+ assertEquals("group-0", groupDescription.groupId());
+ assertEquals(2, groupDescription.members().size());
+ }
+ }
+
+ @Test
+ public void testDescribeConsumerGroupOffsets() throws Exception {
+ final HashMap nodes = new HashMap<>();
+ nodes.put(0, new Node(0, "localhost", 8121));
+
+ final Cluster cluster =
+ new Cluster(
+ "mockClusterId",
+ nodes.values(),
+ Collections.emptyList(),
+ Collections.emptySet(),
+ Collections.emptySet(), nodes.get(0));
+
+ try (AdminClientUnitTestEnv env = new AdminClientUnitTestEnv(cluster)) {
+ env.kafkaClient().setNodeApiVersions(NodeApiVersions.create());
+ env.kafkaClient().prepareMetadataUpdate(env.cluster(), Collections.emptySet());
+ env.kafkaClient().setNode(env.cluster().controller());
+
+ env.kafkaClient().prepareResponse(new FindCoordinatorResponse(Errors.NONE, env.cluster().controller()));
+
+ TopicPartition myTopicPartition0 = new TopicPartition("my_topic", 0);
+ TopicPartition myTopicPartition1 = new TopicPartition("my_topic", 1);
+ TopicPartition myTopicPartition2 = new TopicPartition("my_topic", 2);
+
+ final Map responseData = new HashMap<>();
+ responseData.put(myTopicPartition0, new OffsetFetchResponse.PartitionData(10, "", Errors.NONE));
+ responseData.put(myTopicPartition1, new OffsetFetchResponse.PartitionData(0, "", Errors.NONE));
+ responseData.put(myTopicPartition2, new OffsetFetchResponse.PartitionData(20, "", Errors.NONE));
+ env.kafkaClient().prepareResponse(new OffsetFetchResponse(Errors.NONE, responseData));
+
+ final ListConsumerGroupOffsetsResult result = env.adminClient().listConsumerGroupOffsets("group-0");
+
+ assertEquals(3, result.partitionsToOffsetAndMetadata().get().size());
+ final TopicPartition topicPartition = result.partitionsToOffsetAndMetadata().get().keySet().iterator().next();
+ assertEquals("my_topic", topicPartition.topic());
+ final OffsetAndMetadata offsetAndMetadata = result.partitionsToOffsetAndMetadata().get().values().iterator().next();
+ assertEquals(10, offsetAndMetadata.offset());
+ }
+ }
+
+ @Test
+ public void testDeleteConsumerGroups() throws Exception {
+ final HashMap nodes = new HashMap<>();
+ nodes.put(0, new Node(0, "localhost", 8121));
+
+ final Cluster cluster =
+ new Cluster(
+ "mockClusterId",
+ nodes.values(),
+ Collections.emptyList(),
+ Collections.emptySet(),
+ Collections.emptySet(), nodes.get(0));
+
+ final List groupIds = Collections.singletonList("group-0");
+
+ try (AdminClientUnitTestEnv env = new AdminClientUnitTestEnv(cluster)) {
+ env.kafkaClient().setNodeApiVersions(NodeApiVersions.create());
+ env.kafkaClient().prepareMetadataUpdate(env.cluster(), Collections.emptySet());
+ env.kafkaClient().setNode(env.cluster().controller());
+
+ env.kafkaClient().prepareResponse(new FindCoordinatorResponse(Errors.NONE, env.cluster().controller()));
+
+ final Map response = new HashMap<>();
+ response.put("group-0", Errors.NONE);
+ env.kafkaClient().prepareResponse(new DeleteGroupsResponse(response));
+
+ final DeleteConsumerGroupsResult result = env.adminClient().deleteConsumerGroups(groupIds);
+
+ final Map> results = result.deletedGroups().get();
+ assertNull(results.get("group-0").get());
+ }
+ }
+
private static void assertCollectionIs(Collection collection, T... elements) {
for (T element : elements) {
assertTrue("Did not find " + element, collection.contains(element));
diff --git a/clients/src/test/java/org/apache/kafka/clients/admin/MockAdminClient.java b/clients/src/test/java/org/apache/kafka/clients/admin/MockAdminClient.java
index c141a8acac9e9..2fc7048b759aa 100644
--- a/clients/src/test/java/org/apache/kafka/clients/admin/MockAdminClient.java
+++ b/clients/src/test/java/org/apache/kafka/clients/admin/MockAdminClient.java
@@ -276,6 +276,46 @@ public DeleteRecordsResult deleteRecords(Map re
}
}
+ @Override
+ public CreateDelegationTokenResult createDelegationToken(CreateDelegationTokenOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public RenewDelegationTokenResult renewDelegationToken(byte[] hmac, RenewDelegationTokenOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public ExpireDelegationTokenResult expireDelegationToken(byte[] hmac, ExpireDelegationTokenOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public DescribeDelegationTokenResult describeDelegationToken(DescribeDelegationTokenOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public DescribeConsumerGroupsResult describeConsumerGroups(Collection groupIds, DescribeConsumerGroupsOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public ListConsumerGroupsResult listConsumerGroups(ListConsumerGroupsOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public ListConsumerGroupOffsetsResult listConsumerGroupOffsets(String groupId, ListConsumerGroupOffsetsOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public DeleteConsumerGroupsResult deleteConsumerGroups(Collection groupIds, DeleteConsumerGroupsOptions options) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
@Override
public CreateAclsResult createAcls(Collection