This is an automated email from the ASF dual-hosted git repository. sergehuber pushed a commit to branch UNOMI-875-pr4-security-tenant in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 621c8f24cc2142f77f13fa1164db17987075a859 Author: Serge Huber <[email protected]> AuthorDate: Sun Jul 5 14:54:48 2026 +0200 UNOMI-942: Fix tenant quota counts and TenantService error contracts Add per-tenant getAllItemsCount(itemType, tenantId) to persistence and wire TenantQuotaService to use tenant-scoped profile/event counts instead of cluster-wide totals. --- .../ElasticSearchPersistenceServiceImpl.java | 10 ++- .../OpenSearchPersistenceServiceImpl.java | 11 ++- .../unomi/persistence/spi/PersistenceService.java | 10 +++ .../services/impl/tenants/TenantQuotaService.java | 4 +- .../impl/InMemoryPersistenceServiceImpl.java | 21 ++++++ .../impl/tenants/TenantQuotaServiceTest.java | 85 ++++++++++++++++++++++ 6 files changed, 137 insertions(+), 4 deletions(-) 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 3f62cb884..dc90146c2 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 @@ -777,6 +777,10 @@ public class ElasticSearchPersistenceServiceImpl implements PersistenceService, return queryCount(Query.of(q -> q.matchAll(m -> m)), itemType); } + @Override public long getAllItemsCount(String itemType, String tenantId) { + return queryCount(Query.of(q -> q.matchAll(m -> m)), itemType, tenantId); + } + @Override public <T extends Item> PartialList<T> getAllItems(final Class<T> clazz, int offset, int size, String sortBy) { return getAllItems(clazz, offset, size, sortBy, null); } @@ -2090,12 +2094,16 @@ public class ElasticSearchPersistenceServiceImpl implements PersistenceService, } private long queryCount(final Query query, final String itemType) { + return queryCount(query, itemType, getTenantId()); + } + + private long queryCount(final Query query, final String itemType, final String tenantId) { return new InClassLoaderExecute<Long>(metricsService, this.getClass().getName() + ".queryCount", this.bundleContext, this.fatalIllegalStateErrors, throwExceptions) { @Override protected Long execute(Object... args) throws IOException { CountRequest countRequest = CountRequest.of( - builder -> builder.index(getIndexNameForQuery(itemType)).query(wrapWithTenantAndItemTypeQuery(itemType, query, getTenantId()))); + builder -> builder.index(getIndexNameForQuery(itemType)).query(wrapWithTenantAndItemTypeQuery(itemType, query, tenantId))); return esClient.count(countRequest).count(); } }.catchingExecuteInClassLoader(true); 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 48ca106d7..05c2c50c6 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 @@ -685,6 +685,11 @@ public class OpenSearchPersistenceServiceImpl implements PersistenceService, Syn return queryCount(Query.of(q -> q.matchAll(t -> t)), itemType); } + @Override + public long getAllItemsCount(String itemType, String tenantId) { + return queryCount(Query.of(q -> q.matchAll(t -> t)), itemType, tenantId); + } + @Override public <T extends Item> PartialList<T> getAllItems(final Class<T> clazz, int offset, int size, String sortBy) { return getAllItems(clazz, offset, size, sortBy, null); @@ -1974,12 +1979,16 @@ public class OpenSearchPersistenceServiceImpl implements PersistenceService, Syn } private long queryCount(final Query filter, final String itemType) { + return queryCount(filter, itemType, getTenantId()); + } + + private long queryCount(final Query filter, final String itemType, final String tenantId) { return new InClassLoaderExecute<Long>(metricsService, this.getClass().getName() + ".queryCount", this.bundleContext, this.fatalIllegalStateErrors, throwExceptions) { @Override protected Long execute(Object... args) throws IOException { CountResponse response = client.count(count -> count.index(getIndexNameForQuery(itemType)) - .query(wrapWithTenantAndItemTypeQuery(itemType, filter, getTenantId()))); + .query(wrapWithTenantAndItemTypeQuery(itemType, filter, tenantId))); return response.count(); } }.catchingExecuteInClassLoader(true); diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java index 8e8b22622..0f6ca0d3b 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java @@ -619,6 +619,16 @@ public interface PersistenceService { */ long getAllItemsCount(String itemType); + /** + * Retrieves the number of items with the specified type as defined by the Item subclass public field {@code ITEM_TYPE} for the given tenant. + * + * @param itemType the String representation of the item type we want to retrieve the count of, as defined by its class' {@code ITEM_TYPE} field + * @param tenantId the ID of the tenant whose items should be counted + * @return the number of items of the specified type for the given tenant + * @see Item Item for a discussion of {@code ITEM_TYPE} + */ + long getAllItemsCount(String itemType, String tenantId); + /** * Retrieves the number of items with the specified type as defined by the Item subclass public field {@code ITEM_TYPE} matching the optional specified condition and * aggregated according to the specified {@link BaseAggregate}. diff --git a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantQuotaService.java b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantQuotaService.java index 9b3dc4ac1..c08775a26 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantQuotaService.java +++ b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantQuotaService.java @@ -107,8 +107,8 @@ public class TenantQuotaService { if (shutdownNow) return; // Check shutdown flag during iteration TenantUsage usage = usageCache.get(tenantId); - usage.setProfileCount(persistenceService.getAllItemsCount("profile")); - usage.setEventCount(persistenceService.getAllItemsCount("event")); + usage.setProfileCount(persistenceService.getAllItemsCount("profile", tenantId)); + usage.setEventCount(persistenceService.getAllItemsCount("event", tenantId)); // Note: Storage size calculation would require additional implementation } } catch (Exception e) { diff --git a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java index facd96002..1afbb3d32 100644 --- a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java +++ b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java @@ -1358,6 +1358,27 @@ public class InMemoryPersistenceServiceImpl implements PersistenceService { return filteredItems.size(); } + @Override + public long getAllItemsCount(String itemType, String tenantId) { + if (itemType == null || tenantId == null) { + return 0; + } + + LOGGER.debug("Counting all items of type {} for tenant {}", itemType, tenantId); + + Map<String, Item> filteredItems = new HashMap<>(); + for (Map.Entry<String, Item> entry : itemsById.entrySet()) { + Item item = entry.getValue(); + if (item.getItemType().equals(itemType) && tenantId.equals(item.getTenantId())) { + String itemKey = entry.getKey(); + if (isItemAvailableForQuery(itemKey, itemType)) { + filteredItems.put(itemKey, item); + } + } + } + return filteredItems.size(); + } + @Override public Map<String, Double> getSingleValuesMetrics(Condition condition, String[] metrics, String field, String itemType) { if (metrics == null || metrics.length == 0 || field == null) { diff --git a/services/src/test/java/org/apache/unomi/services/impl/tenants/TenantQuotaServiceTest.java b/services/src/test/java/org/apache/unomi/services/impl/tenants/TenantQuotaServiceTest.java new file mode 100644 index 000000000..5125f1e38 --- /dev/null +++ b/services/src/test/java/org/apache/unomi/services/impl/tenants/TenantQuotaServiceTest.java @@ -0,0 +1,85 @@ +/* + * 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.unomi.services.impl.tenants; + +import org.apache.unomi.persistence.spi.PersistenceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TenantQuotaServiceTest { + + @Mock + private PersistenceService persistenceService; + + private TenantQuotaService tenantQuotaService; + + @BeforeEach + public void setUp() throws Exception { + tenantQuotaService = new TenantQuotaService(); + tenantQuotaService.setPersistenceService(persistenceService); + + Field usageCacheField = TenantQuotaService.class.getDeclaredField("usageCache"); + usageCacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map<String, TenantUsage> usageCache = (Map<String, TenantUsage>) usageCacheField.get(tenantQuotaService); + usageCache.put("tenant-a", new TenantUsage()); + usageCache.put("tenant-b", new TenantUsage()); + } + + @Test + public void updateUsageStatisticsUsesPerTenantCounts() throws Exception { + when(persistenceService.getAllItemsCount(eq("profile"), eq("tenant-a"))).thenReturn(10L); + when(persistenceService.getAllItemsCount(eq("event"), eq("tenant-a"))).thenReturn(20L); + when(persistenceService.getAllItemsCount(eq("profile"), eq("tenant-b"))).thenReturn(100L); + when(persistenceService.getAllItemsCount(eq("event"), eq("tenant-b"))).thenReturn(200L); + + Method updateMethod = TenantQuotaService.class.getDeclaredMethod("updateUsageStatistics"); + updateMethod.setAccessible(true); + updateMethod.invoke(tenantQuotaService); + + Field usageCacheField = TenantQuotaService.class.getDeclaredField("usageCache"); + usageCacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map<String, TenantUsage> usageCache = (Map<String, TenantUsage>) usageCacheField.get(tenantQuotaService); + + assertEquals(10L, usageCache.get("tenant-a").getProfileCount()); + assertEquals(20L, usageCache.get("tenant-a").getEventCount()); + assertEquals(100L, usageCache.get("tenant-b").getProfileCount()); + assertEquals(200L, usageCache.get("tenant-b").getEventCount()); + + verify(persistenceService).getAllItemsCount("profile", "tenant-a"); + verify(persistenceService).getAllItemsCount("event", "tenant-a"); + verify(persistenceService).getAllItemsCount("profile", "tenant-b"); + verify(persistenceService).getAllItemsCount("event", "tenant-b"); + verify(persistenceService, never()).getAllItemsCount("profile"); + verify(persistenceService, never()).getAllItemsCount("event"); + } +}
