Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
52035da
chore(server): bump REST API version
imbajin Aug 16, 2026
88a762a
fix(server): package current reactor artifacts
imbajin Aug 16, 2026
3d9d954
fix(pd): validate raft peer addresses
imbajin Aug 17, 2026
ed1310c
feat(server): support GraphSpace-wide observer
imbajin Aug 17, 2026
0752ec2
fix(server): enforce Gremlin mutations
imbajin Aug 17, 2026
ddfec94
fix(server): inspect traversal mutation steps
imbajin Aug 17, 2026
3d23131
fix(server): proxy copied traversal strategies
imbajin Aug 17, 2026
eae9401
fix(server): isolate GraphSpace membership
imbajin Aug 17, 2026
e2d7fff
fix(server): prepare audit limiter after login
imbajin Aug 17, 2026
7a57568
fix(server): scope metadata callback admin
imbajin Aug 17, 2026
006a2b3
fix(server): scope space manager user access
imbajin Aug 17, 2026
2f844ed
fix(server): honor custom admin mutations
imbajin Aug 17, 2026
ca0478a
fix(server): allow admin template management
imbajin Aug 17, 2026
d8699ae
refactor(style): set line width to 120
imbajin Aug 18, 2026
28641f5
fix(server): preserve anonymous template ownership
imbajin Aug 18, 2026
8c69441
fix(server): guard merge traversal writes
imbajin Aug 18, 2026
17615dc
fix(server): scope merge step detection
imbajin Aug 18, 2026
8d03d9c
fix(server): verify merge traversal guards
imbajin Aug 18, 2026
2ed446b
fix(server): isolate merge test fixtures
imbajin Aug 18, 2026
b94347b
fix(server): bind merge fixtures to tinkerpop
imbajin Aug 18, 2026
f38b4ad
fix(server): align merge fixture API
imbajin Aug 18, 2026
6f78df5
chore(server): remove speculative merge guards
imbajin Aug 18, 2026
7083242
chore(ci): upgrade dependency review
imbajin Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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}]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 43 additions & 4 deletions .github/workflows/docker-build-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Important: Removing the old **/Dockerfile* filter leaves the repository's docker/hbase/** tree outside every pull_request.paths entry, although it still contains a Dockerfile, entrypoint, and HBase configuration. Changes to that image will no longer trigger Docker Build CI and can merge without any image validation. Please retain docker/hbase/** in the trigger or add a dedicated HBase build job with checks appropriate to that image.

- 'hugegraph-cluster-test/**'
- 'hugegraph-pd/**'
- 'hugegraph-store/**'
- 'hugegraph-struct/**'
- 'hugegraph-server/**'
- 'install-dist/**'

jobs:
docker-build:
Expand All @@ -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")
Expand Down Expand Up @@ -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|.*<Implementation-Version>\([^<]*\)</Implementation-Version>.*|\1|p' \
hugegraph-server/hugegraph-api/pom.xml)
ACTUAL_MANIFEST=$(unzip -p "$API_JAR" META-INF/MANIFEST.MF |
sed -n 's/^Implementation-Version: *//p' | tr -d '\r')
EXPECTED_PROPERTY=$(sed -n 's/^ApiVersion=//p' \
hugegraph-commons/hugegraph-common/src/main/resources/version.properties)
ACTUAL_PROPERTY=$(unzip -p "$COMMON_JAR" version.properties |
sed -n 's/^ApiVersion=//p' | tr -d '\r')

[[ "$ACTUAL_MANIFEST" == "$EXPECTED_MANIFEST" ]] || {
echo "ERROR: API manifest is $ACTUAL_MANIFEST; expected $EXPECTED_MANIFEST"
exit 1
}
[[ "$ACTUAL_PROPERTY" == "$EXPECTED_PROPERTY" ]] || {
echo "ERROR: API property is $ACTUAL_PROPERTY; expected $EXPECTED_PROPERTY"
exit 1
}
2 changes: 1 addition & 1 deletion .serena/memories/code_style_and_conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion hugegraph-pd/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*`)

Expand Down
5 changes: 3 additions & 2 deletions hugegraph-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions hugegraph-server/Dockerfile-hstore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions hugegraph-server/hugegraph-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@
</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!-- TODO: update it -->
<Implementation-Version>0.71.0.0</Implementation-Version>
<!-- Bump when the public REST API contract changes -->
<Implementation-Version>0.72.0.0</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,20 +287,24 @@ 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);
} 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,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);
}
Expand All @@ -164,6 +161,11 @@ public String setDefaultRole(@Context GraphManager manager,
result.put("graph", graph);
} else {
authManager.createSpaceDefaultRole(name, user, role);
if (role.equals(HugeDefaultRole.OBSERVER)) {
for (String currentGraph : manager.graphs(name)) {
authManager.deleteDefaultRole(name, user, role, currentGraph);
}
}
}

return manager.serializer().writeMap(result);
Expand Down Expand Up @@ -203,20 +205,25 @@ 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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ Critical: A space-wide OBSERVER is persisted through createSpaceDefaultRole() under the ALL_GRAPHS marker, but this graph-specific branch checks only isDefaultRole(name, graph, user, defaultRole). A user with the new space-wide role is therefore reported as false when the same check includes a concrete graph parameter; the parallel ManagerAPI path has the same omission. Please treat the ALL_GRAPHS role as covering the requested graph, and add a regression test for both endpoints.

} 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));
}
Expand Down Expand Up @@ -259,16 +266,19 @@ 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 {
authManager.deleteDefaultRole(name, user, defaultRole);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Important: Space-wide OBSERVER deletion removes the ALL_GRAPHS role first and then performs independent per-graph deletes. If any later metadata delete fails, the request returns an error while the global role is already gone and some legacy graph-level grants remain active, leaving authorization state partially migrated. Please make this migration transactional or explicitly idempotent/resumable with compensation, and cover an injected mid-loop failure.

if (defaultRole.equals(HugeDefaultRole.OBSERVER)) {
for (String currentGraph : manager.graphs(name)) {
authManager.deleteDefaultRole(name, user, defaultRole, currentGraph);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand All @@ -180,6 +180,17 @@ public String update(@Context GraphManager manager,

}

private static boolean canManage(Supplier<AuthManager> authManagerSupplier,
String graphSpace, String creator,
String username) {
if (Objects.equals(creator, username)) {
return true;
}
AuthManager authManager = authManagerSupplier.get();
return authManager.isAdminManager(username) ||
authManager.isSpaceManager(graphSpace, username);
}

private static class JsonSchemaTemplate implements Checkable {

@JsonProperty("name")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ private static Object matchedAction(HugePermission action,
}
for (Map.Entry<HugePermission, Object> e : perms.entrySet()) {
HugePermission permission = e.getKey();
if (permission == HugePermission.SPACE ||
permission == HugePermission.SPACE_MEMBER) {
continue;
}
// Maybe required = ANY
if (action.match(permission) ||
action.equals(HugePermission.EXECUTE)) {
Expand Down Expand Up @@ -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 &&

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Important: This new authorization shortcut returns true when the operator is a SPACE manager for the current graph space and the target grant merely contains an entry for that space. UserAPI.role() then serializes the complete rolePermission(user) object, so a space-a manager can read the space-b entries of a multi-space grant. Please return a graph-space-scoped projection (or reject mixed grants) before allowing this path, and add a cross-space response-isolation test.

!RolePermission.isAdmin(grantedRole) &&
grantedRole.roles().containsKey(resourceObject.graphSpace()) &&
rolePerm.matchSpace(resourceObject.graphSpace(), "space")) {
return true;
}
return RolePermission.fromJson(role).contains(grantedRole);
}

@SuppressWarnings({"unchecked", "rawtypes"})
Expand Down
Loading
Loading