-
Notifications
You must be signed in to change notification settings - Fork 597
feat(server): adapt Hubble 2.0 frontend APIs and implement default graph/role management #3008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
77cee94
42f3dfd
e5e5028
19af891
1a810c0
01e64e8
79ea319
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,9 +22,12 @@ | |||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||
| import java.util.List; | ||||||||||||||||||
|
|
||||||||||||||||||
| import org.apache.commons.lang3.StringUtils; | ||||||||||||||||||
|
|
||||||||||||||||||
| import org.apache.hugegraph.api.API; | ||||||||||||||||||
| import org.apache.hugegraph.api.filter.StatusFilter; | ||||||||||||||||||
| import org.apache.hugegraph.auth.AuthManager; | ||||||||||||||||||
| import org.apache.hugegraph.auth.HugeDefaultRole; | ||||||||||||||||||
| import org.apache.hugegraph.auth.HugeGraphAuthProxy; | ||||||||||||||||||
| import org.apache.hugegraph.auth.HugePermission; | ||||||||||||||||||
| import org.apache.hugegraph.core.GraphManager; | ||||||||||||||||||
|
|
@@ -259,6 +262,41 @@ public String getRolesInGs(@Context GraphManager manager, | |||||||||||||||||
| result)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| @GET | ||||||||||||||||||
| @Timed | ||||||||||||||||||
| @Path("default") | ||||||||||||||||||
| @Consumes(APPLICATION_JSON) | ||||||||||||||||||
| public String checkDefaultRole(@Context GraphManager manager, | ||||||||||||||||||
| @QueryParam("graphspace") String graphSpace, | ||||||||||||||||||
| @QueryParam("role") String role, | ||||||||||||||||||
| @QueryParam("graph") String graph) { | ||||||||||||||||||
| LOG.debug("check if current user is default role: {} {} {}", | ||||||||||||||||||
| role, graphSpace, graph); | ||||||||||||||||||
| ensurePdModeEnabled(manager); | ||||||||||||||||||
| AuthManager authManager = manager.authManager(); | ||||||||||||||||||
| String user = HugeGraphAuthProxy.username(); | ||||||||||||||||||
|
|
||||||||||||||||||
| E.checkArgument(StringUtils.isNotEmpty(role) && | ||||||||||||||||||
| StringUtils.isNotEmpty(graphSpace), | ||||||||||||||||||
| "Must pass graphspace and role params"); | ||||||||||||||||||
|
|
||||||||||||||||||
| HugeDefaultRole defaultRole = | ||||||||||||||||||
| HugeDefaultRole.valueOf(role.toUpperCase()); | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
| 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, | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,7 @@ public class GraphsAPI extends API { | |||||||||||||||
| private static final String CONFIRM_DROP = "I'm sure to drop the graph"; | ||||||||||||||||
| private static final String GRAPH_DESCRIPTION = "description"; | ||||||||||||||||
| private static final String GRAPH_ACTION = "action"; | ||||||||||||||||
| private static final String UPDATE = "update"; | ||||||||||||||||
| private static final String GRAPH_ACTION_RELOAD = "reload"; | ||||||||||||||||
|
|
||||||||||||||||
| private static Map<String, Object> convConfig(Map<String, Object> config) { | ||||||||||||||||
|
|
@@ -120,6 +126,86 @@ public Object list(@Context GraphManager manager, | |||||||||||||||
| return ImmutableMap.of("graphs", filterGraphs); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @GET | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("profile") | ||||||||||||||||
| @Produces(APPLICATION_JSON_WITH_CHARSET) | ||||||||||||||||
| @RolesAllowed({"space_member", "$dynamic"}) | ||||||||||||||||
| public Object listProfile(@Context GraphManager manager, | ||||||||||||||||
| @Parameter(description = "The graph space name") | ||||||||||||||||
| @PathParam("graphspace") String graphSpace, | ||||||||||||||||
| @Parameter(description = "Filter graphs by name or nickname prefix") | ||||||||||||||||
| @QueryParam("prefix") String prefix, | ||||||||||||||||
| @Context SecurityContext sc) { | ||||||||||||||||
| LOG.debug("List graph profiles in graph space {}", graphSpace); | ||||||||||||||||
| if (null == manager.graphSpace(graphSpace)) { | ||||||||||||||||
| throw new HugeException("Graphspace not exist!"); | ||||||||||||||||
| } | ||||||||||||||||
| GraphSpace gs = manager.graphSpace(graphSpace); | ||||||||||||||||
| String gsNickname = gs.nickname(); | ||||||||||||||||
|
|
||||||||||||||||
| AuthManager authManager = manager.authManager(); | ||||||||||||||||
| String user = HugeGraphAuthProxy.username(); | ||||||||||||||||
| Map<String, Date> defaultGraphs = authManager.getDefaultGraph(graphSpace, user); | ||||||||||||||||
|
|
||||||||||||||||
| SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
private static final DateTimeFormatter DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
||||||||||||||||
| Set<String> graphs = manager.graphs(graphSpace); | ||||||||||||||||
| List<Map<String, Object>> profiles = new ArrayList<>(); | ||||||||||||||||
| List<Map<String, Object>> defaultProfiles = new ArrayList<>(); | ||||||||||||||||
| for (String graph : graphs) { | ||||||||||||||||
| String role = RequiredPerm.roleFor(graphSpace, graph, | ||||||||||||||||
| HugePermission.READ); | ||||||||||||||||
| if (!sc.isUserInRole(role)) { | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| try { | ||||||||||||||||
| HugeGraph hg = graph(manager, graphSpace, graph); | ||||||||||||||||
| HugeConfig config = (HugeConfig) hg.configuration(); | ||||||||||||||||
| String configResp = ConfigUtil.writeConfigToString(config); | ||||||||||||||||
| Map<String, Object> profile = | ||||||||||||||||
| JsonUtil.fromJson(configResp, Map.class); | ||||||||||||||||
|
Comment on lines
+163
to
+166
|
||||||||||||||||
| 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; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
Yeaury marked this conversation as resolved.
|
||||||||||||||||
| private static boolean isPrefix(Map<String, Object> profile, String prefix) { | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||
| if (StringUtils.isEmpty(prefix)) { | ||||||||||||||||
| return true; | ||||||||||||||||
| } | ||||||||||||||||
| // graph name or nickname is not empty | ||||||||||||||||
| String name = profile.get("name").toString(); | ||||||||||||||||
| Object nicknameObj = profile.get("nickname"); | ||||||||||||||||
| String nickname = nicknameObj != null ? nicknameObj.toString() : ""; | ||||||||||||||||
| return name.startsWith(prefix) || nickname.startsWith(prefix); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @GET | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("{name}") | ||||||||||||||||
|
|
@@ -136,6 +222,61 @@ public Object get(@Context GraphManager manager, | |||||||||||||||
| return ImmutableMap.of("name", g.name(), "backend", g.backend()); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @POST | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("{name}/default") | ||||||||||||||||
| @Produces(APPLICATION_JSON_WITH_CHARSET) | ||||||||||||||||
| @RolesAllowed({"space_member", "$owner=$name"}) | ||||||||||||||||
| public Map<String, Object> setDefault(@Context GraphManager manager, | ||||||||||||||||
| @Parameter(description = "The graph space name") | ||||||||||||||||
| @PathParam("graphspace") String graphSpace, | ||||||||||||||||
| @Parameter(description = "The graph name") | ||||||||||||||||
| @PathParam("name") String name) { | ||||||||||||||||
| LOG.debug("Set default graph '{}' in graph space '{}'", name, graphSpace); | ||||||||||||||||
| E.checkArgument(manager.graph(graphSpace, name) != null, | ||||||||||||||||
| "Graph '%s/%s' does not exist", graphSpace, name); | ||||||||||||||||
| String user = HugeGraphAuthProxy.username(); | ||||||||||||||||
| AuthManager authManager = manager.authManager(); | ||||||||||||||||
| authManager.setDefaultGraph(graphSpace, name, user); | ||||||||||||||||
| Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user); | ||||||||||||||||
| return ImmutableMap.of("default_graph", defaults.keySet()); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @DELETE | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("{name}/default") | ||||||||||||||||
| @Produces(APPLICATION_JSON_WITH_CHARSET) | ||||||||||||||||
| @RolesAllowed({"space_member", "$owner=$name"}) | ||||||||||||||||
| public Map<String, Object> unsetDefault(@Context GraphManager manager, | ||||||||||||||||
| @Parameter(description = "The graph space name") | ||||||||||||||||
| @PathParam("graphspace") String graphSpace, | ||||||||||||||||
| @Parameter(description = "The graph name") | ||||||||||||||||
| @PathParam("name") String name) { | ||||||||||||||||
| LOG.debug("Unset default graph '{}' in graph space '{}'", name, graphSpace); | ||||||||||||||||
| E.checkArgument(manager.graph(graphSpace, name) != null, | ||||||||||||||||
| "Graph '%s/%s' does not exist", graphSpace, name); | ||||||||||||||||
| String user = HugeGraphAuthProxy.username(); | ||||||||||||||||
| AuthManager authManager = manager.authManager(); | ||||||||||||||||
| authManager.unsetDefaultGraph(graphSpace, name, user); | ||||||||||||||||
| Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user); | ||||||||||||||||
| return ImmutableMap.of("default_graph", defaults.keySet()); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @GET | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("default") | ||||||||||||||||
| @Produces(APPLICATION_JSON_WITH_CHARSET) | ||||||||||||||||
| @RolesAllowed({"space_member", "$dynamic"}) | ||||||||||||||||
| public Map<String, Object> getDefault(@Context GraphManager manager, | ||||||||||||||||
| @Parameter(description = "The graph space name") | ||||||||||||||||
| @PathParam("graphspace") String graphSpace) { | ||||||||||||||||
| LOG.debug("Get default graphs in graph space '{}'", graphSpace); | ||||||||||||||||
| String user = HugeGraphAuthProxy.username(); | ||||||||||||||||
| AuthManager authManager = manager.authManager(); | ||||||||||||||||
| Map<String, Date> defaults = authManager.getDefaultGraph(graphSpace, user); | ||||||||||||||||
| return ImmutableMap.of("default_graph", defaults.keySet()); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @DELETE | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("{name}") | ||||||||||||||||
|
|
@@ -155,6 +296,60 @@ public void drop(@Context GraphManager manager, | |||||||||||||||
| manager.dropGraph(graphSpace, name, true); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @PUT | ||||||||||||||||
| @Timed | ||||||||||||||||
| @Path("{name}") | ||||||||||||||||
| @Consumes(APPLICATION_JSON) | ||||||||||||||||
| @Produces(APPLICATION_JSON_WITH_CHARSET) | ||||||||||||||||
| @RolesAllowed({"space"}) | ||||||||||||||||
| public Map<String, String> manage(@Context GraphManager manager, | ||||||||||||||||
| @Parameter(description = "The graph space name") | ||||||||||||||||
| @PathParam("graphspace") String graphSpace, | ||||||||||||||||
| @Parameter(description = "The graph name") | ||||||||||||||||
| @PathParam("name") String name, | ||||||||||||||||
| @Parameter(description = "Action map: {'action':'update','update':{...}}") | ||||||||||||||||
| Map<String, Object> actionMap) { | ||||||||||||||||
| LOG.debug("Manage graph '{}' with action '{}'", name, actionMap); | ||||||||||||||||
| E.checkArgument(actionMap != null && actionMap.size() == 2 && | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
要求请求体恰好包含 2 个 key。如果前端在 JSON 中额外带了字段(很常见的兼容场景),请求会被拒绝。建议放宽为只校验必需字段:
Suggested change
|
||||||||||||||||
| actionMap.containsKey(GRAPH_ACTION), | ||||||||||||||||
| "Invalid request body '%s'", actionMap); | ||||||||||||||||
| Object value = actionMap.get(GRAPH_ACTION); | ||||||||||||||||
| E.checkArgument(value instanceof String, | ||||||||||||||||
| "Invalid action type '%s', must be string", | ||||||||||||||||
| value.getClass()); | ||||||||||||||||
| String action = (String) value; | ||||||||||||||||
| switch (action) { | ||||||||||||||||
| case UPDATE: | ||||||||||||||||
| E.checkArgument(actionMap.containsKey(UPDATE), | ||||||||||||||||
| "Please pass '%s' for graph update", | ||||||||||||||||
| UPDATE); | ||||||||||||||||
| value = actionMap.get(UPDATE); | ||||||||||||||||
| E.checkArgument(value instanceof Map, | ||||||||||||||||
| "The '%s' must be map, but got %s", | ||||||||||||||||
| UPDATE, value.getClass()); | ||||||||||||||||
| @SuppressWarnings("unchecked") | ||||||||||||||||
| Map<String, Object> graphMap = (Map<String, Object>) value; | ||||||||||||||||
| String graphName = (String) graphMap.get("name"); | ||||||||||||||||
| E.checkArgument(graphName != null && graphName.equals(name), | ||||||||||||||||
| "Different name in update body '%s' with path '%s'", | ||||||||||||||||
| graphName, name); | ||||||||||||||||
| HugeGraph exist = graph(manager, graphSpace, name); | ||||||||||||||||
| String nickname = (String) graphMap.get("nickname"); | ||||||||||||||||
| if (!Strings.isEmpty(nickname)) { | ||||||||||||||||
| GraphManager.checkNickname(nickname); | ||||||||||||||||
| E.checkArgument(!manager.isExistedGraphNickname(graphSpace, nickname) || | ||||||||||||||||
| nickname.equals(exist.nickname()), | ||||||||||||||||
|
Yeaury marked this conversation as resolved.
|
||||||||||||||||
| "Nickname '%s' has already existed in graphspace '%s'", | ||||||||||||||||
| nickname, graphSpace); | ||||||||||||||||
|
||||||||||||||||
| nickname, graphSpace); | |
| nickname, graphSpace); | |
| Map<String, Object> updatedGraphConfig = new HashMap<>(); | |
| updatedGraphConfig.put("nickname", nickname); | |
| manager.meta().updateGraphConfig(graphSpace, name, | |
| updatedGraphConfig); | |
| manager.meta().notifyGraphUpdate(graphSpace, name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isExistedGraphNickname 在非 PD 模式下会 NPE
两个问题:
-
exist.nickname(nickname)只修改了内存中的StandardHugeGraph.nickname字段,没有调用metaManager.updateGraphConfig()写回存储。创建图时有完整的持久化链路(GraphManager:1345设内存 →:1353写 metaManager),但这里的更新路径缺少持久化步骤,重启后 nickname 会丢失。 -
manager.isExistedGraphNickname()内部调用metaManager.graphConfigs(graphSpace)— 在非 PD(RocksDB 单机)模式下 MetaManager 未初始化,会抛 NPE。GraphsAPI 的端点在非 PD 模式下通过graphspace=DEFAULT正常使用,所以这里需要加isPDEnabled()分支做兼容(可参考GraphManager.graphs()的处理方式)。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backend=hstore 会导致非 PD 模式创建图失败
GraphsAPI 的创建端点在非 PD(RocksDB 单机)模式下也会被调用(通过 graphspace=DEFAULT)。这里无条件填充 backend=hstore 会导致单机版用户创建图时使用错误的后端,应该加 PD 模式判断:
| configs.putIfAbsent("backend", "hstore"); | |
| // Auto-fill defaults for PD/HStore mode when not provided | |
| if (manager.isPDEnabled()) { | |
| configs.putIfAbsent("backend", "hstore"); | |
| configs.putIfAbsent("serializer", "binary"); | |
| } | |
| configs.putIfAbsent("store", name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkDefaultRole()doesn’t callensurePdModeEnabled(manager)while the other endpoints inManagerAPIdo. If this API requires PD mode, add the same guard here to avoid inconsistent behavior when PD is disabled (or explicitly handle the non-PD case).