CommandAuthTest.java
/*
* 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.zookeeper.server.admin;
import static org.apache.zookeeper.ZooDefs.Ids.OPEN_ACL_UNSAFE;
import static org.apache.zookeeper.server.admin.Commands.AUTH_INFO_SEPARATOR;
import static org.apache.zookeeper.server.admin.Commands.ROOT_PATH;
import static org.apache.zookeeper.server.admin.JettyAdminServerTest.HTTPS_URL_FORMAT;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletResponse;
import org.apache.zookeeper.PortAssignment;
import org.apache.zookeeper.ZKTestCase;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.client.ZKClientConfig;
import org.apache.zookeeper.common.ClientX509Util;
import org.apache.zookeeper.common.QuorumX509Util;
import org.apache.zookeeper.common.X509Exception;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.server.NettyServerCnxnFactory;
import org.apache.zookeeper.server.ServerCnxnFactory;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import org.apache.zookeeper.server.auth.ProviderRegistry;
import org.apache.zookeeper.server.auth.X509AuthenticationProvider;
import org.apache.zookeeper.test.ClientBase;
import org.eclipse.jetty.http.HttpHeader;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class CommandAuthTest extends ZKTestCase {
private static final String DIGEST_SCHEMA = "digest";
private static final String X509_SCHEMA = "x509";
private static final String IP_SCHEMA = "ip";
private static final String ROOT_USER = "root";
private static final String ROOT_PASSWORD = "root_passwd";
private static final String AUTH_TEST_COMMAND_NAME = "authtest";
private static final String X509_SUBJECT_PRINCIPAL = "CN=localhost,OU=ZooKeeper,O=Apache,L=Unknown,ST=Unknown,C=Unknown";
public enum AuthSchema {
DIGEST,
X509,
IP
}
private final int jettyAdminPort = PortAssignment.unique();
private final String hostPort = "127.0.0.1:" + PortAssignment.unique();
private final ClientX509Util clientX509Util = new ClientX509Util();
private final QuorumX509Util quorumX509Util = new QuorumX509Util();
private ZooKeeperServer zks;
private ServerCnxnFactory cnxnFactory;
private JettyAdminServer adminServer;
private ZooKeeper zk;
@TempDir
static File dataDir;
@TempDir
static File logDir;
@BeforeAll
public void setup() throws Exception {
Commands.registerCommand(new AuthTestCommand(true, ZooDefs.Perms.ALL, ROOT_PATH));
setupTLS();
// start ZookeeperServer
System.setProperty("zookeeper.4lw.commands.whitelist", "*");
zks = new ZooKeeperServer(dataDir, logDir, 3000);
final int port = Integer.parseInt(hostPort.split(":")[1]);
cnxnFactory = ServerCnxnFactory.createFactory(port, -1);
cnxnFactory.startup(zks);
assertTrue(ClientBase.waitForServerUp(hostPort, 120000));
// start AdminServer
System.setProperty("zookeeper.admin.enableServer", "true");
System.setProperty("zookeeper.admin.serverPort", String.valueOf(jettyAdminPort));
adminServer = new JettyAdminServer();
adminServer.setZooKeeperServer(zks);
adminServer.start();
}
@AfterAll
public void tearDown() throws Exception {
clearTLS();
System.clearProperty("zookeeper.4lw.commands.whitelist");
System.clearProperty("zookeeper.admin.enableServer");
System.clearProperty("zookeeper.admin.serverPort");
if (adminServer != null) {
adminServer.shutdown();
}
if (cnxnFactory != null) {
cnxnFactory.shutdown();
}
if (zks != null) {
zks.shutdown();
}
}
@BeforeEach
public void setupEach() throws Exception {
zk = ClientBase.createZKClient(hostPort);
}
@AfterEach
public void tearDownEach() throws Exception {
if (zk != null) {
zk.close();
}
}
@ParameterizedTest
@EnumSource(AuthSchema.class)
public void testAuthCheck_authorized(final AuthSchema authSchema) throws Exception {
setupRootACL(authSchema);
try {
final HttpURLConnection authTestConn = sendAuthTestCommandRequest(authSchema, true);
assertEquals(HttpURLConnection.HTTP_OK, authTestConn.getResponseCode());
} finally {
addAuthInfo(zk, authSchema);
resetRootACL(zk);
}
}
@ParameterizedTest
@EnumSource(value = AuthSchema.class, names = {"DIGEST"})
public void testAuthCheck_notAuthorized(final AuthSchema authSchema) throws Exception {
setupRootACL(authSchema);
try {
final HttpURLConnection authTestConn = sendAuthTestCommandRequest(authSchema, false);
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, authTestConn.getResponseCode());
} finally {
addAuthInfo(zk, authSchema);
resetRootACL(zk);
}
}
@ParameterizedTest
@EnumSource(AuthSchema.class)
public void testAuthCheck_noACL(final AuthSchema authSchema) throws Exception {
final HttpURLConnection authTestConn = sendAuthTestCommandRequest(authSchema, false);
assertEquals(HttpURLConnection.HTTP_OK, authTestConn.getResponseCode());
}
@Test
public void testAuthCheck_invalidServerRequiredConfig() {
assertThrows("An active server is required for auth check",
IllegalArgumentException.class,
() -> new AuthTestCommand(false, ZooDefs.Perms.ALL, ROOT_PATH));
}
@Test
public void testAuthCheck_noAuthInfo() {
testAuthCheck_invalidAuthInfo(null);
}
@Test
public void testAuthCheck_noAuthInfoSeparator() {
final String invalidAuthInfo = String.format("%s%s%s:%s", DIGEST_SCHEMA, "", ROOT_USER, ROOT_PASSWORD);
testAuthCheck_invalidAuthInfo(invalidAuthInfo);
}
@Test
public void testAuthCheck_invalidAuthInfoSeparator() {
final String invalidAuthInfo = String.format("%s%s%s:%s", DIGEST_SCHEMA, ":", ROOT_USER, ROOT_PASSWORD);
testAuthCheck_invalidAuthInfo(invalidAuthInfo);
}
@Test
public void testAuthCheck_invalidAuthSchema() {
final String invalidAuthInfo = String.format("%s%s%s:%s", "InvalidAuthSchema", AUTH_INFO_SEPARATOR, ROOT_USER, ROOT_PASSWORD);
testAuthCheck_invalidAuthInfo(invalidAuthInfo);
}
@Test
public void testAuthCheck_authProviderNotFound() {
final String invalidAuthInfo = String.format("%s%s%s:%s", "sasl", AUTH_INFO_SEPARATOR, ROOT_USER, ROOT_PASSWORD);
testAuthCheck_invalidAuthInfo(invalidAuthInfo);
}
private void testAuthCheck_invalidAuthInfo(final String invalidAuthInfo) {
final CommandResponse commandResponse = Commands.runGetCommand(AUTH_TEST_COMMAND_NAME, zks, new HashMap<>(), invalidAuthInfo, null);
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, commandResponse.getStatusCode());
}
private static class AuthTestCommand extends GetCommand {
public AuthTestCommand(final boolean serverRequired, final int perm, final String path) {
super(Arrays.asList(AUTH_TEST_COMMAND_NAME, "at"), serverRequired, new AuthRequest(perm, path));
}
@Override
public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
return initializeResponse();
}
}
private void setupTLS() throws Exception {
System.setProperty("zookeeper.authProvider.x509", "org.apache.zookeeper.server.auth.X509AuthenticationProvider");
String testDataPath = System.getProperty("test.data.dir", "src/test/resources/data");
System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks");
System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass");
System.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks");
System.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass");
// client
System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty");
System.setProperty(ZKClientConfig.SECURE_CLIENT, "true");
// server
System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory");
System.setProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY, Boolean.TRUE.toString());
// admin server
System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks");
System.setProperty(quorumX509Util.getSslKeystorePasswdProperty(), "testpass");
System.setProperty(quorumX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks");
System.setProperty(quorumX509Util.getSslTruststorePasswdProperty(), "testpass");
System.setProperty("zookeeper.admin.forceHttps", "true");
System.setProperty("zookeeper.admin.needClientAuth", "true");
// create SSLContext
final SSLContext sslContext = SSLContext.getInstance(ClientX509Util.DEFAULT_PROTOCOL);
final X509AuthenticationProvider authProvider = (X509AuthenticationProvider) ProviderRegistry.getProvider("x509");
if (authProvider == null) {
throw new X509Exception.SSLContextException("Could not create SSLContext with x509 auth provider");
}
sslContext.init(new X509KeyManager[]{authProvider.getKeyManager()}, new X509TrustManager[]{authProvider.getTrustManager()}, null);
// set SSLSocketFactory
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
}
public void clearTLS() {
System.clearProperty("zookeeper.authProvider.x509");
System.clearProperty(clientX509Util.getSslKeystoreLocationProperty());
System.clearProperty(clientX509Util.getSslKeystorePasswdProperty());
System.clearProperty(clientX509Util.getSslTruststoreLocationProperty());
System.clearProperty(clientX509Util.getSslTruststorePasswdProperty());
// client side
System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
System.clearProperty(ZKClientConfig.SECURE_CLIENT);
// server side
System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
System.clearProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY);
// admin server
System.clearProperty(quorumX509Util.getSslKeystoreLocationProperty());
System.clearProperty(quorumX509Util.getSslKeystorePasswdProperty());
System.clearProperty(quorumX509Util.getSslTruststoreLocationProperty());
System.clearProperty(quorumX509Util.getSslTruststorePasswdProperty());
System.clearProperty("zookeeper.admin.forceHttps");
System.clearProperty("zookeeper.admin.needClientAuth");
}
private void setupRootACL(final AuthSchema authSchema) throws Exception {
switch (authSchema) {
case DIGEST:
setupRootACLForDigest(zk);
break;
case X509:
setupRootACLForX509(zk);
break;
case IP:
setupRootACLForIP(zk);
break;
default:
throw new IllegalArgumentException("Unknown auth schema");
}
}
private HttpURLConnection sendAuthTestCommandRequest(final AuthSchema authSchema, final boolean validAuthInfo) throws Exception {
final URL authTestURL = new URL(String.format(HTTPS_URL_FORMAT + "/" + AUTH_TEST_COMMAND_NAME, jettyAdminPort));
final HttpURLConnection authTestConn = (HttpURLConnection) authTestURL.openConnection();
addAuthHeader(authTestConn, authSchema, validAuthInfo);
authTestConn.setRequestMethod("GET");
return authTestConn;
}
private void addAuthInfo(final ZooKeeper zk, final AuthSchema authSchema) {
switch (authSchema) {
case DIGEST:
addAuthInfoForDigest(zk);
break;
case X509:
addAuthInfoForX509(zk);
break;
case IP:
addAuthInfoForIP(zk);
break;
default:
throw new IllegalArgumentException("Unknown auth schema");
}
}
public static void resetRootACL(final ZooKeeper zk) throws Exception {
zk.setACL(Commands.ROOT_PATH, OPEN_ACL_UNSAFE, -1);
}
public static void setupRootACLForDigest(final ZooKeeper zk) throws Exception {
final String idPassword = String.format("%s:%s", ROOT_USER, ROOT_PASSWORD);
final String digest = DigestAuthenticationProvider.generateDigest(idPassword);
final ACL acl = new ACL(ZooDefs.Perms.ALL, new Id(DIGEST_SCHEMA, digest));
zk.setACL(Commands.ROOT_PATH, Collections.singletonList(acl), -1);
}
private static void setupRootACLForX509(final ZooKeeper zk) throws Exception {
final ACL acl = new ACL(ZooDefs.Perms.ALL, new Id(X509_SCHEMA, X509_SUBJECT_PRINCIPAL));
zk.setACL(Commands.ROOT_PATH, Collections.singletonList(acl), -1);
}
private static void setupRootACLForIP(final ZooKeeper zk) throws Exception {
final ACL acl = new ACL(ZooDefs.Perms.ALL, new Id(IP_SCHEMA, "127.0.0.1"));
zk.setACL(Commands.ROOT_PATH, Collections.singletonList(acl), -1);
}
public static void addAuthInfoForDigest(final ZooKeeper zk) {
final String idPassword = String.format("%s:%s", ROOT_USER, ROOT_PASSWORD);
zk.addAuthInfo(DIGEST_SCHEMA, idPassword.getBytes(StandardCharsets.UTF_8));
}
public static void addAuthInfoForX509(final ZooKeeper zk) {
zk.addAuthInfo(X509_SCHEMA, X509_SUBJECT_PRINCIPAL.getBytes(StandardCharsets.UTF_8));
}
private void addAuthInfoForIP(final ZooKeeper zk) {
zk.addAuthInfo(IP_SCHEMA, "127.0.0.1".getBytes(StandardCharsets.UTF_8));
}
public static void addAuthHeader(final HttpURLConnection conn, final AuthSchema authSchema, final boolean validAuthInfo) {
String authInfo;
switch (authSchema) {
case DIGEST:
authInfo = validAuthInfo ? buildAuthorizationForDigest() : buildInvalidAuthorizationForDigest();
break;
case X509:
authInfo = buildAuthorizationForX509();
break;
case IP:
authInfo = buildAuthorizationForIP();
break;
default:
throw new IllegalArgumentException("Unknown auth schema");
}
conn.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), authInfo);
}
public static String buildAuthorizationForDigest() {
return String.format("%s%s%s:%s", DIGEST_SCHEMA, Commands.AUTH_INFO_SEPARATOR, ROOT_USER, ROOT_PASSWORD);
}
private static String buildInvalidAuthorizationForDigest() {
return String.format("%s%s%s:%s", DIGEST_SCHEMA, Commands.AUTH_INFO_SEPARATOR, "InvalidUser", "InvalidPassword");
}
private static String buildAuthorizationForX509() {
return String.format("%s%s", X509_SCHEMA, Commands.AUTH_INFO_SEPARATOR);
}
private static String buildAuthorizationForIP() {
return String.format("%s%s", IP_SCHEMA, Commands.AUTH_INFO_SEPARATOR);
}
}