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">