This is an automated email from the ASF dual-hosted git repository.

wangdan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pegasus.git


The following commit(s) were added to refs/heads/master by this push:
     new ea554b23c feat: support encrypted password file during SASL 
authentication for ZooKeeper C client (#2293)
ea554b23c is described below

commit ea554b23cfa505f6b43877980c8fe90ddd04287e
Author: Dan Wang <[email protected]>
AuthorDate: Thu Oct 16 16:28:44 2025 +0800

    feat: support encrypted password file during SASL authentication for 
ZooKeeper C client (#2293)
    
    https://github.com/apache/incubator-pegasus/issues/2292
    
    In addition to supporting plaintext password files, this PR adds support for
    **encrypted password files** during SASL authentication. A new configuration
    item, `sasl_password_encryption_scheme`, is introduced to specify the
    encryption scheme — currently, only **Base64** is supported.
    
    This PR also introduces a new configuration option, `zoo_log_level`, which
    allows setting the **log level** for the ZooKeeper C client to help diagnose
    ZooKeeper-related issues.
    
    Finally, SASL authentication tests for the ZooKeeper C client — which were
    previously missing — have been added to ensure proper functionality.
    
    Following configurations are newly added:
    ```diff
    [zookeeper]
    + zoo_log_level = "ZOO_LOG_LEVEL_INFO"
    + sasl_password_encryption_scheme = "base64"
    ```
---
 .github/workflows/lint_and_test_cpp.yaml           |   9 +-
 admin_tools/start_zk.sh                            |  24 +-
 run.sh                                             |  15 +-
 src/zookeeper/test/CMakeLists.txt                  |   2 +
 src/zookeeper/test/config-test.ini                 |   1 -
 src/zookeeper/test/sasl_auth/CMakeLists.txt        |  51 ++++
 src/zookeeper/test/sasl_auth/config-test.ini       |  80 ++++++
 src/zookeeper/test/sasl_auth/main.cpp              |  67 +++++
 src/zookeeper/test/sasl_auth/run.sh                |  39 +++
 .../sasl_auth/zookeeper_session_sasl_auth_test.cpp | 125 +++++++++
 .../test/zookeeper_session_no_auth_test.cpp        |  35 +++
 src/zookeeper/test/zookeeper_session_test.h        | 178 +++++++++++++
 src/zookeeper/test/zookeeper_session_test_base.cpp | 281 +++++++++++++++++++++
 src/zookeeper/test/zookeeper_session_test_base.h   | 158 ++++++++++++
 src/zookeeper/zookeeper_session.cpp                | 203 ++++++++++-----
 src/zookeeper/zookeeper_session.h                  |  27 +-
 16 files changed, 1219 insertions(+), 76 deletions(-)

diff --git a/.github/workflows/lint_and_test_cpp.yaml 
b/.github/workflows/lint_and_test_cpp.yaml
index 97a25634a..be73a099d 100644
--- a/.github/workflows/lint_and_test_cpp.yaml
+++ b/.github/workflows/lint_and_test_cpp.yaml
@@ -173,7 +173,7 @@ jobs:
           - dsn.zookeeper.tests
           # TODO(yingchun): Disable it because we find it's too flaky, we will 
re-enable it after
           # it has been optimized.
-#          - partition_split_test
+          # - partition_split_test
           - pegasus_geo_test
           - pegasus_rproxy_test
           - pegasus_unit_test
@@ -181,6 +181,7 @@ jobs:
           - restore_test
           - security_test
           - throttle_test
+          - zookeeper_sasl_auth_test
     needs: build_Release
     runs-on: ubuntu-latest
     env:
@@ -259,7 +260,7 @@ jobs:
           - dsn.zookeeper.tests
           # TODO(yingchun): Disable it because we find it's too flaky, we will 
re-enable it after
           # it has been optimized.
-#          - partition_split_test
+          # - partition_split_test
           - pegasus_geo_test
           - pegasus_rproxy_test
           - pegasus_unit_test
@@ -268,7 +269,8 @@ jobs:
           - security_test
           # TODO(yingchun): Disable it because we find it's too flaky, we will 
re-enable it after
           # it has been optimized.
-#          - throttle_test
+          # - throttle_test
+          - zookeeper_sasl_auth_test
     needs: build_ASAN
     runs-on: ubuntu-latest
     env:
@@ -356,6 +358,7 @@ jobs:
 #          - restore_test
 #          - security_test
 #          - throttle_test
+#          - zookeeper_sasl_auth_test
 #    needs: build_UBSAN
 #    runs-on: ubuntu-latest
 #    env:
diff --git a/admin_tools/start_zk.sh b/admin_tools/start_zk.sh
index 2bc37356b..03b7f9f00 100755
--- a/admin_tools/start_zk.sh
+++ b/admin_tools/start_zk.sh
@@ -23,6 +23,7 @@ CWD=$(cd $(dirname $0) && pwd)
 # Options:
 #    INSTALL_DIR    <dir>
 #    PORT           <port>
+#    SASL_AUTH      YES|NO
 
 if [ -z "$INSTALL_DIR" ]
 then
@@ -36,6 +37,11 @@ then
     exit 1
 fi
 
+if [ -z "$SASL_AUTH" ]
+then
+    SASL_AUTH=NO
+fi
+
 if ! mkdir -p "$INSTALL_DIR";
 then
     echo "ERROR: mkdir $INSTALL_DIR failed"
@@ -54,7 +60,23 @@ fi
 
 ZOOKEEPER_PORT=$PORT
 
-cp $ZOOKEEPER_HOME/conf/zoo_sample.cfg $ZOOKEEPER_HOME/conf/zoo.cfg
+if [ "$SASL_AUTH" == "YES" ]; then
+    {
+        echo "Server {"
+        echo "    org.apache.zookeeper.server.auth.DigestLoginModule required"
+        echo "    user_myuser=\"mypassword\";"
+        echo "};"
+    } > $ZOOKEEPER_HOME/conf/jaas.conf
+
+    PROPERTIES=(
+        "-Dzookeeper.sessionRequireClientSASLAuth=true"
+        
"-Dzookeeper.authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider"
+        "-Djava.security.auth.login.config=$ZOOKEEPER_HOME/conf/jaas.conf"
+    )
+    export JVMFLAGS="${JVMFLAGS} ${PROPERTIES[@]}"
+fi
+
+cp -a $ZOOKEEPER_HOME/conf/zoo_sample.cfg $ZOOKEEPER_HOME/conf/zoo.cfg
 sed -i "s@dataDir=/tmp/zookeeper@dataDir=$ZOOKEEPER_HOME/data@" 
$ZOOKEEPER_HOME/conf/zoo.cfg
 sed -i "s@clientPort=2181@clientPort=$ZOOKEEPER_PORT@" 
$ZOOKEEPER_HOME/conf/zoo.cfg
 echo "admin.enableServer=false" >> $ZOOKEEPER_HOME/conf/zoo.cfg
diff --git a/run.sh b/run.sh
index c8fc079f5..04ab68ea2 100755
--- a/run.sh
+++ b/run.sh
@@ -441,6 +441,7 @@ function run_test()
       recovery_test
       restore_test
       throttle_test
+      zookeeper_sasl_auth_test
     )
     local onebox_opts=""
     local test_opts=""
@@ -538,7 +539,12 @@ function run_test()
         else
             # Restart ZK in what ever case.
             run_stop_zk
-            run_start_zk
+
+            if [ "${module}" == "zookeeper_sasl_auth_test" ]; then
+                run_start_zk --sasl_auth
+            else
+                run_start_zk
+            fi
         fi
 
         # Run server test.
@@ -605,6 +611,7 @@ function usage_start_zk()
     echo "                     zookeeper install directory,"
     echo "                     if not set, then default is './.zk_install'"
     echo "   -p|--port <port>  listen port of zookeeper, default is 22181"
+    echo "   -s|--sasl_auth    start zookeeper with SASL Auth, default no"
 }
 
 function run_start_zk()
@@ -618,6 +625,7 @@ function run_start_zk()
 
     INSTALL_DIR=`pwd`/.zk_install
     PORT=22181
+    SASL_AUTH=NO
     while [[ $# > 0 ]]; do
         key="$1"
         case $key in
@@ -633,6 +641,9 @@ function run_start_zk()
                 PORT=$2
                 shift
                 ;;
+            -s|--sasl_auth)
+                SASL_AUTH=YES
+                ;;
             *)
                 echo "ERROR: unknown option \"$key\""
                 echo
@@ -660,7 +671,7 @@ function run_start_zk()
         fi
     fi
 
-    INSTALL_DIR="$INSTALL_DIR" PORT="$PORT" $ROOT/admin_tools/start_zk.sh
+    INSTALL_DIR="$INSTALL_DIR" PORT="$PORT" SASL_AUTH="$SASL_AUTH" 
$ROOT/admin_tools/start_zk.sh
 }
 
 #####################
diff --git a/src/zookeeper/test/CMakeLists.txt 
b/src/zookeeper/test/CMakeLists.txt
index 7dabd1faf..63bf23444 100644
--- a/src/zookeeper/test/CMakeLists.txt
+++ b/src/zookeeper/test/CMakeLists.txt
@@ -54,3 +54,5 @@ set(MY_BINPLACES
     )
 
 dsn_add_test()
+
+add_subdirectory(sasl_auth)
diff --git a/src/zookeeper/test/config-test.ini 
b/src/zookeeper/test/config-test.ini
index 5a07160ae..bceaaf999 100644
--- a/src/zookeeper/test/config-test.ini
+++ b/src/zookeeper/test/config-test.ini
@@ -65,7 +65,6 @@ pause_on_start = false
 logging_start_level = LOG_LEVEL_DEBUG
 logging_factory_name = dsn::tools::simple_logger
 
-
 [tools.simple_logger]
 fast_flush = true
 short_header = false
diff --git a/src/zookeeper/test/sasl_auth/CMakeLists.txt 
b/src/zookeeper/test/sasl_auth/CMakeLists.txt
new file mode 100644
index 000000000..add58ccda
--- /dev/null
+++ b/src/zookeeper/test/sasl_auth/CMakeLists.txt
@@ -0,0 +1,51 @@
+# 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.
+
+set(MY_PROJ_NAME zookeeper_sasl_auth_test)
+project(${MY_PROJ_NAME} C CXX)
+
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../")
+
+# Source files under CURRENT project directory will be automatically included.
+# You can manually set MY_PROJ_SRC to include source files under other 
directories.
+set(MY_PROJ_SRC
+        "${CMAKE_CURRENT_SOURCE_DIR}/../zookeeper_session_test_base.cpp")
+
+# Search mode for source files under CURRENT project directory?
+# "GLOB_RECURSE" for recursive search
+# "GLOB" for non-recursive search
+set(MY_SRC_SEARCH_MODE "GLOB")
+
+set(MY_PROJ_LIBS
+        dsn.replication.zookeeper_provider
+        dsn_runtime
+        zookeeper
+        hashtable
+        gtest
+        sasl2
+        rocksdb
+        lz4
+        zstd
+        snappy)
+
+set(MY_BOOST_LIBS Boost::system Boost::filesystem)
+
+set(MY_BINPLACES
+        "${CMAKE_CURRENT_SOURCE_DIR}/config-test.ini"
+        "${CMAKE_CURRENT_SOURCE_DIR}/run.sh")
+
+dsn_add_test()
diff --git a/src/zookeeper/test/sasl_auth/config-test.ini 
b/src/zookeeper/test/sasl_auth/config-test.ini
new file mode 100644
index 000000000..fbafa4c06
--- /dev/null
+++ b/src/zookeeper/test/sasl_auth/config-test.ini
@@ -0,0 +1,80 @@
+; 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.
+
+[apps..default]
+run = true
+count = 1
+;network.client.RPC_CHANNEL_TCP = dsn::tools::sim_network_provider, 65536
+;network.client.RPC_CHANNEL_UDP = dsn::tools::sim_network_provider, 65536
+;network.server.0.RPC_CHANNEL_TCP = dsn::tools::sim_network_provider, 65536
+
+[apps.client]
+type = test
+arguments =
+run = true
+ports =
+count = 1
+delay_seconds = 1
+pools = THREAD_POOL_DEFAULT
+
+[core]
+;tool = simulator
+tool = nativerun
+
+;toollets = tracer, profiler
+;fault_injector
+pause_on_start = false
+
+logging_start_level = LOG_LEVEL_DEBUG
+logging_factory_name = dsn::tools::simple_logger
+
+[tools.simple_logger]
+fast_flush = true
+short_header = false
+stderr_start_level = LOG_LEVEL_WARNING
+
+[tools.simulator]
+random_seed = 0
+
+[network]
+; how many network threads for network library (used by asio)
+io_service_worker_count = 2
+
+[task..default]
+is_trace = true
+is_profile = true
+allow_inline = false
+rpc_call_channel = RPC_CHANNEL_TCP
+rpc_message_header_format = dsn
+rpc_timeout_milliseconds = 5000
+
+[task.LPC_RPC_TIMEOUT]
+is_trace = false
+is_profile = false
+
+; specification for each thread pool
+[threadpool..default]
+worker_count = 2
+
+[threadpool.THREAD_POOL_DEFAULT]
+partitioned = false
+worker_priority = THREAD_xPRIORITY_NORMAL
+
+[zookeeper]
+hosts_list = localhost:22181
+timeout_ms = 30000
+zoo_log_level = ZOO_LOG_LEVEL_DEBUG
diff --git a/src/zookeeper/test/sasl_auth/main.cpp 
b/src/zookeeper/test/sasl_auth/main.cpp
new file mode 100644
index 000000000..d163b962c
--- /dev/null
+++ b/src/zookeeper/test/sasl_auth/main.cpp
@@ -0,0 +1,67 @@
+// 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.
+
+#include <gtest/gtest.h>
+#include <atomic>
+#include <string>
+#include <vector>
+
+#include "runtime/app_model.h"
+#include "runtime/service_app.h"
+#include "utils/error_code.h"
+#include "utils/synchronize.h"
+
+dsn::utils::notify_event g_on_completed;
+std::atomic_int g_test_result{0};
+
+// Mock a service_app which is required by ZookeeperSessionConnector.
+class test_client : public dsn::service_app
+{
+public:
+    explicit test_client(const dsn::service_app_info *info) : 
dsn::service_app(info) {}
+
+    dsn::error_code start(const std::vector<std::string> &args) override
+    {
+        g_test_result = RUN_ALL_TESTS();
+
+        // All tests have been finished.
+        g_on_completed.notify();
+
+        return ::dsn::ERR_OK;
+    }
+
+    dsn::error_code stop(bool cleanup = false) override { return dsn::ERR_OK; 
} // NOLINT
+};
+
+GTEST_API_ int main(int argc, char **argv)
+{
+    testing::InitGoogleTest(&argc, argv);
+
+    // Register all services.
+    dsn::service_app::register_factory<test_client>("test");
+
+    dsn_run_config("config-test.ini", false);
+
+    // Wait until all tests are finished.
+    g_on_completed.wait();
+
+#ifndef ENABLE_GCOV
+    dsn_exit(g_test_result);
+#endif
+
+    return g_test_result;
+}
diff --git a/src/zookeeper/test/sasl_auth/run.sh 
b/src/zookeeper/test/sasl_auth/run.sh
new file mode 100755
index 000000000..50f64f818
--- /dev/null
+++ b/src/zookeeper/test/sasl_auth/run.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# 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.
+
+if [ -z "${REPORT_DIR}" ]; then
+    REPORT_DIR="."
+fi
+
+output_xml="${REPORT_DIR}/zookeeper_sasl_auth_test.xml"
+GTEST_OUTPUT="xml:${output_xml}" ./zookeeper_sasl_auth_test
+
+if [ $? -ne 0 ]; then
+    echo "run zookeeper_sasl_auth_test failed"
+    echo "---- ls ----"
+    ls -l
+    if [ `find . -name pegasus.log.* | wc -l` -ne 0 ]; then
+        echo "---- tail -n 100 pegasus.log.* ----"
+        tail -n 100 `find . -name pegasus.log.*`
+    fi
+    if [ -f core ]; then
+        echo "---- gdb ./zookeeper_sasl_auth_test core ----"
+        gdb ./zookeeper_sasl_auth_test core -ex "thread apply all bt" -ex "set 
pagination 0" -batch
+    fi
+    exit 1
+fi
diff --git a/src/zookeeper/test/sasl_auth/zookeeper_session_sasl_auth_test.cpp 
b/src/zookeeper/test/sasl_auth/zookeeper_session_sasl_auth_test.cpp
new file mode 100644
index 000000000..f2a7e0d16
--- /dev/null
+++ b/src/zookeeper/test/sasl_auth/zookeeper_session_sasl_auth_test.cpp
@@ -0,0 +1,125 @@
+// 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.
+
+#include <zookeeper/zookeeper.h>
+#include <cstdio>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "utils/flags.h"
+#include "zookeeper/test/zookeeper_session_test_base.h"
+#include "zookeeper_session_test.h"
+
+DSN_DECLARE_string(sasl_mechanisms_type);
+DSN_DECLARE_string(sasl_user_name);
+DSN_DECLARE_string(sasl_password_file);
+DSN_DECLARE_string(sasl_password_encryption_scheme);
+
+namespace dsn::dist {
+
+namespace {
+
+// The correct password plaintext and its encrypted strings.
+const std::string kPlaintextPassword("mypassword");
+const std::string kBase64Password("bXlwYXNzd29yZA==");
+
+// Write the given `password` into the file. The `password` can be either 
unencrypted
+// or encrypted.
+void write_password(const std::string &password)
+{
+    FILE *f = fopen("sasl_auth.password", "w");
+    ASSERT_NE(nullptr, f);
+    ASSERT_EQ(password.size(), fwrite(password.c_str(), 1, password.size(), 
f));
+    ASSERT_EQ(0, fclose(f));
+}
+
+// Configure SASL auth with specified `encryption_scheme`.
+void config_sasl(const char *encryption_scheme)
+{
+    FLAGS_sasl_mechanisms_type = "DIGEST-MD5";
+    FLAGS_sasl_user_name = "myuser";
+    FLAGS_sasl_password_file = "sasl_auth.password";
+    FLAGS_sasl_password_encryption_scheme = encryption_scheme;
+}
+
+} // anonymous namespace
+
+class ZookeeperSessionSASLConnectTest : public ZookeeperSessionConnector
+{
+};
+
+TEST_F(ZookeeperSessionSASLConnectTest, CorrectPlaintextPasswordByEmptyScheme)
+{
+    write_password(kPlaintextPassword);
+    config_sasl("");
+    test_connect(ZOO_CONNECTED_STATE);
+}
+
+TEST_F(ZookeeperSessionSASLConnectTest, 
CorrectPlaintextPasswordBySpecifiedScheme)
+{
+    write_password(kPlaintextPassword);
+    config_sasl("plaintext");
+    test_connect(ZOO_CONNECTED_STATE);
+}
+
+TEST_F(ZookeeperSessionSASLConnectTest, CorrectBase64Password)
+{
+    write_password(kBase64Password);
+    config_sasl("base64");
+    test_connect(ZOO_CONNECTED_STATE);
+}
+
+TEST_F(ZookeeperSessionSASLConnectTest, WrongPlaintextPasswordByEmptyScheme)
+{
+    write_password("password");
+    config_sasl("");
+    test_connect(ZOO_AUTH_FAILED_STATE);
+}
+
+TEST_F(ZookeeperSessionSASLConnectTest, 
WrongPlaintextPasswordBySpecifiedScheme)
+{
+    write_password("password");
+    config_sasl("plaintext");
+    test_connect(ZOO_AUTH_FAILED_STATE);
+}
+
+TEST_F(ZookeeperSessionSASLConnectTest, WrongBase64Password)
+{
+    write_password("cGFzc3dvcmQ="); // base64 encoding for "password".
+    config_sasl("base64");
+    test_connect(ZOO_AUTH_FAILED_STATE);
+}
+
+class ZookeeperSessionSASLAuthTest : public ZookeeperSessionTestBase
+{
+protected:
+    ZookeeperSessionSASLAuthTest();
+};
+
+ZookeeperSessionSASLAuthTest::ZookeeperSessionSASLAuthTest()
+{
+    write_password(kBase64Password);
+    config_sasl("base64");
+    // test_connect() will be called in ZookeeperSessionTestBase::SetUp().
+}
+
+using ZookeeperSessionSASLAuthTestImpl = 
::testing::Types<ZookeeperSessionSASLAuthTest>;
+INSTANTIATE_TYPED_TEST_SUITE_P(ZookeeperSASLAuthTest,
+                               ZookeeperSessionTest,
+                               ZookeeperSessionSASLAuthTestImpl);
+
+} // namespace dsn::dist
diff --git a/src/zookeeper/test/zookeeper_session_no_auth_test.cpp 
b/src/zookeeper/test/zookeeper_session_no_auth_test.cpp
new file mode 100644
index 000000000..6d527ee97
--- /dev/null
+++ b/src/zookeeper/test/zookeeper_session_no_auth_test.cpp
@@ -0,0 +1,35 @@
+// 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.
+
+#include <memory>
+
+#include "gtest/gtest.h"
+#include "zookeeper_session_test.h"
+#include "zookeeper_session_test_base.h"
+
+namespace dsn::dist {
+
+class ZookeeperSessionNoAuthTest : public ZookeeperSessionTestBase
+{
+};
+
+using ZookeeperSessionNoAuthTestImpl = 
::testing::Types<ZookeeperSessionNoAuthTest>;
+INSTANTIATE_TYPED_TEST_SUITE_P(ZookeeperNoAuthTest,
+                               ZookeeperSessionTest,
+                               ZookeeperSessionNoAuthTestImpl);
+
+} // namespace dsn::dist
diff --git a/src/zookeeper/test/zookeeper_session_test.h 
b/src/zookeeper/test/zookeeper_session_test.h
new file mode 100644
index 000000000..7091098de
--- /dev/null
+++ b/src/zookeeper/test/zookeeper_session_test.h
@@ -0,0 +1,178 @@
+// 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.
+
+#include <fmt/core.h>
+#include <gtest/gtest.h>
+#include <zookeeper/zookeeper.h>
+#include <atomic>
+#include <cstddef>
+#include <iterator>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "runtime/service_app.h"
+#include "utils/ports.h"
+
+namespace dsn::dist {
+
+// Test fixure that provides cases for zookeeper_session. The initialization 
processes
+// of the BaseFixtures it derives from may vary, which is also the reason why 
we use
+// a fixture class template and define a type-parameterized test suite.
+//
+// Any test that depends on the ZooKeeper server that starts with SASL auth 
should be put
+// into `zookeeper_sasl_auth_test`, which will start ZooKeeper server with 
configurations
+// that enable SASL auth.
+template <typename BaseFixture>
+class ZookeeperSessionTest : public BaseFixture
+{
+public:
+    ~ZookeeperSessionTest() override = default;
+
+protected:
+    ZookeeperSessionTest() = default;
+
+    void test_node_operations(const std::string &path,
+                              const std::string &sub_path,
+                              const std::string &data)
+    {
+        // The "this" pointer should be kept here since it is required to 
delay name lookup
+        // while accessing members of the base class that depends on the 
template parameters.
+
+        // Delete the node if any in case previous tests failed.
+        this->test_delete_node(path);
+
+        // Ensure currently the node does not exist.
+        this->test_no_node(path);
+
+        // Updating the node will fail since it has not been created.
+        this->test_set_data(path, data, ZNONODE);
+
+        // The node has not been created, thus its sub nodes cannot be created.
+        this->test_create_node(sub_path, data, ZNONODE);
+
+        // Create the node with some data.
+        this->test_create_node(path, data, ZOK);
+        this->test_has_data(path, data);
+
+        // Creating the node repeatedly will fail.
+        this->test_create_node(path, data, ZNODEEXISTS);
+
+        // Updating the node with another data will succeed since it has been 
existing.
+        const auto another_data = fmt::format("another_{}", data);
+        this->test_set_data(path, another_data, ZOK);
+        this->test_has_data(path, another_data);
+
+        // Delete the node.
+        this->test_delete_node(path, ZOK);
+        this->test_no_node(path);
+
+        // Deleting the node repeatedly will fail.
+        this->test_delete_node(path, ZNONODE);
+    }
+
+    void delete_nodes(const std::string &path, const std::vector<std::string> 
&sub_nodes)
+    {
+        for (const auto &sub_node : sub_nodes) {
+            this->test_delete_node(fmt::format("{}/{}", path, sub_node));
+        }
+        this->test_delete_node(path);
+    }
+
+    void test_sub_nodes(const std::string &path,
+                        const std::string &new_sub_node,
+                        std::vector<std::string> &&expected_sub_nodes)
+    {
+        const auto new_sub_path = fmt::format("{}/{}", path, new_sub_node);
+
+        // Create the new sub node.
+        this->test_create_node(new_sub_path, new_sub_node, ZOK);
+        this->test_has_data(new_sub_path, new_sub_node);
+
+        // Check whether all fetched sub nodes are expected.
+        this->test_get_sub_nodes(path, ZOK, std::move(expected_sub_nodes));
+    }
+
+private:
+    DISALLOW_COPY_AND_ASSIGN(ZookeeperSessionTest);
+    DISALLOW_MOVE_AND_ASSIGN(ZookeeperSessionTest);
+};
+
+TYPED_TEST_SUITE_P(ZookeeperSessionTest);
+
+TYPED_TEST_P(ZookeeperSessionTest, OperateNode)
+{
+    // The node with single-level path.
+    static const std::string kPath("/ZookeeperSessionTest");
+
+    // The sub node with two-level path.
+    static const std::string kSubPath(fmt::format("{}/OperateNode", kPath));
+
+    // The data of the node.
+    static const std::string kData("hello");
+
+    // Test the node whose path is single-level.
+    this->test_node_operations(kPath, kSubPath, kData);
+
+    // Create the node again since next we will test its sub node.
+    this->test_create_node(kPath, kData, ZOK);
+    this->test_has_data(kPath, kData);
+
+    // Test the sub node whose path is two-level.
+    this->test_node_operations(kSubPath, fmt::format("{}/ThirdLevelNode", 
kSubPath), "world");
+}
+
+TYPED_TEST_P(ZookeeperSessionTest, GetSubNodes)
+{
+    // The node with single-level path.
+    static const std::string kPath("/ZookeeperSessionTest");
+
+    // The data of the node.
+    static const std::string kData("hello");
+
+    // The sub nodes.
+    static const std::vector<std::string> kSubNodes{
+        "SubNode0",
+        "SubNode1",
+        "SubNode2",
+    };
+
+    // Delete all of the nodes if any in case previous tests failed.
+    this->delete_nodes(kPath, kSubNodes);
+    this->test_no_node(kPath);
+
+    // Create the node.
+    this->test_create_node(kPath, kData, ZOK);
+    this->test_has_data(kPath, kData);
+
+    for (size_t i = 0; i < kSubNodes.size(); ++i) {
+        // Create the sub node one by one.
+        this->test_sub_nodes(kPath,
+                             kSubNodes[i],
+                             std::vector<std::string>(
+                                 kSubNodes.begin(),
+                                 std::next(kSubNodes.begin(), 
static_cast<std::ptrdiff_t>(i + 1))));
+    }
+
+    // Clear all of the nodes.
+    this->delete_nodes(kPath, kSubNodes);
+    this->test_no_node(kPath);
+}
+
+REGISTER_TYPED_TEST_SUITE_P(ZookeeperSessionTest, OperateNode, GetSubNodes);
+
+} // namespace dsn::dist
diff --git a/src/zookeeper/test/zookeeper_session_test_base.cpp 
b/src/zookeeper/test/zookeeper_session_test_base.cpp
new file mode 100644
index 000000000..1d331691e
--- /dev/null
+++ b/src/zookeeper/test/zookeeper_session_test_base.cpp
@@ -0,0 +1,281 @@
+// 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.
+
+#include <zookeeper/zookeeper.h>
+#include <zookeeper/zookeeper.jute.h>
+#include <algorithm>
+#include <atomic>
+#include <iostream>
+#include <iterator>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "runtime/service_app.h"
+#include "utils/blob.h"
+#include "utils/defer.h"
+#include "utils/flags.h"
+#include "utils/synchronize.h"
+#include "zookeeper_session_test_base.h"
+
+DSN_DECLARE_int32(timeout_ms);
+
+namespace dsn::dist {
+
+void ZookeeperSessionConnector::TearDown() { _session->detach(this); }
+
+void ZookeeperSessionConnector::test_connect(int expected_zoo_state)
+{
+    _session = 
std::make_unique<zookeeper_session>(service_app::current_service_app_info());
+
+    std::atomic_int actual_zoo_state{0};
+    std::atomic_bool first_call{true};
+    utils::notify_event on_attached;
+
+    const int zoo_state = _session->attach(
+        this,
+        [expected_zoo_state, &actual_zoo_state, &first_call, &on_attached](int 
zoo_state) mutable {
+            std::cout << "[on_zoo_session_event] zoo_state = " << zoo_state << 
std::endl;
+
+            actual_zoo_state = zoo_state;
+
+            if (first_call && zoo_state == expected_zoo_state) {
+                first_call = false;
+                on_attached.notify();
+                return;
+            }
+        });
+
+    ASSERT_TRUE(_session);
+
+    if (zoo_state != expected_zoo_state) {
+        on_attached.wait_for(FLAGS_timeout_ms);
+
+        // Callback for attach() may not be called while the state is changed.
+        if (actual_zoo_state != 0) {
+            ASSERT_EQ(expected_zoo_state, actual_zoo_state);
+        }
+
+        ASSERT_EQ(expected_zoo_state, _session->session_state());
+    }
+}
+
+void ZookeeperSessionTestBase::SetUp() { test_connect(ZOO_CONNECTED_STATE); }
+
+void ZookeeperSessionTestBase::TearDown() { 
ZookeeperSessionConnector::TearDown(); }
+
+void ZookeeperSessionTestBase::operate_node(
+    const std::string &path,
+    zookeeper_session::ZOO_OPERATION op_type,
+    const std::string &data,
+    std::function<void(zookeeper_session::zoo_opcontext *)> &&callback,
+    int &zerr)
+{
+    utils::notify_event on_completed;
+
+    auto *op = zookeeper_session::create_context();
+    op->_optype = op_type;
+    op->_input._path = path;
+    op->_input._value = blob::create_from_bytes(std::string(data));
+    op->_callback_function =
+        [&zerr, &callback, &on_completed](zookeeper_session::zoo_opcontext 
*op) mutable {
+            zerr = op->_output.error;
+
+            if (callback) {
+                callback(op);
+            }
+
+            on_completed.notify();
+        };
+
+    _session->visit(op);
+    on_completed.wait();
+}
+
+void ZookeeperSessionTestBase::operate_node(const std::string &path,
+                                            zookeeper_session::ZOO_OPERATION 
op_type,
+                                            int &zerr)
+{
+    operate_node(path, op_type, "", nullptr, zerr);
+}
+
+void ZookeeperSessionTestBase::test_operate_node(
+    const std::string &path,
+    zookeeper_session::ZOO_OPERATION op_type,
+    const std::string &data,
+    std::function<void(zookeeper_session::zoo_opcontext *)> &&callback,
+    int expected_zerr)
+{
+    int actual_zerr{0};
+    operate_node(path, op_type, data, std::move(callback), actual_zerr);
+
+    ASSERT_EQ(expected_zerr, actual_zerr)
+        << "expected_zerr = \"" << zerror(expected_zerr) << "\", actual_zerr = 
\""
+        << zerror(actual_zerr) << "\", path = " << path << ", op_type = " << 
op_type
+        << ", data = \"" << data << "\"";
+}
+
+void ZookeeperSessionTestBase::test_operate_node(const std::string &path,
+                                                 
zookeeper_session::ZOO_OPERATION op_type,
+                                                 const std::string &data,
+                                                 int expected_zerr)
+{
+    test_operate_node(path, op_type, data, nullptr, expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_operate_node(
+    const std::string &path,
+    zookeeper_session::ZOO_OPERATION op_type,
+    std::function<void(zookeeper_session::zoo_opcontext *)> &&callback,
+    int expected_zerr)
+{
+    test_operate_node(path, op_type, "", std::move(callback), expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_operate_node(const std::string &path,
+                                                 
zookeeper_session::ZOO_OPERATION op_type,
+                                                 int expected_zerr)
+{
+    test_operate_node(path, op_type, "", expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_create_node(const std::string &path,
+                                                const std::string &data,
+                                                int expected_zerr)
+{
+    test_operate_node(path, zookeeper_session::ZOO_CREATE, data, 
expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_delete_node(const std::string &path, int 
expected_zerr)
+{
+    test_operate_node(path, zookeeper_session::ZOO_DELETE, expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_delete_node(const std::string &path)
+{
+    using ::testing::AnyOf;
+    using ::testing::Eq;
+
+    int actual_zerr{0};
+    operate_node(path, zookeeper_session::ZOO_DELETE, actual_zerr);
+    ASSERT_THAT(actual_zerr, AnyOf(Eq(ZOK), Eq(ZNONODE)))
+        << "expected_zerr = \"" << zerror(ZOK) << "\" or \"" << zerror(ZNONODE)
+        << "\", actual_zerr = \"" << zerror(actual_zerr) << "\", path = " << 
path
+        << ", op_type = " << zookeeper_session::ZOO_DELETE;
+}
+
+void ZookeeperSessionTestBase::test_set_data(const std::string &path,
+                                             const std::string &data,
+                                             int expected_zerr)
+{
+    test_operate_node(path, zookeeper_session::ZOO_SET, data, expected_zerr);
+}
+
+void ZookeeperSessionTestBase::test_get_data(const std::string &path,
+                                             int expected_zerr,
+                                             const std::string &expected_data)
+{
+    std::string actual_data;
+    test_operate_node(
+        path,
+        zookeeper_session::ZOO_GET,
+        [&actual_data](zookeeper_session::zoo_opcontext *op) mutable {
+            actual_data.assign(op->_output.get_op.value, 
op->_output.get_op.value_length);
+        },
+        expected_zerr);
+
+    if (expected_zerr != ZOK) {
+        return;
+    }
+
+    ASSERT_EQ(expected_data, actual_data);
+}
+
+void ZookeeperSessionTestBase::test_get_data(const std::string &path, int 
expected_zerr)
+{
+    test_get_data(path, expected_zerr, "");
+}
+
+void ZookeeperSessionTestBase::test_exists_node(const std::string &path, int 
expected_zerr)
+{
+    test_operate_node(path, zookeeper_session::ZOO_EXISTS, expected_zerr);
+}
+
+void ZookeeperSessionTestBase::get_sub_nodes(const std::string &path,
+                                             int &zerr,
+                                             std::vector<std::string> 
&sub_nodes)
+{
+    utils::notify_event on_completed;
+
+    auto *op = zookeeper_session::create_context();
+    op->_optype = zookeeper_session::ZOO_GETCHILDREN;
+    op->_input._path = path;
+    op->_callback_function =
+        [&zerr, &sub_nodes, &on_completed](zookeeper_session::zoo_opcontext 
*op) mutable {
+            zerr = op->_output.error;
+
+            const auto notifier = defer([&on_completed]() { 
on_completed.notify(); });
+
+            if (zerr != ZOK) {
+                return;
+            }
+
+            const String_vector *strings = op->_output.getchildren_op.strings;
+
+            sub_nodes.clear();
+            sub_nodes.reserve(strings->count);
+            std::transform(strings->data,
+                           strings->data + strings->count,
+                           std::back_inserter(sub_nodes),
+                           [](const char *str) { return std::string(str); });
+        };
+
+    _session->visit(op);
+    on_completed.wait();
+}
+
+void ZookeeperSessionTestBase::test_get_sub_nodes(const std::string &path,
+                                                  int expected_zerr,
+                                                  std::vector<std::string> 
&&expected_sub_nodes)
+{
+    int actual_zerr{0};
+    std::vector<std::string> actual_sub_nodes;
+    get_sub_nodes(path, actual_zerr, actual_sub_nodes);
+
+    ASSERT_EQ(expected_zerr, actual_zerr)
+        << "expected_zerr = \"" << zerror(expected_zerr) << "\", actual_zerr = 
\""
+        << zerror(actual_zerr) << "\", path = " << path;
+
+    std::sort(expected_sub_nodes.begin(), expected_sub_nodes.end());
+    std::sort(actual_sub_nodes.begin(), actual_sub_nodes.end());
+    ASSERT_EQ(expected_sub_nodes, actual_sub_nodes);
+}
+
+void ZookeeperSessionTestBase::test_no_node(const std::string &path)
+{
+    test_exists_node(path, ZNONODE);
+    test_get_data(path, ZNONODE);
+    test_get_sub_nodes(path, ZNONODE, std::vector<std::string>());
+}
+
+void ZookeeperSessionTestBase::test_has_data(const std::string &path, const 
std::string &data)
+{
+    test_exists_node(path, ZOK);
+    test_get_data(path, ZOK, data);
+}
+
+} // namespace dsn::dist
diff --git a/src/zookeeper/test/zookeeper_session_test_base.h 
b/src/zookeeper/test/zookeeper_session_test_base.h
new file mode 100644
index 000000000..9f63fde11
--- /dev/null
+++ b/src/zookeeper/test/zookeeper_session_test_base.h
@@ -0,0 +1,158 @@
+// 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.
+
+#include <gtest/gtest.h>
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "utils/ports.h"
+#include "zookeeper/zookeeper_session.h"
+
+namespace dsn::dist {
+
+// As the base class for ZookeeperSessionTest, provides APIs used to test 
connecting to
+// the ZooKeeper.
+class ZookeeperSessionConnector : public testing::Test
+{
+public:
+    ~ZookeeperSessionConnector() override = default;
+
+protected:
+    ZookeeperSessionConnector() = default;
+
+    void TearDown() override;
+
+    // Connect to ZooKeeper and test. After connected, `expected_zoo_state` is 
the expected
+    // state.
+    void test_connect(int expected_zoo_state);
+
+    std::unique_ptr<zookeeper_session> _session;
+
+    DISALLOW_COPY_AND_ASSIGN(ZookeeperSessionConnector);
+    DISALLOW_MOVE_AND_ASSIGN(ZookeeperSessionConnector);
+};
+
+// As the base class for ZookeeperSessionTest, provides APIs used to test 
performing
+// operations on the ZooKeeper.
+class ZookeeperSessionTestBase : public ZookeeperSessionConnector
+{
+public:
+    ~ZookeeperSessionTestBase() override = default;
+
+protected:
+    ZookeeperSessionTestBase() = default;
+
+    void SetUp() override;
+
+    void TearDown() override;
+
+    // Test creating the node `path` with `data`: the expected zoo error is 
`expected_zerr`.
+    void test_create_node(const std::string &path, const std::string &data, 
int expected_zerr);
+
+    // Test deleting the node `path`: the expected zoo error is 
`expected_zerr`.
+    void test_delete_node(const std::string &path, int expected_zerr);
+
+    // Delete the node `path` whether it exists or not.
+    void test_delete_node(const std::string &path);
+
+    // Test updating the node `path` with `data`: the expected zoo error is 
`expected_zerr`.
+    void test_set_data(const std::string &path, const std::string &data, int 
expected_zerr);
+
+    // Test getting data from the node `path`: the expected zoo error and the 
data it holds
+    // are `expected_zerr` and `expected_data`.
+    void
+    test_get_data(const std::string &path, int expected_zerr, const 
std::string &expected_data);
+
+    // Test getting data from the node `path`: the expected zoo error is 
`expected_zerr`
+    // while the data it holds is ignored.
+    void test_get_data(const std::string &path, int expected_zerr);
+
+    // Test whether the node `path` exists.
+    void test_exists_node(const std::string &path, int expected_zerr);
+
+    // Test getting the sub nodes from the node `path`: the expected zoo error 
and the
+    // obtained sub nodes are `expected_zerr` and `expected_sub_nodes`.
+    void test_get_sub_nodes(const std::string &path,
+                            int expected_zerr,
+                            std::vector<std::string> &&expected_sub_nodes);
+
+    // Assert that the node `path` does not exist.
+    void test_no_node(const std::string &path);
+
+    // Assert that the node `path` holds `data`.
+    void test_has_data(const std::string &path, const std::string &data);
+
+private:
+    // Perform an operation on a ZooKeeper node synchronously:
+    // - path: the path of the ZooKeeper node.
+    // - op_type: the operation type.
+    // - data: used for ZOO_CREATE and ZOO_SET.
+    // - callback: called after the operation was finished (nothing would be 
called if
+    // nullptr).
+    // - zerr: zoo error for the operation.
+    void operate_node(const std::string &path,
+                      zookeeper_session::ZOO_OPERATION op_type,
+                      const std::string &data,
+                      std::function<void(zookeeper_session::zoo_opcontext *)> 
&&callback,
+                      int &zerr);
+
+    // The same as the above, except that `data` is empty and `callback` is 
nullptr.
+    void operate_node(const std::string &path, 
zookeeper_session::ZOO_OPERATION op_type, int &zerr);
+
+    // Test an operation on a ZooKeeper node:
+    // - path: the path of the ZooKeeper node.
+    // - op_type: the operation type.
+    // - data: used for ZOO_CREATE and ZOO_SET.
+    // - callback: called after the operation was finished (nothing would be 
called if
+    // nullptr).
+    // - expected_zerr: expected zoo error for the operation.
+    void test_operate_node(const std::string &path,
+                           zookeeper_session::ZOO_OPERATION op_type,
+                           const std::string &data,
+                           std::function<void(zookeeper_session::zoo_opcontext 
*)> &&callback,
+                           int expected_zerr);
+
+    // The same as the above, except that `callback` is nullptr.
+    void test_operate_node(const std::string &path,
+                           zookeeper_session::ZOO_OPERATION op_type,
+                           const std::string &data,
+                           int expected_zerr);
+
+    // The same as the above, except that `data` is empty.
+    void test_operate_node(const std::string &path,
+                           zookeeper_session::ZOO_OPERATION op_type,
+                           std::function<void(zookeeper_session::zoo_opcontext 
*)> &&callback,
+                           int expected_zerr);
+
+    // The same as the above, except that `data` is empty and `callback` is 
nullptr.
+    void test_operate_node(const std::string &path,
+                           zookeeper_session::ZOO_OPERATION op_type,
+                           int expected_zerr);
+
+    // Get the sub nodes of a ZooKeeper node:
+    // - path: the path of the ZooKeeper node.
+    // - zerr: zoo error for the operation.
+    // - sub_nodes: the obtained sub nodes.
+    void get_sub_nodes(const std::string &path, int &zerr, 
std::vector<std::string> &sub_nodes);
+
+    DISALLOW_COPY_AND_ASSIGN(ZookeeperSessionTestBase);
+    DISALLOW_MOVE_AND_ASSIGN(ZookeeperSessionTestBase);
+};
+
+} // namespace dsn::dist
diff --git a/src/zookeeper/zookeeper_session.cpp 
b/src/zookeeper/zookeeper_session.cpp
index dfff72b47..3e264f4f6 100644
--- a/src/zookeeper/zookeeper_session.cpp
+++ b/src/zookeeper/zookeeper_session.cpp
@@ -24,22 +24,37 @@
  * THE SOFTWARE.
  */
 
+#include <openssl/bio.h>
+#include <openssl/evp.h>
+#include <openssl/types.h>
 #include <sasl/sasl.h>
-#include <stdlib.h>
 #include <zookeeper/zookeeper.h>
 #include <algorithm>
+#include <cerrno>
+#include <cstdlib>
 #include <utility>
 
-#include "runtime/app_model.h"
 #include "rpc/rpc_address.h"
+#include "runtime/app_model.h"
+#include "utils/defer.h"
+#include "utils/enum_helper.h"
 #include "utils/filesystem.h"
 #include "utils/flags.h"
 #include "utils/fmt_logging.h"
+#include "utils/safe_strerror_posix.h"
 #include "utils/strings.h"
 #include "zookeeper/proto.h"
 #include "zookeeper/zookeeper.jute.h"
 #include "zookeeper_session.h"
 
+#define INVALID_ZOO_LOG_LEVEL static_cast<ZooLogLevel>(0)
+ENUM_BEGIN(ZooLogLevel, INVALID_ZOO_LOG_LEVEL)
+ENUM_REG(ZOO_LOG_LEVEL_ERROR)
+ENUM_REG(ZOO_LOG_LEVEL_WARN)
+ENUM_REG(ZOO_LOG_LEVEL_INFO)
+ENUM_REG(ZOO_LOG_LEVEL_DEBUG)
+ENUM_END(ZooLogLevel)
+
 DSN_DECLARE_bool(enable_zookeeper_kerberos);
 DSN_DEFINE_string(security,
                   zookeeper_kerberos_service_name,
@@ -55,7 +70,8 @@ DSN_DEFINE_int32(zookeeper,
                  timeout_ms,
                  30000,
                  "The timeout of accessing ZooKeeper, in milliseconds");
-DSN_DEFINE_string(zookeeper, hosts_list, "", "Zookeeper hosts list");
+DSN_DEFINE_string(zookeeper, zoo_log_level, "ZOO_LOG_LEVEL_INFO", "ZooKeeper 
log level");
+DSN_DEFINE_string(zookeeper, hosts_list, "", "ZooKeeper hosts list");
 DSN_DEFINE_string(zookeeper, sasl_service_name, "zookeeper", "");
 DSN_DEFINE_string(zookeeper,
                   sasl_service_fqdn,
@@ -71,6 +87,11 @@ DSN_DEFINE_string(zookeeper,
                   sasl_password_file,
                   "",
                   "File containing the password (recommended for 
SASL/DIGEST-MD5)");
+DSN_DEFINE_string(zookeeper,
+                  sasl_password_encryption_scheme,
+                  "",
+                  "If non-empty, specify the scheme in which the password is 
encrypted; "
+                  "otherwise, the password is unencrypted plaintext");
 DSN_DEFINE_group_validator(enable_zookeeper_kerberos, [](std::string &message) 
-> bool {
     if (FLAGS_enable_zookeeper_kerberos &&
         !dsn::utils::equals(FLAGS_sasl_mechanisms_type, "GSSAPI")) {
@@ -191,69 +212,135 @@ const char *zookeeper_session::string_zoo_state(int 
zoo_state)
     return "invalid_state";
 }
 
-zookeeper_session::~zookeeper_session() {}
+zookeeper_session::zookeeper_session(service_app_info info)
+    : _info(std::move(info)), _handle(nullptr)
+{
+}
+
+namespace {
+
+int decode_base64(
+    const char *content, size_t content_len, char *buf, size_t buf_len, size_t 
*passwd_len)
+{
+    if (content_len == 0) {
+        return SASL_BADPARAM;
+    }
+
+    BIO *bio = BIO_new_mem_buf(content, static_cast<int>(content_len));
+    if (bio == nullptr) {
+        return SASL_FAIL;
+    }
+
+    BIO *b64 = BIO_new(BIO_f_base64());
+    if (b64 == nullptr) {
+        BIO_free(bio);
+        return SASL_FAIL;
+    }
+
+    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+
+    bio = BIO_push(b64, bio);
+    const auto cleanup = dsn::defer([bio]() { BIO_free_all(bio); });
+
+    const int read_len = BIO_read(bio, buf, static_cast<int>(buf_len));
+    if (read_len <= 0) {
+        return SASL_FAIL;
+    }
+
+    *passwd_len = read_len;
+
+    return SASL_OK;
+}
+
+int zookeeper_password_decoder(const char *content,
+                               size_t content_len,
+                               void *context,
+                               char *buf,
+                               size_t buf_len,
+                               size_t *passwd_len)
+{
+    if (dsn::utils::iequals(FLAGS_sasl_password_encryption_scheme, "base64")) {
+        return decode_base64(content, content_len, buf, buf_len, passwd_len);
+    }
+
+    return SASL_BADPARAM;
+}
 
-zookeeper_session::zookeeper_session(const service_app_info &node) : 
_handle(nullptr)
+bool is_password_file_plaintext()
 {
-    _srv_node = node;
+    return dsn::utils::is_empty(FLAGS_sasl_password_encryption_scheme) ||
+           dsn::utils::iequals(FLAGS_sasl_password_encryption_scheme, 
"plaintext");
+}
+
+zhandle_t *create_zookeeper_handle(watcher_fn watcher, void *context)
+{
+    const auto zoo_log_level = enum_from_string(FLAGS_zoo_log_level, 
INVALID_ZOO_LOG_LEVEL);
+    CHECK(zoo_log_level != INVALID_ZOO_LOG_LEVEL, "Invalid zoo log level: {}", 
FLAGS_zoo_log_level);
+    zoo_set_debug_level(zoo_log_level);
+
+    // SASL auth is enabled iff FLAGS_sasl_mechanisms_type is non-empty.
+    if (dsn::utils::is_empty(FLAGS_sasl_mechanisms_type)) {
+        return zookeeper_init(FLAGS_hosts_list, watcher, FLAGS_timeout_ms, 
nullptr, context, 0);
+    }
+
+    const int err = sasl_client_init(nullptr);
+    CHECK_EQ_MSG(err,
+                 SASL_OK,
+                 "Unable to initialize SASL library {}",
+                 sasl_errstring(err, nullptr, nullptr));
+
+    CHECK(!dsn::utils::is_empty(FLAGS_sasl_password_file),
+          "sasl_password_file must be specified when SASL auth is enabled");
+    CHECK(utils::filesystem::file_exists(FLAGS_sasl_password_file),
+          "sasl_password_file {} not exist!",
+          FLAGS_sasl_password_file);
+
+    const char *host = "";
+    if (!dsn::utils::is_empty(FLAGS_sasl_service_fqdn)) {
+        CHECK(dsn::rpc_address::from_host_port(FLAGS_sasl_service_fqdn),
+              "sasl_service_fqdn '{}' is invalid",
+              FLAGS_sasl_service_fqdn);
+        host = FLAGS_sasl_service_fqdn;
+    }
+
+    // DIGEST-MD5 requires '--server-fqdn zk-sasl-md5' for historical reasons 
on ZooKeeper
+    // C client.
+    if (dsn::utils::equals(FLAGS_sasl_mechanisms_type, "DIGEST-MD5")) {
+        host = "zk-sasl-md5";
+    }
+
+    // Only encrypted passwords need their decoders.
+    zoo_sasl_password_t passwd = {FLAGS_sasl_password_file,
+                                  nullptr,
+                                  is_password_file_plaintext() ? nullptr
+                                                               : 
zookeeper_password_decoder};
+
+    zoo_sasl_params_t sasl_params = {};
+    sasl_params.service = FLAGS_sasl_service_name;
+    sasl_params.host = host;
+    sasl_params.mechlist = FLAGS_sasl_mechanisms_type;
+    sasl_params.callbacks =
+        zoo_sasl_make_password_callbacks(FLAGS_sasl_user_name, 
FLAGS_sasl_realm, &passwd);
+
+    return zookeeper_init_sasl(
+        FLAGS_hosts_list, watcher, FLAGS_timeout_ms, nullptr, context, 0, 
nullptr, &sasl_params);
 }
 
+} // anonymous namespace
+
 int zookeeper_session::attach(void *callback_owner, const state_callback &cb)
 {
     utils::auto_write_lock l(_watcher_lock);
-    do {
-        if (nullptr != _handle) {
-            break;
-        }
-        if (utils::is_empty(FLAGS_sasl_mechanisms_type)) {
-            _handle = zookeeper_init(
-                FLAGS_hosts_list, global_watcher, FLAGS_timeout_ms, nullptr, 
this, 0);
-            break;
-        }
-        int err = sasl_client_init(nullptr);
-        CHECK_EQ_MSG(err,
-                     SASL_OK,
-                     "Unable to initialize SASL library {}",
-                     sasl_errstring(err, nullptr, nullptr));
-
-        if (!utils::is_empty(FLAGS_sasl_password_file)) {
-            CHECK(utils::filesystem::file_exists(FLAGS_sasl_password_file),
-                  "sasl_password_file {} not exist!",
-                  FLAGS_sasl_password_file);
-        }
 
-        auto param_host = "";
-        if (!utils::is_empty(FLAGS_sasl_service_fqdn)) {
-            CHECK(dsn::rpc_address::from_host_port(FLAGS_sasl_service_fqdn),
-                  "sasl_service_fqdn '{}' is invalid",
-                  FLAGS_sasl_service_fqdn);
-            param_host = FLAGS_sasl_service_fqdn;
-        }
-        // DIGEST-MD5 requires '--server-fqdn zk-sasl-md5' for historical 
reasons on zk c client
-        if (dsn::utils::equals(FLAGS_sasl_mechanisms_type, "DIGEST-MD5")) {
-            param_host = "zk-sasl-md5";
-        }
+    if (_handle == nullptr) {
+        // zookeeper_init* functions will set `errno` while initialization 
failed due to some
+        // error.
+        errno = 0;
+        _handle = create_zookeeper_handle(global_watcher, this);
+        CHECK_NOTNULL(_handle, "zookeeper session init failed: {}", 
utils::safe_strerror(errno));
+    }
 
-        zoo_sasl_params_t sasl_params = {0};
-        sasl_params.service = FLAGS_sasl_service_name;
-        sasl_params.mechlist = FLAGS_sasl_mechanisms_type;
-        sasl_params.host = param_host;
-        sasl_params.callbacks = zoo_sasl_make_basic_callbacks(
-            FLAGS_sasl_user_name, FLAGS_sasl_realm, FLAGS_sasl_password_file);
-
-        _handle = zookeeper_init_sasl(FLAGS_hosts_list,
-                                      global_watcher,
-                                      FLAGS_timeout_ms,
-                                      nullptr,
-                                      this,
-                                      0,
-                                      nullptr,
-                                      &sasl_params);
-    } while (false);
-
-    CHECK_NOTNULL(_handle, "zookeeper session init failed");
-
-    _watchers.push_back(watcher_object());
+    _watchers.emplace_back();
     _watchers.back().watcher_path = "";
     _watchers.back().callback_owner = callback_owner;
     _watchers.back().watcher_callback = cb;
@@ -379,7 +466,7 @@ void zookeeper_session::init_non_dsn_thread()
 {
     static __thread int dsn_context_init = 0;
     if (dsn_context_init == 0) {
-        dsn_mimic_app(_srv_node.role_name.c_str(), _srv_node.index);
+        dsn_mimic_app(_info.role_name.c_str(), _info.index);
         dsn_context_init = 1;
     }
 }
diff --git a/src/zookeeper/zookeeper_session.h 
b/src/zookeeper/zookeeper_session.h
index e46f6d6a2..5fcaa9f9a 100644
--- a/src/zookeeper/zookeeper_session.h
+++ b/src/zookeeper/zookeeper_session.h
@@ -24,9 +24,9 @@
  * THE SOFTWARE.
  */
 
-#include <stdint.h>
-#include <string.h>
 #include <zookeeper/zookeeper.h>
+#include <cstdint>
+#include <cstring>
 #include <functional>
 #include <list>
 #include <memory>
@@ -37,12 +37,12 @@
 #include "utils/autoref_ptr.h"
 #include "utils/blob.h"
 #include "utils/fmt_utils.h"
+#include "utils/ports.h"
 #include "utils/synchronize.h"
 
 struct String_vector;
 
-namespace dsn {
-namespace dist {
+namespace dsn::dist {
 
 // A C++ wrapper of zookeeper c async APIs.
 class zookeeper_session
@@ -164,10 +164,10 @@ public:
     static const char *string_zoo_event(int zoo_event);
     static const char *string_zoo_state(int zoo_state);
 
-public:
-    typedef std::function<void(int)> state_callback;
-    zookeeper_session(const service_app_info &info);
-    ~zookeeper_session();
+    using state_callback = std::function<void(int)>;
+
+    explicit zookeeper_session(service_app_info info);
+    ~zookeeper_session() = default;
     int attach(void *callback_owner, const state_callback &cb);
     void detach(void *callback_owner);
 
@@ -177,6 +177,7 @@ public:
 
 private:
     utils::rw_lock_nr _watcher_lock;
+
     struct watcher_object
     {
         std::string watcher_path;
@@ -184,7 +185,8 @@ private:
         state_callback watcher_callback;
     };
     std::list<watcher_object> _watchers;
-    service_app_info _srv_node;
+
+    service_app_info _info;
     zhandle_t *_handle;
 
     void dispatch_event(int type, int zstate, const char *path);
@@ -196,8 +198,11 @@ private:
     static void
     global_strings_completion(int rc, const struct String_vector *strings, 
const void *data);
     static void global_void_completion(int rc, const void *data);
+
+    DISALLOW_COPY_AND_ASSIGN(zookeeper_session);
+    DISALLOW_MOVE_AND_ASSIGN(zookeeper_session);
 };
-} // namespace dist
-} // namespace dsn
+
+} // namespace dsn::dist
 
 USER_DEFINED_STRUCTURE_FORMATTER(::dsn::dist::zookeeper_session);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to