From 2b7eb6859cde780fd9af5b623a082bd0b6e2bf53 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 21 Jul 2026 12:35:11 +0200 Subject: [PATCH 1/2] fix: fail fast when multitenancy is enabled without shared object store In multitenant deployments the OSS handler only prefixes object keys with the tenant ID when the object store kind is 'shared'. With any other (or no) kind, keys are unprefixed and tenants share a single keyspace with no isolation, while separate per-tenant stores are not yet supported. The OSS registration now refuses to start when multitenancy is enabled and the object store kind is not 'shared', instead of silently registering a handler that provides no tenant isolation. --- .../attachments/oss/configuration/Registration.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 3fbca05e..78c087e9 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -7,6 +7,7 @@ import com.sap.cds.feature.attachments.oss.client.OSClientFactory; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntimeConfiguration; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -30,6 +31,17 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { boolean multitenancyEnabled = isMultitenancyEnabled(env); String objectStoreKind = getObjectStoreKind(env); + // Fail fast on an insecure multitenant configuration: without a shared object store the + // object keys are not prefixed with the tenant ID, so tenants are not isolated in the + // bucket. Refuse to start rather than silently storing all tenants under the same keyspace. + if (multitenancyEnabled && !"shared".equals(objectStoreKind)) { + throw new ServiceException( + "Multitenancy is enabled but the object store kind is not 'shared' (was: '{}'). " + + "A shared object store is required to isolate tenants via tenant-prefixed object " + + "keys. Configure 'cds.attachments.objectStore.kind: shared'.", + objectStoreKind); + } + // Fixed thread pool for background I/O operations (upload, download, delete). // Default 16 is tuned for I/O-bound cloud storage calls, not CPU-bound work. int threadPoolSize = From 5a42ad99dee351ea34df89f04b7d8da84dbd1ab6 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 21 Jul 2026 12:35:15 +0200 Subject: [PATCH 2/2] test: assert startup fails on multitenant non-shared object store Updates RegistrationTest so that enabling multitenancy with a non-shared or unset object store kind now expects a ServiceException and no handler registration, replacing the previous assertions that documented the insecure silent-fallback behavior. --- .../oss/configuration/RegistrationTest.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 3987e1a7..09a517d1 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.configuration; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -11,6 +12,7 @@ import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -87,29 +89,31 @@ void testEventHandlersNoBindingDoesNotRegister() { } @Test - void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { + void testMtEnabledNonSharedKindFailsStartup() { when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) .thenReturn("dedicated"); - registration.eventHandlers(configurer); + assertThatThrownBy(() -> registration.eventHandlers(configurer)) + .isInstanceOf(ServiceException.class) + .hasMessageContaining("shared"); - verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer, times(1)).eventHandler(any()); + verify(configurer, never()).eventHandler(any()); } @Test - void testMtEnabledNullKindRegistersOnlyOSSHandler() { + void testMtEnabledNullKindFailsStartup() { when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); - registration.eventHandlers(configurer); + assertThatThrownBy(() -> registration.eventHandlers(configurer)) + .isInstanceOf(ServiceException.class) + .hasMessageContaining("shared"); - verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer, times(1)).eventHandler(any()); + verify(configurer, never()).eventHandler(any()); } @Test