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... strategyClasses) { - return this.strategies.removeStrategies(strategyClasses); + for (Class 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 wrapper = + GraphManager.ConsumerWrapper.wrap(value -> { + callbackUser.set(HugeGraphAuthProxy.username()); + callbackSpace.set( + HugeGraphAuthProxy.getRequestGraphSpace()); + throw new RuntimeException("expected"); + }); + + wrapper.accept(new Object()); + + Assert.assertEquals(HugeAuthenticator.USER_ADMIN, + callbackUser.get()); + Assert.assertNull(callbackSpace.get()); + Assert.assertEquals("anonymous", HugeGraphAuthProxy.username()); + Assert.assertNull(HugeGraphAuthProxy.getRequestGraphSpace()); + + callbackUser.set(null); + GraphManager.ConsumerWrapper.wrap(value -> { + callbackUser.set(HugeGraphAuthProxy.username()); + callbackSpace.set(HugeGraphAuthProxy.getRequestGraphSpace()); + }).accept(new Object()); + Assert.assertEquals(HugeAuthenticator.USER_ADMIN, + callbackUser.get()); + Assert.assertNull(callbackSpace.get()); + Assert.assertEquals("anonymous", HugeGraphAuthProxy.username()); + Assert.assertNull(HugeGraphAuthProxy.getRequestGraphSpace()); + } finally { + TaskManager.resetContext(); + HugeGraphAuthProxy.resetContext(); + } + } + @Test public void testGetContextReturnsNull() { // Ensure both TaskManager context and CONTEXTS are null @@ -223,6 +360,7 @@ public void testDefaultRoleMutationInvalidatesUserRoleCache() 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 +373,11 @@ public void testDefaultRoleMutationInvalidatesUserRoleCache() 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 +386,15 @@ public void testDefaultRoleMutationInvalidatesUserRoleCache() HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph); AuthManager proxyAuthManager = proxy.authManager(); + proxyAuthManager.validateUser("invalid", "wrong"); proxyAuthManager.validateUser("cache_user", "pass"); + Cache 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 +413,7 @@ public void testLogoutInvalidatesTokenRoleCache() throws Exception { 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 +426,22 @@ public void testLogoutInvalidatesTokenRoleCache() throws Exception { 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 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 +452,60 @@ public void testLogoutInvalidatesTokenRoleCache() throws Exception { Mockito.verify(authManager, Mockito.times(2)).validateUser(token); } + @Test + public void testDeleteUserInvalidatesUsernameAuditLimiter() { + 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.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 auditLimiters = + Whitebox.getInternalState(proxy, "auditLimiters"); + Assert.assertTrue(auditLimiters.containsKey( + IdGenerator.of("cache_user"))); + + 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.assertFalse(auditLimiters.containsKey( + IdGenerator.of("cache_user"))); + 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); @@ -322,6 +538,47 @@ public void testProxyOverridesEveryScopedDefaultMethod() throws Exception { } } + @Test + public void testProxyEvaluatesDefaultRoleGraphsLocally() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + AuthManager origin = Mockito.mock(AuthManager.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + HugeBelong observer = new HugeBelong( + "DEFAULT", IdGenerator.of("alice"), + IdGenerator.of("deleted_graph_observer")); + + 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(origin); + 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(origin.listBelongByUser( + "DEFAULT", IdGenerator.of("alice"), -1L)) + .thenReturn(Collections.singletonList(observer)); + Mockito.when(origin.isSpaceManager("DEFAULT", "space_manager")) + .thenReturn(true); + HugeAuthenticator.User spaceManager = new HugeAuthenticator.User( + "space_manager", RolePermission.role( + "DEFAULT", "hugegraph", HugePermission.SPACE)); + setContext(new HugeGraphAuthProxy.Context(spaceManager)); + + AuthManager proxy = new HugeGraphAuthProxy(graph).authManager(); + Assert.assertEquals(Collections.singleton("deleted_graph"), + proxy.listDefaultRoleGraphs( + "DEFAULT", "alice", + HugeDefaultRole.OBSERVER)); + Mockito.verify(origin, Mockito.never()).listDefaultRoleGraphs( + Mockito.anyString(), Mockito.anyString(), Mockito.any()); + } + @Test public void testValidateUserDoesNotLogBearerToken() { String token = "secret-proxy-bearer-token"; @@ -366,6 +623,369 @@ public void testValidateUserDoesNotLogBearerToken() { } } + @Test + public void testTraversalPermissions() throws Exception { + Traversal.Admin read = __.V().asAdmin(); + Assert.assertTrue(traversalPermissions(read).isEmpty()); + + Traversal.Admin write = + __.addV("person").property("name", "marko").asAdmin(); + Assert.assertEquals(Collections.singleton(HugePermission.WRITE), + traversalPermissions(write)); + + Traversal.Admin delete = __.V().drop().asAdmin(); + Assert.assertEquals(Collections.singleton(HugePermission.DELETE), + traversalPermissions(delete)); + + Traversal.Admin parent = + __.V().sideEffect(__.addE("knows")).asAdmin(); + Assert.assertEquals(Collections.singleton(HugePermission.WRITE), + traversalPermissions(parent)); + + Traversal.Admin lambda = __.V().map(t -> t.get()).asAdmin(); + Assert.assertEquals(Set.of(HugePermission.WRITE, + HugePermission.DELETE), + traversalPermissions(lambda)); + + Comparator comparator = (left, right) -> 0; + Traversal.Admin ordered = + __.V().order().by(comparator).asAdmin(); + Assert.assertEquals(Set.of(HugePermission.WRITE, + HugePermission.DELETE), + traversalPermissions(ordered)); + + Traversal.Admin orderedByKey = + __.V().order().by("name").asAdmin(); + Assert.assertTrue(traversalPermissions(orderedByKey).isEmpty()); + + Traversal.Admin customPredicate = + __.V().has("name", P.test((left, right) -> true, + "marko")).asAdmin(); + Assert.assertEquals(Set.of(HugePermission.WRITE, + HugePermission.DELETE), + traversalPermissions(customPredicate)); + + Traversal.Admin wherePredicate = + __.V().as("person").where(P.test( + (left, right) -> true, "other")).asAdmin(); + Assert.assertEquals(Set.of(HugePermission.WRITE, + HugePermission.DELETE), + traversalPermissions(wherePredicate)); + + Traversal.Admin builtInPredicate = + __.V().has("name", P.eq("marko")).asAdmin(); + Assert.assertTrue(traversalPermissions(builtInPredicate).isEmpty()); + + Traversal.Admin hugeGraphPredicate = + __.V().has("tags", ConditionP.contains("graph")).asAdmin(); + Assert.assertTrue(traversalPermissions(hugeGraphPredicate).isEmpty()); + + Traversal.Admin negatedHugeGraphPredicate = + __.V().has("tags", P.not(ConditionP.contains("graph"))) + .asAdmin(); + Assert.assertTrue(traversalPermissions(negatedHugeGraphPredicate) + .isEmpty()); + + P disguisedPredicate = new P(Compare.eq, "marko") { + + private static final long serialVersionUID = 1L; + + @Override + public boolean test(Object value) { + return true; + } + }; + Traversal.Admin disguised = + __.V().has("name", disguisedPredicate).asAdmin(); + Assert.assertEquals(Set.of(HugePermission.WRITE, + HugePermission.DELETE), + traversalPermissions(disguised)); + } + + @Test + public void testExecuteOnlyUserCannotApplyLambdaMutationTraversal() { + 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.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); + + RolePermission executeOnly = RolePermission.role( + "DEFAULT", "hugegraph", HugePermission.EXECUTE); + HugeAuthenticator.User user = new HugeAuthenticator.User( + "execute_only", executeOnly); + setContext(new HugeGraphAuthProxy.Context(user)); + Traversal.Admin traversal = __.V().sideEffect(t -> { + t.get().property("k", "v"); + }).asAdmin(); + List> + strategies = new HugeGraphAuthProxy(graph).traversal() + .getStrategies() + .toList(); + + Assert.assertThrows(ForbiddenException.class, () -> { + strategies.forEach(strategy -> strategy.apply(traversal)); + }); + } + + @Test + public void testTraversalStrategyListKeepsAuthProxy() { + 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 testTraversalStrategiesCannotRemoveAuthChecks() { + 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); + + TraversalStrategies strategies = + new HugeGraphAuthProxy(graph).traversal().getStrategies(); + Assert.assertSame(strategies, strategies.addStrategies()); + strategies.clone().toList().forEach(strategy -> { + Assert.assertEquals("TraversalStrategyProxy", + strategy.getClass().getSimpleName()); + }); + TraversalStrategies origin = Whitebox.getInternalState(strategies, + "strategies"); + @SuppressWarnings("unchecked") + Class ordinary = origin.toList().stream() + .filter(strategy -> !strategy.getClass().getSimpleName() + .equals("AuthorizationStrategy")) + .map(strategy -> + (Class) strategy.getClass()) + .findFirst().orElseThrow(AssertionError::new); + Assert.assertSame(strategies, strategies.removeStrategies(ordinary)); + Assert.assertFalse(origin.getStrategy(ordinary).isPresent()); + + @SuppressWarnings("unchecked") + Class authorization = origin.toList().stream() + .filter(strategy -> strategy.getClass().getSimpleName() + .equals("AuthorizationStrategy")) + .map(strategy -> + (Class) strategy.getClass()) + .findFirst().orElseThrow(AssertionError::new); + Assert.assertThrows(UnsupportedOperationException.class, + () -> strategies.removeStrategies(authorization)); + Assert.assertFalse(strategies.toList().isEmpty()); + } + + @Test + public void testAddedStrategyCannotAppendUncheckedCallback() { + 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("added-strategy-graph"); + 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); + HugeAuthenticator.User user = new HugeAuthenticator.User( + "execute_only", RolePermission.role( + "DEFAULT", "hugegraph", HugePermission.EXECUTE)); + setContext(new HugeGraphAuthProxy.Context(user)); + + HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph); + GraphTraversalSource first = proxy.traversal(); + GraphTraversalSource second = proxy.traversal(); + TraversalStrategies strategies = first.getStrategies(); + TraversalStrategy.VerificationStrategy appended = traversal -> + traversal.addStep(new LambdaSideEffectStep<>( + traversal, value -> { + // The callback is intentionally unscoped. + })); + strategies.addStrategies(appended); + TraversalStrategies secondOrigin = Whitebox.getInternalState( + second.getStrategies(), "strategies"); + Assert.assertFalse(secondOrigin.getStrategy(appended.getClass()) + .isPresent()); + Traversal.Admin traversal = __.V().asAdmin(); + + Assert.assertThrows(ForbiddenException.class, () -> + strategies.toList().forEach(strategy -> + strategy.apply(traversal))); + } + + @Test + public void testSafeTraversalRequirementsRemainReadOnly() { + 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.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); + HugeAuthenticator.User reader = new HugeAuthenticator.User( + "read_only", RolePermission.role( + "DEFAULT", "hugegraph", HugePermission.READ)); + setContext(new HugeGraphAuthProxy.Context(reader)); + + GraphTraversalSource source = new HugeGraphAuthProxy(graph).traversal(); + Traversal.Admin withBulk = source.withBulk(false).V().asAdmin(); + Traversal.Admin withPath = source.withPath().V().asAdmin(); + Traversal.Admin withOption = + source.with("evaluationTimeout", 1000L).V().asAdmin(); + + withBulk.applyStrategies(); + withPath.applyStrategies(); + withOption.applyStrategies(); + } + + @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.assertTrue(HugeAuthenticator.RolePerm.match( + managerRole, multiSpaceGrant, ownSpace)); + Assert.assertFalse(HugeAuthenticator.RolePerm.match( + managerRole, memberGrant, admin)); + Assert.assertFalse(HugeAuthenticator.RolePerm.match( + managerRole, RolePermission.admin(), ownSpace)); + } + + @SuppressWarnings("unchecked") + private static Set traversalPermissions( + Traversal.Admin traversal) throws Exception { + Method method = HugeGraphAuthProxy.class.getDeclaredMethod( + "traversalPermissions", Traversal.Admin.class); + method.setAccessible(true); + return (Set) method.invoke(null, traversal); + } + private static class TestAppender extends AbstractAppender { private final List 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 0000000000..a0f6bb3f5a --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java @@ -0,0 +1,117 @@ +/* + * 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.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 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 eec890ec25..d028e10b20 100644 --- a/style/checkstyle.xml +++ b/style/checkstyle.xml @@ -27,7 +27,7 @@ - +