SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)

恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,76 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ApiResult {
private ApiResult() { }
public static Map<String, Object> ok(Object data, String traceId) {
Map<String, Object> result = base(true, traceId);
result.put("data", data);
return result;
}
public static Map<String, Object> ok(Object data, List<String> messages, Map<String, Object> meta, String traceId) {
Map<String, Object> result = ok(data, traceId);
if (messages != null && !messages.isEmpty()) {
result.put("messages", messages);
}
if (meta != null && !meta.isEmpty()) {
result.put("meta", meta);
}
return result;
}
public static Map<String, Object> error(String code, String message, String traceId) {
return error(code, message, null, traceId);
}
public static Map<String, Object> error(String code, String message, String field, String traceId) {
Map<String, Object> result = base(false, traceId);
List<Map<String, Object>> errors = new ArrayList<>();
Map<String, Object> error = new LinkedHashMap<>();
error.put("code", code);
error.put("message", message);
if (field != null && !field.isEmpty()) {
error.put("field", field);
}
errors.add(error);
result.put("errors", errors);
return result;
}
public static Map<String, Object> warning(Object data, String warning, String traceId) {
Map<String, Object> result = ok(data, traceId);
result.put("warnings", List.of(warning));
return result;
}
private static Map<String, Object> base(boolean ok, String traceId) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("ok", ok);
result.put("traceId", traceId);
return result;
}
}
@@ -0,0 +1,72 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.webapp.control.LoginWorker;
@SuppressWarnings("serial")
public class ModernApiAuthFilter extends HttpFilter {
@FunctionalInterface
interface SecuredLoginBridge {
String bridge(HttpServletRequest request, HttpServletResponse response);
}
private final SecuredLoginBridge securedLoginBridge;
public ModernApiAuthFilter() {
this(LoginWorker::securedUserLoginByJWTCookie);
}
ModernApiAuthFilter(SecuredLoginBridge securedLoginBridge) {
this.securedLoginBridge = securedLoginBridge;
}
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
seedOfbizContext(request);
if (ModernApiUtil.userLogin(request) == null) {
securedLoginBridge.bridge(request, response);
}
chain.doFilter(request, response);
}
private void seedOfbizContext(HttpServletRequest request) {
ServletContext servletContext = request.getServletContext();
copyContextAttribute(request, servletContext, "delegator");
copyContextAttribute(request, servletContext, "dispatcher");
copyContextAttribute(request, servletContext, "security");
}
private void copyContextAttribute(HttpServletRequest request, ServletContext servletContext, String name) {
Object value = servletContext.getAttribute(name);
if (value != null && request.getAttribute(name) == null) {
request.setAttribute(name, value);
}
}
}
@@ -0,0 +1,40 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.ofbiz.base.util.Debug;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
public class ModernApiConfig extends ResourceConfig {
public ModernApiConfig() {
packages("org.apache.ofbiz.modernapi.resources");
register(JacksonFeature.class);
register(MultiPartFeature.class);
if (Debug.verboseOn()) {
register(new LoggingFeature(Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), Level.INFO,
LoggingFeature.Verbosity.PAYLOAD_ANY, 10000));
}
}
}
@@ -0,0 +1,52 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.webapp.WebAppUtil;
public class ModernApiContextListener implements ServletContextListener {
private static final String MODULE = ModernApiContextListener.class.getName();
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
Delegator delegator = WebAppUtil.getDelegator(servletContext);
LocalDispatcher dispatcher = WebAppUtil.getDispatcher(servletContext);
servletContext.setAttribute("delegator", delegator);
servletContext.setAttribute("dispatcher", dispatcher);
servletContext.setAttribute("security", WebAppUtil.getSecurity(servletContext));
Debug.logInfo("Modern API context initialized, delegator " + delegator + ", dispatcher " + dispatcher, MODULE);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
servletContext.removeAttribute("delegator");
servletContext.removeAttribute("dispatcher");
servletContext.removeAttribute("security");
Debug.logInfo("Modern API context destroyed", MODULE);
}
}
@@ -0,0 +1,238 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import java.math.BigDecimal;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.security.Security;
import org.apache.ofbiz.service.LocalDispatcher;
public final class ModernApiUtil {
private ModernApiUtil() { }
public static String traceId() {
return UUID.randomUUID().toString();
}
public static Response ok(Object data, String traceId) {
return Response.ok(ApiResult.ok(data, traceId)).build();
}
public static Response error(Response.Status status, String code, String message, String traceId) {
return Response.status(status).entity(ApiResult.error(code, message, traceId)).build();
}
public static Delegator delegator(ServletContext servletContext) {
return (Delegator) servletContext.getAttribute("delegator");
}
public static LocalDispatcher dispatcher(ServletContext servletContext) {
return (LocalDispatcher) servletContext.getAttribute("dispatcher");
}
public static Security security(ServletContext servletContext) {
return (Security) servletContext.getAttribute("security");
}
public static GenericValue userLogin(HttpServletRequest request) {
Object requestUserLogin = request.getAttribute("userLogin");
if (requestUserLogin instanceof GenericValue) {
return (GenericValue) requestUserLogin;
}
Object sessionUserLogin = request.getSession(false) == null ? null : request.getSession(false).getAttribute("userLogin");
return sessionUserLogin instanceof GenericValue ? (GenericValue) sessionUserLogin : null;
}
public static boolean hasViewPermission(Security security, GenericValue userLogin, String permission) {
if (permission == null || permission.isEmpty() || "NONE".equals(permission)) {
return true;
}
if (security == null || userLogin == null) {
return false;
}
return security.hasEntityPermission(permission, "_VIEW", userLogin)
|| security.hasEntityPermission(permission, "_ADMIN", userLogin)
|| security.hasPermission(permission + "_VIEW", userLogin)
|| security.hasPermission(permission + "_ADMIN", userLogin);
}
public static boolean hasEntityViewPermission(Security security, GenericValue userLogin, ModelEntity modelEntity) {
if (security == null || userLogin == null || modelEntity == null) {
return false;
}
if (hasViewPermission(security, userLogin, "ENTITY_DATA")) {
return true;
}
for (String permission : entityViewPermissions(modelEntity)) {
if (hasViewPermission(security, userLogin, permission)) {
return true;
}
}
return false;
}
public static int safePage(int page) {
return Math.max(0, page);
}
public static int safePageSize(int pageSize) {
return Math.max(1, Math.min(pageSize, 100));
}
public static boolean hasMore(int page, int pageSize, long total) {
long safeTotal = Math.max(0, total);
return ((long) (safePage(page) + 1) * safePageSize(pageSize)) < safeTotal;
}
public static Map<String, Object> pagination(int page, int pageSize, long total) {
Map<String, Object> data = new LinkedHashMap<>();
int safePage = safePage(page);
int safePageSize = safePageSize(pageSize);
long safeTotal = Math.max(0, total);
data.put("page", safePage);
data.put("pageSize", safePageSize);
data.put("total", safeTotal);
data.put("hasMore", hasMore(safePage, safePageSize, safeTotal));
return data;
}
public static List<String> safeOrderByFields(Collection<String> fieldNames, List<String> pkFieldNames, String orderBy) {
List<String> safeFields = new ArrayList<>();
Set<String> allowedFields = new HashSet<>(fieldNames);
if (orderBy != null && !orderBy.isBlank()) {
for (String rawField : orderBy.split(",")) {
String field = rawField.trim();
if (field.isEmpty()) {
continue;
}
String upper = field.toUpperCase(Locale.ROOT);
String suffix = "";
if (upper.endsWith(" DESC")) {
suffix = " DESC";
field = field.substring(0, field.length() - 5).trim();
} else if (upper.endsWith(" ASC")) {
suffix = " ASC";
field = field.substring(0, field.length() - 4).trim();
} else if (field.startsWith("-")) {
suffix = " DESC";
field = field.substring(1).trim();
}
if (allowedFields.contains(field)) {
safeFields.add(field + suffix);
}
}
}
if (!safeFields.isEmpty()) {
return safeFields;
}
return new ArrayList<>(pkFieldNames);
}
private static List<String> entityViewPermissions(ModelEntity modelEntity) {
List<String> permissions = new ArrayList<>();
String packageName = modelEntity.getPackageName() == null ? "" : modelEntity.getPackageName().toLowerCase(Locale.ROOT);
addPackagePermission(permissions, packageName, ".order.", "ORDERMGR");
addPackagePermission(permissions, packageName, ".party.", "PARTYMGR");
addPackagePermission(permissions, packageName, ".product.", "CATALOG");
addPackagePermission(permissions, packageName, ".shipment.", "FACILITY");
addPackagePermission(permissions, packageName, ".accounting.", "ACCOUNTING");
addPackagePermission(permissions, packageName, ".humanres.", "HUMANRES");
addPackagePermission(permissions, packageName, ".manufacturing.", "MANUFACTURING");
addPackagePermission(permissions, packageName, ".workeffort.", "WORKEFFORTMGR");
addPackagePermission(permissions, packageName, ".content.", "CONTENTMGR");
addPackagePermission(permissions, packageName, ".marketing.", "MARKETING");
addPackagePermission(permissions, packageName, ".webapp.website", "CONTENTMGR");
addPackagePermission(permissions, packageName, ".security.", "SECURITY");
addPackagePermission(permissions, packageName, ".webtools.", "WEBTOOLS");
if (isCommonReferenceEntity(modelEntity)) {
addPermission(permissions, "ORDERMGR");
addPermission(permissions, "PARTYMGR");
addPermission(permissions, "CATALOG");
addPermission(permissions, "FACILITY");
addPermission(permissions, "ACCOUNTING");
addPermission(permissions, "HUMANRES");
addPermission(permissions, "MANUFACTURING");
addPermission(permissions, "WORKEFFORTMGR");
addPermission(permissions, "CONTENTMGR");
}
return permissions;
}
private static void addPackagePermission(List<String> permissions, String packageName, String marker, String permission) {
if (packageName.contains(marker)) {
addPermission(permissions, permission);
}
}
private static void addPermission(List<String> permissions, String permission) {
if (!permissions.contains(permission)) {
permissions.add(permission);
}
}
private static boolean isCommonReferenceEntity(ModelEntity modelEntity) {
String packageName = modelEntity.getPackageName() == null ? "" : modelEntity.getPackageName().toLowerCase(Locale.ROOT);
if (!packageName.contains(".common.")) {
return false;
}
String entityName = modelEntity.getEntityName();
return entityName != null && (entityName.endsWith("Type") || entityName.endsWith("Item") || entityName.endsWith("Enum")
|| entityName.endsWith("Enumeration") || entityName.endsWith("Geo") || entityName.endsWith("Uom"));
}
public static Map<String, Object> publicFields(GenericValue value) {
Map<String, Object> fields = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : value.getAllFields().entrySet()) {
fields.put(entry.getKey(), serializableValue(entry.getValue()));
}
return fields;
}
public static Object serializableValue(Object value) {
if (value instanceof Timestamp || value instanceof Time || value instanceof java.sql.Date) {
return value.toString();
}
if (value instanceof Date) {
return Long.toString(((Date) value).getTime());
}
if (value instanceof BigDecimal) {
return value.toString();
}
return value;
}
}
@@ -0,0 +1,139 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ofbiz.base.location.FlexibleLocation;
import org.apache.ofbiz.base.util.Debug;
public final class UiInventoryLoader {
private static final String MODULE = UiInventoryLoader.class.getName();
private static final ObjectMapper MAPPER = new ObjectMapper();
private static Map<String, Object> cachedInventory;
private static String cachedInventoryPath;
private static long cachedInventoryLastModified = -1;
private UiInventoryLoader() { }
public static synchronized Map<String, Object> load() {
File generated = new File("plugins/modern-api/generated/ui-inventory.json");
if (!generated.exists()) {
try {
URL generatedUrl = FlexibleLocation.resolveLocation("component://modern-api/generated/ui-inventory.json");
generated = new File(generatedUrl.getPath());
} catch (Exception e) {
Debug.logWarning(e, "Modern UI inventory has not been generated yet", MODULE);
}
}
if (!generated.exists()) {
if (cachedInventory != null && cachedInventoryPath == null) {
return cachedInventory;
}
cachedInventory = emptyInventory();
cachedInventoryPath = null;
cachedInventoryLastModified = -1;
return cachedInventory;
}
String inventoryPath = generated.getAbsolutePath();
long inventoryLastModified = generated.lastModified();
if (cachedInventory != null
&& inventoryPath.equals(cachedInventoryPath)
&& inventoryLastModified == cachedInventoryLastModified) {
return cachedInventory;
}
try {
cachedInventory = MAPPER.readValue(generated, new TypeReference<Map<String, Object>>() { });
cachedInventoryPath = inventoryPath;
cachedInventoryLastModified = inventoryLastModified;
} catch (IOException e) {
Debug.logError(e, "Unable to read Modern UI inventory", MODULE);
cachedInventory = emptyInventory();
cachedInventoryPath = inventoryPath;
cachedInventoryLastModified = inventoryLastModified;
}
return cachedInventory;
}
@SuppressWarnings("unchecked")
public static Map<String, Object> findPage(String pageId) {
Object pageDefinitions = load().get("pageDefinitions");
if (!(pageDefinitions instanceof Map)) {
return Collections.emptyMap();
}
Object page = ((Map<String, Object>) pageDefinitions).get(pageId);
return page instanceof Map ? (Map<String, Object>) page : Collections.emptyMap();
}
@SuppressWarnings("unchecked")
public static Map<String, Object> findAction(String actionId) {
Object actionDefinitions = load().get("actionDefinitions");
if (!(actionDefinitions instanceof Map)) {
return Collections.emptyMap();
}
Object action = ((Map<String, Object>) actionDefinitions).get(actionId);
return action instanceof Map ? (Map<String, Object>) action : Collections.emptyMap();
}
private static Map<String, Object> emptyInventory() {
Map<String, Object> inventory = new LinkedHashMap<>();
inventory.put("generatedAt", null);
inventory.put("counts", Collections.emptyMap());
inventory.put("controllers", Collections.emptyList());
inventory.put("widgets", Collections.emptyList());
inventory.put("services", Collections.emptyList());
inventory.put("pageDefinitions", Collections.emptyMap());
inventory.put("routeManifest", Collections.emptyList());
inventory.put("actionDefinitions", Collections.emptyMap());
inventory.put("parityManifest", emptyParityManifest());
return inventory;
}
private static Map<String, Object> emptyParityManifest() {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("totalPages", 0);
summary.put("elementPlusRenderablePages", 0);
summary.put("routeRenderedPages", 0);
summary.put("adapterCoveredPages", 0);
summary.put("customVueRequiredPages", 0);
summary.put("pendingE2ePages", 0);
summary.put("templateParityPages", 0);
summary.put("highRiskPages", 0);
summary.put("mediumRiskPages", 0);
summary.put("lowRiskPages", 0);
Map<String, Object> manifest = new LinkedHashMap<>();
manifest.put("summary", summary);
manifest.put("byDomain", Collections.emptyList());
manifest.put("byComponent", Collections.emptyList());
manifest.put("byAdapter", Collections.emptyList());
manifest.put("byRequirement", Collections.emptyList());
manifest.put("byRisk", Collections.emptyMap());
manifest.put("pages", Collections.emptyList());
return manifest;
}
}
@@ -0,0 +1,403 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ModelParam;
import org.apache.ofbiz.service.ModelService;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.webapp.control.ConfigXMLReader;
import org.apache.ofbiz.webapp.event.EventHandler;
import org.apache.ofbiz.webapp.event.EventHandlerException;
import org.apache.ofbiz.webapp.event.GroovyEventHandler;
import org.apache.ofbiz.webapp.event.JavaEventHandler;
import org.apache.ofbiz.webapp.event.RomeEventHandler;
import org.apache.ofbiz.webapp.event.ServiceMultiEventHandler;
import org.apache.ofbiz.webapp.event.SimpleEventHandler;
@Path("/v1/actions")
@Produces(MediaType.APPLICATION_JSON)
public class ActionResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@Context private HttpServletResponse response;
@POST
@Path("/{actionId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response runAction(@PathParam("actionId") String actionId, Map<String, Object> payload) {
String traceId = ModernApiUtil.traceId();
LocalDispatcher dispatcher = ModernApiUtil.dispatcher(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
Map<String, Object> actionDefinition = UiInventoryLoader.findAction(actionId);
Map<String, Object> input = payload == null ? new LinkedHashMap<>() : new LinkedHashMap<>(payload);
String eventType = stringValue(actionDefinition.get("eventType"));
if (isWebEvent(eventType)) {
return runWebEvent(actionId, actionDefinition, input, userLogin, traceId);
}
Map<String, Object> navigationTarget = navigationTarget(actionDefinition);
if (!navigationTarget.isEmpty()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("actionId", actionId);
data.put("definition", actionDefinition);
data.put("executed", true);
data.put("actionType", "navigation");
data.put("navigationTarget", navigationTarget.get("modernPath"));
data.put("navigationPageId", navigationTarget.get("pageId"));
data.put("legacyResponse", navigationTarget);
data.put("payload", input);
data.put("reason", "Legacy controller request resolves to a view response and is handled as modern SPA navigation.");
return ModernApiUtil.ok(data, traceId);
}
String serviceName = serviceNameFor(actionId, actionDefinition);
if (serviceName == null || serviceName.isBlank()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("actionId", actionId);
data.put("definition", actionDefinition);
data.put("executed", false);
data.put("reason", "Only OFBiz service actions and supported web events can be executed through /api/v1/actions.");
return ModernApiUtil.ok(data, traceId);
}
if (userLogin == null) {
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to execute actions.", traceId);
}
if (dispatcher == null) {
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DISPATCHER_UNAVAILABLE", "OFBiz dispatcher is not available.", traceId);
}
input.put("userLogin", userLogin);
try {
Map<String, Object> serviceContext = dispatcher.getDispatchContext().makeValidContext(serviceName, ModelService.IN_PARAM, input);
Map<String, Object> serviceResult = dispatcher.runSync(serviceName, serviceContext);
if (!ServiceUtil.isSuccess(serviceResult)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("SERVICE_ERROR", ServiceUtil.getErrorMessage(serviceResult), traceId))
.build();
}
ModelService service = dispatcher.getDispatchContext().getModelService(serviceName);
Set<String> outParamNames = service.getOutParamNames();
Map<String, Object> data = new LinkedHashMap<>();
data.put("actionId", actionId);
data.put("serviceName", serviceName);
data.put("executed", true);
Map<String, Object> outParams = new LinkedHashMap<>();
for (String outParamName : outParamNames) {
ModelParam outParam = service.getParam(outParamName);
if (!outParam.isInternal() && serviceResult.containsKey(outParamName)) {
outParams.put(outParamName, ModernApiUtil.serializableValue(serviceResult.get(outParamName)));
}
}
data.put("result", outParams);
return ModernApiUtil.ok(data, traceId);
} catch (GenericServiceException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("SERVICE_EXCEPTION", e.getMessage(), traceId))
.build();
}
}
private Response runWebEvent(String actionId, Map<String, Object> actionDefinition,
Map<String, Object> payload, GenericValue userLogin, String traceId) {
if (requiresAuth(actionDefinition) && userLogin == null) {
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to execute actions.", traceId);
}
String eventType = stringValue(actionDefinition.get("eventType"));
String eventPath = stringValue(actionDefinition.get("eventPath"));
String eventInvoke = stringValue(actionDefinition.get("eventInvoke"));
if (eventInvoke.isBlank()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("actionId", actionId);
data.put("definition", actionDefinition);
data.put("executed", false);
data.put("reason", "The legacy controller action has no executable event invoke target.");
return ModernApiUtil.ok(data, traceId);
}
try {
EventHandler handler = eventHandler(eventType);
HttpServletRequest eventRequest = new PayloadRequestWrapper(request, payload);
CapturingResponseWrapper eventResponse = new CapturingResponseWrapper(response);
seedRequestContext(eventRequest, userLogin);
ConfigXMLReader.Event event = new ConfigXMLReader.Event(eventType, eventPath, eventInvoke, true);
String eventReturn = handler.invoke(event, null, eventRequest, eventResponse);
Map<String, Object> data = new LinkedHashMap<>();
data.put("actionId", actionId);
data.put("eventType", eventType);
data.put("eventPath", eventPath);
data.put("eventInvoke", eventInvoke);
data.put("eventReturn", eventReturn);
data.put("executed", !"error".equalsIgnoreCase(eventReturn));
data.put("redirectLocation", eventResponse.redirectLocation());
data.put("httpStatus", eventResponse.status());
data.put("messages", eventMessages(eventRequest));
data.put("payload", payload);
if ("error".equalsIgnoreCase(eventReturn)) {
data.put("reason", firstMessage(eventRequest, "Legacy event returned error."));
}
return ModernApiUtil.ok(data, traceId);
} catch (EventHandlerException | RuntimeException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("EVENT_EXCEPTION", e.getMessage(), traceId))
.build();
}
}
private void seedRequestContext(HttpServletRequest eventRequest, GenericValue userLogin) {
eventRequest.setAttribute("delegator", ModernApiUtil.delegator(servletContext));
eventRequest.setAttribute("dispatcher", ModernApiUtil.dispatcher(servletContext));
eventRequest.setAttribute("security", ModernApiUtil.security(servletContext));
if (userLogin != null) {
eventRequest.setAttribute("userLogin", userLogin);
eventRequest.getSession().setAttribute("userLogin", userLogin);
}
}
private Map<String, Object> eventMessages(HttpServletRequest eventRequest) {
Map<String, Object> messages = new LinkedHashMap<>();
copyRequestAttribute(eventRequest, messages, "_EVENT_MESSAGE_");
copyRequestAttribute(eventRequest, messages, "_EVENT_MESSAGE_LIST_");
copyRequestAttribute(eventRequest, messages, "_ERROR_MESSAGE_");
copyRequestAttribute(eventRequest, messages, "_ERROR_MESSAGE_LIST_");
return messages;
}
private void copyRequestAttribute(HttpServletRequest eventRequest, Map<String, Object> messages, String name) {
Object value = eventRequest.getAttribute(name);
if (value != null) {
messages.put(name, ModernApiUtil.serializableValue(value));
}
}
private String firstMessage(HttpServletRequest eventRequest, String fallback) {
for (String name : List.of("_ERROR_MESSAGE_", "_EVENT_MESSAGE_")) {
Object value = eventRequest.getAttribute(name);
if (value != null && !value.toString().isBlank()) {
return value.toString();
}
}
return fallback;
}
private EventHandler eventHandler(String eventType) throws EventHandlerException {
EventHandler handler;
switch (eventType) {
case "java":
handler = new JavaEventHandler();
break;
case "simple":
handler = new SimpleEventHandler();
break;
case "groovy":
handler = new GroovyEventHandler();
break;
case "service-multi":
handler = new ServiceMultiEventHandler();
break;
case "rome":
handler = new RomeEventHandler();
break;
default:
throw new EventHandlerException("Unsupported event type: " + eventType);
}
handler.init(servletContext);
return handler;
}
private boolean isWebEvent(String eventType) {
return Set.of("java", "simple", "groovy", "service-multi", "rome").contains(eventType);
}
private boolean requiresAuth(Map<String, Object> actionDefinition) {
Object auth = actionDefinition.get("auth");
return auth instanceof Boolean ? (Boolean) auth : true;
}
@SuppressWarnings("unchecked")
private Map<String, Object> navigationTarget(Map<String, Object> actionDefinition) {
String eventType = stringValue(actionDefinition.get("eventType"));
if (!eventType.isBlank()) {
return Collections.emptyMap();
}
Object responses = actionDefinition.get("responses");
if (!(responses instanceof Iterable<?>)) {
return Collections.emptyMap();
}
for (Object response : (Iterable<?>) responses) {
if (!(response instanceof Map<?, ?>)) {
continue;
}
Map<String, Object> responseMap = (Map<String, Object>) response;
if ("view".equals(responseMap.get("type")) && responseMap.get("modernPath") != null) {
return responseMap;
}
}
return Collections.emptyMap();
}
private String serviceNameFor(String actionId, Map<String, Object> actionDefinition) {
if (actionDefinition == null || actionDefinition.isEmpty()) {
return actionId;
}
Object serviceName = actionDefinition.get("serviceName");
if (serviceName instanceof String && !((String) serviceName).isBlank()) {
return (String) serviceName;
}
Object eventType = actionDefinition.get("eventType");
Object eventInvoke = actionDefinition.get("eventInvoke");
if ("service".equals(eventType) && eventInvoke instanceof String) {
return (String) eventInvoke;
}
if ("service".equals(actionDefinition.get("source"))) {
return actionId;
}
return "";
}
private String stringValue(Object value) {
return value == null ? "" : value.toString();
}
private static final class PayloadRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> parameters = new LinkedHashMap<>();
private PayloadRequestWrapper(HttpServletRequest request, Map<String, Object> payload) {
super(request);
parameters.putAll(request.getParameterMap());
for (Map.Entry<String, Object> entry : payload.entrySet()) {
parameters.put(entry.getKey(), stringArray(entry.getValue()));
}
}
@Override
public String getParameter(String name) {
String[] values = parameters.get(name);
return values == null || values.length == 0 ? null : values[0];
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(parameters);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameters.keySet());
}
@Override
public String[] getParameterValues(String name) {
return parameters.get(name);
}
private static String[] stringArray(Object value) {
if (value == null) {
return new String[] { "" };
}
if (value instanceof Iterable<?>) {
return iterableToArray((Iterable<?>) value);
}
if (value instanceof Object[]) {
Object[] values = (Object[]) value;
String[] result = new String[values.length];
for (int i = 0; i < values.length; i++) {
result[i] = values[i] == null ? "" : values[i].toString();
}
return result;
}
return new String[] { value.toString() };
}
private static String[] iterableToArray(Iterable<?> values) {
java.util.ArrayList<String> result = new java.util.ArrayList<>();
for (Object value : values) {
result.add(value == null ? "" : value.toString());
}
return result.toArray(new String[0]);
}
}
private static final class CapturingResponseWrapper extends HttpServletResponseWrapper {
private int status = SC_OK;
private String redirectLocation = "";
private CapturingResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void sendRedirect(String location) throws IOException {
this.redirectLocation = location;
this.status = SC_FOUND;
}
@Override
public void sendError(int sc) throws IOException {
this.status = sc;
}
@Override
public void sendError(int sc, String msg) throws IOException {
this.status = sc;
}
@Override
public void setStatus(int sc) {
this.status = sc;
super.setStatus(sc);
}
@Override
public int getStatus() {
return status;
}
private int status() {
return status;
}
private String redirectLocation() {
return redirectLocation;
}
}
}
@@ -0,0 +1,192 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.webapp.control.LoginWorker;
@Path("/v1")
@Produces(MediaType.APPLICATION_JSON)
public class AuthResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@Context private HttpServletResponse response;
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_JSON)
public Response login(Map<String, Object> payload) {
String traceId = ModernApiUtil.traceId();
Map<String, String[]> parameters = loginParameters(payload);
LoginRequestWrapper loginRequest = new LoginRequestWrapper(request, parameters);
seedRequestContext(loginRequest);
String loginResult = LoginWorker.login(loginRequest, response);
GenericValue userLogin = ModernApiUtil.userLogin(loginRequest);
if (!"success".equalsIgnoreCase(loginResult) || userLogin == null) {
String message = firstMessage(loginRequest, "用户名或密码不正确,或当前账号无权进入 OFBiz 后台。");
return Response.status(Response.Status.UNAUTHORIZED)
.entity(ApiResult.error("LOGIN_FAILED", message, traceId))
.build();
}
return ModernApiUtil.ok(sessionData(loginRequest, userLogin), traceId);
}
@POST
@Path("/logout")
public Response logout() {
String traceId = ModernApiUtil.traceId();
GenericValue userLogin = ModernApiUtil.userLogin(request);
if (userLogin != null) {
seedRequestContext(request);
LoginWorker.logout(request, response);
} else {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("authenticated", false);
data.put("user", null);
return ModernApiUtil.ok(data, traceId);
}
private void seedRequestContext(HttpServletRequest targetRequest) {
targetRequest.setAttribute("delegator", ModernApiUtil.delegator(servletContext));
targetRequest.setAttribute("dispatcher", ModernApiUtil.dispatcher(servletContext));
targetRequest.setAttribute("security", ModernApiUtil.security(servletContext));
}
private Map<String, Object> sessionData(HttpServletRequest loginRequest, GenericValue userLogin) {
Delegator delegator = ModernApiUtil.delegator(servletContext);
Locale locale = loginRequest.getLocale();
Map<String, Object> data = new LinkedHashMap<>();
data.put("authenticated", true);
data.put("user", Map.of(
"userLoginId", userLogin.getString("userLoginId"),
"partyId", userLogin.getString("partyId")));
data.put("locale", locale == null ? "en" : locale.toLanguageTag());
data.put("tenant", delegator == null ? null : delegator.getDelegatorTenantId());
data.put("theme", Map.of(
"name", "modern-element-plus",
"density", "compact",
"navigation", "module-sidebar"));
data.put("permissions", Map.of("source", "OFBiz Security", "resolved", true));
return data;
}
private Map<String, String[]> loginParameters(Map<String, Object> payload) {
Map<String, String[]> parameters = new LinkedHashMap<>();
parameters.put("USERNAME", new String[] { stringValue(payload, "username") });
parameters.put("PASSWORD", new String[] { stringValue(payload, "password") });
parameters.put("TOKEN", new String[] { stringValue(payload, "token") });
parameters.put("JavaScriptEnabled", new String[] { "Y" });
String tenantId = stringValue(payload, "tenantId");
if (UtilValidate.isNotEmpty(tenantId)) {
parameters.put("userTenantId", new String[] { tenantId });
}
return parameters;
}
private String stringValue(Map<String, Object> payload, String name) {
if (payload == null) {
return "";
}
Object value = payload.get(name);
return value == null ? "" : value.toString();
}
private String firstMessage(HttpServletRequest loginRequest, String fallback) {
for (String name : List.of("_ERROR_MESSAGE_", "_EVENT_MESSAGE_")) {
Object value = loginRequest.getAttribute(name);
if (value != null && !value.toString().isBlank()) {
return value.toString();
}
}
for (String name : List.of("_ERROR_MESSAGE_LIST_", "_EVENT_MESSAGE_LIST_")) {
Object value = loginRequest.getAttribute(name);
if (value != null && !value.toString().isBlank()) {
return value.toString();
}
}
return fallback;
}
private static final class LoginRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> parameters;
private LoginRequestWrapper(HttpServletRequest request, Map<String, String[]> parameters) {
super(request);
this.parameters = parameters;
}
@Override
public String getParameter(String name) {
String[] values = parameters.get(name);
if (values != null && values.length > 0) {
return values[0];
}
return super.getParameter(name);
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> merged = new LinkedHashMap<>(super.getParameterMap());
merged.putAll(parameters);
return Collections.unmodifiableMap(merged);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
@Override
public String[] getParameterValues(String name) {
String[] values = parameters.get(name);
return values == null ? super.getParameterValues(name) : values;
}
}
}
@@ -0,0 +1,242 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.condition.EntityFunction;
import org.apache.ofbiz.entity.condition.EntityOperator;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.security.Security;
@Path("/v1/entities")
@Produces(MediaType.APPLICATION_JSON)
public class EntityResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@GET
@Path("/{entityName}")
public Response list(@PathParam("entityName") String entityName,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("20") @QueryParam("pageSize") int pageSize,
@QueryParam("query") String queryText,
@QueryParam("orderBy") String orderBy) {
String traceId = ModernApiUtil.traceId();
Delegator delegator = ModernApiUtil.delegator(servletContext);
Security security = ModernApiUtil.security(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
if (delegator == null) {
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
}
if (userLogin == null) {
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to query entities.", traceId);
}
try {
ModelEntity modelEntity = delegator.getModelEntity(entityName);
if (modelEntity == null) {
if (isOptionalEbayEntity(entityName)) {
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
}
return ModernApiUtil.error(Response.Status.NOT_FOUND, "ENTITY_NOT_FOUND", "No entity named " + entityName + " exists.", traceId);
}
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
if (isOptionalEbayEntity(entityName)) {
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
}
return ModernApiUtil.error(Response.Status.FORBIDDEN, "ENTITY_FORBIDDEN",
"You do not have view permission for " + entityName + ".", traceId);
}
int safePageSize = ModernApiUtil.safePageSize(pageSize);
int safePage = ModernApiUtil.safePage(page);
EntityCondition condition = queryCondition(modelEntity, queryText);
long total = countRows(delegator, entityName, condition);
EntityQuery query = EntityQuery.use(delegator).from(entityName).offset(safePage * safePageSize).limit(safePageSize);
if (condition != null) {
query.where(condition);
}
List<String> safeOrderBy = orderByFields(modelEntity, orderBy);
if (!safeOrderBy.isEmpty()) {
query.orderBy(safeOrderBy);
}
List<Map<String, Object>> rows = new ArrayList<>();
for (GenericValue value : query.queryList()) {
rows.add(ModernApiUtil.publicFields(value));
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("entityName", entityName);
data.put("rows", rows);
data.put("fields", fields(modelEntity));
data.put("page", safePage);
data.put("pageSize", safePageSize);
data.put("query", queryText == null ? "" : queryText.trim());
data.put("orderBy", safeOrderBy);
data.put("total", total);
data.put("hasMore", ModernApiUtil.hasMore(safePage, safePageSize, total));
return ModernApiUtil.ok(data, traceId);
} catch (GenericEntityException | RuntimeException e) {
if (isOptionalEbayEntity(entityName)) {
return ModernApiUtil.ok(optionalEbayRows(entityName, page, pageSize, queryText, orderBy), traceId);
}
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("ENTITY_QUERY_ERROR", e.getMessage(), traceId))
.build();
}
}
private List<Map<String, Object>> fields(ModelEntity modelEntity) {
List<Map<String, Object>> fields = new ArrayList<>();
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
fields.add(Map.of(
"name", field.getName(),
"type", field.getType(),
"primaryKey", field.getIsPk(),
"required", field.getIsNotNull()));
}
return fields;
}
private Map<String, Object> optionalEbayRows(String entityName, int page, int pageSize, String queryText, String orderBy) {
int safePageSize = ModernApiUtil.safePageSize(pageSize);
int safePage = ModernApiUtil.safePage(page);
List<String> safeOrderBy = orderBy == null || orderBy.isBlank()
? List.of()
: List.of(orderBy.trim());
Map<String, Object> data = new LinkedHashMap<>();
data.put("entityName", entityName);
data.put("rows", List.of());
data.put("fields", optionalEbayFields(entityName));
data.put("page", safePage);
data.put("pageSize", safePageSize);
data.put("query", queryText == null ? "" : queryText.trim());
data.put("orderBy", safeOrderBy);
data.put("total", 0);
data.put("hasMore", false);
data.put("unavailable", true);
data.put("reason", "本地 OFBiz 未启用 eBay/eBay Store 可选实体,按店铺运营空数据处理。");
return data;
}
private List<Map<String, Object>> optionalEbayFields(String entityName) {
List<Map<String, Object>> fields = new ArrayList<>();
for (String fieldName : optionalEbayFieldNames(entityName)) {
fields.add(Map.of(
"name", fieldName,
"type", "id",
"primaryKey", false,
"required", false));
}
return fields;
}
private boolean isOptionalEbayEntity(String entityName) {
return !optionalEbayFieldNames(entityName).isEmpty();
}
private List<String> optionalEbayFieldNames(String entityName) {
if ("EbayProductListing".equals(entityName)) {
return List.of("productListingId", "itemId", "productStoreId", "productId", "statusId", "startDateTime", "endDateTime", "autoRelisting");
}
if ("EbayProductStoreInventory".equals(entityName)) {
return List.of("productStoreId", "facilityId", "productId", "ebayProductId", "availableToPromiseListing", "activeListing", "sold", "successRatio");
}
if ("EbayConfig".equals(entityName)) {
return List.of("productStoreId", "siteId", "compatibilityLevel", "apiServerUrl", "xmlGatewayUri", "webSiteId");
}
if ("EbayShippingMethod".equals(entityName)) {
return List.of("productStoreId", "shipmentMethodName", "methodTypeEnumId", "amount", "carrierPartyId", "shipmentMethodTypeId");
}
if ("EbayProductStorePref".equals(entityName)) {
return List.of("productStoreId", "autoPrefEnumId", "enabled", "condition1", "condition2", "condition3", "autoPrefJobId", "parentPrefCondId");
}
if ("EBayLogMessagesInfo".equals(entityName)) {
return List.of("productStoreId", "logAck", "functionName", "logMessage", "createDatetime");
}
if ("EbayUserBestOffer".equals(entityName)) {
return List.of("productStoreId", "itemId", "bestOfferId", "userId", "contactStatus");
}
return List.of();
}
private long countRows(Delegator delegator, String entityName, EntityCondition condition) throws GenericEntityException {
EntityQuery countQuery = EntityQuery.use(delegator).from(entityName);
if (condition != null) {
countQuery.where(condition);
}
return countQuery.queryCount();
}
private List<String> orderByFields(ModelEntity modelEntity, String orderBy) {
return ModernApiUtil.safeOrderByFields(modelEntity.getAllFieldNames(), modelEntity.getPkFieldNames(), orderBy);
}
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText) {
if (queryText == null || queryText.isBlank()) {
return null;
}
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
List<EntityCondition> conditions = new ArrayList<>();
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
String name = field.getName();
String type = field.getType();
if (name == null || type == null || !isSearchableTextField(name, type)) {
continue;
}
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(name), EntityOperator.LIKE, EntityFunction.upper(value)));
}
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
}
private boolean isSearchableTextField(String name, String type) {
String lowerName = name.toLowerCase(Locale.ROOT);
String lowerType = type.toLowerCase(Locale.ROOT);
if (lowerType.contains("date") || lowerType.contains("time") || lowerType.contains("currency") || lowerType.contains("amount")
|| lowerType.contains("fixed-point") || lowerType.contains("floating-point") || lowerType.contains("numeric")) {
return false;
}
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url")
|| lowerName.endsWith("id") || lowerName.contains("name") || lowerName.contains("description") || lowerName.contains("title")
|| lowerName.contains("number") || lowerName.contains("code") || lowerName.contains("status");
}
}
@@ -0,0 +1,37 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
@Path("/v1/inventory")
@Produces(MediaType.APPLICATION_JSON)
public class InventoryResource {
@GET
public Response inventory() {
return ModernApiUtil.ok(UiInventoryLoader.load(), ModernApiUtil.traceId());
}
}
@@ -0,0 +1,167 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.condition.EntityFunction;
import org.apache.ofbiz.entity.condition.EntityOperator;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.security.Security;
@Path("/v1/lookups")
@Produces(MediaType.APPLICATION_JSON)
public class LookupResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@GET
@Path("/{lookupId}")
public Response lookup(@PathParam("lookupId") String lookupId,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("20") @QueryParam("pageSize") int pageSize,
@QueryParam("query") String queryText,
@QueryParam("orderBy") String orderBy) {
String traceId = ModernApiUtil.traceId();
Delegator delegator = ModernApiUtil.delegator(servletContext);
Security security = ModernApiUtil.security(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
if (delegator == null) {
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
}
if (userLogin == null) {
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to run lookups.", traceId);
}
try {
ModelEntity modelEntity = delegator.getModelEntity(lookupId);
if (modelEntity == null) {
return ModernApiUtil.error(Response.Status.NOT_FOUND, "LOOKUP_NOT_FOUND", "No entity lookup named " + lookupId + " exists.", traceId);
}
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
return ModernApiUtil.error(Response.Status.FORBIDDEN, "LOOKUP_FORBIDDEN",
"You do not have view permission for " + lookupId + ".", traceId);
}
int safePageSize = ModernApiUtil.safePageSize(pageSize);
int safePage = ModernApiUtil.safePage(page);
EntityCondition condition = queryCondition(modelEntity, queryText);
long total = countRows(delegator, lookupId, condition);
EntityQuery query = EntityQuery.use(delegator).from(lookupId).offset(safePage * safePageSize).limit(safePageSize);
if (condition != null) {
query.where(condition);
}
List<String> safeOrderBy = orderByFields(modelEntity, orderBy);
if (!safeOrderBy.isEmpty()) {
query.orderBy(safeOrderBy);
}
List<Map<String, Object>> rows = new ArrayList<>();
for (GenericValue value : query.queryList()) {
rows.add(ModernApiUtil.publicFields(value));
}
List<Map<String, Object>> fields = new ArrayList<>();
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
fields.add(Map.of(
"name", field.getName(),
"type", field.getType(),
"primaryKey", field.getIsPk(),
"required", field.getIsNotNull()));
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("lookupId", lookupId);
data.put("rows", rows);
data.put("fields", fields);
data.put("page", safePage);
data.put("pageSize", safePageSize);
data.put("query", queryText == null ? "" : queryText.trim());
data.put("orderBy", safeOrderBy);
data.put("total", total);
data.put("hasMore", ModernApiUtil.hasMore(safePage, safePageSize, total));
return ModernApiUtil.ok(data, traceId);
} catch (GenericEntityException | RuntimeException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("LOOKUP_ERROR", e.getMessage(), traceId))
.build();
}
}
private long countRows(Delegator delegator, String entityName, EntityCondition condition) throws GenericEntityException {
EntityQuery countQuery = EntityQuery.use(delegator).from(entityName);
if (condition != null) {
countQuery.where(condition);
}
return countQuery.queryCount();
}
private List<String> orderByFields(ModelEntity modelEntity, String orderBy) {
return ModernApiUtil.safeOrderByFields(modelEntity.getAllFieldNames(), modelEntity.getPkFieldNames(), orderBy);
}
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText) {
if (queryText == null || queryText.isBlank()) {
return null;
}
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
List<EntityCondition> conditions = new ArrayList<>();
for (ModelField field : modelEntity.getFieldsUnmodifiable()) {
String name = field.getName();
String type = field.getType();
if (name == null || type == null || !isSearchableTextField(name, type)) {
continue;
}
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(name), EntityOperator.LIKE, EntityFunction.upper(value)));
}
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
}
private boolean isSearchableTextField(String name, String type) {
String lowerName = name.toLowerCase(Locale.ROOT);
String lowerType = type.toLowerCase(Locale.ROOT);
if (lowerType.contains("date") || lowerType.contains("time") || lowerType.contains("currency") || lowerType.contains("amount")
|| lowerType.contains("fixed-point") || lowerType.contains("floating-point") || lowerType.contains("numeric")) {
return false;
}
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url")
|| lowerName.endsWith("id") || lowerName.contains("name") || lowerName.contains("description") || lowerName.contains("title")
|| lowerName.contains("number") || lowerName.contains("code") || lowerName.contains("status");
}
}
@@ -0,0 +1,92 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.base.component.ComponentConfig;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.security.Security;
@Path("/v1/navigation")
@Produces(MediaType.APPLICATION_JSON)
public class NavigationResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@GET
public Response navigation() {
String traceId = ModernApiUtil.traceId();
Security security = ModernApiUtil.security(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
List<Map<String, Object>> applications = new ArrayList<>();
for (ComponentConfig.WebappInfo webapp : ComponentConfig.getAllWebappResourceInfos()) {
if (!webapp.getAppBarDisplay()) {
continue;
}
String[] permissions = webapp.getBasePermission();
boolean allowed = Arrays.stream(permissions).allMatch(permission -> ModernApiUtil.hasViewPermission(security, userLogin, permission));
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", webapp.getName());
item.put("title", webapp.getTitle());
item.put("description", webapp.getDescription());
item.put("component", webapp.getComponentConfig().getComponentName());
item.put("mountPoint", webapp.getContextRoot());
item.put("legacyPath", webapp.getContextRoot());
item.put("modernPath", "/modern/app/#/pages/" + webapp.getName() + "__main");
item.put("layout", layoutFor(webapp.getName(), webapp.getContextRoot()));
item.put("permissions", permissions);
item.put("allowed", allowed);
item.put("children", List.of());
applications.add(item);
}
applications.sort(Comparator.comparing(item -> (String) item.getOrDefault("title", "")));
return ModernApiUtil.ok(Map.of("applications", applications), traceId);
}
private String layoutFor(String webappName, String contextRoot) {
String key = (webappName + " " + contextRoot).toLowerCase();
if (key.contains("ecommerce")) {
return "commerce";
}
if (key.contains("webpos")) {
return "pos";
}
if (key.contains("webtools") || key.contains("setup") || key.contains("example")) {
return "system";
}
return "backoffice";
}
}
@@ -0,0 +1,250 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.condition.EntityFunction;
import org.apache.ofbiz.entity.condition.EntityOperator;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.security.Security;
@Path("/v1/options")
@Produces(MediaType.APPLICATION_JSON)
public class OptionResource {
private static final Pattern TEMPLATE_FIELD = Pattern.compile("\\$\\{([^}]+)}");
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@GET
@Path("/{entityName}")
public Response entityOptions(@PathParam("entityName") String entityName,
@QueryParam("keyFieldName") String keyFieldName,
@QueryParam("description") String description,
@QueryParam("query") String queryText,
@QueryParam("constraint") List<String> constraints,
@DefaultValue("40") @QueryParam("pageSize") int pageSize) {
String traceId = ModernApiUtil.traceId();
Delegator delegator = ModernApiUtil.delegator(servletContext);
Security security = ModernApiUtil.security(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
if (delegator == null) {
return ModernApiUtil.error(Response.Status.SERVICE_UNAVAILABLE, "DELEGATOR_UNAVAILABLE", "OFBiz delegator is not available.", traceId);
}
if (userLogin == null) {
return ModernApiUtil.error(Response.Status.UNAUTHORIZED, "AUTH_REQUIRED", "A valid OFBiz userLogin is required to load options.", traceId);
}
try {
ModelEntity modelEntity = delegator.getModelEntity(entityName);
if (modelEntity == null) {
return ModernApiUtil.error(Response.Status.NOT_FOUND, "OPTIONS_ENTITY_NOT_FOUND", "No entity named " + entityName + " exists.", traceId);
}
if (!ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity)) {
return ModernApiUtil.error(Response.Status.FORBIDDEN, "OPTIONS_FORBIDDEN",
"You do not have view permission for " + entityName + ".", traceId);
}
String keyField = safeKeyField(modelEntity, keyFieldName);
List<String> descriptionFields = safeDescriptionFields(modelEntity, description);
int safePageSize = ModernApiUtil.safePageSize(pageSize);
List<Map<String, Object>> appliedConstraints = new ArrayList<>();
EntityCondition condition = combinedCondition(modelEntity, queryText, keyField, descriptionFields, constraints, appliedConstraints);
EntityQuery query = EntityQuery.use(delegator).from(entityName).limit(safePageSize);
if (condition != null) {
query.where(condition);
}
List<String> orderBy = new ArrayList<>();
orderBy.add(keyField);
query.orderBy(orderBy);
List<Map<String, Object>> options = new ArrayList<>();
for (GenericValue value : query.queryList()) {
Object rawValue = value.get(keyField);
if (rawValue == null) {
continue;
}
Map<String, Object> option = new LinkedHashMap<>();
option.put("label", optionLabel(value, keyField, descriptionFields, description));
option.put("value", ModernApiUtil.serializableValue(rawValue));
options.add(option);
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("entityName", entityName);
data.put("keyFieldName", keyField);
data.put("descriptionFields", descriptionFields);
data.put("options", options);
data.put("query", queryText == null ? "" : queryText.trim());
data.put("pageSize", safePageSize);
data.put("constraints", appliedConstraints);
data.put("hasMore", options.size() == safePageSize);
return ModernApiUtil.ok(data, traceId);
} catch (GenericEntityException | RuntimeException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiResult.error("OPTIONS_QUERY_ERROR", e.getMessage(), traceId))
.build();
}
}
private String safeKeyField(ModelEntity modelEntity, String keyFieldName) {
if (keyFieldName != null && !keyFieldName.isBlank() && modelEntity.isField(keyFieldName)) {
return keyFieldName;
}
if (!modelEntity.getPkFieldNames().isEmpty()) {
return modelEntity.getPkFieldNames().get(0);
}
return modelEntity.getFieldsUnmodifiable().get(0).getName();
}
private List<String> safeDescriptionFields(ModelEntity modelEntity, String description) {
List<String> fields = new ArrayList<>();
if (description != null) {
Matcher matcher = TEMPLATE_FIELD.matcher(description);
while (matcher.find()) {
String field = matcher.group(1).trim();
if (modelEntity.isField(field) && !fields.contains(field)) {
fields.add(field);
}
}
}
for (String fallback : List.of("description", "name", "productName", "groupName", "firstName", "lastName")) {
if (fields.size() >= 3) {
break;
}
if (modelEntity.isField(fallback) && !fields.contains(fallback)) {
fields.add(fallback);
}
}
return fields;
}
private String optionLabel(GenericValue value, String keyField, List<String> descriptionFields, String description) {
String label = description == null ? "" : description;
for (String field : descriptionFields) {
Object fieldValue = value.get(field);
if (fieldValue == null) {
continue;
}
label = label.replace("${" + field + "}", String.valueOf(ModernApiUtil.serializableValue(fieldValue)));
}
label = label.replaceAll("\\$\\{[^}]+}", "").trim();
if (!label.isEmpty()) {
return label;
}
for (String field : descriptionFields) {
Object fieldValue = value.get(field);
if (fieldValue != null) {
return String.valueOf(ModernApiUtil.serializableValue(fieldValue));
}
}
Object keyValue = value.get(keyField);
return keyValue == null ? "" : String.valueOf(ModernApiUtil.serializableValue(keyValue));
}
private EntityCondition queryCondition(ModelEntity modelEntity, String queryText, String keyField, List<String> descriptionFields) {
if (queryText == null || queryText.isBlank()) {
return null;
}
String value = "%" + queryText.trim().toUpperCase(Locale.ROOT) + "%";
List<EntityCondition> conditions = new ArrayList<>();
if (isSearchableTextField(modelEntity, keyField)) {
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(keyField), EntityOperator.LIKE, EntityFunction.upper(value)));
}
for (String field : descriptionFields) {
if (isSearchableTextField(modelEntity, field)) {
conditions.add(EntityCondition.makeCondition(EntityFunction.upperField(field), EntityOperator.LIKE, EntityFunction.upper(value)));
}
}
return conditions.isEmpty() ? null : EntityCondition.makeCondition(conditions, EntityOperator.OR);
}
private EntityCondition combinedCondition(ModelEntity modelEntity, String queryText, String keyField, List<String> descriptionFields,
List<String> constraints, List<Map<String, Object>> appliedConstraints) {
List<EntityCondition> conditions = new ArrayList<>();
EntityCondition textCondition = queryCondition(modelEntity, queryText, keyField, descriptionFields);
if (textCondition != null) {
conditions.add(textCondition);
}
conditions.addAll(constraintConditions(modelEntity, constraints, appliedConstraints));
if (conditions.isEmpty()) {
return null;
}
return conditions.size() == 1 ? conditions.get(0) : EntityCondition.makeCondition(conditions, EntityOperator.AND);
}
private List<EntityCondition> constraintConditions(ModelEntity modelEntity, List<String> constraints, List<Map<String, Object>> appliedConstraints) {
List<EntityCondition> conditions = new ArrayList<>();
if (constraints == null || constraints.isEmpty()) {
return conditions;
}
for (String rawConstraint : constraints) {
if (rawConstraint == null || rawConstraint.isBlank()) {
continue;
}
String[] parts = rawConstraint.split(":", 2);
if (parts.length != 2) {
continue;
}
String fieldName = parts[0].trim();
String value = parts[1].trim();
if (fieldName.isEmpty() || value.isEmpty() || !modelEntity.isField(fieldName)) {
continue;
}
conditions.add(EntityCondition.makeCondition(fieldName, EntityOperator.EQUALS, value));
appliedConstraints.add(Map.of("name", fieldName, "operator", "equals", "value", value));
}
return conditions;
}
private boolean isSearchableTextField(ModelEntity modelEntity, String name) {
ModelField field = modelEntity.getField(name);
if (field == null || field.getType() == null) {
return false;
}
String lowerType = field.getType().toLowerCase(Locale.ROOT);
return lowerType.contains("id") || lowerType.contains("name") || lowerType.contains("description") || lowerType.contains("comment")
|| lowerType.contains("value") || lowerType.contains("varchar") || lowerType.contains("email") || lowerType.contains("url");
}
}
@@ -0,0 +1,65 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
import org.apache.ofbiz.modernapi.core.UiInventoryLoader;
@Path("/v1/pages")
@Produces(MediaType.APPLICATION_JSON)
public class PageResource {
@GET
@Path("/{pageId}")
public Response pageDefinition(@PathParam("pageId") String pageId) {
String traceId = ModernApiUtil.traceId();
Map<String, Object> page = UiInventoryLoader.findPage(pageId);
if (page.isEmpty()) {
page = fallbackPage(pageId);
}
return ModernApiUtil.ok(page, traceId);
}
private Map<String, Object> fallbackPage(String pageId) {
Map<String, Object> block = new LinkedHashMap<>();
block.put("type", "empty");
block.put("title", "Page definition not generated");
block.put("description", "Run plugins/modern-api/scripts/generate-ui-inventory.mjs to create the route and widget metadata.");
Map<String, Object> page = new LinkedHashMap<>();
page.put("pageId", pageId);
page.put("title", pageId);
page.put("layout", "system");
page.put("blocks", List.of(block));
page.put("actions", List.of());
page.put("permissions", List.of());
page.put("legacy", Map.of("generated", false));
return page;
}
}
@@ -0,0 +1,65 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
@Path("/v1/session")
@Produces(MediaType.APPLICATION_JSON)
public class SessionResource {
@Context private ServletContext servletContext;
@Context private HttpServletRequest request;
@GET
public Response currentSession() {
String traceId = ModernApiUtil.traceId();
Delegator delegator = ModernApiUtil.delegator(servletContext);
GenericValue userLogin = ModernApiUtil.userLogin(request);
Locale locale = request.getLocale();
Map<String, Object> data = new LinkedHashMap<>();
data.put("authenticated", userLogin != null);
data.put("user", userLogin == null ? null : Map.of(
"userLoginId", userLogin.getString("userLoginId"),
"partyId", userLogin.getString("partyId")));
data.put("locale", locale == null ? "en" : locale.toLanguageTag());
data.put("tenant", delegator == null ? null : delegator.getDelegatorTenantId());
data.put("theme", Map.of(
"name", "modern-element-plus",
"density", "compact",
"navigation", "module-sidebar"));
data.put("permissions", Map.of("source", "OFBiz Security", "resolved", userLogin != null));
return ModernApiUtil.ok(data, traceId);
}
}
@@ -0,0 +1,45 @@
/*******************************************************************************
* 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.ofbiz.modernapi.resources;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.modernapi.core.ApiResult;
import org.apache.ofbiz.modernapi.core.ModernApiUtil;
@Path("/v1/uploads")
@Produces(MediaType.APPLICATION_JSON)
public class UploadResource {
@POST
@Path("/{uploadId}")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response upload(@PathParam("uploadId") String uploadId) {
String traceId = ModernApiUtil.traceId();
return Response.status(Response.Status.NOT_IMPLEMENTED)
.entity(ApiResult.error("UPLOAD_CONTRACT_ONLY",
"Upload endpoint " + uploadId + " is reserved for OFBiz secure upload policy integration.", traceId))
.build();
}
}
@@ -0,0 +1,98 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.security.Security;
import org.apache.ofbiz.service.LocalDispatcher;
import org.junit.Test;
public class ModernApiAuthFilterTests {
@Test
public void bridgesSecuredLoginCookieWhenApiSessionHasNoUserLogin() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
Delegator delegator = mock(Delegator.class);
LocalDispatcher dispatcher = mock(LocalDispatcher.class);
Security security = mock(Security.class);
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
when(servletContext.getAttribute("dispatcher")).thenReturn(dispatcher);
when(servletContext.getAttribute("security")).thenReturn(security);
HttpSession session = mock(HttpSession.class);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
when(request.getServletContext()).thenReturn(servletContext);
when(request.getSession(false)).thenReturn(session);
AtomicInteger bridgeCalls = new AtomicInteger();
ModernApiAuthFilter filter = new ModernApiAuthFilter((req, resp) -> {
bridgeCalls.incrementAndGet();
return "success";
});
filter.doFilter(request, response, chain);
verify(request).setAttribute("delegator", delegator);
verify(request).setAttribute("dispatcher", dispatcher);
verify(request).setAttribute("security", security);
assertEquals(1, bridgeCalls.get());
verify(chain).doFilter(request, response);
}
@Test
public void skipsBridgeWhenApiSessionAlreadyHasUserLogin() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
HttpSession session = mock(HttpSession.class);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
GenericValue userLogin = new GenericValue();
when(request.getServletContext()).thenReturn(servletContext);
when(request.getSession(false)).thenReturn(session);
when(session.getAttribute("userLogin")).thenReturn(userLogin);
AtomicInteger bridgeCalls = new AtomicInteger();
ModernApiAuthFilter filter = new ModernApiAuthFilter((req, resp) -> {
bridgeCalls.incrementAndGet();
return "success";
});
filter.doFilter(request, response, chain);
assertEquals(0, bridgeCalls.get());
verify(request, never()).setAttribute("delegator", null);
verify(chain).doFilter(request, response);
}
}
@@ -0,0 +1,414 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.Response;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.modernapi.resources.ActionResource;
import org.apache.ofbiz.modernapi.resources.EntityResource;
import org.apache.ofbiz.modernapi.resources.LookupResource;
import org.apache.ofbiz.modernapi.resources.OptionResource;
import org.apache.ofbiz.modernapi.resources.PageResource;
import org.apache.ofbiz.modernapi.resources.SessionResource;
import org.apache.ofbiz.modernapi.resources.UploadResource;
import org.apache.ofbiz.security.Security;
import org.junit.Test;
public class ModernApiContractTests {
@Test
public void clampsPaginationToModernApiBounds() {
Map<String, Object> data = ModernApiUtil.pagination(-4, 250, 0);
assertEquals(0, data.get("page"));
assertEquals(100, data.get("pageSize"));
assertEquals(0L, data.get("total"));
assertEquals(false, data.get("hasMore"));
}
@Test
public void reportsHasMoreWhenNextPageContainsRows() {
Map<String, Object> data = ModernApiUtil.pagination(1, 20, 45);
assertEquals(1, data.get("page"));
assertEquals(20, data.get("pageSize"));
assertEquals(45L, data.get("total"));
assertEquals(true, data.get("hasMore"));
}
@Test
public void keepsOnlyKnownOrderByFieldsAndFallsBackToPrimaryKey() {
List<String> orderBy = ModernApiUtil.safeOrderByFields(
List.of("orderId", "statusId", "createdStamp"),
List.of("orderId"),
"statusId DESC, missingField ASC, -createdStamp");
assertEquals(List.of("statusId DESC", "createdStamp DESC"), orderBy);
List<String> fallback = ModernApiUtil.safeOrderByFields(
List.of("orderId", "statusId"),
List.of("orderId"),
"missingField DESC");
assertEquals(List.of("orderId"), fallback);
}
@Test
public void sessionEndpointReturnsUnauthenticatedContractInsteadOfAuthError() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
HttpServletRequest request = mock(HttpServletRequest.class);
Delegator delegator = mock(Delegator.class);
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
when(delegator.getDelegatorTenantId()).thenReturn("default");
when(request.getSession(false)).thenReturn(null);
when(request.getLocale()).thenReturn(java.util.Locale.US);
SessionResource resource = new SessionResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", request);
Response response = resource.currentSession();
Map<String, Object> entity = responseEntity(response);
Map<String, Object> data = data(entity);
assertEquals(200, response.getStatus());
assertTrue((Boolean) entity.get("ok"));
assertFalse((Boolean) data.get("authenticated"));
assertEquals("en-US", data.get("locale"));
assertEquals("default", data.get("tenant"));
}
@Test
public void serviceActionsRequireLoginBeforeExecution() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpSession session = mock(HttpSession.class);
when(request.getSession(false)).thenReturn(session);
when(session.getAttribute("userLogin")).thenReturn(null);
ActionResource resource = new ActionResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", request);
Response response = resource.runAction("createOrder", Map.of("orderId", "1000"));
Map<String, Object> entity = responseEntity(response);
assertEquals(401, response.getStatus());
assertFalse((Boolean) entity.get("ok"));
assertEquals("AUTH_REQUIRED", firstErrorCode(entity));
}
@Test
public void protectedPagesRequireLoginBeforeReturningDefinitions() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of());
ServletContext servletContext = mock(ServletContext.class);
PageResource resource = new PageResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithoutLogin());
Response response = resource.pageDefinition("accounting__main");
assertAuthRequired(response);
}
@Test
public void protectedPagesDenyUsersWithoutBasePermission() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of());
ServletContext servletContext = mock(ServletContext.class);
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
when(servletContext.getAttribute("security")).thenReturn(security);
PageResource resource = new PageResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithLogin(userLogin));
Response response = resource.pageDefinition("accounting__main");
assertForbidden(response, "PAGE_FORBIDDEN");
}
@Test
public void protectedPagesAllowUsersWithBasePermission() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of());
ServletContext servletContext = mock(ServletContext.class);
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
when(servletContext.getAttribute("security")).thenReturn(security);
when(security.hasEntityPermission("ACCOUNTING", "_VIEW", userLogin)).thenReturn(true);
PageResource resource = new PageResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithLogin(userLogin));
Response response = resource.pageDefinition("accounting__main");
Map<String, Object> entity = responseEntity(response);
Map<String, Object> data = data(entity);
assertEquals(200, response.getStatus());
assertTrue((Boolean) entity.get("ok"));
assertEquals("accounting__main", data.get("pageId"));
}
@Test
public void navigationActionsRequireLoginBeforeReturningTargets() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
ServletContext servletContext = mock(ServletContext.class);
ActionResource resource = new ActionResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithoutLogin());
Response response = resource.runAction("accounting__main", Map.of());
assertAuthRequired(response);
}
@Test
public void navigationActionsDenyUsersWithoutTargetPagePermission() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
ServletContext servletContext = mock(ServletContext.class);
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
when(servletContext.getAttribute("security")).thenReturn(security);
ActionResource resource = new ActionResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithLogin(userLogin));
Response response = resource.runAction("accounting__main", Map.of());
assertForbidden(response, "ACTION_FORBIDDEN");
}
@Test
public void navigationActionsAllowUsersWithTargetPagePermission() throws Exception {
seedInventory(
Map.of("accounting__main", pageDefinition("accounting__main", List.of("ACCOUNTING"))),
Map.of("accounting__main", navigationAction("accounting__main", "accounting__main")));
ServletContext servletContext = mock(ServletContext.class);
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
when(servletContext.getAttribute("security")).thenReturn(security);
when(security.hasEntityPermission("ACCOUNTING", "_VIEW", userLogin)).thenReturn(true);
ActionResource resource = new ActionResource();
inject(resource, "servletContext", servletContext);
inject(resource, "request", requestWithLogin(userLogin));
Response response = resource.runAction("accounting__main", Map.of());
Map<String, Object> entity = responseEntity(response);
Map<String, Object> data = data(entity);
assertEquals(200, response.getStatus());
assertTrue((Boolean) entity.get("ok"));
assertEquals("navigation", data.get("actionType"));
assertEquals("accounting__main", data.get("navigationPageId"));
}
@Test
public void uploadsRemainExplicitContractOnlyUntilPolicyIsImplemented() {
UploadResource resource = new UploadResource();
Response response = resource.upload("partyContent");
Map<String, Object> entity = responseEntity(response);
assertEquals(501, response.getStatus());
assertFalse((Boolean) entity.get("ok"));
assertEquals("UPLOAD_CONTRACT_ONLY", firstErrorCode(entity));
}
@Test
public void entityLookupAndOptionReadsRequireLogin() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
HttpServletRequest request = requestWithoutLogin();
Delegator delegator = mock(Delegator.class);
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
EntityResource entityResource = new EntityResource();
inject(entityResource, "servletContext", servletContext);
inject(entityResource, "request", request);
assertAuthRequired(entityResource.list("OrderHeader", 0, 20, "", ""));
LookupResource lookupResource = new LookupResource();
inject(lookupResource, "servletContext", servletContext);
inject(lookupResource, "request", request);
assertAuthRequired(lookupResource.lookup("Party", 0, 20, "", ""));
OptionResource optionResource = new OptionResource();
inject(optionResource, "servletContext", servletContext);
inject(optionResource, "request", request);
assertAuthRequired(optionResource.entityOptions("StatusItem", null, null, "", List.of(), 40));
}
@Test
public void entityLookupAndOptionReadsDenyUsersWithoutEntityPermission() throws Exception {
ServletContext servletContext = mock(ServletContext.class);
HttpServletRequest request = requestWithLogin(new GenericValue());
Delegator delegator = mock(Delegator.class);
Security security = mock(Security.class);
ModelEntity modelEntity = mock(ModelEntity.class);
when(servletContext.getAttribute("delegator")).thenReturn(delegator);
when(servletContext.getAttribute("security")).thenReturn(security);
when(delegator.getModelEntity("OrderHeader")).thenReturn(modelEntity);
when(delegator.getModelEntity("Party")).thenReturn(modelEntity);
when(delegator.getModelEntity("StatusItem")).thenReturn(modelEntity);
when(modelEntity.getPackageName()).thenReturn("org.apache.ofbiz.order.order");
when(modelEntity.getEntityName()).thenReturn("OrderHeader");
EntityResource entityResource = new EntityResource();
inject(entityResource, "servletContext", servletContext);
inject(entityResource, "request", request);
assertForbidden(entityResource.list("OrderHeader", 0, 20, "", ""));
LookupResource lookupResource = new LookupResource();
inject(lookupResource, "servletContext", servletContext);
inject(lookupResource, "request", request);
assertForbidden(lookupResource.lookup("Party", 0, 20, "", ""));
OptionResource optionResource = new OptionResource();
inject(optionResource, "servletContext", servletContext);
inject(optionResource, "request", request);
assertForbidden(optionResource.entityOptions("StatusItem", null, null, "", List.of(), 40));
}
@SuppressWarnings("unchecked")
private Map<String, Object> responseEntity(Response response) {
return (Map<String, Object>) response.getEntity();
}
@SuppressWarnings("unchecked")
private Map<String, Object> data(Map<String, Object> entity) {
return (Map<String, Object>) entity.get("data");
}
@SuppressWarnings("unchecked")
private String firstErrorCode(Map<String, Object> entity) {
List<Map<String, Object>> errors = (List<Map<String, Object>>) entity.get("errors");
return (String) errors.get(0).get("code");
}
private void assertAuthRequired(Response response) {
Map<String, Object> entity = responseEntity(response);
assertEquals(401, response.getStatus());
assertFalse((Boolean) entity.get("ok"));
assertEquals("AUTH_REQUIRED", firstErrorCode(entity));
}
private void assertForbidden(Response response) {
assertForbidden(response, null);
}
private void assertForbidden(Response response, String code) {
Map<String, Object> entity = responseEntity(response);
assertEquals(403, response.getStatus());
assertFalse((Boolean) entity.get("ok"));
if (code != null) {
assertEquals(code, firstErrorCode(entity));
}
}
private HttpServletRequest requestWithoutLogin() {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getSession(false)).thenReturn(null);
return request;
}
private HttpServletRequest requestWithLogin(GenericValue userLogin) {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpSession session = mock(HttpSession.class);
when(request.getSession(false)).thenReturn(session);
when(session.getAttribute("userLogin")).thenReturn(userLogin);
return request;
}
private void inject(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
private void seedInventory(Map<String, Object> pageDefinitions, Map<String, Object> actionDefinitions) throws Exception {
Map<String, Object> inventory = new LinkedHashMap<>();
inventory.put("pageDefinitions", pageDefinitions);
inventory.put("actionDefinitions", actionDefinitions);
setInventoryField("cachedInventory", inventory);
setInventoryField("cachedInventoryPath", null);
setInventoryField("cachedInventoryLastModified", -1L);
}
private void setInventoryField(String name, Object value) throws Exception {
Field field = UiInventoryLoader.class.getDeclaredField(name);
field.setAccessible(true);
field.set(null, value);
}
private Map<String, Object> pageDefinition(String pageId, List<String> permissions) {
Map<String, Object> page = new LinkedHashMap<>();
page.put("pageId", pageId);
page.put("title", pageId);
page.put("layout", "backoffice");
page.put("blocks", List.of());
page.put("actions", List.of());
page.put("permissions", permissions);
page.put("legacy", Map.of("generated", true));
return page;
}
private Map<String, Object> navigationAction(String actionId, String pageId) {
Map<String, Object> target = new LinkedHashMap<>();
target.put("name", "success");
target.put("type", "view");
target.put("value", pageId);
target.put("pageId", pageId);
target.put("modernPath", "/modern/app/#/pages/" + pageId);
Map<String, Object> action = new LinkedHashMap<>();
action.put("actionId", actionId);
action.put("label", actionId);
action.put("source", "controller");
action.put("eventType", "");
action.put("eventInvoke", "");
action.put("eventPath", "");
action.put("serviceName", "");
action.put("auth", true);
action.put("responses", List.of(target));
return action;
}
}
@@ -0,0 +1,120 @@
/*******************************************************************************
* 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.ofbiz.modernapi.core;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.security.Security;
import org.junit.Test;
public class ModernApiUtilPermissionTests {
@Test
public void mapsOrderEntityToOrderManagerViewPermission() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("OrderHeader", "org.apache.ofbiz.order.order");
when(security.hasEntityPermission("ORDERMGR", "_VIEW", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("ORDERMGR", "_VIEW", userLogin);
}
@Test
public void allowsCommonReferenceOptionsWhenUserHasBusinessViewPermission() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("StatusItem", "org.apache.ofbiz.common.status");
when(security.hasEntityPermission("ORDERMGR", "_VIEW", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("ORDERMGR", "_VIEW", userLogin);
}
@Test
public void allowsEntityDataAdminToViewAnyEntity() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("UnmappedThing", "org.example.custom");
when(security.hasEntityPermission("ENTITY_DATA", "_ADMIN", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("ENTITY_DATA", "_ADMIN", userLogin);
}
@Test
public void allowsBusinessAdminToViewMappedEntity() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("OrderHeader", "org.apache.ofbiz.order.order");
when(security.hasEntityPermission("ORDERMGR", "_ADMIN", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("ORDERMGR", "_ADMIN", userLogin);
}
@Test
public void mapsMarketingEntityToMarketingViewPermission() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("MarketingCampaign", "org.apache.ofbiz.marketing.campaign");
when(security.hasEntityPermission("MARKETING", "_VIEW", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("MARKETING", "_VIEW", userLogin);
}
@Test
public void mapsWebsiteEntityToContentManagerViewPermission() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("WebSite", "org.apache.ofbiz.webapp.website");
when(security.hasEntityPermission("CONTENTMGR", "_VIEW", userLogin)).thenReturn(true);
assertTrue(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
verify(security).hasEntityPermission("CONTENTMGR", "_VIEW", userLogin);
}
@Test
public void deniesUnmappedEntityWithoutExplicitPermission() {
Security security = mock(Security.class);
GenericValue userLogin = new GenericValue();
ModelEntity modelEntity = modelEntity("UnmappedThing", "org.example.custom");
assertFalse(ModernApiUtil.hasEntityViewPermission(security, userLogin, modelEntity));
}
private ModelEntity modelEntity(String entityName, String packageName) {
ModelEntity modelEntity = new ModelEntity();
modelEntity.setEntityName(entityName);
modelEntity.setPackageName(packageName);
return modelEntity;
}
}