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

imbajin pushed a commit to branch cx/bump-server-api-version
in repository https://gitbox.apache.org/repos/asf/hugegraph.git

commit ed1310c55fb4d46fe90cdb764863e05dbbf903ff
Author: dark <[email protected]>
AuthorDate: Mon Aug 17 10:01:40 2026 +0800

    feat(server): support GraphSpace-wide observer
    
    - persist observer access against the all-graphs target
    - apply read-only access to existing and future graphs
    - migrate and remove legacy graph-scoped observer grants
    - document the GraphSpace-wide contract in API version 0.72
---
 .../org/apache/hugegraph/api/auth/ManagerAPI.java  | 14 +++-
 .../apache/hugegraph/api/space/GraphSpaceAPI.java  | 37 +++++++---
 .../org/apache/hugegraph/version/ApiVersion.java   |  2 +-
 .../hugegraph/auth/StandardAuthManagerV2.java      | 10 ++-
 .../unit/api/space/GraphSpaceAPITest.java          | 81 +++++++++++++++++++++-
 5 files changed, 127 insertions(+), 17 deletions(-)

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..5989d4889 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,8 @@ 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);
         }
@@ -301,6 +300,15 @@ public class ManagerAPI extends API {
         } else {
             result = authManager.isDefaultRole(graphSpace, user,
                                                defaultRole);
+            if (!result && defaultRole.equals(HugeDefaultRole.OBSERVER)) {
+                for (String currentGraph : manager.graphs(graphSpace)) {
+                    if (authManager.isDefaultRole(
+                            graphSpace, currentGraph, user, defaultRole)) {
+                        result = true;
+                        break;
+                    }
+                }
+            }
         }
         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..934508ed3 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,8 @@ 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);
         }
@@ -164,6 +162,12 @@ public class GraphSpaceAPI extends API {
             result.put("graph", graph);
         } else {
             authManager.createSpaceDefaultRole(name, user, role);
+            if (role.equals(HugeDefaultRole.OBSERVER)) {
+                for (String currentGraph : manager.graphs(name)) {
+                    authManager.deleteDefaultRole(
+                            name, user, role, currentGraph);
+                }
+            }
         }
 
         return manager.serializer().writeMap(result);
@@ -203,9 +207,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);
         }
@@ -217,6 +220,15 @@ public class GraphSpaceAPI extends API {
         } else {
             result = authManager.isDefaultRole(name, user,
                                                defaultRole);
+            if (!result && defaultRole.equals(HugeDefaultRole.OBSERVER)) {
+                for (String currentGraph : manager.graphs(name)) {
+                    if (authManager.isDefaultRole(
+                            name, currentGraph, user, defaultRole)) {
+                        result = true;
+                        break;
+                    }
+                }
+            }
         }
         return manager.serializer().writeMap(ImmutableMap.of("check", result));
     }
@@ -259,9 +271,8 @@ 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);
         }
@@ -269,6 +280,12 @@ public class GraphSpaceAPI extends API {
             authManager.deleteDefaultRole(name, user, defaultRole, graph);
         } else {
             authManager.deleteDefaultRole(name, user, defaultRole);
+            if (defaultRole.equals(HugeDefaultRole.OBSERVER)) {
+                for (String currentGraph : manager.graphs(name)) {
+                    authManager.deleteDefaultRole(
+                            name, user, defaultRole, currentGraph);
+                }
+            }
         }
     }
 
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 faadd1f5a..00e8dad03 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,7 +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] Support GraphSpace default-role management APIs
+     * [0.72] Support GraphSpace-wide default-role management APIs
      */
 
     /**
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..aaf2a9df1 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,7 +1815,10 @@ 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
@@ -1828,7 +1831,10 @@ public class StandardAuthManagerV2 implements 
AuthManager {
     @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-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..6315f3ae7 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,81 @@ 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 testCurrentUserObserverCheckFallsBackToLegacyGraphRole() {
+        ManagerAPI api = new ManagerAPI();
+        GraphManager manager = managerWithDefaultRoleContext(TARGET, false);
+        AuthManager auth = manager.authManager();
+        Mockito.when(auth.isDefaultRole(
+                     GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER))
+               .thenReturn(false);
+        setContext(TARGET);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE,
+                                             "OBSERVER", null);
+
+        Assert.assertContains("\"check\":true", result);
+        Mockito.verify(auth).isDefaultRole(
+                GRAPHSPACE, GRAPH, TARGET, HugeDefaultRole.OBSERVER);
+    }
+
+    @Test
+    public void testObserverCheckFallsBackToLegacyGraphRole() {
+        GraphSpaceAPI api = new GraphSpaceAPI();
+        GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+        AuthManager auth = manager.authManager();
+        Mockito.when(auth.isDefaultRole(
+                     GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER))
+               .thenReturn(false);
+        setContext(ADMIN);
+
+        String result = api.checkDefaultRole(manager, GRAPHSPACE, TARGET,
+                                             "OBSERVER", null);
+
+        Assert.assertContains("\"check\":true", result);
+        Mockito.verify(auth).isDefaultRole(
+                GRAPHSPACE, GRAPH, TARGET, HugeDefaultRole.OBSERVER);
+    }
+
+    @Test
+    public void testObserverDeleteCleansSpaceAndLegacyGraphRoles() {
+        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).deleteDefaultRole(
+                GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER, GRAPH);
+    }
+
     @Test
     public void testManagerDefaultRoleRejectsMissingGraphSpace() {
         ManagerAPI api = new ManagerAPI();
@@ -191,6 +266,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 +284,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<>();

Reply via email to