This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 5d00c0cefb Improved: Route the layout-image events through the
permission-checked content services and tighten data resource template
rendering (#1846)
5d00c0cefb is described below
commit 5d00c0cefb533b9c4982145e710a4141ba172634
Author: Krishna Uprit <[email protected]>
AuthorDate: Fri Sep 4 16:10:17 2026 +0530
Improved: Route the layout-image events through the permission-checked
content services and tighten data resource template rendering (#1846)
- Aligns updateLayoutImage and cloneLayout with the rest of the content
component: their DataResource / Content writes now go through the
permission-checked services (updateDataResource, updateContent) instead
of storing the entities directly, and updateLayoutImage can no longer
change a resource's type.
- Also removes the unused updateLayoutImageOnly mapping, requires
CONTENTMGR_SUPER on createDataTemplateType / updateDataTemplateType /
deleteDataTemplateType, and restricts DataResourceWorker template
rendering — no *_OBJECT source, FTL allow-list instead of deny-list,
SCREEN_COMBINED only from a component:// location.
- Adds UpdateLayoutPermissionTests for the low-privilege (OFBTOOLS_VIEW
/ CONTENTMGR_VIEW) case.
Thank you Krishna Uprit for the contribution.
---
applications/content/servicedef/services_data.xml | 12 +++
.../ofbiz/content/data/DataResourceWorker.java | 53 ++++++++-----
.../apache/ofbiz/content/layout/LayoutEvents.java | 87 +++++++++++++++++-----
.../permission/UpdateLayoutPermissionTests.groovy | 36 +++++++++
.../content/webapp/content/WEB-INF/controller.xml | 6 --
5 files changed, 150 insertions(+), 44 deletions(-)
diff --git a/applications/content/servicedef/services_data.xml
b/applications/content/servicedef/services_data.xml
index d9432eeaf5..8cf83d4db6 100644
--- a/applications/content/servicedef/services_data.xml
+++ b/applications/content/servicedef/services_data.xml
@@ -315,18 +315,30 @@
<auto-attributes include="pk" mode="IN" />
</service>
+ <!-- A data template type decides which rendering engine a data resource
may drive
+ (FTL / SCREEN_COMBINED / FORM_COMBINED / XSLT), so maintaining these
rows is gated on
+ CONTENTMGR_SUPER, consistent with the other template-related content
services. -->
<service name="createDataTemplateType" engine="entity-auto"
invoke="create" default-entity-name="DataTemplateType" auth="true">
<description>Create a Data Template Type</description>
+ <required-permissions join-type="AND">
+ <check-permission permission="CONTENTMGR_SUPER" action="_CREATE"/>
+ </required-permissions>
<auto-attributes include="pk" mode="INOUT" optional="true"/>
<auto-attributes include="nonpk" mode="IN" optional="true"/>
</service>
<service name="updateDataTemplateType" engine="entity-auto"
invoke="update" default-entity-name="DataTemplateType" auth="true">
<description>Update a Data Template Type</description>
+ <required-permissions join-type="AND">
+ <check-permission permission="CONTENTMGR_SUPER" action="_UPDATE"/>
+ </required-permissions>
<auto-attributes include="pk" mode="IN"/>
<auto-attributes include="nonpk" mode="IN" optional="true"/>
</service>
<service name="deleteDataTemplateType" engine="entity-auto"
invoke="delete" default-entity-name="DataTemplateType" auth="true">
<description>Delete a Data Template Type</description>
+ <required-permissions join-type="AND">
+ <check-permission permission="CONTENTMGR_SUPER" action="_DELETE"/>
+ </required-permissions>
<auto-attributes include="pk" mode="IN"/>
</service>
diff --git
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
index 10ea5aa822..0398e3447b 100644
---
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
+++
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
@@ -45,6 +45,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.TreeMap;
import jakarta.servlet.http.HttpServletRequest;
@@ -118,6 +119,15 @@ public class DataResourceWorker implements
org.apache.ofbiz.widget.content.DataR
private static final String ERR_RESOURCE = "ContentErrorUiLabels";
private static final String PROPERTY_RESOURCE = "content";
+ // Data resource source types whose stored text may be rendered as a
FreeMarker template
+ // (dataTemplateTypeId=FTL). Limited to the file- and URL-backed types,
each of which is
+ // independently path- or protocol-restricted elsewhere
(OFBIZ/LOCAL/CONTEXT file path
+ // allow-lists in SecurityUtil; URL_RESOURCE http(s) and host checks in
checkUrlResourceAllowed).
+ // Free-text types (ELECTRONIC_TEXT/SHORT_TEXT/LINK) are not supported as
FreeMarker templates.
+ // Kept as an allow-list so a data resource type added later is not
renderable by default.
+ private static final Set<String> FTL_ALLOWED_SOURCE_TYPES =
+ UtilMisc.toSet("OFBIZ_FILE", "LOCAL_FILE", "CONTEXT_FILE",
"URL_RESOURCE");
+
/**
* Traverses the DataCategory parent/child structure and put it in
categoryNode. Returns non-null error string if there is an error.
* @param depth The place on the categoryTypesIds to start collecting.
@@ -883,6 +893,16 @@ public class DataResourceWorker implements
org.apache.ofbiz.widget.content.DataR
// a template is defined; render the template first
templateContext.put("mimeTypeId", targetMimeTypeId);
+ // A template engine (FTL, XSLT, SCREEN_COMBINED, FORM_COMBINED)
is only given content that
+ // is authored template text. For an *_OBJECT data resource
getDataResourceText() returns
+ // the data resource's own primary key (see
writeDataResourceText), not template text, so
+ // an *_OBJECT is never a valid template source for any template
type.
+ String drSourceTypeId =
dataResource.getString("dataResourceTypeId");
+ if (drSourceTypeId != null && drSourceTypeId.endsWith("_OBJECT")) {
+ throw new GeneralException("Data resource [" + dataResourceId
+ "] of type [" + drSourceTypeId
+ + "] cannot be rendered as a [" + dataTemplateTypeId +
"] template.");
+ }
+
// FTL template
if ("FTL".equals(dataTemplateTypeId)) {
try {
@@ -904,11 +924,12 @@ public class DataResourceWorker implements
org.apache.ofbiz.widget.content.DataR
// render the FTL template
boolean useTemplateCache = cache &&
!UtilProperties.getPropertyAsBoolean("content", "disable.ftl.template.cache",
false);
- if
("ELECTRONIC_TEXT".equals(dataResource.getString("dataResourceTypeId"))
- ||
"SHORT_TEXT".equalsIgnoreCase(dataResource.getString("dataResourceTypeId"))
- ||
"LINK".equalsIgnoreCase(dataResource.getString("dataResourceTypeId"))) {
- throw new GeneralException("Error rendering template:
FreeMarker templates are no longer supported for "
- + dataResource.getString("dataResourceTypeId")
+ " data resources.");
+ // Only file- and URL-backed source types may be rendered
as FreeMarker templates
+ // (see FTL_ALLOWED_SOURCE_TYPES); free-text data
resources (ELECTRONIC_TEXT /
+ // SHORT_TEXT / LINK) are not supported.
+ if (!FTL_ALLOWED_SOURCE_TYPES.contains(drSourceTypeId)) {
+ throw new GeneralException("Error rendering template:
FreeMarker templates are not supported for "
+ + drSourceTypeId + " data resources.");
}
FreeMarkerWorker.renderTemplateFromString("delegator:" +
delegator.getDelegatorName() + ":DataResource:"
@@ -966,20 +987,16 @@ public class DataResourceWorker implements
org.apache.ofbiz.widget.content.DataR
ModelScreen modelScreen = null;
ScreenStringRenderer renderer =
screens.getScreenStringRenderer();
String combinedName = dataResource.getString("objectInfo");
- if
("URL_RESOURCE".equals(dataResource.getString("dataResourceTypeId")) &&
UtilValidate.isNotEmpty(combinedName)
- && combinedName.startsWith("component://")) {
- modelScreen =
ScreenFactory.getScreenFromLocation(combinedName);
- } else { // stored in a single file, long or short text
- Document screenXml =
UtilXml.readXmlDocument(getDataResourceText(dataResource, targetMimeTypeId,
locale, templateContext,
- delegator, cache), true, true);
- Map<String, ModelScreen> modelScreenMap =
ScreenFactory.readScreenDocument(screenXml, "DataResourceId: "
- + dataResource.getString("dataResourceId"));
- if (UtilValidate.isNotEmpty(modelScreenMap)) {
- Map.Entry<String, ModelScreen> entry =
modelScreenMap.entrySet().iterator().next();
- // get first entry, only one screen allowed per
file
- modelScreen = entry.getValue();
- }
+ // A SCREEN_COMBINED data resource is rendered only when
it references a deployed
+ // screen definition (URL_RESOURCE with a component://
objectInfo location).
+ // Building a ModelScreen from screen XML stored in the
data resource text is not
+ // supported: that text can carry widget actions and
scripts.
+ if
(!"URL_RESOURCE".equals(dataResource.getString("dataResourceTypeId")) ||
UtilValidate.isEmpty(combinedName)
+ || !combinedName.startsWith("component://")) {
+ throw new GeneralException("SCREEN_COMBINED data
resource [" + dataResource.getString("dataResourceId")
+ + "] must reference a component:// screen
location via objectInfo.");
}
+ modelScreen =
ScreenFactory.getScreenFromLocation(combinedName);
if (UtilValidate.isNotEmpty(modelScreen)) {
modelScreen.renderScreenString(out, context, renderer);
} else {
diff --git
a/applications/content/src/main/java/org/apache/ofbiz/content/layout/LayoutEvents.java
b/applications/content/src/main/java/org/apache/ofbiz/content/layout/LayoutEvents.java
index cba2143a7e..260c970e21 100644
---
a/applications/content/src/main/java/org/apache/ofbiz/content/layout/LayoutEvents.java
+++
b/applications/content/src/main/java/org/apache/ofbiz/content/layout/LayoutEvents.java
@@ -46,6 +46,7 @@ import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.minilang.MiniLangException;
import org.apache.ofbiz.minilang.SimpleMapProcessor;
+import org.apache.ofbiz.security.Security;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ServiceUtil;
@@ -185,7 +186,7 @@ public class LayoutEvents {
}
String imageFileName = (String) uploadResults.get("imageFileName");
if (Debug.verboseOn()) {
- Debug.logVerbose("in createLayoutImage(java), context:" +
context, "");
+ Debug.logVerbose("in updateLayoutImage(java), context:" +
context, "");
}
context.put("userLogin", session.getAttribute("userLogin"));
context.put("dataResourceTypeId", "IMAGE_OBJECT");
@@ -199,32 +200,62 @@ public class LayoutEvents {
String dataResourceId = (String) context.get("drDataResourceId");
if (Debug.verboseOn()) {
- Debug.logVerbose("in createLayoutImage(java), dataResourceId:"
+ dataResourceId, "");
+ Debug.logVerbose("in updateLayoutImage(java), dataResourceId:"
+ dataResourceId, "");
}
GenericValue dataResource =
EntityQuery.use(delegator).from("DataResource").where("dataResourceId",
dataResourceId).queryOne();
if (Debug.verboseOn()) {
- Debug.logVerbose("in createLayoutImage(java), dataResource:" +
dataResource, "");
+ Debug.logVerbose("in updateLayoutImage(java), dataResource:" +
dataResource + ", imageFileName:" + imageFileName, "");
}
- // Use objectInfo field to store the name of the file, since there
is no
- // place in ImageDataResource for it.
- if (Debug.verboseOn()) {
- Debug.logVerbose("in createLayoutImage(java), imageFileName:"
+ imageFileName, "");
+ if (dataResource == null) {
+ request.setAttribute("_ERROR_MESSAGE_",
+ UtilProperties.getMessage(ERR_RESOURCE,
"layoutEvents.data_ressource_id_null", locale));
+ return "error";
}
- if (dataResource != null) {
- dataResource.setNonPKFields(context);
- dataResource.store();
+ // This event only maintains an existing plain IMAGE_OBJECT layout
resource: it updates the
+ // editable image metadata and the image bytes through the
permission-checked services
+ // (updateDataResource, then create/updateImageDataResource - all
gated by
+ // genericDataResourcePermission, main-action UPDATE / CREATE),
and never writes the
+ // DataResource or ImageDataResource entities directly. A resource
that is template-bearing,
+ // or that is not an IMAGE_OBJECT, is rejected rather than
converted, so this event can no
+ // longer reclassify a resource (the alignment updateLayout
received in OFBIZ #1745).
+ String existingTemplateType =
dataResource.getString("dataTemplateTypeId");
+ if ((UtilValidate.isNotEmpty(existingTemplateType) &&
!"NONE".equals(existingTemplateType))
+ ||
!"IMAGE_OBJECT".equals(dataResource.getString("dataResourceTypeId"))) {
+ request.setAttribute("_ERROR_MESSAGE_",
+ UtilProperties.getMessage("ContentUiLabels",
"ContentPermissionNotGranted", locale));
+ return "error";
}
-
- // See if this needs to be a create or an update procedure
- GenericValue imageDataResource =
EntityQuery.use(delegator).from("ImageDataResource").where("dataResourceId",
dataResourceId).queryOne();
- if (imageDataResource == null) {
- imageDataResource = delegator.makeValue("ImageDataResource",
UtilMisc.toMap("dataResourceId", dataResourceId));
- imageDataResource.set("imageData", byteWrap.array());
- imageDataResource.create();
- } else {
- imageDataResource.set("imageData", byteWrap.array());
- imageDataResource.store();
+ LocalDispatcher dispatcher = (LocalDispatcher)
request.getAttribute("dispatcher");
+ GenericValue userLogin = (GenericValue)
session.getAttribute("userLogin");
+ Map<String, Object> updateDrCtx = new HashMap<>();
+ updateDrCtx.put("dataResourceId", dataResourceId);
+ updateDrCtx.put("userLogin", userLogin);
+ for (String editableField : UtilMisc.toList("dataResourceName",
"statusId", "mimeTypeId",
+ "localeString", "isPublic", "objectInfo")) {
+ Object value = context.get(editableField);
+ if (value != null) {
+ updateDrCtx.put(editableField, value);
+ }
+ }
+ try {
+ Map<String, Object> updateDrResult =
dispatcher.runSync("updateDataResource", updateDrCtx);
+ if (ServiceUtil.isError(updateDrResult)) {
+ request.setAttribute("_ERROR_MESSAGE_",
ServiceUtil.getErrorMessage(updateDrResult));
+ return "error";
+ }
+ GenericValue imageDataResource =
EntityQuery.use(delegator).from("ImageDataResource")
+ .where("dataResourceId", dataResourceId).queryOne();
+ String imageService = (imageDataResource == null) ?
"createImageDataResource" : "updateImageDataResource";
+ Map<String, Object> imageResult =
dispatcher.runSync(imageService, UtilMisc.toMap("dataResourceId",
+ dataResourceId, "imageData", byteWrap.array(),
"userLogin", userLogin));
+ if (ServiceUtil.isError(imageResult)) {
+ request.setAttribute("_ERROR_MESSAGE_",
ServiceUtil.getErrorMessage(imageResult));
+ return "error";
+ }
+ } catch (GenericServiceException e) {
+ request.setAttribute("_ERROR_MESSAGE_", e.getMessage());
+ return "error";
}
} catch (GenericEntityException e3) {
request.setAttribute("_ERROR_MESSAGE_", e3.getMessage());
@@ -338,6 +369,22 @@ public class LayoutEvents {
request.setAttribute("_ERROR_MESSAGE_", errMsg);
return "error";
}
+ // The clone-master step below writes new Content, DataResource
and ContentAssoc rows
+ // directly (no service call), so gate it here: CONTENTMGR create,
plus CONTENTMGR_SUPER
+ // when the source data resource is template-bearing (the clone
would be template-bearing
+ // too) - the same permissions createContent / createDataResource
would require.
+ // Converting these direct writes to service calls is left as
separate follow-up.
+ Security security = (Security) request.getAttribute("security");
+ GenericValue srcDataResource =
EntityQuery.use(delegator).from("DataResource")
+ .where("dataResourceId",
content.getString("dataResourceId")).queryOne();
+ String srcTemplateType = srcDataResource != null ?
srcDataResource.getString("dataTemplateTypeId") : null;
+ boolean srcIsTemplate = UtilValidate.isNotEmpty(srcTemplateType)
&& !"NONE".equals(srcTemplateType);
+ if (security == null ||
!security.hasEntityPermission("CONTENTMGR", "_CREATE", userLogin)
+ || (srcIsTemplate &&
!security.hasEntityPermission("CONTENTMGR_SUPER", "_CREATE", userLogin))) {
+ request.setAttribute("_ERROR_MESSAGE_",
+ UtilProperties.getMessage("ContentUiLabels",
"ContentPermissionNotGranted", locale));
+ return "error";
+ }
newContent = delegator.makeValue("Content", content);
if (Debug.verboseOn()) {
Debug.logVerbose("in cloneLayout, newContent:" + newContent,
"");
diff --git
a/applications/content/src/test/groovy/org/apache/ofbiz/content/permission/UpdateLayoutPermissionTests.groovy
b/applications/content/src/test/groovy/org/apache/ofbiz/content/permission/UpdateLayoutPermissionTests.groovy
index 2f3aa882f5..ac26aa9f94 100644
---
a/applications/content/src/test/groovy/org/apache/ofbiz/content/permission/UpdateLayoutPermissionTests.groovy
+++
b/applications/content/src/test/groovy/org/apache/ofbiz/content/permission/UpdateLayoutPermissionTests.groovy
@@ -72,6 +72,42 @@ class UpdateLayoutPermissionTests implements
JupiterTestHelper {
}
}
+ /**
+ * updateLayoutImage now writes both the DataResource metadata
(updateDataResource) and the image
+ * bytes (create/updateImageDataResource) only through the
permission-checked services, and rejects
+ * a target that is not a plain IMAGE_OBJECT. cloneLayout still copies the
entities directly but is
+ * gated by an explicit CONTENTMGR / CONTENTMGR_SUPER check. These
assertions exercise the service
+ * boundaries those paths rely on for the view-only profile - each must be
denied.
+ */
+ @Test
+ void testViewOnlyUserCannotWriteImageBytesOrCloneTemplateResource() {
+ GenericValue lowPrivUserLogin = createViewOnlyUserLogin()
+ GenericValue dataResource = createTestDataResource()
+
+ // updateLayoutImage's image-bytes write goes through
create/updateImageDataResource,
+ // gated by genericDataResourcePermission (CREATE / UPDATE) on the
target record
+ assertThrows(ServiceAuthException) {
+ dispatcher.runSync('createImageDataResource', [dataResourceId:
dataResource.dataResourceId,
+ imageData: 'x'.bytes, userLogin: lowPrivUserLogin])
+ }
+
+ // updateLayoutImage's metadata write goes through updateDataResource
(UPDATE) on the
+ // real target - denied for a user with no CONTENTMGR_UPDATE and no
role standing
+ assertThrows(ServiceAuthException) {
+ dispatcher.runSync('updateDataResource', [dataResourceId:
dataResource.dataResourceId,
+ dataResourceName: 'renamed by view-only user',
dataResourceTypeId: 'IMAGE_OBJECT',
+ dataTemplateTypeId: 'NONE', userLogin: lowPrivUserLogin])
+ }
+
+ // the boundary cloneLayout's inline CONTENTMGR_SUPER check mirrors:
creating a
+ // template-bearing DataResource is denied for a view-only user
+ assertThrows(ServiceAuthException) {
+ dispatcher.runSync('createDataResource', [dataResourceTypeId:
'ELECTRONIC_TEXT',
+ dataTemplateTypeId: 'FTL', dataResourceName: 'clone',
statusId: 'CTNT_PUBLISHED',
+ userLogin: lowPrivUserLogin])
+ }
+ }
+
private GenericValue createViewOnlyUserLogin() {
GenericValue existing = from('UserLogin').where(userLoginId:
USER_LOGIN_ID).queryOne()
if (existing) {
diff --git a/applications/content/webapp/content/WEB-INF/controller.xml
b/applications/content/webapp/content/WEB-INF/controller.xml
index c809c8b1b6..2dd673184f 100644
--- a/applications/content/webapp/content/WEB-INF/controller.xml
+++ b/applications/content/webapp/content/WEB-INF/controller.xml
@@ -1528,12 +1528,6 @@ under the License.
<response name="success" type="view" value="EditLayoutImage"/>
<response name="error" type="view" value="EditLayoutImage"/>
</request-map>
- <request-map uri="updateLayoutImageOnly">
- <security auth="true" https="true"/>
- <event type="java" invoke="updateLayoutImageOnly"
path="org.apache.ofbiz.content.layout.LayoutEvents"/>
- <response name="success" type="view" value="EditLayoutImage"/>
- <response name="error" type="view" value="EditLayoutImage"/>
- </request-map>
<request-map uri="cloneLayout">
<security auth="true" https="true"/>
<event type="java" invoke="cloneLayout"
path="org.apache.ofbiz.content.layout.LayoutEvents"/>