Skip to content

Commit 94d88da

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

File tree

8 files changed

+526
-3
lines changed

8 files changed

+526
-3
lines changed

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

Lines changed: 109 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,98 @@ 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+
} catch (Exception e) {
1191+
LOG.error(e.getMessage(), e);
1192+
throw new SaslException(e.getMessage());
1193+
}
11791194
} else {
11801195
authServer = new NullQuorumAuthServer();
11811196
authLearner = new NullQuorumAuthLearner();
11821197
}
11831198
}
11841199

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

2286+
public void setSslAuthServerProvider(String sslAuthServerProvider) {
2287+
this.sslAuthServerProvider = sslAuthServerProvider;
2288+
}
2289+
2290+
public void setSslAuthLearnerProvider(String sslAuthLearnerProvider) {
2291+
this.sslAuthLearnerProvider = sslAuthLearnerProvider;
2292+
}
2293+
21932294
public void setUsePortUnification(boolean shouldUsePortUnification) {
21942295
LOG.info("Port unification {}", shouldUsePortUnification ? "enabled" : "disabled");
21952296
this.shouldUsePortUnification = shouldUsePortUnification;
@@ -2740,6 +2841,14 @@ boolean isQuorumSaslAuthEnabled() {
27402841
return quorumSaslEnableAuth;
27412842
}
27422843

2844+
public QuorumAuthServer getQuorumAuthServer() {
2845+
return authServer;
2846+
}
2847+
2848+
public QuorumAuthLearner getQuorumAuthLearner() {
2849+
return authLearner;
2850+
}
2851+
27432852
private boolean isQuorumServerSaslAuthRequired() {
27442853
return quorumServerSaslAuthRequired;
27452854
}

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

Lines changed: 13 additions & 1 deletion
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,6 +393,10 @@ public void parseProperties(Properties zkProp) throws IOException, ConfigExcepti
390393
multiAddressReachabilityCheckEnabled = parseBoolean(key, value);
391394
} else if (key.equals("oraclePath")) {
392395
oraclePath = value;
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;
393400
} else {
394401
System.setProperty("zookeeper." + key, value);
395402
}
@@ -875,7 +882,12 @@ public boolean isLocalSessionsUpgradingEnabled() {
875882
public boolean isSslQuorum() {
876883
return sslQuorum;
877884
}
878-
885+
public String getSslAuthServerProvider() {
886+
return sslAuthServerProvider;
887+
}
888+
public String getSslAuthLearnerProvider() {
889+
return sslAuthLearnerProvider;
890+
}
879891
public boolean shouldUsePortUnification() {
880892
return shouldUsePortUnification;
881893
}

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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 static org.junit.jupiter.api.Assertions.assertTrue;
22+
import java.io.File;
23+
import java.lang.reflect.Method;
24+
import java.util.Properties;
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.NullQuorumAuthLearner;
28+
import org.apache.zookeeper.server.quorum.auth.NullQuorumAuthServer;
29+
import org.apache.zookeeper.server.quorum.auth.QuorumAuth;
30+
import org.apache.zookeeper.server.quorum.auth.QuorumAuthLearner;
31+
import org.apache.zookeeper.server.quorum.auth.QuorumAuthServer;
32+
import org.junit.jupiter.api.Test;
33+
34+
/**
35+
* Unit tests for pluggable SSL quorum auth providers in {@link QuorumPeer}.
36+
*/
37+
public class QuorumPeerAuthProviderTest {
38+
39+
/**
40+
* When sslAuthServerProvider is set to a custom provider, ensure it instantiates correctly.
41+
*/
42+
@Test
43+
public void testCustomSslAuthServerProvider() throws Exception {
44+
// Prepare config with custom server auth provider
45+
QuorumPeerConfig config = new QuorumPeerConfig();
46+
Properties zkProp = getDefaultZKProperties();
47+
zkProp.setProperty("sslQuorum", "true");
48+
zkProp.setProperty(QuorumAuth.QUORUM_SSL_AUTHPROVIDER,
49+
MockSslQuorumAuthServer.class.getName());
50+
config.parseProperties(zkProp);
51+
52+
// Set on peer and invoke private method
53+
QuorumPeer peer = new QuorumPeer();
54+
peer.setSslAuthServerProvider(config.getSslAuthServerProvider());
55+
QuorumAuthServer authServer = invokeGetSslQuorumAuthServer(peer);
56+
57+
assertTrue(authServer instanceof MockSslQuorumAuthServer,
58+
"Expected MockSSLQuorumAuthServer when provider is configured");
59+
}
60+
61+
/**
62+
* Without any provider configured, default should be NullQuorumAuthServer.
63+
*/
64+
@Test
65+
public void testDefaultSslAuthServerProvider() throws Exception {
66+
QuorumPeer peer = new QuorumPeer();
67+
QuorumAuthServer authServer = invokeGetSslQuorumAuthServer(peer);
68+
assertTrue(authServer instanceof NullQuorumAuthServer,
69+
"Expected NullQuorumAuthServer when no provider is configured");
70+
}
71+
72+
/**
73+
* When sslAuthLearnerProvider is set to a custom provider, ensure it instantiates correctly.
74+
*/
75+
@Test
76+
public void testCustomSslAuthLearnerProvider() throws Exception {
77+
QuorumPeerConfig config = new QuorumPeerConfig();
78+
Properties zkProp = getDefaultZKProperties();
79+
zkProp.setProperty("sslQuorum", "true");
80+
zkProp.setProperty(QuorumAuth.QUORUM_SSL_LEARNER_AUTHPROVIDER,
81+
MockSSLQuorumAuthLearner.class.getName());
82+
config.parseProperties(zkProp);
83+
84+
QuorumPeer peer = new QuorumPeer();
85+
peer.setSslAuthLearnerProvider(config.getSslAuthLearnerProvider());
86+
QuorumAuthLearner authLearner = invokeGetSslQuorumAuthLearner(peer);
87+
88+
assertTrue(authLearner instanceof MockSSLQuorumAuthLearner,
89+
"Expected MockSSLQuorumAuthLearner when learner provider is configured");
90+
}
91+
92+
/**
93+
* Without any learner provider configured, default should be NullQuorumAuthLearner.
94+
*/
95+
@Test
96+
public void testDefaultSslAuthLearnerProvider() throws Exception {
97+
QuorumPeer peer = new QuorumPeer();
98+
QuorumAuthLearner authLearner = invokeGetSslQuorumAuthLearner(peer);
99+
assertTrue(authLearner instanceof NullQuorumAuthLearner,
100+
"Expected NullQuorumAuthLearner when no learner provider is configured");
101+
}
102+
103+
// Reflection helpers to access private methods
104+
105+
private QuorumAuthServer invokeGetSslQuorumAuthServer(QuorumPeer peer) throws Exception {
106+
Method m = QuorumPeer.class.getDeclaredMethod("getSslQuorumAuthServer");
107+
m.setAccessible(true);
108+
return (QuorumAuthServer) m.invoke(peer);
109+
}
110+
111+
private QuorumAuthLearner invokeGetSslQuorumAuthLearner(QuorumPeer peer) throws Exception {
112+
Method m = QuorumPeer.class.getDeclaredMethod("getSslQuorumAuthLearner");
113+
m.setAccessible(true);
114+
return (QuorumAuthLearner) m.invoke(peer);
115+
}
116+
private Properties getDefaultZKProperties() {
117+
Properties zkProp = new Properties();
118+
zkProp.setProperty("dataDir", new File("myDataDir").getAbsolutePath());
119+
zkProp.setProperty("oraclePath", new File("mastership").getAbsolutePath());
120+
return zkProp;
121+
}
122+
}

0 commit comments

Comments
 (0)