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

imbajin 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 321ba4dad refactor(server): adapt REST API for hubble & enhance k8s 
mode (#3159)
321ba4dad is described below

commit 321ba4dadad7bd37b8c659cdec34b056912d34b8
Author: imbajin <[email protected]>
AuthorDate: Sat Aug 29 22:09:31 2026 +0800

    refactor(server): adapt REST API for hubble & enhance k8s mode (#3159)
    
    - add a configurable PD Raft whitelist toggle
    - preserve whitelist behavior by default
    - skip the IP handler when disabled
    
    * fix: unify audit limiter cache key
    
    - use username consistently for audit limiter lookup
    - cover distinct user id and username cleanup
    - verify PD IP whitelist switch pipeline behavior
    
    * fix(server): close auth review gaps
    
    - honor ALL_GRAPHS and reject mixed-space grants
    - make callback elevation explicit and restore request contexts
    - remove incomplete Gremlin and legacy observer compatibility paths
    - preserve HStore clear safety and the HBase Docker trigger
    
    * docs(server): reference PR for API 0.72
    
    - identify the GraphSpace default-role change as PR-3159
    - keep the existing API 0.72 contract description
    - leave runtime version behavior unchanged
---
 .editorconfig                                      |   4 +-
 .github/workflows/check-dependencies.yml           |   2 +-
 .github/workflows/docker-build-ci.yml              |  48 +++-
 .serena/memories/code_style_and_conventions.md     |   2 +-
 AGENTS.md                                          |   2 +-
 README.md                                          |   2 +-
 .../src/main/resources/version.properties          |   2 +-
 hugegraph-pd/docs/development.md                   |   2 +-
 .../org/apache/hugegraph/pd/config/PDConfig.java   |   2 +
 .../org/apache/hugegraph/pd/raft/RaftEngine.java   |  15 +-
 .../pd/raft/RaftEngineIpAuthIntegrationTest.java   |  45 +++
 hugegraph-server/Dockerfile                        |   5 +-
 hugegraph-server/Dockerfile-hstore                 |   5 +-
 hugegraph-server/hugegraph-api/pom.xml             |   4 +-
 .../org/apache/hugegraph/api/auth/ManagerAPI.java  |  10 +-
 .../apache/hugegraph/api/space/GraphSpaceAPI.java  |  20 +-
 .../hugegraph/api/space/SchemaTemplateAPI.java     |  23 +-
 .../apache/hugegraph/auth/HugeAuthenticator.java   |  17 +-
 .../hugegraph/auth/HugeFactoryAuthProxy.java       |   2 +-
 .../apache/hugegraph/auth/HugeGraphAuthProxy.java  |  98 +++++--
 .../org/apache/hugegraph/core/GraphManager.java    |  26 +-
 .../org/apache/hugegraph/version/ApiVersion.java   |   1 +
 .../org/apache/hugegraph/StandardHugeGraph.java    |   7 +
 .../hugegraph/auth/StandardAuthManagerV2.java      |  17 +-
 .../backend/cache/CachedSchemaTransactionV2.java   |   1 +
 .../org/apache/hugegraph/unit/UnitTestSuite.java   |   4 +
 .../unit/api/space/GraphSpaceAPITest.java          |  82 +++++-
 .../unit/api/space/SchemaTemplateAPITest.java      |  81 ++++++
 .../unit/auth/HugeGraphAuthProxyTest.java          | 314 ++++++++++++++++++++-
 .../core/StandardHugeGraphClearBackendTest.java    | 128 +++++++++
 style/checkstyle.xml                               |   2 +-
 31 files changed, 875 insertions(+), 98 deletions(-)

diff --git a/.editorconfig b/.editorconfig
index 04a6e64a9..c64c7bae2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -21,9 +21,9 @@ root = true
 charset = utf-8
 end_of_line = lf
 insert_final_newline = true
-max_line_length = 100
+max_line_length = 120
 ij_wrap_on_typing = true
-ij_visual_guides = 100
+ij_visual_guides = 120
 
 
 [*.{java,xml,py}]
diff --git a/.github/workflows/check-dependencies.yml 
b/.github/workflows/check-dependencies.yml
index fa804e260..447162d67 100644
--- a/.github/workflows/check-dependencies.yml
+++ b/.github/workflows/check-dependencies.yml
@@ -47,7 +47,7 @@ jobs:
       - name: 'Checkout Repository'
         uses: actions/checkout@v4
       - name: 'Dependency Review'
-        uses: actions/dependency-review-action@v3
+        uses: actions/dependency-review-action@v5
         # Refer: https://github.com/actions/dependency-review-action
         with:
           # TODO: reset critical to low before releasing
diff --git a/.github/workflows/docker-build-ci.yml 
b/.github/workflows/docker-build-ci.yml
index ada012be8..9c34b2147 100644
--- a/.github/workflows/docker-build-ci.yml
+++ b/.github/workflows/docker-build-ci.yml
@@ -24,10 +24,18 @@ on:
       - 'release-*'
   pull_request:
     paths:
-      - '**/Dockerfile*'
+      - '.github/workflows/docker-build-ci.yml'
       - '.dockerignore'
-      - 'hugegraph-server/hugegraph-dist/docker/**'
-      - 'hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh'
+      - '.mvn/**'
+      - 'pom.xml'
+      - 'hugegraph-commons/**'
+      - 'hugegraph-cluster-test/**'
+      - 'hugegraph-pd/**'
+      - 'hugegraph-store/**'
+      - 'hugegraph-struct/**'
+      - 'hugegraph-server/**'
+      - 'docker/hbase/**'
+      - 'install-dist/**'
 
 jobs:
   docker-build:
@@ -47,7 +55,8 @@ jobs:
 
       - name: Build ${{ matrix.dockerfile }}
         run: |
-          IMAGE_ID=$(docker build -q -f ${{ matrix.dockerfile }} .)
+          IMAGE_ID=$(docker build -q --build-arg SOURCE_REVISION="$GITHUB_SHA" 
\
+            -f ${{ matrix.dockerfile }} .)
           echo "Built: $IMAGE_ID"
           echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV"
           HC=$(docker inspect --format='{{json .Config.Healthcheck}}' 
"$IMAGE_ID")
@@ -78,3 +87,34 @@ jobs:
             echo "ERROR: no usable socket-table tool (ss/netstat) in ${{ 
matrix.dockerfile }}"
             exit 1
           }
+
+      - name: Server image API versions match source
+        if: ${{ startsWith(matrix.dockerfile, 'hugegraph-server/') }}
+        run: |
+          CHECK_DIR=$(mktemp -d)
+          trap 'rm -rf "$CHECK_DIR"' EXIT
+          docker run --rm --entrypoint bash \
+            -v "$CHECK_DIR:/check" "$IMAGE_ID" -c \
+            'cp /hugegraph-server/lib/hugegraph-api-*.jar \
+                /hugegraph-server/lib/hugegraph-common-*.jar /check/'
+
+          API_JAR=$(find "$CHECK_DIR" -name 'hugegraph-api-*.jar' -print -quit)
+          COMMON_JAR=$(find "$CHECK_DIR" -name 'hugegraph-common-*.jar' -print 
-quit)
+          EXPECTED_MANIFEST=$(sed -n \
+            
's|.*<Implementation-Version>\([^<]*\)</Implementation-Version>.*|\1|p' \
+            hugegraph-server/hugegraph-api/pom.xml)
+          ACTUAL_MANIFEST=$(unzip -p "$API_JAR" META-INF/MANIFEST.MF |
+            sed -n 's/^Implementation-Version: *//p' | tr -d '\r')
+          EXPECTED_PROPERTY=$(sed -n 's/^ApiVersion=//p' \
+            
hugegraph-commons/hugegraph-common/src/main/resources/version.properties)
+          ACTUAL_PROPERTY=$(unzip -p "$COMMON_JAR" version.properties |
+            sed -n 's/^ApiVersion=//p' | tr -d '\r')
+
+          [[ "$ACTUAL_MANIFEST" == "$EXPECTED_MANIFEST" ]] || {
+            echo "ERROR: API manifest is $ACTUAL_MANIFEST; expected 
$EXPECTED_MANIFEST"
+            exit 1
+          }
+          [[ "$ACTUAL_PROPERTY" == "$EXPECTED_PROPERTY" ]] || {
+            echo "ERROR: API property is $ACTUAL_PROPERTY; expected 
$EXPECTED_PROPERTY"
+            exit 1
+          }
diff --git a/.serena/memories/code_style_and_conventions.md 
b/.serena/memories/code_style_and_conventions.md
index 159920cd3..7a4c310e0 100644
--- a/.serena/memories/code_style_and_conventions.md
+++ b/.serena/memories/code_style_and_conventions.md
@@ -6,7 +6,7 @@
 - `.licenserc.yaml` + apache-rat-plugin + skywalking-eyes — License header 
validation
 
 ## Core Rules
-- **Line length**: 100 chars (120 for XML)
+- **Line length**: 120 chars
 - **Indent**: 4 spaces, continuation 8 spaces
 - **Charset**: UTF-8, LF line endings, final newline
 - **Imports**: Sorted `$*` → `java` → `javax` → `org` → `com` → `*`, no star 
imports (threshold 100)
diff --git a/AGENTS.md b/AGENTS.md
index 2d6e81b15..07daf1766 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,7 +85,7 @@ Before writing new tests, check existing suites under 
`hugegraph-server/hugegrap
 
 ## Style & Pre-commit
 
-- Line 100, 4-space indent, LF, UTF-8, **no star imports**
+- Line 120, 4-space indent, LF, UTF-8, **no star imports**
 - Commit format: `feat|fix|refactor(module): msg`
 - Run before pushing:
   ```bash
diff --git a/README.md b/README.md
index adf979277..f4543e073 100644
--- a/README.md
+++ b/README.md
@@ -342,7 +342,7 @@ For detailed architecture and development guidance, see 
[AGENTS.md](AGENTS.md).
    - Try modifying a test and see what breaks
 
 5. **Code Standards**
-   - Line length: 100 characters
+   - Line length: 120 characters
    - Indentation: 4 spaces
    - No star imports
    - Commit format: `feat|fix|refactor(module): description`
diff --git 
a/hugegraph-commons/hugegraph-common/src/main/resources/version.properties 
b/hugegraph-commons/hugegraph-common/src/main/resources/version.properties
index 2dffc6f3a..8d48ef39c 100644
--- a/hugegraph-commons/hugegraph-common/src/main/resources/version.properties
+++ b/hugegraph-commons/hugegraph-common/src/main/resources/version.properties
@@ -17,7 +17,7 @@
 # hugegraph-common follows the project version defined by ${revision} in the 
root pom.xml,
 # and VersionInBash needs to be updated in this file.
 Version=${revision}
-ApiVersion=0.71
+ApiVersion=0.72
 ApiCheckBeginVersion=1.0
 ApiCheckEndVersion=2.0
 VersionInBash=1.7.0
diff --git a/hugegraph-pd/docs/development.md b/hugegraph-pd/docs/development.md
index 3f01b902e..514bd989a 100644
--- a/hugegraph-pd/docs/development.md
+++ b/hugegraph-pd/docs/development.md
@@ -282,7 +282,7 @@ HugeGraph PD follows Apache HugeGraph code style.
 
 **Key Style Rules**:
 - **Indentation**: 4 spaces (no tabs)
-- **Line length**: 100 characters (Java), 120 characters (comments)
+- **Line length**: 120 characters
 - **Braces**: K&R style (opening brace on same line)
 - **Imports**: No wildcard imports (`import java.util.*`)
 
diff --git 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java
 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java
index 67f1ff47a..acfc2ec29 100644
--- 
a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java
+++ 
b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java
@@ -140,6 +140,8 @@ public class PDConfig {
 
         @Value("${raft.enable:true }")
         private boolean enable;
+        @Value("${raft.ip-whitelist.enabled:true}")
+        private boolean ipWhitelistEnabled = true;
         @Value("${raft.address}")
         private String address;
         @Value("${pd.data-path}")
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 2b08de7d4..314c9e57e 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
@@ -127,7 +127,8 @@ public class RaftEngine {
 
         final PeerId serverId = JRaftUtils.getPeerId(config.getAddress());
 
-        rpcServer = createRaftRpcServer(config.getAddress(), 
initConf.getPeers());
+        rpcServer = createRaftRpcServer(config.getAddress(), 
initConf.getPeers(),
+                                        config.isIpWhitelistEnabled());
         // construct raft group and start raft
         this.raftGroupService =
                 new RaftGroupService(groupId, serverId, nodeOptions, 
rpcServer, true);
@@ -140,16 +141,22 @@ public class RaftEngine {
     /**
      * Create a Raft RPC Server for communication between PDs
      */
-    private RpcServer createRaftRpcServer(String raftAddr, List<PeerId> peers) 
{
+    private RpcServer createRaftRpcServer(String raftAddr, List<PeerId> peers,
+                                          boolean ipWhitelistEnabled) {
         Endpoint endpoint = JRaftUtils.getEndPoint(raftAddr);
         RpcServer rpcServer = 
RaftRpcServerFactory.createRaftRpcServer(endpoint);
-        configureRaftServerIpWhitelist(peers, rpcServer);
+        configureRaftServerIpWhitelist(peers, rpcServer, ipWhitelistEnabled);
         RaftRpcProcessor.registerProcessor(rpcServer, this);
         rpcServer.init(null);
         return rpcServer;
     }
 
-    private static void configureRaftServerIpWhitelist(List<PeerId> peers, 
RpcServer rpcServer) {
+    private static void configureRaftServerIpWhitelist(List<PeerId> peers, 
RpcServer rpcServer,
+                                                       boolean enabled) {
+        if (!enabled) {
+            log.info("PD Raft IP whitelist is disabled");
+            return;
+        }
         if (rpcServer instanceof BoltRpcServer) {
             ((BoltRpcServer) rpcServer).getServer().option(
                     BoltServerOption.EXTENDED_NETTY_CHANNEL_HANDLER,
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
index 1f9857df0..f7b88ac8c 100644
--- 
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
@@ -18,7 +18,9 @@
 package org.apache.hugegraph.pd.raft;
 
 import java.util.Collections;
+import java.util.List;
 
+import org.apache.hugegraph.pd.config.PDConfig;
 import org.apache.hugegraph.pd.raft.auth.IpAuthHandler;
 import org.apache.hugegraph.testutil.Whitebox;
 import org.junit.After;
@@ -26,11 +28,17 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.alipay.remoting.ExtendedNettyChannelHandler;
+import com.alipay.remoting.config.BoltServerOption;
 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 com.alipay.sofa.jraft.rpc.RpcServer;
+import com.alipay.sofa.jraft.rpc.impl.BoltRpcServer;
+
+import io.netty.channel.ChannelHandler;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -116,6 +124,43 @@ public class RaftEngineIpAuthIntegrationTest {
         Assert.assertFalse(invokeIsIpAllowed(handler, "127.0.0.1"));
     }
 
+    @Test
+    public void testIpWhitelistConfigurationControlsPipeline() {
+        assertIpWhitelistPipeline(null, true);
+        assertIpWhitelistPipeline(true, true);
+        assertIpWhitelistPipeline(false, false);
+    }
+
+    private void assertIpWhitelistPipeline(Boolean configured,
+                                           boolean expectedInstalled) {
+        PDConfig pdConfig = new PDConfig();
+        PDConfig.Raft raftConfig = pdConfig.new Raft();
+        if (configured != null) {
+            raftConfig.setIpWhitelistEnabled(configured);
+        }
+
+        com.alipay.remoting.rpc.RpcServer server =
+                new com.alipay.remoting.rpc.RpcServer();
+        RpcServer rpcServer = new BoltRpcServer(server);
+        Whitebox.invokeStatic(
+                RaftEngine.class,
+                new Class[]{List.class, RpcServer.class, boolean.class},
+                "configureRaftServerIpWhitelist",
+                Collections.emptyList(), rpcServer,
+                raftConfig.isIpWhitelistEnabled());
+
+        ExtendedNettyChannelHandler pipeline =
+                server.option(BoltServerOption.EXTENDED_NETTY_CHANNEL_HANDLER);
+        if (!expectedInstalled) {
+            Assert.assertNull(pipeline);
+            return;
+        }
+        Assert.assertNotNull(pipeline);
+        List<ChannelHandler> handlers = pipeline.frontChannelHandlers();
+        Assert.assertEquals(1, handlers.size());
+        Assert.assertTrue(handlers.get(0) instanceof IpAuthHandler);
+    }
+
     private boolean invokeIsIpAllowed(IpAuthHandler handler, String ip) {
         return Whitebox.invoke(IpAuthHandler.class,
                                new Class[]{String.class},
diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile
index 5caadd23c..44bc9aa51 100644
--- a/hugegraph-server/Dockerfile
+++ b/hugegraph-server/Dockerfile
@@ -25,9 +25,10 @@ WORKDIR /pkg
 COPY . .
 
 ARG MAVEN_ARGS
+ARG SOURCE_REVISION=local
 
-RUN --mount=type=cache,target=/root/.m2 \
-    mvn package $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true 
-Dmaven.javadoc.skip=true \
+RUN 
--mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked
 \
+    mvn install $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true 
-Dmaven.javadoc.skip=true \
     && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz 
./hugegraph-store/*.tar.gz
 
 # 2nd stage: runtime env
diff --git a/hugegraph-server/Dockerfile-hstore 
b/hugegraph-server/Dockerfile-hstore
index 7cd64e8f3..5d4d96d77 100644
--- a/hugegraph-server/Dockerfile-hstore
+++ b/hugegraph-server/Dockerfile-hstore
@@ -25,9 +25,10 @@ WORKDIR /pkg
 COPY . .
 
 ARG MAVEN_ARGS
+ARG SOURCE_REVISION=local
 
-RUN --mount=type=cache,target=/root/.m2 \
-    mvn package $MAVEN_ARGS -e -B -ntp -DskipTests -Dmaven.javadoc.skip=true \
+RUN 
--mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked
 \
+    mvn install $MAVEN_ARGS -e -B -ntp -DskipTests -Dmaven.javadoc.skip=true \
     && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz 
./hugegraph-store/*.tar.gz
 
 # 2nd stage: runtime env
diff --git a/hugegraph-server/hugegraph-api/pom.xml 
b/hugegraph-server/hugegraph-api/pom.xml
index f1a8b918b..e5a81be20 100644
--- a/hugegraph-server/hugegraph-api/pom.xml
+++ b/hugegraph-server/hugegraph-api/pom.xml
@@ -201,8 +201,8 @@
                             </addDefaultSpecificationEntries>
                         </manifest>
                         <manifestEntries>
-                            <!-- TODO: update it -->
-                            
<Implementation-Version>0.71.0.0</Implementation-Version>
+                            <!-- Bump when the public REST API contract 
changes -->
+                            
<Implementation-Version>0.72.0.0</Implementation-Version>
                         </manifestEntries>
                     </archive>
                 </configuration>
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
index 37aee8c65..bbc808237 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ManagerAPI.java
@@ -287,9 +287,7 @@ public class ManagerAPI extends API {
             defaultRole = null; // unreachable, satisfies compiler
         }
         validGraphSpace(manager, graphSpace);
-        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
-        E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
-                        "Must set a graph for observer");
+        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER) && 
StringUtils.isNotEmpty(graph);
         if (hasGraph) {
             validGraph(manager, graphSpace, graph);
         }
@@ -297,10 +295,10 @@ public class ManagerAPI extends API {
         boolean result;
         if (hasGraph) {
             result = authManager.isDefaultRole(graphSpace, graph, user,
-                                               defaultRole);
+                                               defaultRole) ||
+                     authManager.isDefaultRole(graphSpace, user, defaultRole);
         } else {
-            result = authManager.isDefaultRole(graphSpace, user,
-                                               defaultRole);
+            result = authManager.isDefaultRole(graphSpace, user, defaultRole);
         }
         return manager.serializer().writeMap(ImmutableMap.of("check", result));
     }
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
index 81f13cf3f..4a81ba8cb 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/GraphSpaceAPI.java
@@ -146,10 +146,7 @@ public class GraphSpaceAPI extends API {
             throw new ForbiddenException("Forbidden to set role " + 
role.toString());
         }
 
-        boolean hasGraph = role.equals(HugeDefaultRole.OBSERVER);
-
-        E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
-                        "Must set a graph for observer");
+        boolean hasGraph = role.equals(HugeDefaultRole.OBSERVER) && 
StringUtils.isNotEmpty(graph);
         if (hasGraph) {
             validGraph(manager, name, graph);
         }
@@ -203,9 +200,8 @@ public class GraphSpaceAPI extends API {
             defaultRole.equals(HugeDefaultRole.SPACE)) {
             throw new ForbiddenException("Forbidden to check role " + role);
         }
-        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
-        E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
-                        "Must set a graph for observer");
+        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER) &&
+                           StringUtils.isNotEmpty(graph);
         if (hasGraph) {
             validGraph(manager, name, graph);
         }
@@ -213,10 +209,10 @@ public class GraphSpaceAPI extends API {
         boolean result;
         if (hasGraph) {
             result = authManager.isDefaultRole(name, graph, user,
-                                               defaultRole);
+                                               defaultRole) ||
+                     authManager.isDefaultRole(name, user, defaultRole);
         } else {
-            result = authManager.isDefaultRole(name, user,
-                                               defaultRole);
+            result = authManager.isDefaultRole(name, user, defaultRole);
         }
         return manager.serializer().writeMap(ImmutableMap.of("check", result));
     }
@@ -259,9 +255,7 @@ public class GraphSpaceAPI extends API {
             E.checkArgument(false, "Invalid role value '%s'", role);
             defaultRole = null; // unreachable, satisfies compiler
         }
-        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
-        E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
-                        "Must set a graph for observer");
+        boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER) && 
StringUtils.isNotEmpty(graph);
         if (hasGraph) {
             validGraph(manager, name, graph);
         }
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
index b2c151687..cffca156c 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java
@@ -20,10 +20,12 @@ package org.apache.hugegraph.api.space;
 import java.util.Date;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hugegraph.HugeException;
 import org.apache.hugegraph.api.API;
+import org.apache.hugegraph.auth.AuthManager;
 import org.apache.hugegraph.api.filter.StatusFilter;
 import org.apache.hugegraph.auth.HugeGraphAuthProxy;
 import org.apache.hugegraph.core.GraphManager;
@@ -134,9 +136,8 @@ public class SchemaTemplateAPI extends API {
                         "Schema template '%s' does not exist", name);
 
         String username = HugeGraphAuthProxy.username();
-        boolean isSpace = manager.authManager()
-                                 .isSpaceManager(graphSpace, username);
-        if (Objects.equals(st.creator(), username) || isSpace) {
+        if (canManage(manager::authManager, graphSpace, st.creator(),
+                      username)) {
             manager.dropSchemaTemplate(graphSpace, name);
         } else {
             throw new ForbiddenException("No permission to delete schema 
template");
@@ -165,9 +166,8 @@ public class SchemaTemplateAPI extends API {
         }
 
         String username = HugeGraphAuthProxy.username();
-        boolean isSpace = manager.authManager()
-                                 .isSpaceManager(graphSpace, username);
-        if (Objects.equals(old.creator(), username) || isSpace) {
+        if (canManage(manager::authManager, graphSpace, old.creator(),
+                      username)) {
             SchemaTemplate template = jsonSchemaTemplate.build(old);
             template.creator(old.creator());
             template.create(old.create());
@@ -180,6 +180,17 @@ public class SchemaTemplateAPI extends API {
 
     }
 
+    private static boolean canManage(Supplier<AuthManager> authManagerSupplier,
+                                     String graphSpace, String creator,
+                                     String username) {
+        if (Objects.equals(creator, username)) {
+            return true;
+        }
+        AuthManager authManager = authManagerSupplier.get();
+        return authManager.isAdminManager(username) ||
+               authManager.isSpaceManager(graphSpace, username);
+    }
+
     private static class JsonSchemaTemplate implements Checkable {
 
         @JsonProperty("name")
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java
index cef1287b1..243342b63 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java
@@ -290,6 +290,10 @@ public interface HugeAuthenticator extends Authenticator {
             }
             for (Map.Entry<HugePermission, Object> e : perms.entrySet()) {
                 HugePermission permission = e.getKey();
+                if (permission == HugePermission.SPACE ||
+                    permission == HugePermission.SPACE_MEMBER) {
+                    continue;
+                }
                 // Maybe required = ANY
                 if (action.match(permission) ||
                     action.equals(HugePermission.EXECUTE)) {
@@ -359,8 +363,17 @@ public interface HugeAuthenticator extends Authenticator {
                 }
             }
 
-            RolePermission rolePerm = RolePermission.fromJson(role);
-            return rolePerm.contains(grant);
+            RolePermission grantedRole = RolePermission.fromJson(grant);
+            RolePermission currentRole = RolePermission.fromJson(role);
+            RolePerm rolePerm = RolePerm.fromJson(currentRole);
+            if (resourceObject != null &&
+                !RolePermission.isAdmin(grantedRole) &&
+                grantedRole.roles().size() == 1 &&
+                grantedRole.roles().containsKey(resourceObject.graphSpace()) &&
+                rolePerm.matchSpace(resourceObject.graphSpace(), "space")) {
+                return true;
+            }
+            return currentRole.contains(grantedRole);
         }
 
         @SuppressWarnings({"unchecked", "rawtypes"})
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java
index b9cdb4fd6..93095e0e2 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java
@@ -159,7 +159,7 @@ public final class HugeFactoryAuthProxy {
                                            "lambda$16", "lambda$17", 
"lambda$18", "lambda$19",
                                            "lambda$20", "lambda$21", 
"lambda$22", "lambda$23",
                                            "lambda$24", "access$8", 
"access$9", "access$10",
-                                           "setContext", "getContext");
+                                           "setContext", "getContext", 
"runAsAdmin");
         
Reflection.registerFieldsToFilter(HugeGraphAuthProxy.AuthManagerProxy.class, 
"authManager",
                                           "this$0");
         
Reflection.registerMethodsToFilter(HugeGraphAuthProxy.AuthManagerProxy.class,
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
index 4b0aed578..510b83843 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java
@@ -154,15 +154,32 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
     }
 
     public static void resetContext() {
+        AuthContext.resetContext();
         CONTEXTS.remove();
         REQUEST_GRAPH_SPACE.remove();
     }
 
     public static void resetSpaceContext() {
+        AuthContext.resetContext();
         CONTEXTS.remove();
         REQUEST_GRAPH_SPACE.remove();
     }
 
+    private void prepareAuditLimiter(UserWithRole user) {
+        if (user == null || user.role() == null ||
+            HugeAuthenticator.ROLE_NONE.equals(user.role())) {
+            return;
+        }
+        Id userKey = auditLimiterKey(user.username());
+        this.auditLimiters.getOrFetch(userKey, id -> {
+            return RateLimiter.create(this.auditLogMaxRate);
+        });
+    }
+
+    private static Id auditLimiterKey(String username) {
+        return IdGenerator.of(username);
+    }
+
     /**
      * Get the graph space from current request URL path
      */
@@ -178,13 +195,49 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
         REQUEST_GRAPH_SPACE.set(graphSpace);
     }
 
-    public static Context setAdmin() {
-        Context old = getContext();
-        AuthContext.useAdmin();
-        return old;
+    public static void runAsAdmin(Runnable runnable) {
+        String oldAuthContext = AuthContext.getContext();
+        Context oldContext = CONTEXTS.get();
+        String oldGraphSpace = REQUEST_GRAPH_SPACE.get();
+        String oldTaskContext = TaskManager.getContext();
+        try {
+            AuthContext.resetContext();
+            CONTEXTS.remove();
+            REQUEST_GRAPH_SPACE.remove();
+            TaskManager.resetContext();
+            AuthContext.setContext(User.ADMIN.toJson());
+            runnable.run();
+        } finally {
+            if (oldAuthContext == null) {
+                AuthContext.resetContext();
+            } else {
+                AuthContext.setContext(oldAuthContext);
+            }
+            if (oldContext == null) {
+                CONTEXTS.remove();
+            } else {
+                CONTEXTS.set(oldContext);
+            }
+            if (oldGraphSpace == null) {
+                REQUEST_GRAPH_SPACE.remove();
+            } else {
+                REQUEST_GRAPH_SPACE.set(oldGraphSpace);
+            }
+            if (oldTaskContext == null) {
+                TaskManager.resetContext();
+            } else {
+                TaskManager.setContext(oldTaskContext);
+            }
+        }
     }
 
     public static Context getContext() {
+        String internalContext = AuthContext.getContext();
+        User internalUser = User.fromJson(internalContext);
+        if (internalUser != null) {
+            return new Context(internalUser);
+        }
+
         // Return task context first
         String taskContext = TaskManager.getContext();
 
@@ -1200,8 +1253,8 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
         }
 
         // Log user action, limit rate for each user
-        Id usrId = context.user().userId();
-        RateLimiter auditLimiter = this.auditLimiters.getOrFetch(usrId, id -> {
+        Id userKey = auditLimiterKey(username);
+        RateLimiter auditLimiter = this.auditLimiters.getOrFetch(userKey, id 
-> {
             return RateLimiter.create(this.auditLogMaxRate);
         });
 
@@ -1546,7 +1599,8 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
             String username = currentUsername();
             HugeUser user = this.authManager.getUser(updatedUser.id());
             if (!user.name().equals(username)) {
-                E.checkArgument(HugeAuthenticator.USER_ADMIN.equals(username),
+                E.checkArgument(HugeAuthenticator.USER_ADMIN.equals(username) 
||
+                                this.authManager.isAdminManager(username),
                                 "Only the user themselves or the admin can 
change this user",
                                 user.name());
                 this.updateCreator(updatedUser);
@@ -1560,9 +1614,12 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
             HugeUser user = this.authManager.getUser(id);
             E.checkArgument(!HugeAuthenticator.USER_ADMIN.equals(user.name()),
                             "Can't delete user '%s'", user.name());
-            
E.checkArgument(HugeAuthenticator.USER_ADMIN.equals(currentUsername()),
+            String username = currentUsername();
+            E.checkArgument(HugeAuthenticator.USER_ADMIN.equals(username) ||
+                            this.authManager.isAdminManager(username),
                             "only admin can delete user", user.name());
-            HugeGraphAuthProxy.this.auditLimiters.invalidate(user.id());
+            HugeGraphAuthProxy.this.auditLimiters.invalidate(
+                    auditLimiterKey(user.name()));
             this.invalidRoleCache();
             return this.authManager.deleteUser(id);
         }
@@ -2006,9 +2063,12 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
 
             try {
                 Id userKey = IdGenerator.of(username + password);
-                return 
HugeGraphAuthProxy.this.usersRoleCache.getOrFetch(userKey, id -> {
-                    return this.authManager.validateUser(username, password);
-                });
+                UserWithRole user =
+                        HugeGraphAuthProxy.this.usersRoleCache.getOrFetch(
+                                userKey, id -> this.authManager.validateUser(
+                                        username, password));
+                HugeGraphAuthProxy.this.prepareAuditLimiter(user);
+                return user;
             } catch (Exception e) {
                 LOG.error("Failed to validate user {} with error: ",
                           username, e);
@@ -2025,9 +2085,12 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
 
             try {
                 Id userKey = IdGenerator.of(token);
-                return 
HugeGraphAuthProxy.this.usersRoleCache.getOrFetch(userKey, id -> {
-                    return this.authManager.validateUser(token);
-                });
+                UserWithRole user =
+                        HugeGraphAuthProxy.this.usersRoleCache.getOrFetch(
+                                userKey,
+                                id -> this.authManager.validateUser(token));
+                HugeGraphAuthProxy.this.prepareAuditLimiter(user);
+                return user;
             } catch (Exception e) {
                 LOG.error("Failed to validate token with error: ", e);
                 throw e;
@@ -2327,7 +2390,9 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
 
         @Override
         public List<TraversalStrategy<?>> toList() {
-            return this.strategies.toList();
+            List<TraversalStrategy<?>> proxies = new ArrayList<>();
+            this.iterator().forEachRemaining(proxies::add);
+            return Collections.unmodifiableList(proxies);
         }
 
         @Override
@@ -2461,4 +2526,5 @@ public final class HugeGraphAuthProxy implements 
HugeGraph {
             return this.origin.toString();
         }
     }
+
 }
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
index 96717e724..c7cca866b 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java
@@ -1314,9 +1314,6 @@ public final class GraphManager {
             throw new ExistedException("graph", key);
         }
         boolean grpcThread = Thread.currentThread().getName().contains("grpc");
-        if (grpcThread) {
-            HugeGraphAuthProxy.setAdmin();
-        }
         E.checkArgumentNotNull(name, "The graph name can't be null");
         checkGraphName(name);
         String nickname;
@@ -1426,9 +1423,6 @@ public final class GraphManager {
             String schemas = this.schemaTemplate(graphSpace, schema).schema();
             prepareSchema(graph, schemas);
         }
-        if (grpcThread) {
-            HugeGraphAuthProxy.resetContext();
-        }
         return graph;
     }
 
@@ -2420,7 +2414,7 @@ public final class GraphManager {
         cores
     }
 
-    public static class ConsumerWrapper<T> implements Consumer<T> {
+    private static class ConsumerWrapper<T> implements Consumer<T> {
 
         private final Consumer<T> consumer;
 
@@ -2428,25 +2422,16 @@ public final class GraphManager {
             this.consumer = consumer;
         }
 
-        public static ConsumerWrapper wrap(Consumer consumer) {
+        private static ConsumerWrapper wrap(Consumer consumer) {
             return new ConsumerWrapper(consumer);
         }
 
         @Override
         public void accept(T t) {
-            boolean grpcThread = false;
             try {
-                grpcThread = Thread.currentThread().getName().contains("grpc");
-                if (grpcThread) {
-                    HugeGraphAuthProxy.setAdmin();
-                }
-                consumer.accept(t);
+                HugeGraphAuthProxy.runAsAdmin(() -> this.consumer.accept(t));
             } catch (Throwable e) {
                 LOG.error("Listener exception occurred.", e);
-            } finally {
-                if (grpcThread) {
-                    HugeGraphAuthProxy.resetContext();
-                }
             }
         }
     }
@@ -2498,11 +2483,6 @@ public final class GraphManager {
                 // TODO: add alias graph
                 graph = this.createGraph(parts[0], parts[1], creator, config, 
false);
                 LOG.info("Add graph space:{} graph:{}", parts[0], parts[1]);
-                // TODO: use a more secure method to determine administrator 
privileges
-                boolean grpcThread = 
Thread.currentThread().getName().contains("grpc");
-                if (grpcThread) {
-                    HugeGraphAuthProxy.setAdmin();
-                }
                 graph.started(true);
                 if (graph.tx().isOpen()) {
                     graph.tx().close();
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/version/ApiVersion.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/version/ApiVersion.java
index 7e314f9ed..6aff46658 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/version/ApiVersion.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/version/ApiVersion.java
@@ -121,6 +121,7 @@ public final class ApiVersion {
      * [0.69] Issue-1748: Support Cypher query RESTful API
      * [0.70] PR-2242: Add edge-existence RESTful API
      * [0.71] PR-2286: Support Arthas API & Metric API prometheus format
+     * [0.72] PR-3159: Support GraphSpace-wide default-role management APIs
      */
 
     /**
diff --git 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java
 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java
index 6f66b4b8a..6af4dde99 100644
--- 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java
+++ 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java
@@ -519,6 +519,13 @@ public class StandardHugeGraph implements HugeGraph {
 
         LockUtil.lock(this.spaceGraphName(), LockUtil.GRAPH_LOCK);
         try {
+            if (this.isHstore()) {
+                E.checkState(this.schemaTransaction() instanceof
+                             CachedSchemaTransactionV2,
+                             "The HStore schema transaction must be %s",
+                             CachedSchemaTransactionV2.class.getSimpleName());
+                ((CachedSchemaTransactionV2) this.schemaTransaction()).clear();
+            }
             this.storeProvider.clear();
         } finally {
             LockUtil.unlock(this.spaceGraphName(), LockUtil.GRAPH_LOCK);
diff --git 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
index 1f34aa459..d2d12b0f1 100644
--- 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
+++ 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java
@@ -1815,20 +1815,31 @@ public class StandardAuthManagerV2 implements 
AuthManager {
     @Override
     public boolean isDefaultRole(String graphSpace, String owner,
                                  HugeDefaultRole role) {
-        return isDefaultRole(graphSpace, owner, role.toString());
+        String roleName = role.isGraphRole() ?
+                          getGraphDefaultRole(ALL_GRAPHS, role.toString()) :
+                          role.toString();
+        return isDefaultRole(graphSpace, owner, roleName);
     }
 
     @Override
     public boolean isDefaultRole(String graphSpace, String graph,
                                  String owner, HugeDefaultRole role) {
         String roleName = getGraphDefaultRole(graph, role.toString());
-        return isDefaultRole(graphSpace, owner, roleName);
+        if (isDefaultRole(graphSpace, owner, roleName)) {
+            return true;
+        }
+        String allGraphsRole = getGraphDefaultRole(ALL_GRAPHS,
+                                                   role.toString());
+        return isDefaultRole(graphSpace, owner, allGraphsRole);
     }
 
     @Override
     public void deleteDefaultRole(String graphSpace, String owner,
                                   HugeDefaultRole role) {
-        deleteDefaultRoleByName(graphSpace, owner, role.toString());
+        String roleName = role.isGraphRole() ?
+                          getGraphDefaultRole(ALL_GRAPHS, role.toString()) :
+                          role.toString();
+        deleteDefaultRoleByName(graphSpace, owner, roleName);
     }
 
     @Override
diff --git 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
index 99a393f6b..74986b2e9 100644
--- 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
+++ 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java
@@ -467,6 +467,7 @@ public class CachedSchemaTransactionV2 extends 
SchemaTransactionV2 {
         // Clear schema info firstly
         super.clear();
         this.clearCache(false);
+        this.notifySchemaCacheClear();
     }
 
     private static final class SchemaCaches<V extends SchemaElement> {
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index 461ea2c0f..ee44a7fce 100644
--- 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -32,6 +32,7 @@ import 
org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest;
 import org.apache.hugegraph.unit.api.filter.PathFilterTest;
 import org.apache.hugegraph.unit.api.gremlin.GremlinQueryAPITest;
 import org.apache.hugegraph.unit.api.space.GraphSpaceAPITest;
+import org.apache.hugegraph.unit.api.space.SchemaTemplateAPITest;
 import org.apache.hugegraph.unit.auth.HugeGraphAuthProxyTest;
 import org.apache.hugegraph.unit.cache.CacheManagerTest;
 import org.apache.hugegraph.unit.cache.CacheTest;
@@ -55,6 +56,7 @@ import org.apache.hugegraph.unit.core.LocksTableTest;
 import org.apache.hugegraph.unit.core.PageStateTest;
 import org.apache.hugegraph.unit.core.QueryResultsTest;
 import org.apache.hugegraph.unit.core.QueryTest;
+import org.apache.hugegraph.unit.core.StandardHugeGraphClearBackendTest;
 import org.apache.hugegraph.unit.core.RangeTest;
 import org.apache.hugegraph.unit.core.RolePermissionTest;
 import org.apache.hugegraph.unit.core.RowLockTest;
@@ -115,6 +117,7 @@ import org.junit.runners.Suite;
 
         /* api space */
         GraphSpaceAPITest.class,
+        SchemaTemplateAPITest.class,
 
         /* cache */
         CacheTest.RamCacheTest.class,
@@ -144,6 +147,7 @@ import org.junit.runners.Suite;
         AnalyzerTest.class,
         BackendMutationTest.class,
         ConditionTest.class,
+        StandardHugeGraphClearBackendTest.class,
         ConditionQueryFlattenTest.class,
         GraphIndexTransactionTest.class,
         QueryTest.class,
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
index caa659a4d..4d893e319 100644
--- 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java
@@ -87,6 +87,82 @@ public class GraphSpaceAPITest extends BaseUnitTest {
         Assert.assertContains("\"check\":true", result);
     }
 
+    @Test
+    public void testAdminCanCheckSpaceWideObserverRole() {
+        GraphSpaceAPI api = new GraphSpaceAPI();
+        GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+        setContext(ADMIN);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE, TARGET,
+                                             "OBSERVER", null);
+
+        Assert.assertContains("\"check\":true", result);
+    }
+
+    @Test
+    public void testCurrentUserCanCheckSpaceWideObserverRole() {
+        ManagerAPI api = new ManagerAPI();
+        GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+        setContext(TARGET);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE,
+                                             "OBSERVER", null);
+
+        Assert.assertContains("\"check\":true", result);
+    }
+
+    @Test
+    public void testCurrentUserConcreteGraphCheckIncludesSpaceObserver() {
+        ManagerAPI api = new ManagerAPI();
+        GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+        AuthManager auth = manager.authManager();
+        Mockito.when(auth.isDefaultRole(GRAPHSPACE, GRAPH, TARGET,
+                                        HugeDefaultRole.OBSERVER))
+               .thenReturn(false);
+        setContext(TARGET);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE,
+                                             "OBSERVER", GRAPH);
+
+        Assert.assertContains("\"check\":true", result);
+        Mockito.verify(auth).isDefaultRole(GRAPHSPACE, TARGET,
+                                           HugeDefaultRole.OBSERVER);
+    }
+
+    @Test
+    public void testConcreteGraphCheckIncludesSpaceObserver() {
+        GraphSpaceAPI api = new GraphSpaceAPI();
+        GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+        AuthManager auth = manager.authManager();
+        Mockito.when(auth.isDefaultRole(GRAPHSPACE, GRAPH, TARGET,
+                                        HugeDefaultRole.OBSERVER))
+               .thenReturn(false);
+        setContext(ADMIN);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE, TARGET,
+                                             "OBSERVER", GRAPH);
+
+        Assert.assertContains("\"check\":true", result);
+        Mockito.verify(auth).isDefaultRole(GRAPHSPACE, TARGET,
+                                           HugeDefaultRole.OBSERVER);
+    }
+
+    @Test
+    public void testSpaceObserverDeleteDoesNotModifyGraphRoles() {
+        GraphSpaceAPI api = new GraphSpaceAPI();
+        GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+        AuthManager auth = manager.authManager();
+        setContext(ADMIN);
+
+        api.deleteDefaultRole(manager, GRAPHSPACE, TARGET, "OBSERVER", null);
+
+        Mockito.verify(auth).deleteDefaultRole(
+                GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+        Mockito.verify(auth, Mockito.never()).deleteDefaultRole(
+                Mockito.eq(GRAPHSPACE), Mockito.eq(TARGET),
+                Mockito.eq(HugeDefaultRole.OBSERVER), Mockito.anyString());
+    }
+
     @Test
     public void testManagerDefaultRoleRejectsMissingGraphSpace() {
         ManagerAPI api = new ManagerAPI();
@@ -191,6 +267,9 @@ public class GraphSpaceAPITest extends BaseUnitTest {
         Mockito.when(authManager.isDefaultRole(GRAPHSPACE, TARGET,
                                                HugeDefaultRole.SPACE))
                .thenReturn(true);
+        Mockito.when(authManager.isDefaultRole(GRAPHSPACE, TARGET,
+                                               HugeDefaultRole.OBSERVER))
+               .thenReturn(true);
         Mockito.when(authManager.findUser(TARGET))
                .thenReturn(new HugeUser(TARGET));
 
@@ -206,7 +285,8 @@ public class GraphSpaceAPITest extends BaseUnitTest {
 
         MetaManager metaManager = Mockito.mock(MetaManager.class);
         Mockito.when(metaManager.graphConfigs(GRAPHSPACE))
-               .thenReturn(Collections.emptyMap());
+               .thenReturn(Collections.singletonMap(
+                       GRAPHSPACE + "-" + GRAPH, Collections.emptyMap()));
         Whitebox.setInternalState(manager, "metaManager", metaManager);
 
         Map<String, Graph> graphs = new ConcurrentHashMap<>();
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/SchemaTemplateAPITest.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/SchemaTemplateAPITest.java
new file mode 100644
index 000000000..12fe7b086
--- /dev/null
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/SchemaTemplateAPITest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.unit.api.space;
+
+import java.util.function.Supplier;
+
+import org.apache.hugegraph.api.space.SchemaTemplateAPI;
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class SchemaTemplateAPITest {
+
+    private static final String GRAPHSPACE = "space";
+    private static final String CREATOR = "creator";
+
+    @Test
+    public void testCreatorCanManageTemplate() {
+        Supplier<AuthManager> authManager =
+                Mockito.mock(Supplier.class);
+
+        Assert.assertTrue(canManage(authManager, CREATOR));
+        Mockito.verifyNoInteractions(authManager);
+    }
+
+    @Test
+    public void testGlobalAdminCanManageAnotherUsersTemplate() {
+        Assert.assertTrue(canManage(authManager(true, false), "admin"));
+    }
+
+    @Test
+    public void testSpaceManagerCanManageAnotherUsersTemplate() {
+        Assert.assertTrue(canManage(authManager(false, true),
+                                    "space-admin"));
+    }
+
+    @Test
+    public void testUnrelatedUserCannotManageTemplate() {
+        Assert.assertFalse(canManage(authManager(false, false), "member"));
+    }
+
+    private static Supplier<AuthManager> authManager(boolean admin,
+                                                     boolean spaceManager) {
+        AuthManager auth = Mockito.mock(AuthManager.class);
+        Mockito.when(auth.isAdminManager(Mockito.anyString()))
+               .thenReturn(admin);
+        Mockito.when(auth.isSpaceManager(GRAPHSPACE, "space-admin"))
+               .thenReturn(spaceManager);
+        return () -> auth;
+    }
+
+    private static boolean canManage(
+            Supplier<AuthManager> authManager,
+            String username) {
+        return Whitebox.invokeStatic(
+                SchemaTemplateAPI.class,
+                new Class<?>[]{Supplier.class, String.class,
+                               String.class, String.class},
+                "canManage",
+                authManager, GRAPHSPACE, CREATOR, username);
+    }
+}
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
index 1b209c913..37d70af13 100644
--- 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
@@ -20,21 +20,29 @@ package org.apache.hugegraph.unit.auth;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.hugegraph.HugeGraph;
 import org.apache.hugegraph.auth.AuthManager;
 import org.apache.hugegraph.auth.HugeAuthenticator;
 import org.apache.hugegraph.auth.HugeDefaultRole;
 import org.apache.hugegraph.auth.HugeGraphAuthProxy;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.ResourceObject;
 import org.apache.hugegraph.auth.RolePermission;
 import org.apache.hugegraph.auth.UserWithRole;
+import org.apache.hugegraph.backend.cache.Cache;
+import org.apache.hugegraph.backend.id.Id;
 import org.apache.hugegraph.backend.id.IdGenerator;
 import org.apache.hugegraph.config.AuthOptions;
 import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.task.TaskManager;
 import org.apache.hugegraph.task.TaskScheduler;
 import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
 import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.util.RateLimiter;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.core.Filter;
@@ -44,6 +52,7 @@ import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.appender.AbstractAppender;
 import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.config.Property;
+import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.junit.After;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -110,6 +119,131 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         Assert.assertEquals("admin", username);
     }
 
+    @Test
+    public void testRunAsAdminRestoresContext() {
+        HugeAuthenticator.User user = new HugeAuthenticator.User(
+                "test_user",
+                RolePermission.admin()
+        );
+        setContext(new HugeGraphAuthProxy.Context(user));
+
+        HugeGraphAuthProxy.runAsAdmin(() -> {
+            Assert.assertEquals(HugeAuthenticator.USER_ADMIN,
+                                HugeGraphAuthProxy.username());
+        });
+
+        Assert.assertEquals("test_user", HugeGraphAuthProxy.username());
+    }
+
+    @Test
+    public void testRunAsAdminOverridesTaskContext() {
+        HugeAuthenticator.User taskUser = new HugeAuthenticator.User(
+                "task_user",
+                RolePermission.admin()
+        );
+        TaskManager.setContext(taskUser.toJson());
+
+        HugeGraphAuthProxy.runAsAdmin(() -> {
+            Assert.assertEquals(HugeAuthenticator.USER_ADMIN,
+                                HugeGraphAuthProxy.username());
+        });
+
+        Assert.assertEquals("task_user", HugeGraphAuthProxy.username());
+    }
+
+    @Test
+    public void testRunAsAdminRestoresContextAfterException() {
+        HugeAuthenticator.User taskUser = new HugeAuthenticator.User(
+                "task_user",
+                RolePermission.admin()
+        );
+        TaskManager.setContext(taskUser.toJson());
+
+        Assert.assertThrows(RuntimeException.class, () -> {
+            HugeGraphAuthProxy.runAsAdmin(() -> {
+                throw new RuntimeException("expected");
+            });
+        });
+
+        Assert.assertEquals("task_user", HugeGraphAuthProxy.username());
+    }
+
+    @Test
+    public void testRunAsAdminClearsAndRestoresAllContexts() {
+        HugeAuthenticator.User requestUser = new HugeAuthenticator.User(
+                "request_user", RolePermission.admin());
+        HugeAuthenticator.User taskUser = new HugeAuthenticator.User(
+                "task_user", RolePermission.admin());
+        setContext(new HugeGraphAuthProxy.Context(requestUser));
+        HugeGraphAuthProxy.setRequestGraphSpace("request_space");
+        TaskManager.setContext(taskUser.toJson());
+
+        HugeGraphAuthProxy.runAsAdmin(() -> {
+            Assert.assertEquals(HugeAuthenticator.USER_ADMIN,
+                                HugeGraphAuthProxy.username());
+            Assert.assertNull(HugeGraphAuthProxy.getRequestGraphSpace());
+            Assert.assertNull(TaskManager.getContext());
+        });
+
+        Assert.assertEquals("task_user", HugeGraphAuthProxy.username());
+        Assert.assertEquals("request_space",
+                            HugeGraphAuthProxy.getRequestGraphSpace());
+        Assert.assertEquals(taskUser.toJson(), TaskManager.getContext());
+        TaskManager.resetContext();
+        Assert.assertEquals("request_user", HugeGraphAuthProxy.username());
+    }
+
+    @Test
+    public void testRunAsAdminDoesNotPropagateToChildThread()
+            throws InterruptedException {
+        AtomicReference<String> username = new AtomicReference<>();
+
+        HugeGraphAuthProxy.runAsAdmin(() -> {
+            Thread child = new Thread(() -> {
+                username.set(HugeGraphAuthProxy.username());
+            });
+            child.start();
+            try {
+                child.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException(e);
+            }
+        });
+
+        Assert.assertEquals("anonymous", username.get());
+    }
+
+    @Test
+    public void testTraversalStrategyListKeepsAuthProxyAndIsImmutable() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        HugeConfig config = Mockito.mock(HugeConfig.class);
+        AuthManager authManager = Mockito.mock(AuthManager.class);
+        TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
+
+        Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph");
+        Mockito.when(graph.configuration()).thenReturn(config);
+        Mockito.when(graph.authManager()).thenReturn(authManager);
+        Mockito.when(graph.taskScheduler()).thenReturn(scheduler);
+        Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE))
+               .thenReturn(3600L);
+        Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY))
+               .thenReturn(100L);
+        Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE))
+               .thenReturn(1000D);
+
+        GraphTraversalSource traversal =
+                new HugeGraphAuthProxy(graph).traversal();
+        List<?> strategies = traversal.getStrategies().toList();
+        Assert.assertFalse(strategies.isEmpty());
+        strategies.forEach(strategy -> {
+            Assert.assertEquals("TraversalStrategyProxy",
+                                strategy.getClass().getSimpleName());
+        });
+        Assert.assertThrows(UnsupportedOperationException.class,
+                            strategies::clear);
+    }
+
     @Test
     public void testGetContextReturnsNull() {
         // Ensure both TaskManager context and CONTEXTS are null
@@ -223,6 +357,7 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         HugeConfig config = Mockito.mock(HugeConfig.class);
         AuthManager authManager = Mockito.mock(AuthManager.class);
         TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
+        Id storedUserId = IdGenerator.of("stored-user-id");
 
         Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph");
         Mockito.when(graph.configuration()).thenReturn(config);
@@ -235,7 +370,11 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE))
                .thenReturn(1000D);
         Mockito.when(authManager.validateUser("cache_user", "pass"))
-               .thenReturn(new UserWithRole("cache_user"));
+               .thenReturn(new UserWithRole(
+                           storedUserId, "cache_user",
+                           RolePermission.all("hugegraph")));
+        Mockito.when(authManager.validateUser("invalid", "wrong"))
+               .thenReturn(new UserWithRole("invalid"));
         Mockito.when(authManager.createDefaultRole("DEFAULT", "cache_user",
                                                   HugeDefaultRole.ANALYST,
                                                   "hugegraph"))
@@ -244,7 +383,15 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph);
         AuthManager proxyAuthManager = proxy.authManager();
 
+        proxyAuthManager.validateUser("invalid", "wrong");
         proxyAuthManager.validateUser("cache_user", "pass");
+        Cache<Id, RateLimiter> auditLimiters =
+                Whitebox.getInternalState(proxy, "auditLimiters");
+        Assert.assertFalse(auditLimiters.containsKey(
+                IdGenerator.of("invalid")));
+        Assert.assertTrue(auditLimiters.containsKey(
+                IdGenerator.of("cache_user")));
+        Assert.assertFalse(auditLimiters.containsKey(storedUserId));
         proxyAuthManager.validateUser("cache_user", "pass");
         Mockito.verify(authManager, Mockito.times(1))
                .validateUser("cache_user", "pass");
@@ -263,6 +410,7 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         AuthManager authManager = Mockito.mock(AuthManager.class);
         TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
         String token = "cached-token";
+        Id storedUserId = IdGenerator.of("stored-user-id");
 
         Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph");
         Mockito.when(graph.configuration()).thenReturn(config);
@@ -275,11 +423,22 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE))
                .thenReturn(1000D);
         Mockito.when(authManager.validateUser(token))
-               .thenReturn(new UserWithRole("cache_user"));
+               .thenReturn(new UserWithRole(
+                           storedUserId, "cache_user",
+                           RolePermission.all("hugegraph")));
+        Mockito.when(authManager.validateUser("invalid-token"))
+               .thenReturn(new UserWithRole(""));
 
-        AuthManager proxyAuthManager =
-                new HugeGraphAuthProxy(graph).authManager();
+        HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph);
+        AuthManager proxyAuthManager = proxy.authManager();
+        proxyAuthManager.validateUser("invalid-token");
         proxyAuthManager.validateUser(token);
+        Cache<Id, RateLimiter> auditLimiters =
+                Whitebox.getInternalState(proxy, "auditLimiters");
+        Assert.assertEquals(1L, auditLimiters.size());
+        Assert.assertTrue(auditLimiters.containsKey(
+                IdGenerator.of("cache_user")));
+        Assert.assertFalse(auditLimiters.containsKey(storedUserId));
         proxyAuthManager.validateUser(token);
         Mockito.verify(authManager, Mockito.times(1)).validateUser(token);
 
@@ -290,6 +449,76 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         Mockito.verify(authManager, Mockito.times(2)).validateUser(token);
     }
 
+    @Test
+    public void testAuditLimiterUsesUsernameAndDeleteInvalidatesIt() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        HugeConfig config = Mockito.mock(HugeConfig.class);
+        AuthManager authManager = Mockito.mock(AuthManager.class);
+        TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
+        Id storedUserId = IdGenerator.of("stored-user-id");
+        HugeUser storedUser = new HugeUser(storedUserId, "cache_user");
+
+        Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph");
+        Mockito.when(graph.graphSpace()).thenReturn("DEFAULT");
+        Mockito.when(graph.name()).thenReturn("hugegraph");
+        Mockito.when(graph.configuration()).thenReturn(config);
+        Mockito.when(graph.authManager()).thenReturn(authManager);
+        Mockito.when(graph.taskScheduler()).thenReturn(scheduler);
+        Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE))
+               .thenReturn(3600L);
+        Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY))
+               .thenReturn(100L);
+        Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE))
+               .thenReturn(1000D);
+        Mockito.when(authManager.validateUser("cache_user", "pass"))
+               .thenReturn(new UserWithRole(
+                           storedUserId, "cache_user",
+                           RolePermission.all("hugegraph")));
+        Mockito.when(authManager.getUser(storedUserId)).thenReturn(storedUser);
+        Mockito.when(authManager.isAdminManager("custom_admin"))
+               .thenReturn(true);
+
+        HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph);
+        AuthManager proxyAuthManager = proxy.authManager();
+        proxyAuthManager.validateUser("cache_user", "pass");
+        Cache<Id, RateLimiter> auditLimiters =
+                Whitebox.getInternalState(proxy, "auditLimiters");
+        Id usernameKey = IdGenerator.of("cache_user");
+        Assert.assertNotEquals(usernameKey, storedUserId);
+        Assert.assertTrue(auditLimiters.containsKey(
+                usernameKey));
+
+        HugeAuthenticator.User cacheUser = new HugeAuthenticator.User(
+                "cache_user", RolePermission.all("hugegraph"));
+        Whitebox.setInternalState(cacheUser, "userId", storedUserId);
+        Assert.assertEquals(storedUserId, cacheUser.userId());
+        setContext(new HugeGraphAuthProxy.Context(cacheUser));
+        proxy.name();
+
+        Assert.assertEquals(1L, auditLimiters.size());
+        Assert.assertTrue(auditLimiters.containsKey(usernameKey));
+        Assert.assertFalse(auditLimiters.containsKey(storedUserId));
+
+        setContext(new HugeGraphAuthProxy.Context(
+                new HugeAuthenticator.User(
+                        HugeAuthenticator.USER_ADMIN,
+                        RolePermission.admin())));
+        proxyAuthManager.updateUser(storedUser);
+
+        setContext(new HugeGraphAuthProxy.Context(
+                new HugeAuthenticator.User(
+                        "custom_admin",
+                        RolePermission.admin())));
+        proxyAuthManager.updateUser(storedUser);
+        proxyAuthManager.deleteUser(storedUserId);
+
+        Assert.assertEquals(0L, auditLimiters.size());
+        Assert.assertFalse(auditLimiters.containsKey(usernameKey));
+        Assert.assertFalse(auditLimiters.containsKey(storedUserId));
+        Mockito.verify(authManager, Mockito.times(2)).updateUser(storedUser);
+        Mockito.verify(authManager).deleteUser(storedUserId);
+    }
+
     @Test
     public void testProxyOverridesEveryScopedDefaultMethod() throws Exception {
         HugeGraph graph = Mockito.mock(HugeGraph.class);
@@ -366,6 +595,83 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest {
         }
     }
 
+    @Test
+    public void testSpaceMemberDoesNotGrantMutationPermissions() {
+        RolePermission role = RolePermission.fromJson(
+                "{\"roles\":{\"DEFAULT\":{\"*\":{" +
+                "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}," +
+                "\"SPACE_MEMBER\":{\"ALL\":[{\"type\":\"ALL\"}]}" +
+                "}}}}");
+        HugeAuthenticator.RequiredPerm read =
+                new HugeAuthenticator.RequiredPerm()
+                        .graphSpace("DEFAULT")
+                        .owner("hugegraph")
+                        .action("read");
+        HugeAuthenticator.RequiredPerm write =
+                new HugeAuthenticator.RequiredPerm()
+                        .graphSpace("DEFAULT")
+                        .owner("hugegraph")
+                        .action("write");
+        HugeAuthenticator.RequiredPerm delete =
+                new HugeAuthenticator.RequiredPerm()
+                        .graphSpace("DEFAULT")
+                        .owner("hugegraph")
+                        .action("delete");
+
+        Assert.assertTrue(HugeAuthenticator.RolePerm.matchApiRequiredPerm(
+                role, read));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.matchApiRequiredPerm(
+                role, write));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.matchApiRequiredPerm(
+                role, delete));
+    }
+
+    @Test
+    public void testSpaceManagerCanManageUserGrantInOwnSpace() {
+        RolePermission managerRole = RolePermission.fromJson(
+                "{\"roles\":{\"space-a\":{\"*\":{" +
+                "\"SPACE\":{\"ALL\":[{\"type\":\"ALL\"}]}" +
+                "}}}}");
+        RolePermission memberGrant = RolePermission.fromJson(
+                "{\"roles\":{\"space-a\":{\"*\":{" +
+                "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}," +
+                "\"WRITE\":{\"ALL\":[{\"type\":\"ALL\"}]}" +
+                "}}}}");
+        RolePermission otherSpaceGrant = RolePermission.fromJson(
+                "{\"roles\":{\"space-b\":{\"*\":{" +
+                "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}," +
+                "\"WRITE\":{\"ALL\":[{\"type\":\"ALL\"}]}" +
+                "}}}}");
+        RolePermission multiSpaceGrant = RolePermission.fromJson(
+                "{\"roles\":{" +
+                "\"space-a\":{\"*\":{" +
+                "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}}}," +
+                "\"space-b\":{\"*\":{" +
+                "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}}}" +
+                "}}");
+        HugeUser member = new HugeUser("member");
+        ResourceObject<?> ownSpace =
+                ResourceObject.of("space-a", "hugegraph", member);
+        ResourceObject<?> otherSpace =
+                ResourceObject.of("space-b", "hugegraph", member);
+        ResourceObject<?> admin =
+                ResourceObject.of("space-a", "hugegraph",
+                                  new HugeUser(HugeAuthenticator.USER_ADMIN));
+
+        Assert.assertTrue(HugeAuthenticator.RolePerm.match(
+                managerRole, memberGrant, ownSpace));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.match(
+                managerRole, memberGrant, otherSpace));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.match(
+                managerRole, otherSpaceGrant, ownSpace));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.match(
+                managerRole, multiSpaceGrant, ownSpace));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.match(
+                managerRole, memberGrant, admin));
+        Assert.assertFalse(HugeAuthenticator.RolePerm.match(
+                managerRole, RolePermission.admin(), ownSpace));
+    }
+
     private static class TestAppender extends AbstractAppender {
 
         private final List<LogEvent> events;
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java
new file mode 100644
index 000000000..f056c9291
--- /dev/null
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.unit.core;
+
+import org.apache.hugegraph.HugeException;
+import org.apache.hugegraph.StandardHugeGraph;
+import org.apache.hugegraph.backend.cache.CachedSchemaTransactionV2;
+import org.apache.hugegraph.backend.store.BackendStore;
+import org.apache.hugegraph.backend.store.BackendStoreProvider;
+import org.apache.hugegraph.backend.tx.ISchemaTransaction;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.task.TaskScheduler;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.LockUtil;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+public class StandardHugeGraphClearBackendTest extends BaseUnitTest {
+
+    private static final String SPACE_GRAPH = "space-graph";
+
+    private StandardHugeGraph graph;
+    private BackendStoreProvider provider;
+    private CachedSchemaTransactionV2 schemaTransaction;
+
+    @Before
+    public void setup() {
+        HugeConfig config = FakeObjects.newConfig();
+        this.graph = Mockito.mock(StandardHugeGraph.class,
+                                  Mockito.CALLS_REAL_METHODS);
+        this.provider = Mockito.mock(BackendStoreProvider.class);
+        this.schemaTransaction = Mockito.mock(CachedSchemaTransactionV2.class);
+        BackendStore schemaStore = Mockito.mock(BackendStore.class);
+        BackendStore systemStore = Mockito.mock(BackendStore.class);
+        BackendStore graphStore = Mockito.mock(BackendStore.class);
+        TaskScheduler scheduler = Mockito.mock(TaskScheduler.class);
+
+        Whitebox.setInternalState(this.graph, "configuration", config);
+        Whitebox.setInternalState(this.graph, "storeProvider", this.provider);
+        Whitebox.setInternalState(this.graph, "name", "graph");
+        Whitebox.setInternalState(this.graph, "graphSpace", "space");
+
+        Mockito.doReturn(scheduler).when(this.graph).taskScheduler();
+        Mockito.doReturn(this.schemaTransaction)
+               .when(this.graph).schemaTransaction();
+        Mockito.when(this.provider.isHstore()).thenReturn(true);
+        Mockito.when(this.provider.loadSchemaStore(config))
+               .thenReturn(schemaStore);
+        Mockito.when(this.provider.loadSystemStore(config))
+               .thenReturn(systemStore);
+        Mockito.when(this.provider.loadGraphStore(config))
+               .thenReturn(graphStore);
+        LockUtil.init(SPACE_GRAPH);
+    }
+
+    @After
+    public void teardown() {
+        LockUtil.destroy(SPACE_GRAPH);
+    }
+
+    @Test
+    public void testHstoreClearSchemaBeforeStore() {
+        this.graph.clearBackend();
+
+        InOrder order = Mockito.inOrder(this.schemaTransaction, this.provider);
+        order.verify(this.schemaTransaction).clear();
+        order.verify(this.provider).clear();
+    }
+
+    @Test
+    public void testHstoreSchemaFailureStopsStoreClear() {
+        Mockito.doThrow(new HugeException("schema clear failed"))
+               .when(this.schemaTransaction).clear();
+
+        Assert.assertThrows(HugeException.class, this.graph::clearBackend);
+        Mockito.verify(this.provider, Mockito.never()).clear();
+    }
+
+    @Test
+    public void testHstoreStoreFailurePropagates() {
+        Mockito.doThrow(new HugeException("store clear failed"))
+               .when(this.provider).clear();
+
+        Assert.assertThrows(HugeException.class, this.graph::clearBackend);
+        Mockito.verify(this.schemaTransaction).clear();
+    }
+
+    @Test
+    public void testHstoreRejectsUnexpectedSchemaTransaction() {
+        ISchemaTransaction unexpected = Mockito.mock(ISchemaTransaction.class);
+        Mockito.doReturn(unexpected).when(this.graph).schemaTransaction();
+
+        Assert.assertThrows(IllegalStateException.class,
+                            this.graph::clearBackend);
+        Mockito.verify(this.provider, Mockito.never()).clear();
+    }
+
+    @Test
+    public void testRocksdbDoesNotClearV2SchemaMetadata() {
+        Mockito.when(this.provider.isHstore()).thenReturn(false);
+
+        this.graph.clearBackend();
+
+        Mockito.verify(this.schemaTransaction, Mockito.never()).clear();
+        Mockito.verify(this.provider).clear();
+    }
+}
diff --git a/style/checkstyle.xml b/style/checkstyle.xml
index eec890ec2..d028e10b2 100644
--- a/style/checkstyle.xml
+++ b/style/checkstyle.xml
@@ -27,7 +27,7 @@
         <property name="eachLine" value="true"/>
     </module>
     <module name="LineLength">
-        <property name="max" value="100"/>
+        <property name="max" value="120"/>
         <property name="ignorePattern" value="^package.*|^import.*|a 
href|href|http://|https://|ftp://|Reflection\.*"/>
     </module>
     <module name="RegexpSingleline">

Reply via email to