Skip to content

Commit 2907668

Browse files
author
purushah
committed
ZOOKEEPER-4849: Option to Provide Custom X509 Implementation of QuorumAuthServer and QuorumAuthLearner
1 parent b997145 commit 2907668

File tree

8 files changed

+522
-4
lines changed

8 files changed

+522
-4
lines changed

zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeer.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.io.StringReader;
2929
import java.io.StringWriter;
3030
import java.io.Writer;
31+
import java.lang.reflect.InvocationTargetException;
3132
import java.net.DatagramPacket;
3233
import java.net.DatagramSocket;
3334
import java.net.InetAddress;
@@ -132,7 +133,9 @@ public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider
132133
// of updates; see the implementation comment at setLastSeenQuorumVerifier().
133134
private AtomicReference<QuorumCnxManager> qcmRef = new AtomicReference<>();
134135

136+
/** Class name to instantiate for SSL QuorumAuthServer */
135137
QuorumAuthServer authServer;
138+
/** Class name to instantiate for SSL QuorumAuthLearner */
136139
QuorumAuthLearner authLearner;
137140

138141
/**
@@ -693,6 +696,10 @@ void setId(long id) {
693696
}
694697

695698
private boolean sslQuorum;
699+
700+
private String sslAuthServerProvider;
701+
private String sslAuthLearnerProvider;
702+
696703
private boolean shouldUsePortUnification;
697704

698705
public boolean isSslQuorum() {
@@ -1176,12 +1183,99 @@ public void initialize() throws SaslException {
11761183
}
11771184
authServer = new SaslQuorumAuthServer(isQuorumServerSaslAuthRequired(), quorumServerLoginContext, authzHosts);
11781185
authLearner = new SaslQuorumAuthLearner(isQuorumLearnerSaslAuthRequired(), quorumServicePrincipal, quorumLearnerLoginContext);
1186+
} else if (isSslQuorum()) {
1187+
try {
1188+
authServer = getSslQuorumAuthServer();
1189+
authLearner = getSslQuorumAuthLearner();
1190+
}
1191+
catch (Exception e) {
1192+
LOG.error(e.getMessage(), e);
1193+
throw new SaslException(e.getMessage());
1194+
}
11791195
} else {
11801196
authServer = new NullQuorumAuthServer();
11811197
authLearner = new NullQuorumAuthLearner();
11821198
}
11831199
}
11841200

1201+
/**
1202+
* Instantiates and returns the configured SSL QuorumAuthServer implementation.
1203+
* <p>
1204+
* Reads the class name from the {@code sslAuthServerProvider} property. If
1205+
* no provider is configured, falls back to {@link NullQuorumAuthServer}.
1206+
* </p>
1207+
*
1208+
* @return an instance of {@link QuorumAuthServer}, or {@link NullQuorumAuthServer}
1209+
* if no provider is defined
1210+
* @throws SaslException if the configured class cannot be found, instantiated,
1211+
* or does not implement {@link QuorumAuthServer}
1212+
*/
1213+
private QuorumAuthServer getSslQuorumAuthServer() throws SaslException {
1214+
if (sslAuthServerProvider == null) {
1215+
LOG.info("sslAuthServerProvider not defined; using NullQuorumAuthServer");
1216+
return new NullQuorumAuthServer();
1217+
}
1218+
try {
1219+
Class<?> cls = Class.forName(sslAuthServerProvider);
1220+
Object inst = cls.getDeclaredConstructor().newInstance();
1221+
if (!(inst instanceof QuorumAuthServer)) {
1222+
throw new SaslException(
1223+
sslAuthServerProvider + " does not implement QuorumAuthServer");
1224+
}
1225+
return (QuorumAuthServer) inst;
1226+
1227+
} catch (ClassNotFoundException e) {
1228+
throw new SaslException(
1229+
"SSL auth server provider class not found: " + sslAuthServerProvider, e);
1230+
} catch (NoSuchMethodException | InstantiationException
1231+
| IllegalAccessException | InvocationTargetException e) {
1232+
throw new SaslException(
1233+
"Failed to instantiate SSL auth server provider: " + sslAuthServerProvider, e);
1234+
} catch (ClassCastException e) {
1235+
throw new SaslException(
1236+
"Configured class is not a QuorumAuthServer: " + sslAuthServerProvider, e);
1237+
}
1238+
}
1239+
1240+
/**
1241+
* Instantiates and returns the configured SSL QuorumAuthLearner implementation.
1242+
* <p>
1243+
* Reads the class name from the {@code sslAuthLearnerProvider} property. If
1244+
* no provider is configured, falls back to {@link NullQuorumAuthLearner}.
1245+
* </p>
1246+
*
1247+
* @return an instance of {@link QuorumAuthLearner}, or {@link NullQuorumAuthLearner}
1248+
* if no provider is defined
1249+
* @throws SaslException if the configured class cannot be found, instantiated,
1250+
* or does not implement {@link QuorumAuthLearner}
1251+
*/
1252+
private QuorumAuthLearner getSslQuorumAuthLearner() throws SaslException {
1253+
if (sslAuthLearnerProvider == null) {
1254+
LOG.info("sslAuthLearnerProvider not defined; using NullQuorumAuthLearner");
1255+
return new NullQuorumAuthLearner();
1256+
}
1257+
try {
1258+
Class<?> cls = Class.forName(sslAuthLearnerProvider);
1259+
Object inst = cls.getDeclaredConstructor().newInstance();
1260+
if (!(inst instanceof QuorumAuthLearner)) {
1261+
throw new SaslException(
1262+
sslAuthLearnerProvider + " does not implement QuorumAuthLearner");
1263+
}
1264+
return (QuorumAuthLearner) inst;
1265+
1266+
} catch (ClassNotFoundException e) {
1267+
throw new SaslException(
1268+
"SSL auth learner provider class not found: " + sslAuthLearnerProvider, e);
1269+
} catch (NoSuchMethodException | InstantiationException
1270+
| IllegalAccessException | InvocationTargetException e) {
1271+
throw new SaslException(
1272+
"Failed to instantiate SSL auth learner provider: " + sslAuthLearnerProvider, e);
1273+
} catch (ClassCastException e) {
1274+
throw new SaslException(
1275+
"Configured class is not a QuorumAuthLearner: " + sslAuthLearnerProvider, e);
1276+
}
1277+
}
1278+
11851279
QuorumStats quorumStats() {
11861280
return quorumStats;
11871281
}
@@ -2190,6 +2284,14 @@ public void setSslQuorum(boolean sslQuorum) {
21902284
this.sslQuorum = sslQuorum;
21912285
}
21922286

2287+
public void setSslAuthServerProvider(String sslAuthServerProvider) {
2288+
this.sslAuthServerProvider = sslAuthServerProvider;
2289+
}
2290+
2291+
public void setSslAuthLearnerProvider(String sslAuthLearnerProvider) {
2292+
this.sslAuthLearnerProvider = sslAuthLearnerProvider;
2293+
}
2294+
21932295
public void setUsePortUnification(boolean shouldUsePortUnification) {
21942296
LOG.info("Port unification {}", shouldUsePortUnification ? "enabled" : "disabled");
21952297
this.shouldUsePortUnification = shouldUsePortUnification;
@@ -2740,6 +2842,14 @@ boolean isQuorumSaslAuthEnabled() {
27402842
return quorumSaslEnableAuth;
27412843
}
27422844

2845+
public QuorumAuthServer getQuorumAuthServer() {
2846+
return authServer;
2847+
}
2848+
2849+
public QuorumAuthLearner getQuorumAuthLearner() {
2850+
return authLearner;
2851+
}
2852+
27432853
private boolean isQuorumServerSaslAuthRequired() {
27442854
return quorumServerSaslAuthRequired;
27452855
}

zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public class QuorumPeerConfig {
7777
protected boolean shouldUsePortUnification = false;
7878
protected int observerMasterPort;
7979
protected boolean sslQuorumReloadCertFiles = false;
80+
private String sslAuthServerProvider;
81+
private String sslAuthLearnerProvider;
82+
8083
protected File dataDir;
8184
protected File dataLogDir;
8285
protected String dynamicConfigFileStr = null;
@@ -390,7 +393,12 @@ public void parseProperties(Properties zkProp) throws IOException, ConfigExcepti
390393
multiAddressReachabilityCheckEnabled = parseBoolean(key, value);
391394
} else if (key.equals("oraclePath")) {
392395
oraclePath = value;
393-
} else {
396+
} else if (key.equals(QuorumAuth.QUORUM_SSL_AUTHPROVIDER)) {
397+
sslAuthServerProvider = value;
398+
} else if (key.equals(QuorumAuth.QUORUM_SSL_LEARNER_AUTHPROVIDER)) {
399+
sslAuthLearnerProvider = value;
400+
}
401+
else {
394402
System.setProperty("zookeeper." + key, value);
395403
}
396404
}
@@ -875,7 +883,8 @@ public boolean isLocalSessionsUpgradingEnabled() {
875883
public boolean isSslQuorum() {
876884
return sslQuorum;
877885
}
878-
886+
public String getSslAuthServerProvider() { return sslAuthServerProvider; }
887+
public String getSslAuthLearnerProvider() { return sslAuthLearnerProvider; }
879888
public boolean shouldUsePortUnification() {
880889
return shouldUsePortUnification;
881890
}

zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServ
201201
quorumPeer.setSecureCnxnFactory(secureCnxnFactory);
202202
quorumPeer.setSslQuorum(config.isSslQuorum());
203203
quorumPeer.setUsePortUnification(config.shouldUsePortUnification());
204+
quorumPeer.setSslAuthServerProvider(config.getSslAuthServerProvider());
205+
quorumPeer.setSslAuthLearnerProvider(config.getSslAuthLearnerProvider());
204206
quorumPeer.setLearnerType(config.getPeerType());
205207
quorumPeer.setSyncEnabled(config.getSyncEnabled());
206208
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());

zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/auth/QuorumAuth.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public class QuorumAuth {
4242
public static final String QUORUM_SERVER_SASL_LOGIN_CONTEXT = "quorum.auth.server.saslLoginContext";
4343
public static final String QUORUM_SERVER_SASL_LOGIN_CONTEXT_DFAULT_VALUE = "QuorumServer";
4444

45+
/** Property key for custom SSL QuorumAuthServer provider (Class name) */
46+
public static final String QUORUM_SSL_AUTHPROVIDER = "ssl.quorum.authProvider";
47+
/** Property key for custom SSL QuorumAuthLearner provider (Class name) */
48+
public static final String QUORUM_SSL_LEARNER_AUTHPROVIDER = "ssl.quorum.learner.authProvider";
49+
4550
static final String QUORUM_SERVER_PROTOCOL_NAME = "zookeeper-quorum";
4651
static final String QUORUM_SERVER_SASL_DIGEST = "zk-quorum-sasl-md5";
4752
static final String QUORUM_AUTH_MESSAGE_TAG = "qpconnect";
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.zookeeper.server.quorum;
20+
21+
import org.apache.zookeeper.server.quorum.auth.NullQuorumAuthServer;
22+
import org.apache.zookeeper.server.quorum.auth.NullQuorumAuthLearner;
23+
import org.apache.zookeeper.server.quorum.auth.QuorumAuthLearner;
24+
import org.apache.zookeeper.server.quorum.auth.QuorumAuthServer;
25+
import org.apache.zookeeper.server.quorum.auth.MockSSLQuorumAuthLearner;
26+
import org.apache.zookeeper.server.quorum.auth.MockSslQuorumAuthServer;
27+
import org.apache.zookeeper.server.quorum.auth.QuorumAuth;
28+
import org.junit.jupiter.api.Test;
29+
30+
import java.lang.reflect.Method;
31+
import java.util.Properties;
32+
33+
import static org.junit.jupiter.api.Assertions.*;
34+
35+
/**
36+
* Unit tests for pluggable SSL quorum auth providers in {@link QuorumPeer}.
37+
*/
38+
public class QuorumPeerAuthProviderTest {
39+
40+
/**
41+
* When sslAuthServerProvider is set to a custom provider, ensure it instantiates correctly.
42+
*/
43+
@Test
44+
public void testCustomSslAuthServerProvider() throws Exception {
45+
// Prepare config with custom server auth provider
46+
QuorumPeerConfig config = new QuorumPeerConfig();
47+
Properties p = new Properties();
48+
p.setProperty("sslQuorum", "true");
49+
p.setProperty(QuorumAuth.QUORUM_SSL_AUTHPROVIDER,
50+
MockSslQuorumAuthServer.class.getName());
51+
config.parseProperties(p);
52+
53+
// Set on peer and invoke private method
54+
QuorumPeer peer = new QuorumPeer();
55+
peer.setSslAuthServerProvider(config.getSslAuthServerProvider());
56+
QuorumAuthServer authServer = invokeGetSslQuorumAuthServer(peer);
57+
58+
assertTrue(authServer instanceof MockSslQuorumAuthServer,
59+
"Expected MockSSLQuorumAuthServer when provider is configured");
60+
}
61+
62+
/**
63+
* Without any provider configured, default should be NullQuorumAuthServer.
64+
*/
65+
@Test
66+
public void testDefaultSslAuthServerProvider() throws Exception {
67+
QuorumPeer peer = new QuorumPeer();
68+
QuorumAuthServer authServer = invokeGetSslQuorumAuthServer(peer);
69+
assertTrue(authServer instanceof NullQuorumAuthServer,
70+
"Expected NullQuorumAuthServer when no provider is configured");
71+
}
72+
73+
/**
74+
* When sslAuthLearnerProvider is set to a custom provider, ensure it instantiates correctly.
75+
*/
76+
@Test
77+
public void testCustomSslAuthLearnerProvider() throws Exception {
78+
QuorumPeerConfig config = new QuorumPeerConfig();
79+
Properties p = new Properties();
80+
p.setProperty("sslQuorum", "true");
81+
p.setProperty(QuorumAuth.QUORUM_SSL_LEARNER_AUTHPROVIDER,
82+
MockSSLQuorumAuthLearner.class.getName());
83+
config.parseProperties(p);
84+
85+
QuorumPeer peer = new QuorumPeer();
86+
peer.setSslAuthLearnerProvider(config.getSslAuthLearnerProvider());
87+
QuorumAuthLearner authLearner = invokeGetSslQuorumAuthLearner(peer);
88+
89+
assertTrue(authLearner instanceof MockSSLQuorumAuthLearner,
90+
"Expected MockSSLQuorumAuthLearner when learner provider is configured");
91+
}
92+
93+
/**
94+
* Without any learner provider configured, default should be NullQuorumAuthLearner.
95+
*/
96+
@Test
97+
public void testDefaultSslAuthLearnerProvider() throws Exception {
98+
QuorumPeer peer = new QuorumPeer();
99+
QuorumAuthLearner authLearner = invokeGetSslQuorumAuthLearner(peer);
100+
assertTrue(authLearner instanceof NullQuorumAuthLearner,
101+
"Expected NullQuorumAuthLearner when no learner provider is configured");
102+
}
103+
104+
// Reflection helpers to access private methods
105+
106+
private QuorumAuthServer invokeGetSslQuorumAuthServer(QuorumPeer peer) throws Exception {
107+
Method m = QuorumPeer.class.getDeclaredMethod("getSslQuorumAuthServer");
108+
m.setAccessible(true);
109+
return (QuorumAuthServer) m.invoke(peer);
110+
}
111+
112+
private QuorumAuthLearner invokeGetSslQuorumAuthLearner(QuorumPeer peer) throws Exception {
113+
Method m = QuorumPeer.class.getDeclaredMethod("getSslQuorumAuthLearner");
114+
m.setAccessible(true);
115+
return (QuorumAuthLearner) m.invoke(peer);
116+
}
117+
}

0 commit comments

Comments
 (0)