diff --git a/.editorconfig b/.editorconfig
index 04a6e64a9f..c64c7bae2b 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 fa804e260c..447162d67f 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 ada012be80..b1be078d91 100644
--- a/.github/workflows/docker-build-ci.yml
+++ b/.github/workflows/docker-build-ci.yml
@@ -24,10 +24,17 @@ 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/**'
+ - 'install-dist/**'
jobs:
docker-build:
@@ -47,7 +54,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 +86,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|.*\([^<]*\).*|\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/.github/workflows/hbase-docker-build-ci.yml b/.github/workflows/hbase-docker-build-ci.yml
new file mode 100644
index 0000000000..aaa20abe33
--- /dev/null
+++ b/.github/workflows/hbase-docker-build-ci.yml
@@ -0,0 +1,78 @@
+#
+# 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.
+#
+
+name: "HBase Docker Build CI"
+
+on:
+ push:
+ branches:
+ - master
+ - 'release-*'
+ paths:
+ - '.github/workflows/hbase-docker-build-ci.yml'
+ - 'docker/hbase/**'
+ pull_request:
+ paths:
+ - '.github/workflows/hbase-docker-build-ci.yml'
+ - 'docker/hbase/**'
+
+jobs:
+ docker-build:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Validate HBase entrypoint and config
+ run: |
+ bash -n docker/hbase/entrypoint.sh
+ python3 -c 'import xml.etree.ElementTree as ET; ET.parse("docker/hbase/hbase-site.xml")'
+
+ - name: Build HBase image
+ run: |
+ IMAGE_ID=$(docker build -q docker/hbase)
+ echo "Built: $IMAGE_ID"
+ ENTRYPOINT=$(docker inspect --format='{{json .Config.Entrypoint}}' "$IMAGE_ID")
+ echo "Entrypoint: $ENTRYPOINT"
+ [[ "$ENTRYPOINT" == '["/entrypoint.sh"]' ]] || {
+ echo "ERROR: unexpected HBase entrypoint: $ENTRYPOINT"
+ exit 1
+ }
+
+ CONTAINER_ID=$(docker run -d --hostname hbase "$IMAGE_ID")
+ cleanup() {
+ docker logs "$CONTAINER_ID" || true
+ docker rm -f "$CONTAINER_ID" || true
+ }
+ trap cleanup EXIT
+
+ for attempt in $(seq 1 240); do
+ if docker logs "$CONTAINER_ID" 2>&1 | grep -Fq "HBase is ready."; then
+ docker exec "$CONTAINER_ID" bash -lc \
+ 'echo "status '\''simple'\''" | "$HBASE_HOME/bin/hbase" shell -n' \
+ | grep -E -q \
+ '([1-9][0-9]*[[:space:]]+live[[:space:]]+servers|[1-9][0-9]*[[:space:]]+servers|servers:[[:space:]]*[1-9])'
+ exit 0
+ fi
+ if [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_ID")" != "true" ]]; then
+ echo "ERROR: HBase container exited before readiness"
+ exit 1
+ fi
+ sleep 2
+ done
+ echo "ERROR: HBase container did not become ready within 480 seconds"
+ exit 1
diff --git a/.serena/memories/code_style_and_conventions.md b/.serena/memories/code_style_and_conventions.md
index 159920cd3b..7a4c310e0b 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 2d6e81b15b..07daf17662 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 adf9792776..f4543e073f 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-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
index 0c24860929..ab4292c312 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
@@ -19,15 +19,21 @@
import static org.apache.hugegraph.ct.base.ClusterConstant.CONF_DIR;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.FileUtils;
import org.apache.hugegraph.ct.base.HGTestLogger;
import org.apache.hugegraph.ct.config.ClusterConfig;
import org.apache.hugegraph.ct.config.GraphConfig;
import org.apache.hugegraph.ct.config.PDConfig;
import org.apache.hugegraph.ct.config.ServerConfig;
import org.apache.hugegraph.ct.config.StoreConfig;
+import org.apache.hugegraph.ct.node.BaseNodeWrapper;
import org.apache.hugegraph.ct.node.PDNodeWrapper;
import org.apache.hugegraph.ct.node.ServerNodeWrapper;
import org.apache.hugegraph.ct.node.StoreNodeWrapper;
@@ -39,6 +45,8 @@
@Slf4j
public abstract class AbstractEnv implements BaseEnv {
+ private static final long NODE_START_TIMEOUT_MINUTES = 5L;
+ private static final int START_LOG_TAIL_LINES = 80;
private static final Logger LOG = HGTestLogger.ENV_LOG;
protected ClusterConfig clusterConfig;
@@ -87,36 +95,64 @@ protected void init(int pdCnt, int storeCnt, int serverCnt) {
}
public void startCluster() {
- for (PDNodeWrapper pdNodeWrapper : pdNodeWrappers) {
- pdNodeWrapper.start();
- while (!pdNodeWrapper.isStarted()) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ try {
+ for (PDNodeWrapper pdNodeWrapper : pdNodeWrappers) {
+ pdNodeWrapper.start();
+ this.waitUntilStarted(pdNodeWrapper);
+ }
+ for (StoreNodeWrapper storeNodeWrapper : storeNodeWrappers) {
+ storeNodeWrapper.start();
+ this.waitUntilStarted(storeNodeWrapper);
+ }
+ for (ServerNodeWrapper serverNodeWrapper : serverNodeWrappers) {
+ serverNodeWrapper.start();
+ this.waitUntilStarted(serverNodeWrapper);
}
+ } catch (RuntimeException | Error e) {
+ this.stopCluster();
+ throw e;
}
- for (StoreNodeWrapper storeNodeWrapper : storeNodeWrappers) {
- storeNodeWrapper.start();
- while (!storeNodeWrapper.isStarted()) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ }
+
+ private void waitUntilStarted(BaseNodeWrapper node) {
+ long deadline = System.nanoTime() +
+ TimeUnit.MINUTES.toNanos(NODE_START_TIMEOUT_MINUTES);
+ while (!node.isStarted()) {
+ if (!node.isAlive()) {
+ throw this.startupFailure(node, "exited before startup");
+ }
+ if (System.nanoTime() >= deadline) {
+ throw this.startupFailure(node, String.format(
+ "did not start within %s minutes",
+ NODE_START_TIMEOUT_MINUTES));
+ }
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
}
}
- for (ServerNodeWrapper serverNodeWrapper : serverNodeWrappers) {
- serverNodeWrapper.start();
- while (!serverNodeWrapper.isStarted()) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ }
+
+ private AssertionError startupFailure(BaseNodeWrapper node, String reason) {
+ StringBuilder message = new StringBuilder(String.format(
+ "Node '%s' %s; startup log: '%s'",
+ node.getID(), reason, node.getLogPath()));
+ try {
+ List lines = FileUtils.readLines(
+ new File(node.getLogPath()), StandardCharsets.UTF_8);
+ int from = Math.max(0, lines.size() - START_LOG_TAIL_LINES);
+ message.append(System.lineSeparator()).append("Startup log tail:");
+ for (String line : lines.subList(from, lines.size())) {
+ message.append(System.lineSeparator()).append(line);
}
+ } catch (IOException e) {
+ message.append(System.lineSeparator())
+ .append("Failed to read startup log: ")
+ .append(e.getMessage());
}
+ return new AssertionError(message.toString());
}
public void stopCluster() {
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
index a89c614c4c..a2a4f31f82 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
@@ -70,8 +70,8 @@ public void start() {
String pdNodeJarPath = getFileInDir(workPath, PD_JAR_PREFIX);
startCmd.addAll(Arrays.asList(
"-Dname=HugeGraphPD" + this.index,
- "-Xms512m",
- "-Xmx4g",
+ "-Xms128m",
+ "-Xmx512m",
"-XX:+HeapDumpOnOutOfMemoryError",
"-XX:HeapDumpPath=" + configPath + "logs",
"-Dlog4j.configurationFile=" + configPath + File.separator +
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
index e16b96781e..475efc502b 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
@@ -124,6 +124,8 @@ public void start() {
startCmd.addAll(Arrays.asList(
"-Dname=HugeGraphServer" + this.index,
+ "-Xms128m",
+ "-Xmx1g",
"--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED",
"--add-modules=jdk.unsupported",
"--add-exports=java.base/sun.nio.ch=ALL-UNNAMED",
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
index 1cb0f67eae..bf20745abe 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
@@ -70,8 +70,8 @@ public void start() {
"-Dlog4j.configurationFile=" + configPath + CONF_DIR
+ File.separator + "log4j2.xml",
"-Dfastjson.parser.safeMode=true",
- "-Xms512m",
- "-Xmx2048m",
+ "-Xms128m",
+ "-Xmx512m",
"-XX:MetaspaceSize=256M",
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
diff --git a/hugegraph-commons/hugegraph-common/src/main/resources/version.properties b/hugegraph-commons/hugegraph-common/src/main/resources/version.properties
index 2dffc6f3a6..8d48ef39ca 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 3f01b902ea..514bd989a1 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-server/Dockerfile b/hugegraph-server/Dockerfile
index 5caadd23cb..44bc9aa515 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 7cd64e8f3b..5d4d96d773 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 f1a8b918bd..e5a81be208 100644
--- a/hugegraph-server/hugegraph-api/pom.xml
+++ b/hugegraph-server/hugegraph-api/pom.xml
@@ -201,8 +201,8 @@
-
- 0.71.0.0
+
+ 0.72.0.0
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 37aee8c657..6a7d04c972 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,20 +287,28 @@ public String checkDefaultRole(@Context GraphManager manager,
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);
}
boolean result;
if (hasGraph) {
- result = authManager.isDefaultRole(graphSpace, graph, user,
- defaultRole);
+ result = authManager.isDefaultRole(graphSpace, graph, user, defaultRole);
+ if (!result) {
+ result = authManager.isDefaultRole(graphSpace, user,
+ defaultRole);
+ }
} else {
- result = authManager.isDefaultRole(graphSpace, user,
- defaultRole);
+ 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/auth/UserAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
index 7504ad7325..c2790458d6 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java
@@ -22,7 +22,10 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter.Status;
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.RolePermission;
import org.apache.hugegraph.backend.id.Id;
import org.apache.hugegraph.backend.id.IdGenerator;
import org.apache.hugegraph.core.GraphManager;
@@ -153,8 +156,14 @@ public String role(@Context GraphManager manager,
@PathParam("id") String id) {
LOG.debug("GraphSpace [{}] get user role: {}", graphSpace, id);
- HugeUser user = manager.authManager().getUser(IdGenerator.of(id));
- return manager.authManager().rolePermission(user).toJson();
+ AuthManager authManager = manager.authManager();
+ HugeUser user = authManager.getUser(IdGenerator.of(id));
+ RolePermission role = authManager.rolePermission(user);
+ String operator = HugeGraphAuthProxy.username();
+ if (authManager.isAdminManager(operator)) {
+ return role.toJson();
+ }
+ return role.toJson(graphSpace);
}
@DELETE
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 81f13cf3f0..dd43261353 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
@@ -21,6 +21,7 @@
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -146,10 +147,7 @@ public String setDefaultRole(@Context GraphManager manager,
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 String setDefaultRole(@Context GraphManager manager,
result.put("graph", graph);
} else {
authManager.createSpaceDefaultRole(name, user, role);
+ if (role.equals(HugeDefaultRole.OBSERVER)) {
+ for (String currentGraph : observerGraphs(
+ manager, authManager, name, user, role)) {
+ authManager.deleteDefaultRole(name, user, role, currentGraph);
+ }
+ }
}
return manager.serializer().writeMap(result);
@@ -203,20 +207,28 @@ public String checkDefaultRole(@Context GraphManager manager,
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);
}
boolean result;
if (hasGraph) {
- result = authManager.isDefaultRole(name, graph, user,
- defaultRole);
+ result = authManager.isDefaultRole(name, graph, user, defaultRole);
+ if (!result) {
+ result = authManager.isDefaultRole(name, user, defaultRole);
+ }
} else {
- result = authManager.isDefaultRole(name, user,
- defaultRole);
+ 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,19 +271,34 @@ public void deleteDefaultRole(@Context GraphManager manager,
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);
}
if (hasGraph) {
authManager.deleteDefaultRole(name, user, defaultRole, graph);
} else {
+ if (defaultRole.equals(HugeDefaultRole.OBSERVER)) {
+ for (String currentGraph : observerGraphs(
+ manager, authManager, name, user, defaultRole)) {
+ authManager.deleteDefaultRole(name, user, defaultRole, currentGraph);
+ }
+ }
authManager.deleteDefaultRole(name, user, defaultRole);
}
}
+ private static Set observerGraphs(GraphManager manager,
+ AuthManager authManager,
+ String graphSpace,
+ String owner,
+ HugeDefaultRole role) {
+ Set graphs = new LinkedHashSet<>(manager.graphs(graphSpace));
+ graphs.addAll(authManager.listDefaultRoleGraphs(graphSpace, owner,
+ role));
+ return graphs;
+ }
+
@GET
@Timed
@Path("profile")
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 b2c151687c..cffca156cd 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 @@
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 void delete(@Context GraphManager manager,
"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 String update(@Context GraphManager manager,
}
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 String update(@Context GraphManager manager,
}
+ private static boolean canManage(Supplier 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 cef1287b14..4bf0edf086 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 @@ private static Object matchedAction(HugePermission action,
}
for (Map.Entry 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,15 @@ public static boolean match(Object role, RolePermission grant,
}
}
- RolePermission rolePerm = RolePermission.fromJson(role);
- return rolePerm.contains(grant);
+ RolePermission grantedRole = RolePermission.fromJson(grant);
+ RolePerm rolePerm = RolePerm.fromJson(role);
+ if (resourceObject != null &&
+ !RolePermission.isAdmin(grantedRole) &&
+ grantedRole.roles().containsKey(resourceObject.graphSpace()) &&
+ rolePerm.matchSpace(resourceObject.graphSpace(), "space")) {
+ return true;
+ }
+ return RolePermission.fromJson(role).contains(grantedRole);
}
@SuppressWarnings({"unchecked", "rawtypes"})
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 4b0aed578f..610b6841fc 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
@@ -22,6 +22,8 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -48,6 +50,7 @@
import org.apache.hugegraph.backend.cache.CacheManager;
import org.apache.hugegraph.backend.id.Id;
import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.backend.query.Condition;
import org.apache.hugegraph.backend.query.Query;
import org.apache.hugegraph.backend.store.BackendFeatures;
import org.apache.hugegraph.backend.store.BackendStoreInfo;
@@ -80,6 +83,7 @@
import org.apache.hugegraph.task.TaskManager;
import org.apache.hugegraph.task.TaskScheduler;
import org.apache.hugegraph.task.TaskStatus;
+import org.apache.hugegraph.traversal.optimize.ConditionP;
import org.apache.hugegraph.traversal.optimize.HugeScriptTraversal;
import org.apache.hugegraph.type.HugeType;
import org.apache.hugegraph.type.Nameable;
@@ -91,12 +95,36 @@
import org.apache.tinkerpop.gremlin.process.computer.GraphComputer;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode.Instruction;
+import org.apache.tinkerpop.gremlin.process.traversal.Compare;
+import org.apache.tinkerpop.gremlin.process.traversal.Contains;
+import org.apache.tinkerpop.gremlin.process.traversal.Order;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Script;
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.Text;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.step.ComparatorHolder;
+import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
+import org.apache.tinkerpop.gremlin.process.traversal.step.LambdaHolder;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DropStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.WherePredicateStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStartStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStep;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.RequirementsStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator;
+import org.apache.tinkerpop.gremlin.process.traversal.util.AndP;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
+import org.apache.tinkerpop.gremlin.process.traversal.util.OrP;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
@@ -106,6 +134,7 @@
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.io.Io;
import org.slf4j.Logger;
+import org.javatuples.Pair;
import com.alipay.remoting.rpc.RpcServer;
@@ -154,15 +183,32 @@ static Context setContext(Context context) {
}
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 +224,27 @@ public static void setRequestGraphSpace(String graphSpace) {
REQUEST_GRAPH_SPACE.set(graphSpace);
}
- public static Context setAdmin() {
- Context old = getContext();
- AuthContext.useAdmin();
- return old;
+ public static void runAsAdmin(Runnable runnable) {
+ String old = AuthContext.getContext();
+ try {
+ AuthContext.setContext(User.ADMIN.toJson());
+ runnable.run();
+ } finally {
+ if (old == null) {
+ AuthContext.resetContext();
+ } else {
+ AuthContext.setContext(old);
+ }
+ }
}
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();
@@ -1546,7 +1606,8 @@ public Id updateUser(HugeUser updatedUser) {
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 +1621,12 @@ public HugeUser deleteUser(Id id) {
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 +2070,12 @@ public UserWithRole validateUser(String username, String password) {
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 +2092,12 @@ public UserWithRole validateUser(String token) {
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;
@@ -2229,6 +2299,23 @@ public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole r
}
}
+ @Override
+ public Set listDefaultRoleGraphs(String graphSpace,
+ String owner,
+ HugeDefaultRole role) {
+ // Evaluate this new compatibility helper locally. Forwarding the
+ // default method would require an upgraded remote auth provider.
+ String operator = HugeGraphAuthProxy.username();
+ if (!this.authManager.isAdminManager(operator) &&
+ !this.authManager.isSpaceManager(graphSpace, operator)) {
+ throw new ForbiddenException(
+ "Permission denied: manage graphspace roles");
+ }
+ List belongs = this.authManager.listBelongByUser(
+ graphSpace, IdGenerator.of(owner), -1L);
+ return AuthManager.defaultRoleGraphs(belongs, owner, role);
+ }
+
@Override
public String loginUser(String username, String password) {
return this.loginUser(username, password, -1L);
@@ -2300,12 +2387,14 @@ public void remove(String key) {
class GraphTraversalSourceProxy extends GraphTraversalSource {
public GraphTraversalSourceProxy(Graph graph) {
- super(graph);
+ super(graph, TraversalStrategies.GlobalCache
+ .getStrategies(graph.getClass())
+ .clone());
}
public GraphTraversalSourceProxy(Graph graph,
TraversalStrategies strategies) {
- super(graph, strategies);
+ super(graph, strategies.clone());
}
@Override
@@ -2323,11 +2412,18 @@ class TraversalStrategiesProxy implements TraversalStrategies {
public TraversalStrategiesProxy(TraversalStrategies strategies) {
this.strategies = strategies;
+ if (this.strategies != null &&
+ !this.strategies.getStrategy(AuthorizationStrategy.class)
+ .isPresent()) {
+ this.strategies.addStrategies(new AuthorizationStrategy(false));
+ }
}
@Override
public List> toList() {
- return this.strategies.toList();
+ List> proxies = new ArrayList<>();
+ this.iterator().forEachRemaining(proxies::add);
+ return Collections.unmodifiableList(proxies);
}
@Override
@@ -2341,20 +2437,43 @@ public Iterator> iterator() {
@Override
public TraversalStrategies addStrategies(TraversalStrategy>... strategies) {
- return this.strategies.addStrategies(strategies);
+ boolean unscopedCallbacks = false;
+ for (TraversalStrategy> strategy : strategies) {
+ if (strategy.getClass() != RequirementsStrategy.class &&
+ strategy.getClass() != OptionsStrategy.class) {
+ unscopedCallbacks = true;
+ break;
+ }
+ }
+ if (unscopedCallbacks) {
+ this.strategies.removeStrategies(AuthorizationStrategy.class);
+ this.strategies.addStrategies(new AuthorizationStrategy(true));
+ }
+ this.strategies.addStrategies(strategies);
+ return this;
}
@SuppressWarnings({"unchecked"})
@Override
public TraversalStrategies removeStrategies(
Class extends TraversalStrategy>... strategyClasses) {
- return this.strategies.removeStrategies(strategyClasses);
+ for (Class extends TraversalStrategy> strategyClass :
+ strategyClasses) {
+ if (strategyClass.isAssignableFrom(
+ AuthorizationStrategy.class)) {
+ throw new UnsupportedOperationException(
+ "Can't remove the authorization strategy");
+ }
+ }
+ this.strategies.removeStrategies(strategyClasses);
+ return this;
}
@Override
public TraversalStrategies clone() {
- // CHECKSTYLE:OFF
- return this.strategies.clone();
+ TraversalStrategies cloned = this.strategies == null ?
+ null : this.strategies.clone();
+ return new TraversalStrategiesProxy(cloned);
}
@SuppressWarnings("unused")
@@ -2373,6 +2492,38 @@ private String translate(Bytecode bytecode) {
}
return sb.toString();
}
+
+ private final class AuthorizationStrategy
+ implements TraversalStrategy.VerificationStrategy {
+
+ private static final long serialVersionUID = -2724020627962254984L;
+ private final boolean unscopedCallbacks;
+
+ private AuthorizationStrategy(boolean unscopedCallbacks) {
+ this.unscopedCallbacks = unscopedCallbacks;
+ }
+
+ @Override
+ public void apply(Traversal.Admin, ?> traversal) {
+ // Authorization is performed by TraversalStrategyProxy.
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public Set>
+ applyPrior() {
+ Set result = new HashSet<>();
+ for (TraversalStrategy> strategy :
+ TraversalStrategiesProxy.this.strategies.toList()) {
+ if (strategy instanceof
+ TraversalStrategy.VerificationStrategy &&
+ !(strategy instanceof AuthorizationStrategy)) {
+ result.add(strategy.getClass());
+ }
+ }
+ return result;
+ }
+ }
}
private final class TraversalStrategyProxy>
@@ -2414,6 +2565,18 @@ public void apply(Traversal.Admin, ?> traversal) {
*/
String caller = Thread.currentThread().getName();
if (!caller.contains(TraversalStrategiesProxy.REST_WORKER)) {
+ Set permissions = traversalPermissions(traversal);
+ if (this.origin instanceof
+ TraversalStrategiesProxy.AuthorizationStrategy &&
+ ((TraversalStrategiesProxy.AuthorizationStrategy)
+ this.origin).unscopedCallbacks) {
+ permissions.add(HugePermission.WRITE);
+ permissions.add(HugePermission.DELETE);
+ }
+ for (HugePermission permission : permissions) {
+ verifyNamePermission(permission, ResourceType.GREMLIN,
+ script);
+ }
verifyNamePermission(HugePermission.EXECUTE,
ResourceType.GREMLIN, script);
}
@@ -2461,4 +2624,106 @@ public String toString() {
return this.origin.toString();
}
}
+
+ private static Set traversalPermissions(
+ Traversal.Admin, ?> traversal) {
+ Set permissions = EnumSet.noneOf(HugePermission.class);
+ collectTraversalPermissions(traversal, permissions);
+ return permissions;
+ }
+
+ private static void collectTraversalPermissions(
+ Traversal.Admin, ?> traversal,
+ Set permissions) {
+ for (Step, ?> step : traversal.getSteps()) {
+ if (step instanceof LambdaHolder || hasUserComparator(step) ||
+ hasUserPredicate(step)) {
+ permissions.add(HugePermission.WRITE);
+ permissions.add(HugePermission.DELETE);
+ }
+ if (step instanceof AddVertexStartStep ||
+ step instanceof AddVertexStep ||
+ step instanceof AddEdgeStartStep ||
+ step instanceof AddEdgeStep ||
+ step instanceof AddPropertyStep) {
+ permissions.add(HugePermission.WRITE);
+ } else if (step instanceof DropStep) {
+ permissions.add(HugePermission.DELETE);
+ }
+ if (step instanceof TraversalParent) {
+ TraversalParent parent = (TraversalParent) step;
+ for (Traversal.Admin, ?> child : parent.getLocalChildren()) {
+ collectTraversalPermissions(child, permissions);
+ }
+ for (Traversal.Admin, ?> child : parent.getGlobalChildren()) {
+ collectTraversalPermissions(child, permissions);
+ }
+ }
+ }
+ }
+
+ private static boolean hasUserPredicate(Step, ?> step) {
+ if (step instanceof HasContainerHolder) {
+ HasContainerHolder holder = (HasContainerHolder) step;
+ for (org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer
+ container : holder.getHasContainers()) {
+ if (hasUserPredicate(container.getPredicate())) {
+ return true;
+ }
+ }
+ }
+ if (step instanceof IsStep) {
+ return hasUserPredicate(((IsStep>) step).getPredicate());
+ }
+ if (step instanceof WherePredicateStep) {
+ return ((WherePredicateStep>) step).getPredicate()
+ .map(HugeGraphAuthProxy::hasUserPredicate)
+ .orElse(false);
+ }
+ return false;
+ }
+
+ private static boolean hasUserPredicate(P> predicate) {
+ if (predicate instanceof ConnectiveP) {
+ if (predicate.getClass() != AndP.class &&
+ predicate.getClass() != OrP.class) {
+ return true;
+ }
+ for (P> child : ((ConnectiveP>) predicate).getPredicates()) {
+ if (hasUserPredicate(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (predicate.getClass() == ConditionP.class) {
+ // ConditionP has no public constructor and only wraps predicates
+ // created by HugeGraph, including its safe negate() override.
+ return false;
+ }
+ if (predicate.getClass() != P.class &&
+ predicate.getClass() != TextP.class) {
+ return true;
+ }
+ Object biPredicate = predicate.getBiPredicate();
+ return !(biPredicate instanceof Compare ||
+ biPredicate instanceof Contains ||
+ biPredicate instanceof Text ||
+ biPredicate instanceof Condition.RelationType);
+ }
+
+ private static boolean hasUserComparator(Step, ?> step) {
+ if (!(step instanceof ComparatorHolder)) {
+ return false;
+ }
+ ComparatorHolder, ?> holder = (ComparatorHolder, ?>) step;
+ for (Object entry : holder.getComparators()) {
+ @SuppressWarnings("rawtypes")
+ Object comparator = ((Pair) entry).getValue1();
+ if (!(comparator instanceof Order)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
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 96717e7240..82ed30a71a 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 HugeGraph createGraph(String graphSpace, String name, String creator,
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 HugeGraph createGraph(String graphSpace, String name, String creator,
String schemas = this.schemaTemplate(graphSpace, schema).schema();
prepareSchema(graph, schemas);
}
- if (grpcThread) {
- HugeGraphAuthProxy.resetContext();
- }
return graph;
}
@@ -2434,21 +2428,20 @@ public static ConsumerWrapper wrap(Consumer consumer) {
@Override
public void accept(T t) {
- boolean grpcThread = false;
try {
- grpcThread = Thread.currentThread().getName().contains("grpc");
- if (grpcThread) {
- HugeGraphAuthProxy.setAdmin();
- }
- consumer.accept(t);
+ resetListenerContext();
+ HugeGraphAuthProxy.runAsAdmin(() -> this.consumer.accept(t));
} catch (Throwable e) {
LOG.error("Listener exception occurred.", e);
} finally {
- if (grpcThread) {
- HugeGraphAuthProxy.resetContext();
- }
+ resetListenerContext();
}
}
+
+ private static void resetListenerContext() {
+ TaskManager.resetContext();
+ HugeGraphAuthProxy.resetContext();
+ }
}
private void graphAddHandler(T response) {
@@ -2498,11 +2491,6 @@ private void graphAddHandler(T response) {
// 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 7e314f9ed6..00e8dad032 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] 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 6f66b4b8a9..bdb04ab6d1 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,9 @@ public void clearBackend() {
LockUtil.lock(this.spaceGraphName(), LockUtil.GRAPH_LOCK);
try {
+ if (this.isHstore()) {
+ ((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/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
index d1c8887238..c285b223c0 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java
@@ -18,6 +18,7 @@
package org.apache.hugegraph.auth;
import java.util.Date;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -26,6 +27,7 @@
import org.apache.hugegraph.auth.SchemaDefine.AuthElement;
import org.apache.hugegraph.backend.id.Id;
+import org.apache.hugegraph.backend.id.IdGenerator;
public interface AuthManager {
@@ -279,4 +281,34 @@ void deleteDefaultRole(String graphSpace, String owner,
void deleteDefaultRole(String graphSpace, String owner,
HugeDefaultRole role, String graph);
+
+ default Set listDefaultRoleGraphs(String graphSpace, String owner,
+ HugeDefaultRole role) {
+ return defaultRoleGraphs(this.listBelongByUser(
+ graphSpace, IdGenerator.of(owner), -1L),
+ owner, role);
+ }
+
+ static Set defaultRoleGraphs(List belongs,
+ String owner,
+ HugeDefaultRole role) {
+ String suffix = "_" + role;
+ Set graphs = new LinkedHashSet<>();
+ for (HugeBelong belong : belongs) {
+ if (!owner.equals(belong.source().asString())) {
+ continue;
+ }
+ String roleName = belong.target().asString();
+ if (!roleName.endsWith(suffix) ||
+ roleName.length() <= suffix.length()) {
+ continue;
+ }
+ String graph = roleName.substring(
+ 0, roleName.length() - suffix.length());
+ if (!"*".equals(graph)) {
+ graphs.add(graph);
+ }
+ }
+ return graphs;
+ }
}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java
index 74577c258e..2ee0f73187 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java
@@ -296,7 +296,9 @@ public static boolean allowed(ResourceObject> resourceObject) {
}
public static HugeResource parseResource(String resource) {
- return JsonUtil.fromJson(resource, HugeResource.class);
+ HugeResource hugeResource = JsonUtil.fromJson(resource, HugeResource.class);
+ hugeResource.checkFormat();
+ return hugeResource;
}
public boolean matchProperties(HugeResource other) {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RolePermission.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RolePermission.java
index 43ad50887d..27391d62f2 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RolePermission.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RolePermission.java
@@ -127,6 +127,18 @@ public String toJson() {
return JsonUtil.toJson(this);
}
+ public String toJson(String graphSpace) {
+ Map>>>> scopedRoles =
+ new TreeMap<>();
+ Map>>>
+ graphSpaceRoles = this.roles.get(graphSpace);
+ if (graphSpaceRoles != null) {
+ scopedRoles.put(graphSpace, graphSpaceRoles);
+ }
+ return new RolePermission(scopedRoles).toJson();
+ }
+
public static RolePermission fromJson(Object json) {
RolePermission role;
if (json instanceof String) {
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 1f34aa4593..aaf2a9df17 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 Id createSpaceDefaultRole(String graphSpace, String owner,
@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 boolean isDefaultRole(String graphSpace, String graph,
@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 99a393f6b9..74986b2e99 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 void clear() {
// Clear schema info firstly
super.clear();
this.clearCache(false);
+ this.notifySchemaCacheClear();
}
private static final class SchemaCaches {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
index e41a0df706..efdd5bc0d1 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
@@ -51,4 +51,10 @@ public static ConditionP eq(Object value) {
// EQ that can compare two array
return new ConditionP(Condition.RelationType.EQ, value);
}
+
+ @Override
+ public ConditionP negate() {
+ return new ConditionP(this.getBiPredicate().negate(),
+ this.getOriginalValue());
+ }
}
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 1733680e3f..c18e0c1ed6 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
@@ -27,10 +27,12 @@
import org.apache.hugegraph.meta.managers.AuthMetaManagerTest;
import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest;
import org.apache.hugegraph.unit.api.auth.LoginAPITest;
+import org.apache.hugegraph.unit.api.auth.UserAPITest;
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;
@@ -51,6 +53,7 @@
import org.apache.hugegraph.unit.core.LocksTableTest;
import org.apache.hugegraph.unit.core.PageStateTest;
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;
@@ -98,6 +101,7 @@
/* api filter */
LoadDetectFilterTest.class,
LoginAPITest.class,
+ UserAPITest.class,
PathFilterTest.class,
/* api gremlin */
@@ -110,6 +114,7 @@
/* api space */
GraphSpaceAPITest.class,
+ SchemaTemplateAPITest.class,
/* cache */
CacheTest.RamCacheTest.class,
@@ -139,6 +144,7 @@
AnalyzerTest.class,
BackendMutationTest.class,
ConditionTest.class,
+ StandardHugeGraphClearBackendTest.class,
ConditionQueryFlattenTest.class,
QueryTest.class,
RangeTest.class,
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/UserAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/UserAPITest.java
new file mode 100644
index 0000000000..957bcf0e81
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/UserAPITest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.auth;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.apache.hugegraph.api.auth.UserAPI;
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeGraphAuthProxy;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.core.GraphManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.junit.After;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import sun.misc.Unsafe;
+
+public class UserAPITest extends BaseUnitTest {
+
+ private static final String SPACE_A = "space-a";
+ private static final String SPACE_B = "space-b";
+ private static final String OPERATOR = "space-manager";
+ private static final String TARGET = "target-user";
+
+ @After
+ public void tearDown() {
+ HugeGraphAuthProxy.resetContext();
+ }
+
+ @Test
+ public void testSpaceManagerReadsOnlyRequestedGraphSpaceRole() {
+ AuthManager auth = Mockito.mock(AuthManager.class);
+ GraphManager manager = managerWithAuthManager(auth);
+ HugeUser target = new HugeUser(TARGET);
+ target.id(IdGenerator.of(TARGET));
+ Mockito.when(auth.getUser(target.id())).thenReturn(target);
+ Mockito.when(auth.rolePermission(target)).thenReturn(multiSpaceRole());
+ Mockito.when(auth.isAdminManager(OPERATOR)).thenReturn(false);
+ setContext(OPERATOR);
+
+ String result = new UserAPI().role(manager, SPACE_A,
+ target.id().asString());
+
+ Assert.assertContains(SPACE_A, result);
+ Assert.assertFalse(result.contains(SPACE_B));
+ }
+
+ @Test
+ public void testGlobalAdminReadsCompleteRole() {
+ AuthManager auth = Mockito.mock(AuthManager.class);
+ GraphManager manager = managerWithAuthManager(auth);
+ HugeUser target = new HugeUser(TARGET);
+ target.id(IdGenerator.of(TARGET));
+ Mockito.when(auth.getUser(target.id())).thenReturn(target);
+ Mockito.when(auth.rolePermission(target)).thenReturn(multiSpaceRole());
+ Mockito.when(auth.isAdminManager(HugeAuthenticator.USER_ADMIN))
+ .thenReturn(true);
+ setContext(HugeAuthenticator.USER_ADMIN);
+
+ String result = new UserAPI().role(manager, SPACE_A,
+ target.id().asString());
+
+ Assert.assertContains(SPACE_A, result);
+ Assert.assertContains(SPACE_B, result);
+ }
+
+ private static RolePermission multiSpaceRole() {
+ return RolePermission.fromJson(
+ "{\"roles\":{" +
+ "\"space-a\":{\"*\":{" +
+ "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}}}," +
+ "\"space-b\":{\"*\":{" +
+ "\"READ\":{\"ALL\":[{\"type\":\"ALL\"}]}}}" +
+ "}}");
+ }
+
+ private static GraphManager managerWithAuthManager(AuthManager auth) {
+ try {
+ Field field = Unsafe.class.getDeclaredField("theUnsafe");
+ field.setAccessible(true);
+ Unsafe unsafe = (Unsafe) field.get(null);
+ GraphManager manager = (GraphManager) unsafe.allocateInstance(
+ GraphManager.class);
+ HugeAuthenticator authenticator = Mockito.mock(HugeAuthenticator.class);
+ Mockito.when(authenticator.authManager()).thenReturn(auth);
+ Whitebox.setInternalState(manager, "authenticator", authenticator);
+ return manager;
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static void setContext(String username) {
+ try {
+ HugeAuthenticator.User user = new HugeAuthenticator.User(
+ username, RolePermission.admin());
+ HugeGraphAuthProxy.Context context =
+ new HugeGraphAuthProxy.Context(user);
+ Method method = HugeGraphAuthProxy.class.getDeclaredMethod(
+ "setContext", HugeGraphAuthProxy.Context.class);
+ method.setAccessible(true);
+ method.invoke(null, context);
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+}
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 caa659a4d4..b3e2ca324f 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
@@ -22,9 +22,11 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hugegraph.HugeGraph;
import org.apache.hugegraph.api.auth.ManagerAPI;
@@ -32,10 +34,12 @@
import org.apache.hugegraph.api.space.GraphSpaceAPI;
import org.apache.hugegraph.auth.AuthManager;
import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeBelong;
import org.apache.hugegraph.auth.HugeDefaultRole;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugeUser;
import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.backend.id.IdGenerator;
import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.meta.MetaManager;
import org.apache.hugegraph.space.GraphSpace;
@@ -58,6 +62,7 @@ public class GraphSpaceAPITest extends BaseUnitTest {
private static final String OPERATOR = "space_manager";
private static final String TARGET = "target_user";
private static final String GRAPH = "hugegraph";
+ private static final String GRAPH_2 = "hugegraph2";
@After
public void tearDown() {
@@ -87,6 +92,207 @@ public void testAdminCanCheckSpaceDefaultRole() {
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 testGraphObserverCheckAcceptsSpaceWideObserverRole() {
+ 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 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 testCurrentUserGraphCheckAcceptsSpaceWideObserverRole() {
+ 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 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);
+
+ org.mockito.InOrder order = Mockito.inOrder(auth);
+ order.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER, GRAPH);
+ order.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+ }
+
+ @Test
+ public void testObserverDeleteCleansDeletedGraphLegacyRole() {
+ GraphSpaceAPI api = new GraphSpaceAPI();
+ GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+ AuthManager auth = manager.authManager();
+ Mockito.when(auth.listDefaultRoleGraphs(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER))
+ .thenReturn(Collections.singleton("deleted_graph"));
+ setContext(ADMIN);
+
+ api.deleteDefaultRole(manager, GRAPHSPACE, TARGET, "OBSERVER", null);
+
+ Mockito.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER, GRAPH);
+ Mockito.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER,
+ "deleted_graph");
+ Mockito.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+ }
+
+ @Test
+ public void testAuthManagerFindsPersistedObserverGraphs() {
+ AuthManager auth = Mockito.mock(AuthManager.class,
+ Mockito.CALLS_REAL_METHODS);
+ HugeBelong deletedGraph = new HugeBelong(
+ GRAPHSPACE, IdGenerator.of(TARGET),
+ IdGenerator.of("deleted_graph_observer"));
+ HugeBelong umbrella = new HugeBelong(
+ GRAPHSPACE, IdGenerator.of(TARGET),
+ IdGenerator.of("*_observer"));
+ HugeBelong otherOwner = new HugeBelong(
+ GRAPHSPACE, IdGenerator.of("other"),
+ IdGenerator.of("other_graph_observer"));
+ Mockito.when(auth.listBelongByUser(GRAPHSPACE,
+ IdGenerator.of(TARGET), -1L))
+ .thenReturn(List.of(deletedGraph, umbrella, otherOwner));
+
+ Set result = auth.listDefaultRoleGraphs(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+
+ Assert.assertEquals(Collections.singleton("deleted_graph"), result);
+ Mockito.verify(auth, Mockito.never()).listAllBelong(
+ Mockito.anyString(), Mockito.anyLong());
+ }
+
+ @Test
+ public void testObserverDeleteKeepsSpaceRoleWhenLegacyCleanupFails() {
+ GraphSpaceAPI api = new GraphSpaceAPI();
+ GraphManager manager = managerWithDefaultRoleContext(ADMIN, true);
+ AuthManager auth = manager.authManager();
+ MetaManager metaManager = Whitebox.getInternalState(manager,
+ "metaManager");
+ Mockito.when(metaManager.graphConfigs(GRAPHSPACE))
+ .thenReturn(Map.of(
+ GRAPHSPACE + "-" + GRAPH, Collections.emptyMap(),
+ GRAPHSPACE + "-" + GRAPH_2, Collections.emptyMap()));
+ AtomicInteger legacyDeletes = new AtomicInteger();
+ Mockito.doAnswer(invocation -> {
+ if (legacyDeletes.incrementAndGet() == 2) {
+ throw new RuntimeException("expected");
+ }
+ return null;
+ }).when(auth).deleteDefaultRole(
+ Mockito.eq(GRAPHSPACE), Mockito.eq(TARGET),
+ Mockito.eq(HugeDefaultRole.OBSERVER), Mockito.anyString());
+ setContext(ADMIN);
+
+ Assert.assertThrows(RuntimeException.class, () -> {
+ api.deleteDefaultRole(manager, GRAPHSPACE, TARGET,
+ "OBSERVER", null);
+ });
+
+ Mockito.verify(auth, Mockito.times(2)).deleteDefaultRole(
+ Mockito.eq(GRAPHSPACE), Mockito.eq(TARGET),
+ Mockito.eq(HugeDefaultRole.OBSERVER), Mockito.anyString());
+ Mockito.verify(auth, Mockito.never()).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+
+ api.deleteDefaultRole(manager, GRAPHSPACE, TARGET,
+ "OBSERVER", null);
+
+ Mockito.verify(auth, Mockito.times(4)).deleteDefaultRole(
+ Mockito.eq(GRAPHSPACE), Mockito.eq(TARGET),
+ Mockito.eq(HugeDefaultRole.OBSERVER), Mockito.anyString());
+ Mockito.verify(auth).deleteDefaultRole(
+ GRAPHSPACE, TARGET, HugeDefaultRole.OBSERVER);
+ }
+
@Test
public void testManagerDefaultRoleRejectsMissingGraphSpace() {
ManagerAPI api = new ManagerAPI();
@@ -191,6 +397,9 @@ private static GraphManager managerWithDefaultRoleContext(String operator,
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 +415,8 @@ private static GraphManager managerWithDefaultRoleContext(String operator,
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 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 0000000000..617957fa5f
--- /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 =
+ Mockito.mock(Supplier.class);
+
+ Assert.assertTrue(canManage(authManager, CREATOR));
+ Mockito.verifyZeroInteractions(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(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,
+ 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 1b209c9139..456eeafd3d 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
@@ -19,22 +19,36 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
+import java.util.Set;
+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.HugeBelong;
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.core.GraphManager;
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.traversal.optimize.ConditionP;
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,10 +58,21 @@
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.Compare;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.LambdaSideEffectStep;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.junit.After;
import org.junit.Test;
import org.mockito.Mockito;
+import jakarta.ws.rs.ForbiddenException;
+
public class HugeGraphAuthProxyTest extends BaseUnitTest {
private static HugeGraphAuthProxy.Context setContext(
@@ -110,6 +135,118 @@ public void testUsernameWithAdminUser() {
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 testRunAsAdminDoesNotPropagateToChildThread()
+ throws InterruptedException {
+ AtomicReference 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 testListenerConsumerClearsStaleContextAfterException() {
+ AtomicReference callbackUser = new AtomicReference<>();
+ AtomicReference callbackSpace = new AtomicReference<>();
+ try {
+ HugeAuthenticator.User staleUser = new HugeAuthenticator.User(
+ "stale_user", RolePermission.admin());
+ TaskManager.setContext(staleUser.toJson());
+ setContext(new HugeGraphAuthProxy.Context(staleUser));
+ HugeGraphAuthProxy.setRequestGraphSpace("stale_space");
+ GraphManager.ConsumerWrapper