Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter;
import org.apache.hugegraph.auth.AuthManager;
import org.apache.hugegraph.auth.HugeDefaultRole;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugePermission;
import org.apache.hugegraph.core.GraphManager;
Expand Down Expand Up @@ -259,6 +262,41 @@ public String getRolesInGs(@Context GraphManager manager,
result));
}

@GET
@Timed
@Path("default")
@Consumes(APPLICATION_JSON)
public String checkDefaultRole(@Context GraphManager manager,
@QueryParam("graphspace") String graphSpace,
@QueryParam("role") String role,
@QueryParam("graph") String graph) {
LOG.debug("check if current user is default role: {} {} {}",
role, graphSpace, graph);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

checkDefaultRole() doesn’t call ensurePdModeEnabled(manager) while the other endpoints in ManagerAPI do. If this API requires PD mode, add the same guard here to avoid inconsistent behavior when PD is disabled (or explicitly handle the non-PD case).

Suggested change
role, graphSpace, graph);
role, graphSpace, graph);
ensurePdModeEnabled(manager);

Copilot uses AI. Check for mistakes.
ensurePdModeEnabled(manager);
AuthManager authManager = manager.authManager();
String user = HugeGraphAuthProxy.username();

E.checkArgument(StringUtils.isNotEmpty(role) &&
StringUtils.isNotEmpty(graphSpace),
"Must pass graphspace and role params");

HugeDefaultRole defaultRole =
HugeDefaultRole.valueOf(role.toUpperCase());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

‼️ Bug: HugeDefaultRole.valueOf() 未做异常处理,非法 role 值返回 500

valueOf(role.toUpperCase()) 在 role 值非法时会抛出 IllegalArgumentException,变成 HTTP 500 而不是 400。同一个 PR 中 GraphSpaceAPI.setDefaultRole() 已经用 try-catch 做了处理,这里应该保持一致:

Suggested change
HugeDefaultRole.valueOf(role.toUpperCase());
HugeDefaultRole defaultRole;
try {
defaultRole = HugeDefaultRole.valueOf(role.toUpperCase());
} catch (IllegalArgumentException e) {
E.checkArgument(false, "Invalid role value '%s'", role);
defaultRole = null; // unreachable
}

boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER);
E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph),
"Must set a graph for observer");

boolean result;
if (hasGraph) {
result = authManager.isDefaultRole(graphSpace, graph, user,
defaultRole);
} else {
result = authManager.isDefaultRole(graphSpace, user,
defaultRole);
}
return manager.serializer().writeMap(ImmutableMap.of("check", result));
}

private void validUser(AuthManager authManager, String user) {
E.checkArgument(authManager.findUser(user) != null ||
authManager.findGroup(user) != null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
package org.apache.hugegraph.api.profile;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.HugeException;
import org.apache.hugegraph.HugeGraph;
import org.apache.hugegraph.api.API;
import org.apache.hugegraph.api.filter.StatusFilter;
import org.apache.hugegraph.auth.AuthManager;
import org.apache.hugegraph.auth.HugeAuthenticator.RequiredPerm;
import org.apache.hugegraph.auth.HugeGraphAuthProxy;
import org.apache.hugegraph.auth.HugePermission;
Expand All @@ -36,6 +40,7 @@
import org.apache.hugegraph.space.GraphSpace;
import org.apache.hugegraph.type.define.GraphMode;
import org.apache.hugegraph.type.define.GraphReadMode;
import org.apache.hugegraph.util.ConfigUtil;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.JsonUtil;
import org.apache.hugegraph.util.Log;
Expand Down Expand Up @@ -74,6 +79,7 @@ public class GraphsAPI extends API {
private static final String CONFIRM_DROP = "I'm sure to drop the graph";
private static final String GRAPH_DESCRIPTION = "description";
private static final String GRAPH_ACTION = "action";
private static final String UPDATE = "update";
private static final String GRAPH_ACTION_RELOAD = "reload";

private static Map<String, Object> convConfig(Map<String, Object> config) {
Expand Down Expand Up @@ -120,6 +126,86 @@ public Object list(@Context GraphManager manager,
return ImmutableMap.of("graphs", filterGraphs);
}

@GET
@Timed
@Path("profile")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space_member", "$dynamic"})
public Object listProfile(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
@Parameter(description = "Filter graphs by name or nickname prefix")
@QueryParam("prefix") String prefix,
@Context SecurityContext sc) {
LOG.debug("List graph profiles in graph space {}", graphSpace);
if (null == manager.graphSpace(graphSpace)) {
throw new HugeException("Graphspace not exist!");
}
GraphSpace gs = manager.graphSpace(graphSpace);
String gsNickname = gs.nickname();

AuthManager authManager = manager.authManager();
String user = HugeGraphAuthProxy.username();
Map<String, Date> defaultGraphs = authManager.getDefaultGraph(graphSpace, user);

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ 建议使用 DateTimeFormatter 替代 SimpleDateFormat

SimpleDateFormat 虽然这里是局部变量(线程安全),但 DateTimeFormatter 是 Java 8+ 推荐的替代方案,线程安全且可复用为 static final 常量,性能更好:

private static final DateTimeFormatter DATE_FORMAT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

Set<String> graphs = manager.graphs(graphSpace);
List<Map<String, Object>> profiles = new ArrayList<>();
List<Map<String, Object>> defaultProfiles = new ArrayList<>();
for (String graph : graphs) {
String role = RequiredPerm.roleFor(graphSpace, graph,
HugePermission.READ);
if (!sc.isUserInRole(role)) {
continue;
}
try {
HugeGraph hg = graph(manager, graphSpace, graph);
HugeConfig config = (HugeConfig) hg.configuration();
String configResp = ConfigUtil.writeConfigToString(config);
Map<String, Object> profile =
JsonUtil.fromJson(configResp, Map.class);
Comment on lines +163 to +166
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

listProfile() parses ConfigUtil.writeConfigToString(config) with JsonUtil.fromJson(...). If the graph is using a local config file, that helper currently returns the raw .properties file content (not JSON), which will cause JSON parsing failures and break this endpoint. Ensure the config is returned in a JSON-compatible format (or avoid JSON parsing and build the profile map directly from HugeConfig).

Copilot uses AI. Check for mistakes.
profile.put("name", graph);
profile.put("nickname", hg.nickname());
if (!isPrefix(profile, prefix)) {
continue;
}
profile.put("graphspace_nickname", gsNickname);

boolean isDefault = defaultGraphs.containsKey(graph);
profile.put("default", isDefault);
if (isDefault) {
profile.put("default_update_time", defaultGraphs.get(graph));
}

Date createTime = hg.createTime();
if (createTime != null) {
profile.put("create_time", format.format(createTime));
}

if (isDefault) {
defaultProfiles.add(profile);
} else {
profiles.add(profile);
}
} catch (ForbiddenException ignored) {
// ignore graphs the current user has no access to
}
}
defaultProfiles.addAll(profiles);
return defaultProfiles;
}

Comment thread
Yeaury marked this conversation as resolved.
private static boolean isPrefix(Map<String, Object> profile, String prefix) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ 重复代码: isPrefix 在 GraphsAPI 和 GraphSpaceAPI 中有两份相同实现

GraphSpaceAPI 中也定义了同样签名和逻辑的 isPrefix 方法。建议提取到公共基类 API 或工具类中,避免后续维护中两处不一致。

if (StringUtils.isEmpty(prefix)) {
return true;
}
// graph name or nickname is not empty
String name = profile.get("name").toString();
Object nicknameObj = profile.get("nickname");
String nickname = nicknameObj != null ? nicknameObj.toString() : "";
return name.startsWith(prefix) || nickname.startsWith(prefix);
}

@GET
@Timed
@Path("{name}")
Expand All @@ -136,6 +222,61 @@ public Object get(@Context GraphManager manager,
return ImmutableMap.of("name", g.name(), "backend", g.backend());
}

@POST
@Timed
@Path("{name}/default")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space_member", "$owner=$name"})
public Map<String, Object> setDefault(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
@Parameter(description = "The graph name")
@PathParam("name") String name) {
LOG.debug("Set default graph '{}' in graph space '{}'", name, graphSpace);
E.checkArgument(manager.graph(graphSpace, name) != null,
"Graph '%s/%s' does not exist", graphSpace, name);
String user = HugeGraphAuthProxy.username();
AuthManager authManager = manager.authManager();
authManager.setDefaultGraph(graphSpace, name, user);
Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
return ImmutableMap.of("default_graph", defaults.keySet());
}

@DELETE
@Timed
@Path("{name}/default")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space_member", "$owner=$name"})
public Map<String, Object> unsetDefault(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
@Parameter(description = "The graph name")
@PathParam("name") String name) {
LOG.debug("Unset default graph '{}' in graph space '{}'", name, graphSpace);
E.checkArgument(manager.graph(graphSpace, name) != null,
"Graph '%s/%s' does not exist", graphSpace, name);
String user = HugeGraphAuthProxy.username();
AuthManager authManager = manager.authManager();
authManager.unsetDefaultGraph(graphSpace, name, user);
Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
return ImmutableMap.of("default_graph", defaults.keySet());
}

@GET
@Timed
@Path("default")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space_member", "$dynamic"})
public Map<String, Object> getDefault(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace) {
LOG.debug("Get default graphs in graph space '{}'", graphSpace);
String user = HugeGraphAuthProxy.username();
AuthManager authManager = manager.authManager();
Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user);
return ImmutableMap.of("default_graph", defaults.keySet());
}

@DELETE
@Timed
@Path("{name}")
Expand All @@ -155,6 +296,60 @@ public void drop(@Context GraphManager manager,
manager.dropGraph(graphSpace, name, true);
}

@PUT
@Timed
@Path("{name}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space"})
public Map<String, String> manage(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
@Parameter(description = "The graph name")
@PathParam("name") String name,
@Parameter(description = "Action map: {'action':'update','update':{...}}")
Map<String, Object> actionMap) {
LOG.debug("Manage graph '{}' with action '{}'", name, actionMap);
E.checkArgument(actionMap != null && actionMap.size() == 2 &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ 校验过严: actionMap.size() == 2 会在前端多传字段时直接 400

要求请求体恰好包含 2 个 key。如果前端在 JSON 中额外带了字段(很常见的兼容场景),请求会被拒绝。建议放宽为只校验必需字段:

Suggested change
E.checkArgument(actionMap != null && actionMap.size() == 2 &&
E.checkArgument(actionMap != null &&
actionMap.containsKey(GRAPH_ACTION),
"Invalid request body '%s'", actionMap);

actionMap.containsKey(GRAPH_ACTION),
"Invalid request body '%s'", actionMap);
Object value = actionMap.get(GRAPH_ACTION);
E.checkArgument(value instanceof String,
"Invalid action type '%s', must be string",
value.getClass());
String action = (String) value;
switch (action) {
case UPDATE:
E.checkArgument(actionMap.containsKey(UPDATE),
"Please pass '%s' for graph update",
UPDATE);
value = actionMap.get(UPDATE);
E.checkArgument(value instanceof Map,
"The '%s' must be map, but got %s",
UPDATE, value.getClass());
@SuppressWarnings("unchecked")
Map<String, Object> graphMap = (Map<String, Object>) value;
String graphName = (String) graphMap.get("name");
E.checkArgument(graphName != null && graphName.equals(name),
"Different name in update body '%s' with path '%s'",
graphName, name);
HugeGraph exist = graph(manager, graphSpace, name);
String nickname = (String) graphMap.get("nickname");
if (!Strings.isEmpty(nickname)) {
GraphManager.checkNickname(nickname);
E.checkArgument(!manager.isExistedGraphNickname(graphSpace, nickname) ||
nickname.equals(exist.nickname()),
Comment thread
Yeaury marked this conversation as resolved.
"Nickname '%s' has already existed in graphspace '%s'",
nickname, graphSpace);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

In the update action, exist.nickname(nickname) only updates the in-memory HugeGraph instance; it doesn’t persist the nickname change to the graph metadata/config (so it will be lost after reload, and isExistedGraphNickname() checks may still see the old value). This endpoint should update the stored graph config via metaManager.updateGraphConfig(...)/notifyGraphUpdate(...) (or a dedicated GraphManager helper) in addition to updating the runtime object.

Suggested change
nickname, graphSpace);
nickname, graphSpace);
Map<String, Object> updatedGraphConfig = new HashMap<>();
updatedGraphConfig.put("nickname", nickname);
manager.meta().updateGraphConfig(graphSpace, name,
updatedGraphConfig);
manager.meta().notifyGraphUpdate(graphSpace, name);

Copilot uses AI. Check for mistakes.
exist.nickname(nickname);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

‼️ Bug: nickname 更新未持久化,且 isExistedGraphNickname 在非 PD 模式下会 NPE

两个问题:

  1. exist.nickname(nickname) 只修改了内存中的 StandardHugeGraph.nickname 字段,没有调用 metaManager.updateGraphConfig() 写回存储。创建图时有完整的持久化链路(GraphManager:1345 设内存 → :1353 写 metaManager),但这里的更新路径缺少持久化步骤,重启后 nickname 会丢失。

  2. manager.isExistedGraphNickname() 内部调用 metaManager.graphConfigs(graphSpace) — 在非 PD(RocksDB 单机)模式下 MetaManager 未初始化,会抛 NPE。GraphsAPI 的端点在非 PD 模式下通过 graphspace=DEFAULT 正常使用,所以这里需要加 isPDEnabled() 分支做兼容(可参考 GraphManager.graphs() 的处理方式)。

}
return ImmutableMap.of(name, "updated");
default:
throw new AssertionError(String.format(
"Invalid graph action: '%s'", action));
}
}

@PUT
@Timed
@Path("manage")
Expand Down Expand Up @@ -207,11 +402,14 @@ public Object create(@Context GraphManager manager,
if (StringUtils.isEmpty(clone)) {
// Only check required parameters when creating new graph, not when cloning
E.checkArgument(configs != null, "Config parameters cannot be null");
String[] requiredKeys = {"backend", "serializer", "store"};
for (String key : requiredKeys) {
Object value = configs.get(key);
E.checkArgument(value instanceof String && !StringUtils.isEmpty((String) value),
"Required parameter '%s' is missing or empty", key);
// Auto-fill defaults for PD/HStore mode when not provided
configs.putIfAbsent("backend", "hstore");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

‼️ Bug: auto-fill backend=hstore 会导致非 PD 模式创建图失败

GraphsAPI 的创建端点在非 PD(RocksDB 单机)模式下也会被调用(通过 graphspace=DEFAULT)。这里无条件填充 backend=hstore 会导致单机版用户创建图时使用错误的后端,应该加 PD 模式判断:

Suggested change
configs.putIfAbsent("backend", "hstore");
// Auto-fill defaults for PD/HStore mode when not provided
if (manager.isPDEnabled()) {
configs.putIfAbsent("backend", "hstore");
configs.putIfAbsent("serializer", "binary");
}
configs.putIfAbsent("store", name);

configs.putIfAbsent("serializer", "binary");
configs.putIfAbsent("store", name);
// Map frontend 'schema' field to backend config key
Object schema = configs.remove("schema");
if (schema != null && !schema.toString().isEmpty()) {
configs.put("schema.init_template", schema.toString());
}
}

Expand All @@ -220,13 +418,15 @@ public Object create(@Context GraphManager manager,
if (StringUtils.isNotEmpty(clone)) {
// Clone from existing graph
LOG.debug("Clone graph '{}' to '{}' in graph space '{}'", clone, name, graphSpace);
graph = manager.cloneGraph(graphSpace, clone, name, convConfig(configs));
Map<String, Object> cloneConfigs = configs != null ? configs : new HashMap<>();
graph = manager.cloneGraph(graphSpace, clone, name, convConfig(cloneConfigs));
} else {
// Create new graph
graph = manager.createGraph(graphSpace, name, creator,
convConfig(configs), true);
}
String description = (String) configs.get(GRAPH_DESCRIPTION);
String description = (configs != null) ?
(String) configs.get(GRAPH_DESCRIPTION) : null;
if (description == null) {
description = Strings.EMPTY;
}
Expand All @@ -239,6 +439,37 @@ public Object create(@Context GraphManager manager,
return result;
}

/**
* Create graph via text/plain (hugegraph-client compatibility).
* Client sends: POST /graphspaces/{graphspace}/graphs/{name}
* with Content-Type: text/plain and body containing JSON config string.
*/
@POST
@Timed
@Path("{name}")
@StatusFilter.Status(StatusFilter.Status.CREATED)
@Consumes("text/plain")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space"})
public Object createByText(@Context GraphManager manager,
@Parameter(description = "The graph space name")
@PathParam("graphspace") String graphSpace,
@Parameter(description = "The graph name to create")
@PathParam("name") String name,
@Parameter(description = "The graph name to clone from (optional)")
@QueryParam("clone_graph_name") String clone,
String configText) {
LOG.debug("Create graph {} with text config in graph space '{}'",
name, graphSpace);
Map<String, Object> configs = null;
if (configText != null && !configText.isEmpty()) {
configs = JsonUtil.fromJson(configText, Map.class);
}
return create(manager, graphSpace, name, clone, configs);
}



@GET
@Timed
@Path("{name}/conf")
Expand Down
Loading
Loading