This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new 1991f3503 UNOMI-983: Refuse to save an item that carries no itemId
(#865)
1991f3503 is described below
commit 1991f3503f2c723509e85fe88cf670bb51b3c942
Author: Jonathan SINOVASSIN-NAIK <[email protected]>
AuthorDate: Mon Sep 28 16:29:41 2026 +0200
UNOMI-983: Refuse to save an item that carries no itemId (#865)
Co-authored-by: Serge Huber <[email protected]>
---
.../org/apache/unomi/itests/ProfileServiceIT.java | 19 ++++---------
.../ElasticSearchPersistenceServiceImpl.java | 8 ++++++
.../OpenSearchPersistenceServiceImpl.java | 8 ++++++
.../rest/endpoints/ProfileServiceEndPoint.java | 3 ++
.../services/impl/profiles/ProfileServiceImpl.java | 5 ++++
.../impl/profiles/ProfileServiceImplTest.java | 33 ++++++++++++++++++++++
6 files changed, 63 insertions(+), 13 deletions(-)
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
index e256c7004..3f9473236 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
@@ -21,7 +21,6 @@ import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.query.Query;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -89,20 +88,14 @@ public class ProfileServiceIT extends BaseIT {
public void testProfileWithoutItemId() throws Exception {
Profile profile = new Profile();
profile.setProperty("name", "testProfileWithoutItemId");
- profileService.saveOrMerge(profile);
- Profile testProfile = keepTrying("Profile not found in the required
time", () -> profileService.findProfilesByPropertyValue("properties.name",
"testProfileWithoutItemId", 0, 10, null), Objects::nonNull,
- DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES).get(0);
- LOGGER.info("Ensure an itemId as been set...");
- Assert.assertNotNull(testProfile.getItemId());
+ assertNull("saveOrMerge must refuse a profile that carries no itemId",
profileService.saveOrMerge(profile));
- LOGGER.info("Delete profile...");
- profileService.delete(testProfile.getItemId(), false);
-
- waitForNullValue("Profile still present after deletion", () ->
profileService.load(testProfile.getItemId()), DEFAULT_TRYING_TIMEOUT,
- DEFAULT_TRYING_TRIES);
-
- LOGGER.info("Profile deleted successfully.");
+ // Stay empty for the same window a successful save would have become
visible in.
+ shouldBeTrueUntilEnd("A profile with no itemId must not be stored",
+ () ->
profileService.findProfilesByPropertyValue("properties.name",
"testProfileWithoutItemId", 0, 10, null),
+ found -> found != null && found.getTotalSize() == 0 &&
found.getList() != null && found.getList().isEmpty(),
+ DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
}
@Test
diff --git
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
index 69ca79dc9..139b87b92 100644
---
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
+++
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
@@ -931,6 +931,14 @@ public class ElasticSearchPersistenceServiceImpl
implements PersistenceService,
}
@Override public boolean save(final Item item, final Boolean
useBatchingOption, final Boolean alwaysOverwriteOption) {
+ // An item states its own identity. Without an itemId the document id
below would read
+ // "<tenant>_null", so every id-less item of the same type would land
on one shared document
+ // and overwrite the item the previous save wrote. Refuse the save and
say so.
+ if (item.getItemId() == null) {
+ LOGGER.warn("Refusing to save an item of type {} that carries no
itemId", item.getItemType());
+ return false;
+ }
+
String finalTenantId =
validateTenantAndGetId(SecurityServiceConfiguration.PERMISSION_SAVE);
item.setTenantId(finalTenantId);
diff --git
a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
index 823c33d3f..c4221ec4d 100644
---
a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
+++
b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
@@ -854,6 +854,14 @@ public class OpenSearchPersistenceServiceImpl implements
PersistenceService, Syn
@Override
public boolean save(final Item item, final Boolean useBatchingOption,
final Boolean alwaysOverwriteOption) {
+ // An item states its own identity. Without an itemId the document id
below would read
+ // "<tenant>_null", so every id-less item of the same type would land
on one shared document
+ // and overwrite the item the previous save wrote. Refuse the save and
say so.
+ if (item.getItemId() == null) {
+ LOGGER.warn("Refusing to save an item of type {} that carries no
itemId", item.getItemType());
+ return false;
+ }
+
String finalTenantId =
validateTenantAndGetId(SecurityServiceConfiguration.PERMISSION_SAVE);
item.setTenantId(finalTenantId);
diff --git
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
index 3c43745f9..305a7bb55 100644
---
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
+++
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
@@ -249,6 +249,9 @@ public class ProfileServiceEndPoint {
@POST
@Path("/")
public Profile save(Profile profile) {
+ if (profile == null || profile.getItemId() == null) {
+ throw new BadRequestException("A profile states its own identity,
so the body must carry an itemId");
+ }
Profile savedProfile = profileService.saveOrMerge(profile);
if (savedProfile != null) {
Event profileUpdated = new Event("profileUpdated", null,
savedProfile, null, null, savedProfile, new Date());
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index f24636ca9..8bb24622a 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -677,6 +677,11 @@ public class ProfileServiceImpl extends
AbstractMultiTypeCachingService implemen
}
public Profile saveOrMerge(Profile profile) {
+ // Same rule as save(Profile, boolean): a profile states its own
identity, and there is
+ // nothing to look up or to merge into without it.
+ if (profile == null || profile.getItemId() == null) {
+ return null;
+ }
Profile previousProfile = persistenceService.load(profile.getItemId(),
Profile.class);
profile.setSystemProperty("lastUpdated", new Date());
if (previousProfile == null) {
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
b/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
index 2df2cd516..32280b264 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/profiles/ProfileServiceImplTest.java
@@ -698,4 +698,37 @@ public class ProfileServiceImplTest {
});
}
+ // A profile states its own identity. Without an itemId there is nothing
to look up and nothing
+ // to merge into, and the persistence layer would write every such profile
to one shared
+ // document, so the second call would destroy what the first one wrote.
+ @Test
+ public void testSaveOrMergeRefusesAProfileWithNoItemId() {
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ Profile profile = new Profile();
+ profile.setProperty("firstName", "Ada");
+
+ assertNull(profileService.saveOrMerge(profile), "saveOrMerge must
refuse a profile that carries no itemId");
+ return null;
+ });
+ }
+
+ // Two profiles with no itemId must not end up on the same document.
+ @Test
+ public void testSaveOrMergeKeepsTwoIdLessProfilesApart() {
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ Profile first = new Profile();
+ first.setProperty("firstName", "First");
+ Profile second = new Profile();
+ second.setProperty("firstName", "Second");
+
+ profileService.saveOrMerge(first);
+ profileService.saveOrMerge(second);
+
+ Query query = new Query();
+ assertEquals(0L, profileService.search(query,
Profile.class).getTotalSize(),
+ "Neither profile is stored, so no profile can overwrite the
other");
+ return null;
+ });
+ }
+
}