Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
77cee94
feat(api): add Hubble-compatible graph profile and management endpoints
Yeaury Apr 24, 2026
42f3dfd
feat(auth): implement default role management for GraphSpace
Yeaury Apr 24, 2026
e5e5028
feat(api): add SchemaTemplate CRUD API
Yeaury Apr 24, 2026
19af891
fix(auth): delegate default graph/role methods in AuthManagerProxy
Yeaury Apr 24, 2026
1a810c0
fix(api):Add createByText for text/plain graph creation compatibility
Yeaury Apr 24, 2026
01e64e8
fix(api):keep consistent with the License header
Yeaury Apr 25, 2026
79ea319
fix: address code review issues for Hubble 2.0 API adaptation (#3008)
Yeaury Apr 25, 2026
f722cd5
fix: ensure HugeGraph Server is usable in non-PD (standalone RocksDB)…
Yeaury Apr 26, 2026
9ac0f2b
fix(api): Fix bugs related to updating user information and passwords.
Yeaury Apr 26, 2026
34222e5
fix(api): fix user lookup to use getUserByName() instead of getUser()…
Yeaury Apr 29, 2026
70d88fb
fix(gremlin): register ContextGremlinServer listener before loading g…
Yeaury May 1, 2026
35002f7
fix(auth): initialize creator field in StandardAuthManagerV2 create m…
Yeaury May 1, 2026
34673b7
fix(api): add missing isPrefix helper method in GraphsAPI
Yeaury May 5, 2026
f868dbc
fix(auth): make unsetDefaultGraph idempotent in StandardAuthManagerV2
Yeaury May 5, 2026
6ddfb17
fix(api): address Copilot review issues in Hubble 2.0 API adaptation
Yeaury May 5, 2026
b67df7b
fix(api): fix compilation error and address imbajin review issues
Yeaury May 5, 2026
2a17b8d
fix(api): route default graph ops through AuthManager in all modes
Yeaury May 5, 2026
dc011c0
fix(auth): address Copilot review issues in auth schema and belong bi…
Yeaury May 5, 2026
5e3796d
test(api): add Hubble 2.0 API tests and fix standalone authManager er…
Yeaury May 8, 2026
93da151
ci: trigger workflow run
Yeaury May 8, 2026
c48300b
fix(api): improve Hubble compatibility and address review feedback
Yeaury May 21, 2026
5977ef2
fix(server): address Hubble API compatibility review issues
Yeaury May 21, 2026
26a2c42
test(server): add hstore Hubble API coverage
imbajin May 21, 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
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 com.alipay.remoting.util.StringUtils;
Comment thread
Yeaury marked this conversation as resolved.
Outdated

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,40 @@ public String getRolesInGs(@Context GraphManager manager,
result));
}

@GET
@Timed
@Path("default")
@Consumes(APPLICATION_JSON)
public String checkDefaultRole(@Context GraphManager manager,
@QueryParam("graphspace") String graphSpace,
Comment thread
Yeaury marked this conversation as resolved.
Outdated
@QueryParam("role") String role,
@QueryParam("graph") String graph) {
LOG.debug("check if current user is default role: {} {} {}",
role, graphSpace, graph);
Comment thread
Yeaury marked this conversation as resolved.
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());
Comment thread
Yeaury marked this conversation as resolved.
Outdated
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));
}
Comment on lines +265 to +302

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,9 @@ 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 CLEAR_SCHEMA = "clear_schema";
private static final String GRAPH_ACTION_CLEAR = "clear";
private static final String GRAPH_ACTION_RELOAD = "reload";

private static Map<String, Object> convConfig(Map<String, Object> config) {
Expand Down Expand Up @@ -120,6 +128,85 @@ 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) {
Comment thread
Yeaury marked this conversation as resolved.
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");
Comment thread
Yeaury marked this conversation as resolved.
Outdated
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 thread
Yeaury marked this conversation as resolved.
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));
Comment thread
Yeaury marked this conversation as resolved.
Outdated
}

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.
public boolean isPrefix(Map<String, Object> profile, String prefix) {
if (StringUtils.isEmpty(prefix)) {
return true;
}
// graph name or nickname is not empty
String name = profile.get("name").toString();
String nickname = profile.get("nickname").toString();
return name.startsWith(prefix) || nickname.startsWith(prefix);
}

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

@GET
Comment thread
Yeaury marked this conversation as resolved.
Outdated
@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());
}

@GET
@Timed
@Path("{name}/undefault")
@Produces(APPLICATION_JSON_WITH_CHARSET)
@RolesAllowed({"space_member", "$owner=$name"})
Comment thread
Yeaury marked this conversation as resolved.
public Map<String, Object> unsetDefault(@Context GraphManager manager,
Comment thread
Yeaury marked this conversation as resolved.
@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());
}
Comment on lines +239 to +304

@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 +297,76 @@ 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 &&
Comment thread
Yeaury marked this conversation as resolved.
Outdated
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;
Comment thread
Yeaury marked this conversation as resolved.
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());
Comment thread
Yeaury marked this conversation as resolved.
Outdated
@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.
Outdated
"Nickname '%s' has already existed in graphspace '%s'",
nickname, graphSpace);
Comment thread
Yeaury marked this conversation as resolved.
exist.nickname(nickname);
Comment thread
Yeaury marked this conversation as resolved.
Outdated
}
return ImmutableMap.of(name, "updated");
//case GRAPH_ACTION_CLEAR:
// String username = manager.authManager().username();
// HugeGraph g = graph(manager, graphSpace, name);
// if ((Boolean) actionMap.getOrDefault(CLEAR_SCHEMA, false)) {
// g.truncateBackend();
// } else {
// g.truncateGraph();
// }
// // truncateBackend() will open tx, so must close here(commit)
// g.tx().commit();
// manager.meta().notifyGraphClear(graphSpace, name);
// LOG.info("user [{}] clear [{}/{}]", username, graphSpace, name);
// return ImmutableMap.of(name, "cleared");
//case GRAPH_ACTION_RELOAD:
// manager.reload(graphSpace, name);
// return ImmutableMap.of(name, "reloaded");
default:
throw new AssertionError(String.format(
Comment thread
Yeaury marked this conversation as resolved.
Outdated
"Invalid graph action: '%s'", action));
}
}

@PUT
@Timed
@Path("manage")
Expand Down Expand Up @@ -207,11 +419,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");
Comment thread
Yeaury marked this conversation as resolved.
Outdated
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 Down Expand Up @@ -239,6 +454,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