From 77cee947e4cbd1fa98eadf97240f300eb894df25 Mon Sep 17 00:00:00 2001 From: yeaury Date: Fri, 24 Apr 2026 10:53:49 +0800 Subject: [PATCH 1/7] feat(api): add Hubble-compatible graph profile and management endpoints - Add listProfile endpoint with default graph sorting and prefix filtering - Add setDefault/unsetDefault/getDefault endpoints for default graph management - Add manage(PUT) endpoint for graph nickname update - Add createByForm for form-urlencoded graph creation compatibility - Auto-fill HStore/PD defaults (backend/serializer/store) during graph creation --- .../hugegraph/api/profile/GraphsAPI.java | 260 +++++++++++++++++- .../hugegraph/auth/HugeGraphAuthProxy.java | 49 ++++ .../apache/hugegraph/auth/AuthManager.java | 26 ++ .../org/apache/hugegraph/util/ConfigUtil.java | 30 ++ 4 files changed, 359 insertions(+), 6 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java index 9316d7341b..f3051ed0b0 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java @@ -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; @@ -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; @@ -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 convConfig(Map config) { @@ -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) { + 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 defaultGraphs = authManager.getDefaultGraph(graphSpace, user); + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Set graphs = manager.graphs(graphSpace); + List> profiles = new ArrayList<>(); + List> 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 profile = + JsonUtil.fromJson(configResp, Map.class); + 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; + } + + public boolean isPrefix(Map 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}") @@ -136,6 +223,61 @@ public Object get(@Context GraphManager manager, return ImmutableMap.of("name", g.name(), "backend", g.backend()); } + @GET + @Timed + @Path("{name}/default") + @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$owner=$name"}) + public Map 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 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"}) + public Map 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 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 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 defaults = authManager.getDefaultGraph(graphSpace, user); + return ImmutableMap.of("default_graph", defaults.keySet()); + } + @DELETE @Timed @Path("{name}") @@ -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 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 actionMap) { + LOG.debug("Manage graph '{}' with action '{}'", name, actionMap); + E.checkArgument(actionMap != null && actionMap.size() == 2 && + 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 graphMap = (Map) 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()), + "Nickname '%s' has already existed in graphspace '%s'", + nickname, graphSpace); + exist.nickname(nickname); + } + 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( + "Invalid graph action: '%s'", action)); + } + } + @PUT @Timed @Path("manage") @@ -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"); + 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()); } } @@ -239,6 +454,39 @@ public Object create(@Context GraphManager manager, return result; } + /** + * Create graph via form-urlencoded (Hubble frontend compatibility). + * Frontend sends: POST /graphspaces/{graphspace}/graphs + * with Content-Type: application/x-www-form-urlencoded + * and body: graph=xx&nickname=yy&schema=zz&auth=false&graphspace=DEFAULT + */ + @POST + @Timed + @StatusFilter.Status(StatusFilter.Status.CREATED) + @Consumes("application/x-www-form-urlencoded") + @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space"}) + public Object createByForm(@Context GraphManager manager, + @Parameter(description = "The graph space name") + @PathParam("graphspace") String graphSpace, + jakarta.ws.rs.core.MultivaluedMap + formParams) { + String name = formParams.getFirst("graph"); + E.checkArgument(name != null && !name.isEmpty(), + "The 'graph' parameter is required"); + + Map configs = new HashMap<>(); + for (Map.Entry> entry : formParams.entrySet()) { + String key = entry.getKey(); + List values = entry.getValue(); + if (values != null && !values.isEmpty()) { + configs.put(key, values.get(0)); + } + } + + return create(manager, graphSpace, name, null, configs); + } + @GET @Timed @Path("{name}/conf") 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 cf390b886e..06cb575a5f 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 @@ -24,6 +24,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -1983,6 +1984,54 @@ public HugeGroup findGroup(String name) { return this.authManager.findGroup(name); } + @Override + public void setDefaultGraph(String graphSpace, String graph, String user) { + + } + + @Override + public void unsetDefaultGraph(String graphSpace, String graph, String user) { + + } + + @Override + public Map getDefaultGraph(String graphSpace, String user) { + return Map.of(); + } + + @Override + public Id createDefaultRole(String graphSpace, String owner, HugeDefaultRole role, + String graph) { + return null; + } + + @Override + public Id createSpaceDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { + return null; + } + + @Override + public boolean isDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { + return false; + } + + @Override + public boolean isDefaultRole(String graphSpace, String graph, String owner, + HugeDefaultRole role) { + return false; + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { + + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role, + String graph) { + + } + @Override public String loginUser(String username, String password) { return this.loginUser(username, password, -1L); 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 af0027c021..8b36dd9be4 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 @@ -17,7 +17,9 @@ package org.apache.hugegraph.auth; +import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; import javax.security.sasl.AuthenticationException; @@ -164,4 +166,28 @@ public interface AuthManager { boolean isAdminManager(String user); HugeGroup findGroup(String name); + + void setDefaultGraph(String graphSpace, String graph, String user); + + void unsetDefaultGraph(String graphSpace, String graph, String user); + + Map getDefaultGraph(String graphSpace, String user); + + Id createDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph); + + Id createSpaceDefaultRole(String graphSpace, String owner, + HugeDefaultRole role); + + boolean isDefaultRole(String graphSpace, String owner, + HugeDefaultRole role); + + boolean isDefaultRole(String graphSpace, String graph, String owner, + HugeDefaultRole role); + + void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role); + + void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java index 8df1b3064f..82b233d253 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java @@ -23,9 +23,13 @@ import java.io.StringReader; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import jakarta.ws.rs.NotSupportedException; + import org.apache.commons.configuration2.FileBasedConfiguration; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.PropertiesConfiguration; @@ -188,4 +192,30 @@ private static void validateGraphName(String graphName) { "Graph name can only contain letters, numbers, hyphens and underscores: %s", graphName); } + + public static String writeConfigToString(HugeConfig config) { + String content; + try { + if (config.file() == null) { + Map configMap = new HashMap<>(); + Iterator iterator = config.getKeys(); + while (iterator.hasNext()) { + String key = iterator.next(); + configMap.put(key, config.getProperty(key)); + } + content = JsonUtil.toJson(configMap); + } else { + File file = config.file(); + if (file == null) { + throw new NotSupportedException( + "Can't access the api in a node which started " + + "with non local file config."); + } + content = FileUtils.readFileToString(file); + } + } catch (IOException e) { + throw new HugeException("Failed to read config of graph", e); + } + return content; + } } From 42f3dfda43011aaaf69843776d8b5027ab132d81 Mon Sep 17 00:00:00 2001 From: yeaury Date: Fri, 24 Apr 2026 10:54:35 +0800 Subject: [PATCH 2/7] feat(auth): implement default role management for GraphSpace - Add setDefaultRole/checkDefaultRole/deleteDefaultRole in GraphSpaceAPI - Add checkDefaultRole endpoint in ManagerAPI - Add default role interfaces in AuthManager - Implement default role CRUD in StandardAuthManager and StandardAuthManagerV2 - Add stub proxy methods in HugeGraphAuthProxy --- .../apache/hugegraph/api/auth/ManagerAPI.java | 37 ++++ .../hugegraph/api/space/GraphSpaceAPI.java | 119 ++++++++++- .../hugegraph/auth/StandardAuthManager.java | 174 +++++++++++++++++ .../hugegraph/auth/StandardAuthManagerV2.java | 184 ++++++++++++++++++ 4 files changed, 513 insertions(+), 1 deletion(-) 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 071e4b8a66..c84ae681a2 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 @@ -22,9 +22,12 @@ import java.util.ArrayList; import java.util.List; +import com.alipay.remoting.util.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; @@ -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, + @QueryParam("role") String role, + @QueryParam("graph") String graph) { + LOG.debug("check if current user is default role: {} {} {}", + role, graphSpace, graph); + 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()); + 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, 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 35bc40aed0..e9d9bfbb9b 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,18 +21,22 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.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.HugeDefaultRole; import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.util.E; @@ -42,6 +46,7 @@ import org.slf4j.Logger; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableMap; @@ -103,6 +108,118 @@ public Object get(@Context GraphManager manager, return gsInfo; } + @POST + @Timed + @Status(Status.CREATED) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + @Path("{graphspace}/role") + @RolesAllowed({"analyst"}) + public String setDefaultRole(@Context GraphManager manager, + @PathParam("graphspace") String name, + JsonDefaultRole jsonRole) { + String user = jsonRole.user; + String graph = jsonRole.graph; + HugeDefaultRole role = + HugeDefaultRole.valueOf(jsonRole.role.toUpperCase()); + LOG.debug("Create default role: {} {} {}", user, role, + name); + AuthManager authManager = manager.authManager(); + E.checkArgument(authManager.findUser(user) != null || + authManager.findGroup(user) != null, + "The user or group is not exist"); + // only admin can set space admin + if (!authManager.isAdminManager(HugeGraphAuthProxy.username()) && + role.equals(HugeDefaultRole.SPACE)) { + throw new HugeException("Forbidden to set role %s", role.toString()); + } + + boolean hasGraph = role.equals(HugeDefaultRole.OBSERVER); + + E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph), + "Must set a graph for observer"); + + Map result = new HashMap<>(); + result.put("user", user); + result.put("role", jsonRole.role); + result.put("graphSpace", name); + + if (hasGraph) { + authManager.createDefaultRole(name, user, role, graph); + result.put("graph", graph); + } else { + authManager.createSpaceDefaultRole(name, user, role); + } + + return manager.serializer().writeMap(result); + } + + @GET + @Timed + @Path("{graphspace}/role") + @Consumes(APPLICATION_JSON) + @RolesAllowed("analyst") + public String checkDefaultRole(@Context GraphManager manager, + @PathParam("graphspace") String name, + @QueryParam("user") String user, + @QueryParam("role") String role, + @QueryParam("graph") String graph) { + LOG.debug("Check space role: {} {} {}", user, role, + name); + AuthManager authManager = manager.authManager(); + + HugeDefaultRole defaultRole = + HugeDefaultRole.valueOf(role.toUpperCase()); + 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(name, graph, user, + defaultRole); + } else { + result = authManager.isDefaultRole(name, user, + defaultRole); + } + return manager.serializer().writeMap(ImmutableMap.of("check", result)); + } + + @DELETE + @Timed + @Path("{graphspace}/role") + @Consumes(APPLICATION_JSON) + @RolesAllowed("analyst") + public void deleteDefaultRole(@Context GraphManager manager, + @PathParam("graphspace") String name, + @QueryParam("user") String user, + @QueryParam("role") String role, + @QueryParam("graph") String graph) { + LOG.debug("Delete space role: {} {} {}", user, role, + name); + + AuthManager authManager = manager.authManager(); + E.checkArgument(authManager.findUser(user) != null || + authManager.findGroup(user) != null, + "The user or group is not exist"); + + if (!authManager.isAdminManager(HugeGraphAuthProxy.username()) && + role.equalsIgnoreCase(HugeDefaultRole.SPACE.toString())) { + throw new HugeException("Forbidden to delete role %s", role); + } + + HugeDefaultRole defaultRole = + HugeDefaultRole.valueOf(role.toUpperCase()); + boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER); + E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph), + "Must set a graph for observer"); + if (hasGraph) { + authManager.deleteDefaultRole(name, user, defaultRole, graph); + } else { + authManager.deleteDefaultRole(name, user, defaultRole); + } + } + @GET @Timed @Path("profile") @@ -113,7 +230,6 @@ public Object listProfile(@Context GraphManager manager, "name or nickname prefix") @QueryParam("prefix") String prefix, @Context SecurityContext sc) { - ensurePdModeEnabled(manager); Set spaces = manager.graphSpaces(); List> spaceList = new ArrayList<>(); List> result = new ArrayList<>(); @@ -346,6 +462,7 @@ private boolean verifyPermission(String user, AuthManager authManager, String gr authManager.isSpaceMember(graphSpace, user); } + @JsonIgnoreProperties(ignoreUnknown = true) private static class JsonGraphSpace implements Checkable { @JsonProperty("name") diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index a3224811f3..82a2921cc2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -813,6 +814,179 @@ public HugeGroup findGroup(String name) { return null; } + private static final String DEFAULT_GRAPH_MARKER = "~default_graph"; + + private String defaultGraphGroupName(String graphSpace, String graph) { + return DEFAULT_GRAPH_MARKER + ":" + graphSpace + ":" + graph; + } + + @Override + public void setDefaultGraph(String graphSpace, String graph, String user) { + // Use a special-named HugeGroup as a marker for the default graph, + // then create a HugeBelong (user -> marker-group) to persist the binding. + String markerName = defaultGraphGroupName(graphSpace, graph); + Id groupId = IdGenerator.of(markerName); + if (!this.groups.exists(groupId)) { + HugeGroup markerGroup = new HugeGroup(markerName); + markerGroup.creator(user); + this.groups.add(markerGroup); + } + Id userId = IdGenerator.of(user); + HugeBelong belong = new HugeBelong(userId, groupId); + belong.creator(user); + Id belongId = IdGenerator.of(userId.asString() + "->ug->" + groupId.asString()); + if (!this.belong.exists(belongId)) { + this.belong.add(belong); + } else { + belong.id(belongId); + this.belong.update(belong); + } + } + + @Override + public void unsetDefaultGraph(String graphSpace, String graph, String user) { + String markerName = defaultGraphGroupName(graphSpace, graph); + Id groupId = IdGenerator.of(markerName); + Id userId = IdGenerator.of(user); + Id belongId = IdGenerator.of(userId.asString() + "->ug->" + groupId.asString()); + if (this.belong.exists(belongId)) { + this.belong.delete(belongId); + } + } + + @Override + public Map getDefaultGraph(String graphSpace, String user) { + Id userId = IdGenerator.of(user); + List belongs = this.belong.list(userId, Directions.OUT, + HugeBelong.P.BELONG, -1); + String prefix = DEFAULT_GRAPH_MARKER + ":" + graphSpace + ":"; + Map result = new LinkedHashMap<>(); + for (HugeBelong b : belongs) { + String targetName = b.target().asString(); + if (targetName.startsWith(prefix)) { + String graphName = targetName.substring(prefix.length()); + result.put(graphName, b.update()); + } + } + return result; + } + + private static final String DEFAULT_ROLE_MARKER = "~default_role"; + + /** + * Build marker group name for a space-level role. + * Format: ~default_role:: + */ + private String defaultRoleGroupName(String graphSpace, String role) { + return DEFAULT_ROLE_MARKER + ":" + graphSpace + ":" + role; + } + + /** + * Build marker group name for a graph-level role (e.g. OBSERVER). + * Format: ~default_role::: + */ + private String defaultRoleGroupName(String graphSpace, String role, + String graph) { + return DEFAULT_ROLE_MARKER + ":" + graphSpace + ":" + role + + ":" + graph; + } + + private Id ensureMarkerGroup(String markerName, String creator) { + Id groupId = IdGenerator.of(markerName); + if (!this.groups.exists(groupId)) { + HugeGroup markerGroup = new HugeGroup(markerName); + markerGroup.creator(creator); + this.groups.add(markerGroup); + } + return groupId; + } + + private Id createBelongBinding(String owner, Id groupId) { + Id userId = IdGenerator.of(owner); + HugeBelong belong = new HugeBelong(userId, groupId); + belong.creator(owner); + Id belongId = IdGenerator.of( + userId.asString() + "->ug->" + groupId.asString()); + if (!this.belong.exists(belongId)) { + this.belong.add(belong); + } else { + belong.id(belongId); + this.belong.update(belong); + } + return belongId; + } + + private void removeBelongBinding(String owner, Id groupId) { + Id userId = IdGenerator.of(owner); + Id belongId = IdGenerator.of( + userId.asString() + "->ug->" + groupId.asString()); + if (this.belong.exists(belongId)) { + this.belong.delete(belongId); + } + } + + private boolean existsBelongBinding(String owner, Id groupId) { + Id userId = IdGenerator.of(owner); + Id belongId = IdGenerator.of( + userId.asString() + "->ug->" + groupId.asString()); + return this.belong.exists(belongId); + } + + @Override + public Id createDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph) { + LOG.debug("Create default role: {} {} {} {}", owner, role, + graphSpace, graph); + String markerName = defaultRoleGroupName(graphSpace, + role.toString(), graph); + Id groupId = ensureMarkerGroup(markerName, owner); + return createBelongBinding(owner, groupId); + } + + @Override + public Id createSpaceDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + LOG.debug("Create space default role: {} {} {}", owner, role, + graphSpace); + String markerName = defaultRoleGroupName(graphSpace, role.toString()); + Id groupId = ensureMarkerGroup(markerName, owner); + return createBelongBinding(owner, groupId); + } + + @Override + public boolean isDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + String markerName = defaultRoleGroupName(graphSpace, role.toString()); + Id groupId = IdGenerator.of(markerName); + return existsBelongBinding(owner, groupId); + } + + @Override + public boolean isDefaultRole(String graphSpace, String graph, + String owner, HugeDefaultRole role) { + String markerName = defaultRoleGroupName(graphSpace, + role.toString(), graph); + Id groupId = IdGenerator.of(markerName); + return existsBelongBinding(owner, groupId); + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + String markerName = defaultRoleGroupName(graphSpace, role.toString()); + Id groupId = IdGenerator.of(markerName); + removeBelongBinding(owner, groupId); + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph) { + String markerName = defaultRoleGroupName(graphSpace, + role.toString(), graph); + Id groupId = IdGenerator.of(markerName); + removeBelongBinding(owner, groupId); + } + public R commit(Callable callable) { this.groups.autoCommit(false); this.access.autoCommit(false); 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 d2df45626c..9e574f41cd 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 @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -1510,6 +1511,189 @@ public HugeGroup findGroup(String name) { return result; } + @Override + public void setDefaultGraph(String graphSpace, String graph, String user) { + try { + HugeBelong belong = new HugeBelong(graphSpace, + IdGenerator.of(user), + IdGenerator.of(graph + + DEFAULT_SETTER_ROLE_KEY)); + this.tryInitDefaultGraph(graphSpace, graph); + this.updateCreator(belong); + belong.create(belong.update()); + this.metaManager.createBelong(graphSpace, belong); + this.invalidateUserCache(); + } catch (Exception e) { + throw new HugeException("Exception occurs when " + + "set default graph", e); + } + } + + @Override + public void unsetDefaultGraph(String graphSpace, String graph, String user) { + String role = graph + DEFAULT_SETTER_ROLE_KEY; + String belongId = this.metaManager.belongId(user, role); + try { + this.metaManager.deleteBelong(graphSpace, IdGenerator.of(belongId)); + this.invalidateUserCache(); + } catch (Exception e) { + throw new HugeException("Exception occurs when unset default " + + "graph", e); + } + } + + @Override + public Map getDefaultGraph(String graphSpace, String user) { + List belongs = this.listBelongBySource(graphSpace, + IdGenerator.of(user), + HugeBelong.UR, -1); + Map map = new HashMap<>(); + for (HugeBelong belong : belongs) { + String role = belong.target().asString(); + if (role.endsWith(DEFAULT_SETTER_ROLE_KEY) && + role.length() != DEFAULT_SETTER_ROLE_KEY.length()) { + map.put(role.substring(0, role.lastIndexOf( + DEFAULT_SETTER_ROLE_KEY)), belong.update()); + } + } + return map; + } + + @Override + public Id createDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph) { + String roleName = (role.isGraphRole()) ? + getGraphDefaultRole(graph, role.toString()) : role.toString(); + try { + HugeBelong belong; + if (HugeGroup.isGroup(owner)) { + belong = new HugeBelong(graphSpace, null, + IdGenerator.of(owner), + IdGenerator.of(roleName), + HugeBelong.GR); + } else { + belong = new HugeBelong(graphSpace, IdGenerator.of(owner), + null, IdGenerator.of(roleName), + HugeBelong.UR); + } + + this.tryInitDefaultRole(graphSpace, roleName, graph); + this.updateCreator(belong); + belong.create(belong.update()); + Id result = this.metaManager.createBelong(graphSpace, belong); + this.invalidateUserCache(); + return result; + } catch (Exception e) { + throw new HugeException("Exception occurs when " + + "create " + role + ".", e); + } + } + + @Override + public Id createSpaceDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + return createDefaultRole(graphSpace, owner, role, ALL_GRAPHS); + } + + @Override + public boolean isDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + return isDefaultRole(graphSpace, owner, role.toString()); + } + + @Override + public boolean isDefaultRole(String graphSpace, String graph, + String owner, HugeDefaultRole role) { + String roleName = getGraphDefaultRole(graph, role.toString()); + return isDefaultRole(graphSpace, owner, roleName); + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role) { + deleteDefaultRoleByName(graphSpace, owner, role.toString()); + } + + @Override + public void deleteDefaultRole(String graphSpace, String owner, + HugeDefaultRole role, String graph) { + String roleName = getGraphDefaultRole(graph, role.toString()); + deleteDefaultRoleByName(graphSpace, owner, roleName); + } + + private boolean isDefaultRole(String graphSpace, String owner, + String role) { + try { + String belongId; + if (HugeGroup.isGroup(owner)) { + belongId = this.metaManager.belongId(owner, role, + HugeBelong.GR); + return this.metaManager.existBelong(graphSpace, + IdGenerator.of(belongId)); + } + + List groups = this.listGroupsByUser(owner, -1); + for (HugeGroup group : groups) { + String belongIdG = this.metaManager.belongId(group.name(), + role, + HugeBelong.GR); + if (this.metaManager.existBelong(graphSpace, + IdGenerator.of(belongIdG))) { + return true; + } + } + + belongId = this.metaManager.belongId(owner, role); + return this.metaManager.existBelong(graphSpace, + IdGenerator.of(belongId)); + } catch (Exception e) { + throw new HugeException("Exception occurs when check if is " + + role + ".", e); + } + } + + private void deleteDefaultRoleByName(String graphSpace, String owner, + String role) { + try { + String belongId; + if (HugeGroup.isGroup(owner)) { + belongId = this.metaManager.belongId(owner, role, + HugeBelong.GR); + } else { + belongId = this.metaManager.belongId(owner, role, + HugeBelong.UR); + } + this.metaManager.deleteBelong(graphSpace, + IdGenerator.of(belongId)); + this.invalidateUserCache(); + } catch (Exception e) { + throw new HugeException("Exception occurs when " + + "delete " + role + ".", e); + } + } + + private void tryInitDefaultGraph(String graphSpace, String graph) { + try { + HugeRole role = this.metaManager.findRole( + graphSpace, + IdGenerator.of(graph + DEFAULT_SETTER_ROLE_KEY)); + if (role != null) { + return; + } + role = new HugeRole(graph + DEFAULT_SETTER_ROLE_KEY, graphSpace); + this.updateCreator(role); + role.create(role.update()); + this.metaManager.createRole(graphSpace, role); + } catch (Exception e) { + throw new HugeException("Exception occurs when " + + "init default graph role", e); + } + } + + public String getGraphDefaultRole(String graph, String role) { + return graph + "_" + role; + } + private void tryInitDefaultRole(String graphSpace, String roleName, String graph) { From e5e5028a3308ea90929279fd392bd7fcffd96945 Mon Sep 17 00:00:00 2001 From: yeaury Date: Fri, 24 Apr 2026 10:54:51 +0800 Subject: [PATCH 3/7] feat(api): add SchemaTemplate CRUD API - Add new SchemaTemplateAPI with list/get/create/update/delete operations - Fix package path from api.profile to api.space - Use HugeGraphAuthProxy.username() instead of authManager.username() --- .../api/space/SchemaTemplateAPI.java | 199 ++++++++++++++++++ .../apache/hugegraph/core/GraphManager.java | 21 ++ 2 files changed, 220 insertions(+) create mode 100644 hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java 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 new file mode 100644 index 0000000000..887dfbfab5 --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java @@ -0,0 +1,199 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * 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.api.space; + +import java.util.Date; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.api.API; +import org.apache.hugegraph.api.filter.StatusFilter; +import org.apache.hugegraph.auth.HugeGraphAuthProxy; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.define.Checkable; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.server.RestServer; +import org.apache.hugegraph.space.SchemaTemplate; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; + +import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Singleton; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; + +@Path("graphspaces/{graphspace}/schematemplates") +@Singleton +@Tag(name = "SchemaTemplateAPI") +public class SchemaTemplateAPI extends API { + + private static final Logger LOG = Log.logger(RestServer.class); + + @GET + @Timed + @Produces(APPLICATION_JSON_WITH_CHARSET) + public Object list(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace) { + LOG.debug("List all schema templates for graph space {}", graphSpace); + + Set templates = manager.schemaTemplates(graphSpace); + return ImmutableMap.of("schema_templates", templates); + } + + @GET + @Timed + @Path("{name}") + @Produces(APPLICATION_JSON_WITH_CHARSET) + public Object get(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("name") String name) { + LOG.debug("Get schema template by name '{}' for graph space {}", + name, graphSpace); + + return manager.serializer().writeSchemaTemplate( + schemaTemplate(manager, graphSpace, name)); + } + + @POST + @Timed + @StatusFilter.Status(StatusFilter.Status.CREATED) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String create(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + JsonSchemaTemplate jsonSchemaTemplate) { + LOG.debug("Create schema template {} for graph space: '{}'", + jsonSchemaTemplate, graphSpace); + jsonSchemaTemplate.checkCreate(false); + + E.checkArgument(manager.graphSpace(graphSpace) != null, + "The graph space '%s' is not exist", graphSpace); + + SchemaTemplate template = jsonSchemaTemplate.toSchemaTemplate(); + template.create(new Date()); + template.creator(HugeGraphAuthProxy.username()); + manager.createSchemaTemplate(graphSpace, template); + return manager.serializer().writeSchemaTemplate(template); + } + + @DELETE + @Timed + @Path("{name}") + @Produces(APPLICATION_JSON_WITH_CHARSET) + public void delete(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("name") String name, + @Context SecurityContext sc) { + LOG.debug("Remove schema template by name '{}' for graph space", + name, graphSpace); + E.checkArgument(manager.graphSpace(graphSpace) != null, + "The graph space '%s' is not exist", graphSpace); + + SchemaTemplate st = schemaTemplate(manager, graphSpace, name); + E.checkArgument(st != null, + "Schema template '%s' does not exist", name); + + String username = HugeGraphAuthProxy.username(); + boolean isSpace = manager.authManager() + .isSpaceManager(graphSpace, username); + if (st.creator().equals(username) || isSpace) { + manager.dropSchemaTemplate(graphSpace, name); + } else { + throw new HugeException("No permission to delete schema template"); + } + } + + @PUT + @Timed + @Path("{name}") + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String update(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("name") String name, + @Context SecurityContext sc, + JsonSchemaTemplate jsonSchemaTemplate) { + + SchemaTemplate old = schemaTemplate(manager, graphSpace, name); + if (null == old) { + throw new HugeException("Schema template {} does not exist", name); + } + + String username = HugeGraphAuthProxy.username(); + boolean isSpace = manager.authManager() + .isSpaceManager(graphSpace, username); + if (old.creator().equals(username) || isSpace) { + SchemaTemplate template = jsonSchemaTemplate.build(old); + template.creator(old.creator()); + template.create(old.create()); + template.refreshUpdateTime(); + + manager.updateSchemaTemplate(graphSpace, template); + return manager.serializer().writeSchemaTemplate(template); + } + throw new HugeException("No permission to update schema template"); + + } + + private static class JsonSchemaTemplate implements Checkable { + + @JsonProperty("name") + public String name; + @JsonProperty("schema") + public String schema; + + @Override + public void checkCreate(boolean isBatch) { + E.checkArgument(this.name != null && !this.name.isEmpty(), + "The name of schema template can't be null or " + + "empty"); + + E.checkArgument(this.schema != null && !this.schema.isEmpty(), + "The schema can't be null or empty"); + } + + public SchemaTemplate toSchemaTemplate() { + return new SchemaTemplate(this.name, this.schema); + } + + public SchemaTemplate build(SchemaTemplate old) { + E.checkArgument(StringUtils.isEmpty(this.name) || old.name().equals(this.name), + "The name of template can't be updated"); + return new SchemaTemplate(old.name(), this.schema); + } + + public String toString() { + return String.format("JsonSchemaTemplate{name=%s, schema=%s}", + this.name, this.schema); + } + } +} 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 770e75cc74..81feb1b322 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 @@ -2182,6 +2182,27 @@ public String cluster() { return this.cluster; } + public Set schemaTemplates(String graphSpace) { + return this.metaManager.schemaTemplates(graphSpace); + } + + public void createSchemaTemplate(String graphSpace, SchemaTemplate template) { + checkSchemaTemplateName(template.name()); + this.metaManager.addSchemaTemplate(graphSpace, template); + } + + public void dropSchemaTemplate(String graphSpace, String name) { + this.metaManager.removeSchemaTemplate(graphSpace, name); + } + + public void updateSchemaTemplate(String graphSpace, SchemaTemplate template) { + this.metaManager.updateSchemaTemplate(graphSpace, template); + } + + private static void checkSchemaTemplateName(String name) { + checkName(name, "schema template"); + } + private enum PdRegisterType { NODE_PORT, DDS From 19af891321ce310688d4d1355fcb40d342e8c35e Mon Sep 17 00:00:00 2001 From: yeaury Date: Fri, 24 Apr 2026 11:01:11 +0800 Subject: [PATCH 4/7] fix(auth): delegate default graph/role methods in AuthManagerProxy --- .../hugegraph/auth/HugeGraphAuthProxy.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 06cb575a5f..2cbb0915d0 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 @@ -1986,50 +1986,50 @@ public HugeGroup findGroup(String name) { @Override public void setDefaultGraph(String graphSpace, String graph, String user) { - + this.authManager.setDefaultGraph(graphSpace, graph, user); } @Override public void unsetDefaultGraph(String graphSpace, String graph, String user) { - + this.authManager.unsetDefaultGraph(graphSpace, graph, user); } @Override public Map getDefaultGraph(String graphSpace, String user) { - return Map.of(); + return this.authManager.getDefaultGraph(graphSpace, user); } @Override public Id createDefaultRole(String graphSpace, String owner, HugeDefaultRole role, String graph) { - return null; + return this.authManager.createDefaultRole(graphSpace, owner, role, graph); } @Override public Id createSpaceDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { - return null; + return this.authManager.createSpaceDefaultRole(graphSpace, owner, role); } @Override public boolean isDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { - return false; + return this.authManager.isDefaultRole(graphSpace, owner, role); } @Override public boolean isDefaultRole(String graphSpace, String graph, String owner, HugeDefaultRole role) { - return false; + return this.authManager.isDefaultRole(graphSpace, graph, owner, role); } @Override public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role) { - + this.authManager.deleteDefaultRole(graphSpace, owner, role); } @Override public void deleteDefaultRole(String graphSpace, String owner, HugeDefaultRole role, String graph) { - + this.authManager.deleteDefaultRole(graphSpace, owner, role, graph); } @Override From 1a810c0910d14afaac94d779b0e4d6f84a763413 Mon Sep 17 00:00:00 2001 From: yeaury Date: Fri, 24 Apr 2026 12:13:05 +0800 Subject: [PATCH 5/7] fix(api):Add createByText for text/plain graph creation compatibility --- .../hugegraph/api/profile/GraphsAPI.java | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java index f3051ed0b0..5907809a17 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java @@ -455,38 +455,36 @@ public Object create(@Context GraphManager manager, } /** - * Create graph via form-urlencoded (Hubble frontend compatibility). - * Frontend sends: POST /graphspaces/{graphspace}/graphs - * with Content-Type: application/x-www-form-urlencoded - * and body: graph=xx&nickname=yy&schema=zz&auth=false&graphspace=DEFAULT + * 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("application/x-www-form-urlencoded") + @Consumes("text/plain") @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space"}) - public Object createByForm(@Context GraphManager manager, + public Object createByText(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, - jakarta.ws.rs.core.MultivaluedMap - formParams) { - String name = formParams.getFirst("graph"); - E.checkArgument(name != null && !name.isEmpty(), - "The 'graph' parameter is required"); - - Map configs = new HashMap<>(); - for (Map.Entry> entry : formParams.entrySet()) { - String key = entry.getKey(); - List values = entry.getValue(); - if (values != null && !values.isEmpty()) { - configs.put(key, values.get(0)); - } + @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 configs = null; + if (configText != null && !configText.isEmpty()) { + configs = JsonUtil.fromJson(configText, Map.class); } - - return create(manager, graphSpace, name, null, configs); + return create(manager, graphSpace, name, clone, configs); } + + @GET @Timed @Path("{name}/conf") From 01e64e8f6f573b4a8fdcec31d36445c9cccf56be Mon Sep 17 00:00:00 2001 From: yeaury Date: Sat, 25 Apr 2026 20:56:56 +0800 Subject: [PATCH 6/7] fix(api):keep consistent with the License header --- .../hugegraph/api/space/GraphSpaceAPI.java | 20 +++++++++---------- .../api/space/SchemaTemplateAPI.java | 20 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) 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 e9d9bfbb9b..5e3d95a429 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 @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * 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 + * 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. + * 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.api.space; 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 887dfbfab5..13711cc6ae 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 @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * 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 + * 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. + * 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.api.space; From 79ea3193e5f6b9efb94b14cc4b0ee314d256fbc9 Mon Sep 17 00:00:00 2001 From: yeaury Date: Sun, 26 Apr 2026 00:29:44 +0800 Subject: [PATCH 7/7] fix: address code review issues for Hubble 2.0 API adaptation (#3008) - fix: use @POST/@DELETE for setDefault/unsetDefault (REST semantics) - fix: add null/empty validation before role field access in GraphSpaceAPI to prevent NPE in setDefaultRole/checkDefaultRole/deleteDefaultRole - fix: change isPrefix to private static and guard nickname null in GraphSpaceAPI and GraphsAPI - fix: ConfigUtil.writeConfigToString always returns JSON regardless of whether config was loaded from file, fixing listProfile endpoint - fix: add @RolesAllowed annotations to SchemaTemplateAPI endpoints - fix: use ForbiddenException (403) instead of HugeException (400) for authorization failures in SchemaTemplateAPI and GraphSpaceAPI - fix: correct LOG placeholder count in SchemaTemplateAPI.delete - fix: use HugeException ('%s') format instead of SLF4J '{}' format - fix: replace com.alipay StringUtils with commons-lang3 in ManagerAPI - fix: add @Consumes and checkUpdate() validation to SchemaTemplate.update - fix: add ensurePdModeEnabled guard to ManagerAPI.checkDefaultRole - fix: guard configs null access in GraphsAPI.create clone branch --- .../apache/hugegraph/api/auth/ManagerAPI.java | 3 +- .../hugegraph/api/profile/GraphsAPI.java | 35 +++++---------- .../hugegraph/api/space/GraphSpaceAPI.java | 45 ++++++++++++++----- .../api/space/SchemaTemplateAPI.java | 25 ++++++++--- .../org/apache/hugegraph/util/ConfigUtil.java | 34 +++++--------- 5 files changed, 78 insertions(+), 64 deletions(-) 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 c84ae681a2..3272f8420a 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 @@ -22,7 +22,7 @@ import java.util.ArrayList; import java.util.List; -import com.alipay.remoting.util.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter; @@ -272,6 +272,7 @@ public String checkDefaultRole(@Context GraphManager manager, @QueryParam("graph") String graph) { LOG.debug("check if current user is default role: {} {} {}", role, graphSpace, graph); + ensurePdModeEnabled(manager); AuthManager authManager = manager.authManager(); String user = HugeGraphAuthProxy.username(); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java index 5907809a17..76986d152a 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java @@ -80,8 +80,6 @@ public class GraphsAPI extends API { 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 convConfig(Map config) { @@ -197,13 +195,14 @@ public Object listProfile(@Context GraphManager manager, return defaultProfiles; } - public boolean isPrefix(Map profile, String prefix) { + private static boolean isPrefix(Map 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(); + Object nicknameObj = profile.get("nickname"); + String nickname = nicknameObj != null ? nicknameObj.toString() : ""; return name.startsWith(prefix) || nickname.startsWith(prefix); } @@ -223,7 +222,7 @@ public Object get(@Context GraphManager manager, return ImmutableMap.of("name", g.name(), "backend", g.backend()); } - @GET + @POST @Timed @Path("{name}/default") @Produces(APPLICATION_JSON_WITH_CHARSET) @@ -243,9 +242,9 @@ public Map setDefault(@Context GraphManager manager, return ImmutableMap.of("default_graph", defaults.keySet()); } - @GET + @DELETE @Timed - @Path("{name}/undefault") + @Path("{name}/default") @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$owner=$name"}) public Map unsetDefault(@Context GraphManager manager, @@ -345,22 +344,6 @@ public Map manage(@Context GraphManager manager, exist.nickname(nickname); } 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( "Invalid graph action: '%s'", action)); @@ -435,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 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; } 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 5e3d95a429..a79c1efc77 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 @@ -27,6 +27,7 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; +import org.apache.hugegraph.HugeException; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; import org.apache.hugegraph.auth.AuthManager; @@ -34,7 +35,6 @@ import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.util.E; @@ -55,6 +55,7 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; @@ -116,10 +117,20 @@ public Object get(@Context GraphManager manager, public String setDefaultRole(@Context GraphManager manager, @PathParam("graphspace") String name, JsonDefaultRole jsonRole) { + E.checkArgumentNotNull(jsonRole, "Request body cannot be null"); + E.checkArgument(StringUtils.isNotEmpty(jsonRole.user), + "The 'user' field cannot be null or empty"); + E.checkArgument(StringUtils.isNotEmpty(jsonRole.role), + "The 'role' field cannot be null or empty"); String user = jsonRole.user; String graph = jsonRole.graph; - HugeDefaultRole role = - HugeDefaultRole.valueOf(jsonRole.role.toUpperCase()); + HugeDefaultRole role; + try { + role = HugeDefaultRole.valueOf(jsonRole.role.toUpperCase()); + } catch (IllegalArgumentException e) { + E.checkArgument(false, "Invalid role value '%s'", jsonRole.role); + role = null; // unreachable, satisfies compiler + } LOG.debug("Create default role: {} {} {}", user, role, name); AuthManager authManager = manager.authManager(); @@ -129,7 +140,7 @@ public String setDefaultRole(@Context GraphManager manager, // only admin can set space admin if (!authManager.isAdminManager(HugeGraphAuthProxy.username()) && role.equals(HugeDefaultRole.SPACE)) { - throw new HugeException("Forbidden to set role %s", role.toString()); + throw new ForbiddenException("Forbidden to set role " + role.toString()); } boolean hasGraph = role.equals(HugeDefaultRole.OBSERVER); @@ -162,12 +173,21 @@ public String checkDefaultRole(@Context GraphManager manager, @QueryParam("user") String user, @QueryParam("role") String role, @QueryParam("graph") String graph) { + E.checkArgument(StringUtils.isNotEmpty(user), + "The 'user' query param cannot be null or empty"); + E.checkArgument(StringUtils.isNotEmpty(role), + "The 'role' query param cannot be null or empty"); LOG.debug("Check space role: {} {} {}", user, role, name); AuthManager authManager = manager.authManager(); - HugeDefaultRole defaultRole = - 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, satisfies compiler + } boolean hasGraph = defaultRole.equals(HugeDefaultRole.OBSERVER); E.checkArgument(!hasGraph || StringUtils.isNotEmpty(graph), "Must set a graph for observer"); @@ -193,6 +213,10 @@ public void deleteDefaultRole(@Context GraphManager manager, @QueryParam("user") String user, @QueryParam("role") String role, @QueryParam("graph") String graph) { + E.checkArgument(StringUtils.isNotEmpty(user), + "The 'user' query param cannot be null or empty"); + E.checkArgument(StringUtils.isNotEmpty(role), + "The 'role' query param cannot be null or empty"); LOG.debug("Delete space role: {} {} {}", user, role, name); @@ -203,7 +227,7 @@ public void deleteDefaultRole(@Context GraphManager manager, if (!authManager.isAdminManager(HugeGraphAuthProxy.username()) && role.equalsIgnoreCase(HugeDefaultRole.SPACE.toString())) { - throw new HugeException("Forbidden to delete role %s", role); + throw new ForbiddenException("Forbidden to delete role " + role); } HugeDefaultRole defaultRole = @@ -289,13 +313,14 @@ public String create(@Context GraphManager manager, return manager.serializer().writeGraphSpace(space); } - public boolean isPrefix(Map profile, String prefix) { + private static boolean isPrefix(Map profile, String prefix) { if (StringUtils.isEmpty(prefix)) { return true; } - // graph name or nickname is not empty + // match by name or nickname (nickname may be null) String name = profile.get("name").toString(); - String nickname = profile.get("nickname").toString(); + Object nicknameObj = profile.get("nickname"); + String nickname = nicknameObj != null ? nicknameObj.toString() : ""; return name.startsWith(prefix) || nickname.startsWith(prefix); } 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 13711cc6ae..68e2850a74 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 @@ -21,12 +21,12 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.HugeException; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter; import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.server.RestServer; import org.apache.hugegraph.space.SchemaTemplate; import org.apache.hugegraph.util.E; @@ -38,9 +38,11 @@ import com.google.common.collect.ImmutableMap; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; @@ -60,6 +62,7 @@ public class SchemaTemplateAPI extends API { @GET @Timed @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$dynamic"}) public Object list(@Context GraphManager manager, @PathParam("graphspace") String graphSpace) { LOG.debug("List all schema templates for graph space {}", graphSpace); @@ -72,6 +75,7 @@ public Object list(@Context GraphManager manager, @Timed @Path("{name}") @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$dynamic"}) public Object get(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, @PathParam("name") String name) { @@ -87,6 +91,7 @@ public Object get(@Context GraphManager manager, @StatusFilter.Status(StatusFilter.Status.CREATED) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$dynamic"}) public String create(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, JsonSchemaTemplate jsonSchemaTemplate) { @@ -108,11 +113,12 @@ public String create(@Context GraphManager manager, @Timed @Path("{name}") @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$dynamic"}) public void delete(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, @PathParam("name") String name, @Context SecurityContext sc) { - LOG.debug("Remove schema template by name '{}' for graph space", + LOG.debug("Remove schema template by name '{}' for graph space '{}'", name, graphSpace); E.checkArgument(manager.graphSpace(graphSpace) != null, "The graph space '%s' is not exist", graphSpace); @@ -127,23 +133,26 @@ public void delete(@Context GraphManager manager, if (st.creator().equals(username) || isSpace) { manager.dropSchemaTemplate(graphSpace, name); } else { - throw new HugeException("No permission to delete schema template"); + throw new ForbiddenException("No permission to delete schema template"); } } @PUT @Timed @Path("{name}") + @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space_member", "$dynamic"}) public String update(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, @PathParam("name") String name, @Context SecurityContext sc, JsonSchemaTemplate jsonSchemaTemplate) { + jsonSchemaTemplate.checkUpdate(); SchemaTemplate old = schemaTemplate(manager, graphSpace, name); if (null == old) { - throw new HugeException("Schema template {} does not exist", name); + throw new HugeException("Schema template '%s' does not exist", name); } String username = HugeGraphAuthProxy.username(); @@ -158,7 +167,7 @@ public String update(@Context GraphManager manager, manager.updateSchemaTemplate(graphSpace, template); return manager.serializer().writeSchemaTemplate(template); } - throw new HugeException("No permission to update schema template"); + throw new ForbiddenException("No permission to update schema template"); } @@ -179,6 +188,12 @@ public void checkCreate(boolean isBatch) { "The schema can't be null or empty"); } + @Override + public void checkUpdate() { + E.checkArgument(this.schema != null && !this.schema.isEmpty(), + "The schema can't be null or empty"); + } + public SchemaTemplate toSchemaTemplate() { return new SchemaTemplate(this.name, this.schema); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java index 82b233d253..f29b4ea9cd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java @@ -28,7 +28,7 @@ import java.util.List; import java.util.Map; -import jakarta.ws.rs.NotSupportedException; + import org.apache.commons.configuration2.FileBasedConfiguration; import org.apache.commons.configuration2.HierarchicalConfiguration; @@ -194,28 +194,16 @@ private static void validateGraphName(String graphName) { } public static String writeConfigToString(HugeConfig config) { - String content; - try { - if (config.file() == null) { - Map configMap = new HashMap<>(); - Iterator iterator = config.getKeys(); - while (iterator.hasNext()) { - String key = iterator.next(); - configMap.put(key, config.getProperty(key)); - } - content = JsonUtil.toJson(configMap); - } else { - File file = config.file(); - if (file == null) { - throw new NotSupportedException( - "Can't access the api in a node which started " + - "with non local file config."); - } - content = FileUtils.readFileToString(file); - } - } catch (IOException e) { - throw new HugeException("Failed to read config of graph", e); + // Always serialize config to a JSON object by iterating all keys, + // regardless of whether the config was loaded from a file or not. + // This ensures callers (e.g. GraphsAPI.listProfile) can safely parse + // the result via JsonUtil.fromJson() without format inconsistencies. + Map configMap = new HashMap<>(); + Iterator iterator = config.getKeys(); + while (iterator.hasNext()) { + String key = iterator.next(); + configMap.put(key, config.getProperty(key)); } - return content; + return JsonUtil.toJson(configMap); } }