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

jin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git


The following commit(s) were added to refs/heads/master by this push:
     new 5adf14cb2 fix(pd): resolve hostname entries in IpAuthHandler allowlist 
(#2962)
5adf14cb2 is described below

commit 5adf14cb24573254113255ad1dab25fd33bdbab2
Author: Himanshu Verma <[email protected]>
AuthorDate: Tue Mar 17 11:46:34 2026 +0530

    fix(pd): resolve hostname entries in IpAuthHandler allowlist (#2962)
    
    - Resolve allowlist hostnames to IPs using InetAddress.getAllByName
    - Add refresh() to update resolved IPs when Raft peer list changes
    - Wire refresh into RaftEngine.changePeerList()
    - Add IpAuthHandlerTest covering hostname resolution, refresh behavior, and 
failure cases
---
 .../org/apache/hugegraph/pd/raft/RaftEngine.java   |  56 ++++++++-
 .../hugegraph/pd/raft/auth/IpAuthHandler.java      |  46 ++++++-
 .../org/apache/hugegraph/pd/rest/MemberAPI.java    |   3 +
 .../org/apache/hugegraph/pd/service/PDService.java |  13 ++
 .../pd/service/interceptor/Authentication.java     |   2 +
 .../hugegraph/pd/util/grpc/GRpcServerConfig.java   |   2 +
 .../apache/hugegraph/pd/core/PDCoreSuiteTest.java  |   4 +
 .../hugegraph/pd/raft/IpAuthHandlerTest.java       | 133 +++++++++++++++++++++
 .../pd/raft/RaftEngineIpAuthIntegrationTest.java   | 124 +++++++++++++++++++
 9 files changed, 374 insertions(+), 9 deletions(-)

diff --git 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java
 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java
index e70ac9234..b73364ae6 100644
--- 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java
+++ 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java
@@ -23,9 +23,11 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 
@@ -40,7 +42,6 @@ import com.alipay.remoting.config.BoltServerOption;
 import com.alipay.sofa.jraft.JRaftUtils;
 import com.alipay.sofa.jraft.Node;
 import com.alipay.sofa.jraft.RaftGroupService;
-import com.alipay.sofa.jraft.ReplicatorGroup;
 import com.alipay.sofa.jraft.Status;
 import com.alipay.sofa.jraft.conf.Configuration;
 import com.alipay.sofa.jraft.core.Replicator;
@@ -54,7 +55,6 @@ import com.alipay.sofa.jraft.rpc.RaftRpcServerFactory;
 import com.alipay.sofa.jraft.rpc.RpcServer;
 import com.alipay.sofa.jraft.rpc.impl.BoltRpcServer;
 import com.alipay.sofa.jraft.util.Endpoint;
-import com.alipay.sofa.jraft.util.ThreadId;
 import com.alipay.sofa.jraft.util.internal.ThrowUtil;
 
 import io.netty.channel.ChannelHandler;
@@ -313,23 +313,66 @@ public class RaftEngine {
 
     public Status changePeerList(String peerList) {
         AtomicReference<Status> result = new AtomicReference<>();
+        Configuration newPeers = new Configuration();
         try {
             String[] peers = peerList.split(",", -1);
             if ((peers.length & 1) != 1) {
                 throw new PDException(-1, "the number of peer list must be 
odd.");
             }
-            Configuration newPeers = new Configuration();
             newPeers.parse(peerList);
             CountDownLatch latch = new CountDownLatch(1);
             this.raftNode.changePeers(newPeers, status -> {
-                result.set(status);
+                // Use compareAndSet so a late callback does not overwrite a 
timeout status
+                result.compareAndSet(null, status);
+                // Refresh inside callback so it fires even if caller already 
timed out
+                // Note: changePeerList() uses Configuration.parse() which 
only supports
+                // plain comma-separated peer addresses with no learner syntax.
+                // getLearners() will always be empty here. Learner support is 
handled
+                // in PDService.updatePdRaft() which uses 
PeerUtil.parseConfig()
+                // and supports the /learner suffix.
+                if (status != null && status.isOk()) {
+                    IpAuthHandler handler = IpAuthHandler.getInstance();
+                    if (handler != null) {
+                        Set<String> newIps = newPeers.getPeers()
+                                                     .stream()
+                                                     .map(PeerId::getIp)
+                                                     
.collect(Collectors.toSet());
+                        handler.refresh(newIps);
+                        log.info("IpAuthHandler refreshed after peer list 
change to: {}",
+                                 peerList);
+                    } else {
+                        log.warn("IpAuthHandler not initialized, skipping 
refresh for "
+                                 + "peer list: {}", peerList);
+                    }
+                }
                 latch.countDown();
             });
-            latch.await();
+            // Use 3x configured RPC timeout — bare await() would block 
forever if
+            // the callback is never invoked (e.g. node not started / RPC 
failure)
+            boolean completed = latch.await(3L * config.getRpcTimeout(),
+                                            TimeUnit.MILLISECONDS);
+            if (!completed && result.get() == null) {
+                Status timeoutStatus = new Status(RaftError.EINTERNAL,
+                                                  "changePeerList timed out 
after %d ms",
+                                                  3L * config.getRpcTimeout());
+                if (!result.compareAndSet(null, timeoutStatus)) {
+                    // Callback arrived just before us — keep its result
+                    timeoutStatus = null;
+                }
+                if (timeoutStatus != null) {
+                    log.error("changePeerList to {} timed out after {} ms",
+                              peerList, 3L * config.getRpcTimeout());
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            result.set(new Status(RaftError.EINTERNAL, "changePeerList 
interrupted"));
+            log.error("changePeerList to {} was interrupted", peerList, e);
         } catch (Exception e) {
             log.error("failed to changePeerList to {},{}", peerList, e);
             result.set(new Status(-1, e.getMessage()));
         }
+
         return result.get();
     }
 
@@ -366,7 +409,8 @@ public class RaftEngine {
         if (p1 == null || p2 == null) {
             return false;
         }
-        return Objects.equals(p1.getIp(), p2.getIp()) && 
Objects.equals(p1.getPort(), p2.getPort());
+        return Objects.equals(p1.getIp(), p2.getIp()) &&
+               Objects.equals(p1.getPort(), p2.getPort());
     }
 
     private Replicator.State getReplicatorState(PeerId peerId) {
diff --git 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/auth/IpAuthHandler.java
 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/auth/IpAuthHandler.java
index 2ac384541..bdccb6dd7 100644
--- 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/auth/IpAuthHandler.java
+++ 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/auth/IpAuthHandler.java
@@ -17,8 +17,11 @@
 
 package org.apache.hugegraph.pd.raft.auth;
 
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 
 import io.netty.channel.ChannelDuplexHandler;
@@ -30,11 +33,11 @@ import lombok.extern.slf4j.Slf4j;
 @ChannelHandler.Sharable
 public class IpAuthHandler extends ChannelDuplexHandler {
 
-    private final Set<String> allowedIps;
+    private volatile Set<String> resolvedIps;
     private static volatile IpAuthHandler instance;
 
     private IpAuthHandler(Set<String> allowedIps) {
-        this.allowedIps = Collections.unmodifiableSet(allowedIps);
+        this.resolvedIps = resolveAll(allowedIps);
     }
 
     public static IpAuthHandler getInstance(Set<String> allowedIps) {
@@ -48,6 +51,25 @@ public class IpAuthHandler extends ChannelDuplexHandler {
         return instance;
     }
 
+    /**
+     * Returns the existing singleton instance, or null if not yet initialized.
+     * Should only be called after getInstance(Set) has been called during 
startup.
+     */
+    public static IpAuthHandler getInstance() {
+        return instance;
+    }
+
+    /**
+     * Refreshes the resolved IP allowlist from a new set of hostnames or IPs.
+     * Should be called when the Raft peer list changes via 
RaftEngine#changePeerList().
+     * Note: DNS-only changes (e.g. container restart with new IP, same 
hostname)
+     * are not automatically detected and still require a process restart.
+     */
+    public void refresh(Set<String> newAllowedIps) {
+        this.resolvedIps = resolveAll(newAllowedIps);
+        log.info("IpAuthHandler allowlist refreshed, resolved {} entries", 
resolvedIps.size());
+    }
+
     @Override
     public void channelActive(ChannelHandlerContext ctx) throws Exception {
         String clientIp = getClientIp(ctx);
@@ -65,7 +87,25 @@ public class IpAuthHandler extends ChannelDuplexHandler {
     }
 
     private boolean isIpAllowed(String ip) {
-        return allowedIps.isEmpty() || allowedIps.contains(ip);
+        Set<String> resolved = this.resolvedIps;
+        // Empty allowlist means no restriction is configured — allow all
+        return resolved.isEmpty() || resolved.contains(ip);
+    }
+
+    private static Set<String> resolveAll(Set<String> entries) {
+        Set<String> result = new HashSet<>(entries);
+
+        for (String entry : entries) {
+            try {
+                for (InetAddress addr : InetAddress.getAllByName(entry)) {
+                    result.add(addr.getHostAddress());
+                }
+            } catch (UnknownHostException e) {
+                log.warn("Could not resolve allowlist entry '{}': {}", entry, 
e.getMessage());
+            }
+        }
+
+        return Collections.unmodifiableSet(result);
     }
 
     @Override
diff --git 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/MemberAPI.java
 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/MemberAPI.java
index 4a796c37c..525b899c9 100644
--- 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/MemberAPI.java
+++ 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/MemberAPI.java
@@ -113,6 +113,9 @@ public class MemberAPI extends API {
      * @return Returns a JSON string containing the modification results
      * @throws Exception If an exception occurs during request processing, 
service invocation, or Peer list modification, it is captured and returned as 
the JSON representation of the exception
      */
+    // TODO: this endpoint has no authentication check — any caller with 
network
+    // access to the management port can trigger a peer list change.
+    // Wire authentication here as part of the planned auth refactor.
     @PostMapping(value = "/members/change", consumes = 
MediaType.APPLICATION_JSON_VALUE,
                  produces = MediaType.APPLICATION_JSON_VALUE)
     @ResponseBody
diff --git 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java
 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java
index 98bc2ee80..94d136a84 100644
--- 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java
+++ 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java
@@ -25,6 +25,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -85,6 +86,7 @@ import org.apache.hugegraph.pd.pulse.PulseListener;
 import org.apache.hugegraph.pd.raft.PeerUtil;
 import org.apache.hugegraph.pd.raft.RaftEngine;
 import org.apache.hugegraph.pd.raft.RaftStateListener;
+import org.apache.hugegraph.pd.raft.auth.IpAuthHandler;
 import org.apache.hugegraph.pd.util.grpc.StreamObserverUtil;
 import org.apache.hugegraph.pd.watch.PDWatchSubject;
 import org.lognet.springboot.grpc.GRpcService;
@@ -1735,6 +1737,17 @@ public class PDService extends PDGrpc.PDImplBase 
implements RaftStateListener {
             node.changePeers(config, status -> {
                 if (status.isOk()) {
                     log.info("updatePdRaft, change peers success");
+                    // Refresh IpAuthHandler so newly added peers are not 
blocked
+                    IpAuthHandler handler = IpAuthHandler.getInstance();
+                    if (handler != null) {
+                        Set<String> newIps = new HashSet<>();
+                        config.getPeers().forEach(p -> newIps.add(p.getIp()));
+                        config.getLearners().forEach(p -> 
newIps.add(p.getIp()));
+                        handler.refresh(newIps);
+                        log.info("IpAuthHandler refreshed after updatePdRaft 
peer change");
+                    } else {
+                        log.warn("IpAuthHandler not initialized, skipping 
refresh");
+                    }
                 } else {
                     log.error("changePeers status: {}, msg:{}, code: {}, raft 
error:{}",
                               status, status.getErrorMsg(), status.getCode(),
diff --git 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/interceptor/Authentication.java
 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/interceptor/Authentication.java
index 83901bca1..48bcf3868 100644
--- 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/interceptor/Authentication.java
+++ 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/interceptor/Authentication.java
@@ -77,6 +77,8 @@ public class Authentication {
             }
 
             String name = info.substring(0, delim);
+            // TODO: password validation is skipped — only service name is 
checked against
+            // innerModules. Full credential validation should be added as 
part of the auth refactor.
             //String pwd = info.substring(delim + 1);
             if (innerModules.contains(name)) {
                 return call.get();
diff --git 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/grpc/GRpcServerConfig.java
 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/grpc/GRpcServerConfig.java
index fce6d2379..2b1103739 100644
--- 
a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/grpc/GRpcServerConfig.java
+++ 
b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/grpc/GRpcServerConfig.java
@@ -40,6 +40,8 @@ public class GRpcServerConfig extends 
GRpcServerBuilderConfigurer {
                 HgExecutorUtil.createExecutor(EXECUTOR_NAME, 
poolGrpc.getCore(), poolGrpc.getMax(),
                                               poolGrpc.getQueue()));
         serverBuilder.maxInboundMessageSize(MAX_INBOUND_MESSAGE_SIZE);
+        // TODO: GrpcAuthentication is instantiated as a Spring bean but never 
registered
+        // here — add serverBuilder.intercept(grpcAuthentication) once auth is 
refactored.
     }
 
 }
diff --git 
a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java
 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java
index 509864512..57fd36717 100644
--- 
a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java
+++ 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java
@@ -19,6 +19,8 @@ package org.apache.hugegraph.pd.core;
 
 import org.apache.hugegraph.pd.core.meta.MetadataKeyHelperTest;
 import org.apache.hugegraph.pd.core.store.HgKVStoreImplTest;
+import org.apache.hugegraph.pd.raft.IpAuthHandlerTest;
+import org.apache.hugegraph.pd.raft.RaftEngineIpAuthIntegrationTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -36,6 +38,8 @@ import lombok.extern.slf4j.Slf4j;
         StoreMonitorDataServiceTest.class,
         StoreServiceTest.class,
         TaskScheduleServiceTest.class,
+        IpAuthHandlerTest.class,
+        RaftEngineIpAuthIntegrationTest.class,
         // StoreNodeServiceTest.class,
 })
 @Slf4j
diff --git 
a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/IpAuthHandlerTest.java
 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/IpAuthHandlerTest.java
new file mode 100644
index 000000000..31647b6d3
--- /dev/null
+++ 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/IpAuthHandlerTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.hugegraph.pd.raft;
+
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.hugegraph.pd.raft.auth.IpAuthHandler;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class IpAuthHandlerTest {
+
+    @Before
+    public void setUp() {
+        // Must reset BEFORE each test — earlier suite classes (e.g. 
ConfigServiceTest)
+        // initialize RaftEngine which creates the IpAuthHandler singleton 
with their
+        // own peer IPs. Without this reset, our getInstance() calls return 
the stale
+        // singleton and ignore the allowlist passed by the test.
+        Whitebox.setInternalState(IpAuthHandler.class, "instance", null);
+    }
+
+    @After
+    public void tearDown() {
+        // Must reset AFTER each test — prevents our test singleton from 
leaking
+        // into later suite classes that also depend on IpAuthHandler state.
+        Whitebox.setInternalState(IpAuthHandler.class, "instance", null);
+    }
+
+    private boolean isIpAllowed(IpAuthHandler handler, String ip) {
+        return Whitebox.invoke(IpAuthHandler.class,
+                               new Class[]{String.class},
+                               "isIpAllowed", handler, ip);
+    }
+
+    @Test
+    public void testHostnameResolvesToIp() throws Exception {
+        // "localhost" should resolve to one or more IPs via 
InetAddress.getAllByName()
+        // This verifies the core fix: hostname allowlists match numeric 
remote addresses
+        // Using dynamic resolution avoids hardcoding "127.0.0.1" which may 
not be
+        // returned on IPv6-only or custom resolver environments
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.singleton("localhost"));
+        InetAddress[] addresses = InetAddress.getAllByName("localhost");
+        // All resolved addresses should be allowed — resolveAll() adds every 
address
+        // returned by getAllByName() so none should be blocked
+        Assert.assertTrue("Expected at least one resolved address",
+                          addresses.length > 0);
+        for (InetAddress address : addresses) {
+            Assert.assertTrue(
+                    "Expected " + address.getHostAddress() + " to be allowed",
+                    isIpAllowed(handler, address.getHostAddress()));
+        }
+    }
+
+    @Test
+    public void testUnresolvableHostnameDoesNotCrash() {
+        // Should log a warning and skip — no exception thrown during 
construction
+        // Uses .invalid TLD which is RFC-2606 reserved and guaranteed to 
never resolve
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.singleton("nonexistent.invalid"));
+        // Handler was still created successfully despite bad hostname
+        Assert.assertNotNull(handler);
+        // Unresolvable entry is skipped so no IPs should be allowed
+        Assert.assertFalse(isIpAllowed(handler, "127.0.0.1"));
+        Assert.assertFalse(isIpAllowed(handler, "192.168.0.1"));
+    }
+
+    @Test
+    public void testRefreshUpdatesResolvedIps() {
+        // Start with 127.0.0.1
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.singleton("127.0.0.1"));
+        Assert.assertTrue(isIpAllowed(handler, "127.0.0.1"));
+
+        // Refresh with a different IP — verifies refresh() swaps the set 
correctly
+        Set<String> newIps = new HashSet<>();
+        newIps.add("192.168.0.1");
+        handler.refresh(newIps);
+
+        // Old IP should no longer be allowed
+        Assert.assertFalse(isIpAllowed(handler, "127.0.0.1"));
+        // New IP should now be allowed
+        Assert.assertTrue(isIpAllowed(handler, "192.168.0.1"));
+    }
+
+    @Test
+    public void testEmptyAllowlistAllowsAll() {
+        // Empty allowlist = no restriction configured = allow all connections
+        // This is intentional fallback behavior and must be explicitly tested
+        // because it is a security-relevant boundary
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.emptySet());
+        Assert.assertTrue(isIpAllowed(handler, "1.2.3.4"));
+        Assert.assertTrue(isIpAllowed(handler, "192.168.99.99"));
+    }
+
+    @Test
+    public void testGetInstanceReturnsSingletonIgnoresNewAllowlist() {
+        // First call creates the singleton with 127.0.0.1
+        IpAuthHandler first = IpAuthHandler.getInstance(
+                Collections.singleton("127.0.0.1"));
+        // Second call with a different set must return the same instance
+        // and must NOT reinitialize or override the existing allowlist
+        IpAuthHandler second = IpAuthHandler.getInstance(
+                Collections.singleton("192.168.0.1"));
+        Assert.assertSame(first, second);
+        // Original allowlist still in effect
+        Assert.assertTrue(isIpAllowed(second, "127.0.0.1"));
+        // New set was ignored — 192.168.0.1 should not be allowed
+        Assert.assertFalse(isIpAllowed(second, "192.168.0.1"));
+    }
+}
diff --git 
a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineIpAuthIntegrationTest.java
 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineIpAuthIntegrationTest.java
new file mode 100644
index 000000000..1f9857df0
--- /dev/null
+++ 
b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineIpAuthIntegrationTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.hugegraph.pd.raft;
+
+import java.util.Collections;
+
+import org.apache.hugegraph.pd.raft.auth.IpAuthHandler;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.alipay.sofa.jraft.Closure;
+import com.alipay.sofa.jraft.Node;
+import com.alipay.sofa.jraft.Status;
+import com.alipay.sofa.jraft.conf.Configuration;
+import com.alipay.sofa.jraft.error.RaftError;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+public class RaftEngineIpAuthIntegrationTest {
+
+    private Node originalRaftNode;
+
+    @Before
+    public void setUp() {
+        // Save original raftNode so we can restore it after the test
+        originalRaftNode = RaftEngine.getInstance().getRaftNode();
+        // Reset IpAuthHandler singleton for a clean state
+        Whitebox.setInternalState(IpAuthHandler.class, "instance", null);
+    }
+
+    @After
+    public void tearDown() {
+        // Restore original raftNode
+        Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", 
originalRaftNode);
+        // Reset IpAuthHandler singleton
+        Whitebox.setInternalState(IpAuthHandler.class, "instance", null);
+    }
+
+    @Test
+    public void testChangePeerListRefreshesIpAuthHandler() throws Exception {
+        // Initialize IpAuthHandler with an old IP
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.singleton("10.0.0.1"));
+        Assert.assertTrue(invokeIsIpAllowed(handler, "10.0.0.1"));
+        Assert.assertFalse(invokeIsIpAllowed(handler, "127.0.0.1"));
+
+        // Mock Node to fire the changePeers callback synchronously with 
Status.OK()
+        // This simulates a successful peer change without a real Raft cluster
+
+        // Important: fire the closure synchronously or changePeerList() will
+        // block on latch.await(...) until the configured timeout elapses
+        Node mockNode = mock(Node.class);
+        doAnswer(invocation -> {
+            Closure closure = invocation.getArgument(1);
+            closure.run(Status.OK());
+            return null;
+        }).when(mockNode).changePeers(any(Configuration.class), 
any(Closure.class));
+
+        // Inject mock node into RaftEngine
+        Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", 
mockNode);
+
+        // Call changePeerList with new peer — must be odd count
+        RaftEngine.getInstance().changePeerList("127.0.0.1:8610");
+
+        // Verify IpAuthHandler was refreshed with the new peer IP
+        Assert.assertTrue(invokeIsIpAllowed(handler, "127.0.0.1"));
+        // Old IP should no longer be allowed
+        Assert.assertFalse(invokeIsIpAllowed(handler, "10.0.0.1"));
+    }
+
+    @Test
+    public void testChangePeerListDoesNotRefreshOnFailure() throws Exception {
+        // Initialize IpAuthHandler with original IP
+        IpAuthHandler handler = IpAuthHandler.getInstance(
+                Collections.singleton("10.0.0.1"));
+        Assert.assertTrue(invokeIsIpAllowed(handler, "10.0.0.1"));
+
+        // Mock Node to fire callback with a failed status
+        // Simulates a failed peer change — handler should NOT be refreshed
+
+        // Important: fire the closure synchronously or changePeerList() will
+        // block on latch.await(...) until the configured timeout elapses
+        Node mockNode = mock(Node.class);
+        doAnswer(invocation -> {
+            Closure closure = invocation.getArgument(1);
+            closure.run(new Status(RaftError.EINTERNAL, "simulated failure"));
+            return null;
+        }).when(mockNode).changePeers(any(Configuration.class), 
any(Closure.class));
+
+        Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", 
mockNode);
+
+        RaftEngine.getInstance().changePeerList("127.0.0.1:8610");
+
+        // Handler should NOT be refreshed — old IP still allowed
+        Assert.assertTrue(invokeIsIpAllowed(handler, "10.0.0.1"));
+        Assert.assertFalse(invokeIsIpAllowed(handler, "127.0.0.1"));
+    }
+
+    private boolean invokeIsIpAllowed(IpAuthHandler handler, String ip) {
+        return Whitebox.invoke(IpAuthHandler.class,
+                               new Class[]{String.class},
+                               "isIpAllowed", handler, ip);
+    }
+}

Reply via email to