This is an automated email from the ASF dual-hosted git repository. alexey pushed a commit to branch branch-1.17.x in repository https://gitbox.apache.org/repos/asf/kudu.git
commit 5a1ac293024241cd44eddb195be84168a10ae685 Author: Zoltan Chovan <[email protected]> AuthorDate: Thu Apr 13 19:26:04 2023 +0200 jwt: Java client usage of JWTs This change introduces modifications to the Negotiator and the SecurityContext classes to be able to handle JWT authentication from the Java client. Also adding a test where the client is set up with a JWT that was generated by the MiniOIDC that starts along with the ExternalMiniCluster. Change-Id: Id02dcd7ee57a838f4763500e78053e21aac21c09 Reviewed-on: http://gerrit.cloudera.org:8080/18477 Reviewed-by: Attila Bukor <[email protected]> Tested-by: Attila Bukor <[email protected]> (cherry picked from commit a3a7c97be031f8fc32402e430eff1a89c19dbdfb) Reviewed-on: http://gerrit.cloudera.org:8080/19972 Tested-by: Alexey Serbin <[email protected]> Reviewed-by: Alexey Serbin <[email protected]> --- .../java/org/apache/kudu/client/Negotiator.java | 24 +++++++++++++ .../org/apache/kudu/client/SecurityContext.java | 24 +++++++++++++ .../java/org/apache/kudu/test/KuduTestHarness.java | 4 +++ .../org/apache/kudu/test/TestMiniKuduCluster.java | 41 +++++++++++++++++----- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java b/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java index 5fe628598..f17dfce4e 100644 --- a/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java @@ -82,6 +82,7 @@ import org.apache.kudu.rpc.RpcHeader.AuthenticationTypePB; import org.apache.kudu.rpc.RpcHeader.NegotiatePB; import org.apache.kudu.rpc.RpcHeader.NegotiatePB.NegotiateStep; import org.apache.kudu.rpc.RpcHeader.RpcFeatureFlag; +import org.apache.kudu.security.Token.JwtRawPB; import org.apache.kudu.security.Token.SignedTokenPB; import org.apache.kudu.util.SecurityUtil; @@ -176,6 +177,7 @@ public class Negotiator extends SimpleChannelInboundHandler<CallResponse> { * ensure that it doesn't change over the course of a negotiation attempt. */ private final SignedTokenPB authnToken; + private final JwtRawPB jsonWebToken; private enum AuthnTokenNotUsedReason { NONE_AVAILABLE("no token is available"), @@ -267,6 +269,9 @@ public class Negotiator extends SimpleChannelInboundHandler<CallResponse> { this.authnToken = null; this.authnTokenNotUsedReason = AuthnTokenNotUsedReason.NONE_AVAILABLE; } + JwtRawPB jwt = securityContext.getJsonWebToken(); + // TODO(zchovan): also guard this on the presence of certs. + this.jsonWebToken = jwt; } public void sendHello(ChannelHandlerContext ctx) { @@ -558,6 +563,12 @@ public class Negotiator extends SimpleChannelInboundHandler<CallResponse> { "but client had no valid token"); } break; + case JWT: + if (jsonWebToken == null) { + throw new IllegalArgumentException("server chose JWT authentication " + + "but client had no valid JWT"); + } + break; default: throw new IllegalArgumentException("server chose bad authn type " + chosenAuthnType); } @@ -745,6 +756,9 @@ public class Negotiator extends SimpleChannelInboundHandler<CallResponse> { case TOKEN: sendTokenExchange(ctx); break; + case JWT: + sendJwtExchange(ctx); + break; default: throw new AssertionError("unreachable"); } @@ -764,6 +778,16 @@ public class Negotiator extends SimpleChannelInboundHandler<CallResponse> { sendSaslMessage(ctx, builder.build()); } + private void sendJwtExchange(ChannelHandlerContext ctx) { + Preconditions.checkNotNull(jsonWebToken); + Preconditions.checkNotNull(sslHandshakeFuture); + Preconditions.checkState(sslHandshakeFuture.isSuccess()); + RpcHeader.NegotiatePB.Builder builder = RpcHeader.NegotiatePB.newBuilder() + .setStep(NegotiateStep.JWT_EXCHANGE) + .setJwtRaw(jsonWebToken); + sendSaslMessage(ctx, builder.build()); + } + private void handleTokenExchangeResponse(ChannelHandlerContext ctx, NegotiatePB response) throws SaslException { Preconditions.checkArgument(response.getStep() == NegotiateStep.TOKEN_EXCHANGE, diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java b/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java index 8073c4dd6..8597e0cd9 100644 --- a/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java @@ -53,6 +53,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.kudu.client.Client.AuthenticationCredentialsPB; +import org.apache.kudu.security.Token.JwtRawPB; import org.apache.kudu.security.Token.SignedTokenPB; import org.apache.kudu.security.Token.TokenPB; import org.apache.kudu.util.Pair; @@ -73,6 +74,10 @@ class SecurityContext { @Nullable private SignedTokenPB authnToken; + @GuardedBy("this") + @Nullable + private JwtRawPB jsonWebToken; + @GuardedBy("this") private String realUser; @@ -310,8 +315,10 @@ class SecurityContext { } LOG.debug("Importing authentication credentials with {} authn token, " + + "JWT={} , " + "{} cert(s), and realUser={}", pb.hasAuthnToken() ? "one" : "no", + pb.hasJwt() ? pb.getJwt() : "<none>", pb.getCaCertDersCount(), pb.hasRealUser() ? pb.getRealUser() : "<none>"); if (pb.hasAuthnToken()) { @@ -319,6 +326,10 @@ class SecurityContext { } trustCertificates(pb.getCaCertDersList()); + if (pb.hasJwt()) { + jsonWebToken = pb.getJwt(); + } + if (pb.hasRealUser()) { realUser = pb.getRealUser(); } @@ -335,6 +346,11 @@ class SecurityContext { return authnToken; } + @Nullable + public synchronized JwtRawPB getJsonWebToken() { + return jsonWebToken; + } + /** * Set the token that we will use to authenticate to servers. Replaces any * prior token. @@ -343,6 +359,14 @@ class SecurityContext { authnToken = token; } + /** + * Set the JWT that we will use to authenticate to the server. Replaces any + * prior JWT. + */ + public synchronized void setJsonWebToken(JwtRawPB jwt) { + jsonWebToken = jwt; + } + /** * Create an SSLEngine which will trust all certificates without verification. */ diff --git a/java/kudu-test-utils/src/main/java/org/apache/kudu/test/KuduTestHarness.java b/java/kudu-test-utils/src/main/java/org/apache/kudu/test/KuduTestHarness.java index afa470b2f..30ea8a365 100644 --- a/java/kudu-test-utils/src/main/java/org/apache/kudu/test/KuduTestHarness.java +++ b/java/kudu-test-utils/src/main/java/org/apache/kudu/test/KuduTestHarness.java @@ -487,6 +487,10 @@ public class KuduTestHarness extends ExternalResource { client = asyncClient.syncClient(); } + public String createJwtFor(String accountId, String subject, boolean isValid) throws IOException { + return miniCluster.createJwtFor(accountId, subject, isValid); + } + /** * An annotation that can be added to each test method to * define additional master server flags to be used when diff --git a/java/kudu-test-utils/src/test/java/org/apache/kudu/test/TestMiniKuduCluster.java b/java/kudu-test-utils/src/test/java/org/apache/kudu/test/TestMiniKuduCluster.java index a36f38d0a..ddfbe6d9b 100644 --- a/java/kudu-test-utils/src/test/java/org/apache/kudu/test/TestMiniKuduCluster.java +++ b/java/kudu-test-utils/src/test/java/org/apache/kudu/test/TestMiniKuduCluster.java @@ -26,16 +26,21 @@ import static org.junit.Assert.fail; import java.io.IOException; import java.net.Socket; +import com.google.protobuf.ByteString; import org.junit.Rule; import org.junit.Test; +import org.apache.kudu.client.AsyncKuduClient; +import org.apache.kudu.client.Client.AuthenticationCredentialsPB; import org.apache.kudu.client.HostAndPort; import org.apache.kudu.client.KuduClient; import org.apache.kudu.client.KuduClient.KuduClientBuilder; import org.apache.kudu.client.ListTablesResponse; import org.apache.kudu.client.TimeoutTracker; +import org.apache.kudu.security.Token.JwtRawPB; import org.apache.kudu.test.cluster.FakeDNS; import org.apache.kudu.test.cluster.MiniKuduCluster; +import org.apache.kudu.test.cluster.MiniKuduCluster.MiniKuduClusterBuilder; import org.apache.kudu.test.junit.RetryRule; import org.apache.kudu.tools.Tool.CreateClusterRequestPB.JwksOptionsPB; import org.apache.kudu.tools.Tool.CreateClusterRequestPB.MiniOidcOptionsPB; @@ -49,6 +54,9 @@ public class TestMiniKuduCluster { @Rule public RetryRule retryRule = new RetryRule(); + @Rule + public KuduTestHarness harness; + @Test(timeout = 50000) public void test() throws Exception { try (MiniKuduCluster cluster = new MiniKuduCluster.MiniKuduClusterBuilder() @@ -111,16 +119,31 @@ public class TestMiniKuduCluster { @Test(timeout = 50000) public void testJwt() throws Exception { - try (MiniKuduCluster cluster = new MiniKuduCluster.MiniKuduClusterBuilder() - .numMasterServers(NUM_MASTERS) - .numTabletServers(0) - .enableClientJwt() - .addJwks("account-id", true) - .build(); - KuduClient client = new KuduClientBuilder(cluster.getMasterAddressesAsString()).build()) { - String jwt = cluster.createJwtFor("account-id", "subject", true); - + try { + MiniKuduClusterBuilder clusterBuilder = new MiniKuduCluster.MiniKuduClusterBuilder() + .numMasterServers(NUM_MASTERS) + .numTabletServers(0) + .enableClientJwt() + .addJwks("account-id", true); + + harness = new KuduTestHarness(clusterBuilder); + harness.before(); + harness.startAllMasterServers(); + + String jwt = harness.createJwtFor("account-id", "subject", true); assertNotNull(jwt); + AuthenticationCredentialsPB credentials = AuthenticationCredentialsPB.newBuilder() + .setJwt(JwtRawPB.newBuilder() + .setJwtData(ByteString.copyFromUtf8(jwt)) + .build()) + .build(); + + AsyncKuduClient c = harness.getAsyncClient(); + c.importAuthenticationCredentials(credentials.toByteArray()); + c.getTablesList(); + + } catch (Exception e) { + throw new RuntimeException(e); } }
