From de4a9c99d7d15be429852994ac8640fb763d33b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Thu, 12 Feb 2026 22:18:57 +0100 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20hexagonal=20architecture=20reme?= =?UTF-8?q?diation=20=E2=80=94=20ECM=20@ComponentScan=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of the Firefly Framework hexagonal architecture remediation. - Remove @ComponentScan from EcmAutoConfiguration - Add 5 explicit @Bean @ConditionalOnMissingBean methods - Remove @Component/@Service from 6 infrastructure classes - Move @ConditionalOnProperty from adapter classes to @Bean methods --- .../ecm/adapter/AdapterRegistry.java | 3 +- .../ecm/adapter/AdapterSelector.java | 3 +- .../local/LocalDocumentSearchAdapter.java | 6 +- .../adapter/local/LocalPermissionAdapter.java | 6 +- .../ecm/adapter/noop/NoOpAdapterFactory.java | 3 +- .../ecm/config/EcmAutoConfiguration.java | 91 ++++++++++++++++++- .../ecm/service/EcmPortProvider.java | 3 +- 7 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/fireflyframework/ecm/adapter/AdapterRegistry.java b/src/main/java/org/fireflyframework/ecm/adapter/AdapterRegistry.java index a9805bc..a33b4f4 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/AdapterRegistry.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/AdapterRegistry.java @@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; + import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -65,7 +65,6 @@ * @see AdapterSelector */ @Slf4j -@Component public class AdapterRegistry { /** Spring application context for bean discovery. */ diff --git a/src/main/java/org/fireflyframework/ecm/adapter/AdapterSelector.java b/src/main/java/org/fireflyframework/ecm/adapter/AdapterSelector.java index ac6582b..20d9dce 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/AdapterSelector.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/AdapterSelector.java @@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; + import java.util.Optional; @@ -51,7 +51,6 @@ * @see EcmPortProvider */ @Slf4j -@Component public class AdapterSelector { /** The adapter registry containing all available adapters. */ diff --git a/src/main/java/org/fireflyframework/ecm/adapter/local/LocalDocumentSearchAdapter.java b/src/main/java/org/fireflyframework/ecm/adapter/local/LocalDocumentSearchAdapter.java index ed07965..8b150ea 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/local/LocalDocumentSearchAdapter.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/local/LocalDocumentSearchAdapter.java @@ -22,8 +22,8 @@ import org.fireflyframework.ecm.domain.model.document.Document; import org.fireflyframework.ecm.port.document.DocumentSearchPort; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; + + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,13 +38,11 @@ * Stores indexed documents in-memory and supports basic filters. */ @Slf4j -@Component @EcmAdapter( type = "local-search", description = "Local in-memory DocumentSearchPort adapter", supportedFeatures = { AdapterFeature.SEARCH, AdapterFeature.METADATA_SEARCH } ) -@ConditionalOnProperty(name = "firefly.ecm.search.enabled", havingValue = "true", matchIfMissing = false) public class LocalDocumentSearchAdapter implements DocumentSearchPort { private final Map index = new ConcurrentHashMap<>(); diff --git a/src/main/java/org/fireflyframework/ecm/adapter/local/LocalPermissionAdapter.java b/src/main/java/org/fireflyframework/ecm/adapter/local/LocalPermissionAdapter.java index f7e326e..c73808d 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/local/LocalPermissionAdapter.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/local/LocalPermissionAdapter.java @@ -24,8 +24,8 @@ import org.fireflyframework.ecm.domain.model.security.Permission; import org.fireflyframework.ecm.port.security.PermissionPort; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; + + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,13 +38,11 @@ * Provides a functional, non-stub adapter to satisfy hexagonal port contracts. */ @Slf4j -@Component @EcmAdapter( type = "local-permissions", description = "Local in-memory PermissionPort adapter", supportedFeatures = { AdapterFeature.PERMISSIONS } ) -@ConditionalOnProperty(name = "firefly.ecm.permissions.enabled", havingValue = "true", matchIfMissing = false) public class LocalPermissionAdapter implements PermissionPort { private final Map store = new ConcurrentHashMap<>(); diff --git a/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterFactory.java b/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterFactory.java index 4151cc1..85be0bb 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterFactory.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterFactory.java @@ -24,7 +24,7 @@ import org.fireflyframework.ecm.port.idp.*; import org.fireflyframework.ecm.port.security.*; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; + /** * Factory for creating no-op adapter implementations for all ECM port interfaces. @@ -48,7 +48,6 @@ * @see NoOpAdapterBase */ @Slf4j -@Component public class NoOpAdapterFactory { /** diff --git a/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java b/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java index 01a1134..e3b42c2 100644 --- a/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java +++ b/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java @@ -17,6 +17,8 @@ import org.fireflyframework.ecm.adapter.AdapterRegistry; import org.fireflyframework.ecm.adapter.AdapterSelector; +import org.fireflyframework.ecm.adapter.local.LocalDocumentSearchAdapter; +import org.fireflyframework.ecm.adapter.local.LocalPermissionAdapter; import org.fireflyframework.ecm.adapter.noop.NoOpAdapterFactory; import org.fireflyframework.ecm.port.document.*; import org.fireflyframework.ecm.port.folder.*; @@ -27,10 +29,11 @@ import org.fireflyframework.ecm.service.EcmPortProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; /** * Spring Boot auto-configuration for the Firefly ECM (Enterprise Content Management) system. @@ -41,7 +44,7 @@ *
  • Adapter discovery and registration
  • *
  • Port provider configuration
  • *
  • Conditional bean creation based on feature flags
  • - *
  • Component scanning for ECM-related beans
  • + *
  • Explicit bean registration for ECM infrastructure components
  • * * *

    The auto-configuration is activated when the property {@code firefly.ecm.enabled} @@ -72,10 +75,57 @@ @Slf4j @AutoConfiguration @EnableConfigurationProperties(EcmProperties.class) -@ComponentScan(basePackages = "org.fireflyframework.ecm") @ConditionalOnProperty(prefix = "firefly.ecm", name = "enabled", havingValue = "true", matchIfMissing = true) public class EcmAutoConfiguration { + /** + * Configures the adapter registry for discovering and managing ECM adapters. + * + *

    The adapter registry automatically discovers all ECM adapters in the Spring + * application context and provides efficient access to them based on type or + * interface requirements.

    + * + * @param applicationContext the Spring application context for bean discovery + * @return a configured AdapterRegistry instance + * @see AdapterRegistry + */ + @Bean + @ConditionalOnMissingBean + public AdapterRegistry adapterRegistry(ApplicationContext applicationContext) { + return new AdapterRegistry(applicationContext); + } + + /** + * Configures the adapter selector for choosing appropriate ECM adapters. + * + *

    The adapter selector implements the adapter selection logic, providing + * intelligent fallback mechanisms and validation capabilities.

    + * + * @param adapterRegistry the registry containing available adapters + * @return a configured AdapterSelector instance + * @see AdapterSelector + */ + @Bean + @ConditionalOnMissingBean + public AdapterSelector adapterSelector(AdapterRegistry adapterRegistry) { + return new AdapterSelector(adapterRegistry); + } + + /** + * Configures the no-op adapter factory for creating fallback adapter implementations. + * + *

    The no-op adapter factory provides a centralized way to create no-op adapters + * that serve as fallbacks when no real adapter implementations are available.

    + * + * @return a configured NoOpAdapterFactory instance + * @see NoOpAdapterFactory + */ + @Bean + @ConditionalOnMissingBean + public NoOpAdapterFactory noOpAdapterFactory() { + return new NoOpAdapterFactory(); + } + /** * Configures the central ECM port provider that manages adapter selection and port provisioning. * @@ -90,11 +140,46 @@ public class EcmAutoConfiguration { * @see AdapterSelector */ @Bean + @ConditionalOnMissingBean public EcmPortProvider ecmPortProvider(AdapterSelector adapterSelector, EcmProperties ecmProperties) { log.info("Configuring ECM Port Provider with adapter type: {}", ecmProperties.getAdapterType()); return new EcmPortProvider(adapterSelector, ecmProperties); } + /** + * Configures the local in-memory document search adapter. + * + *

    This bean is only created when the {@code firefly.ecm.search.enabled} property + * is set to {@code true}. It provides an in-memory search implementation for + * development and testing purposes.

    + * + * @return a configured LocalDocumentSearchAdapter instance + * @see LocalDocumentSearchAdapter + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "firefly.ecm.search.enabled", havingValue = "true", matchIfMissing = false) + public LocalDocumentSearchAdapter localDocumentSearchAdapter() { + return new LocalDocumentSearchAdapter(); + } + + /** + * Configures the local in-memory permission adapter. + * + *

    This bean is only created when the {@code firefly.ecm.permissions.enabled} property + * is set to {@code true}. It provides an in-memory permission management implementation + * for development and testing purposes.

    + * + * @return a configured LocalPermissionAdapter instance + * @see LocalPermissionAdapter + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "firefly.ecm.permissions.enabled", havingValue = "true", matchIfMissing = false) + public LocalPermissionAdapter localPermissionAdapter() { + return new LocalPermissionAdapter(); + } + /** * Configures the document port for basic document CRUD operations. * diff --git a/src/main/java/org/fireflyframework/ecm/service/EcmPortProvider.java b/src/main/java/org/fireflyframework/ecm/service/EcmPortProvider.java index f106a33..4090e32 100644 --- a/src/main/java/org/fireflyframework/ecm/service/EcmPortProvider.java +++ b/src/main/java/org/fireflyframework/ecm/service/EcmPortProvider.java @@ -25,7 +25,7 @@ import org.fireflyframework.ecm.port.esignature.*; import org.fireflyframework.ecm.port.idp.*; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; + import java.util.Optional; @@ -76,7 +76,6 @@ * @see EcmAutoConfiguration */ @Slf4j -@Service public class EcmPortProvider { /** The adapter selector responsible for choosing appropriate adapters. */ From 1e67c519af14bcb18d40c5f7ef055e045605c0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Fri, 13 Feb 2026 01:34:45 +0100 Subject: [PATCH 2/2] fix: NoOp adapters deny-by-default for security ports - Change NoOp PermissionPort to return false (deny) instead of true - Change NoOp DocumentSecurityPort to deny by default - Add loud startup warnings when NoOp adapters handle security - Update tests to match deny-by-default behavior --- .../ecm/adapter/noop/NoOpGenericAdapter.java | 25 +++++++++---------- .../ecm/config/EcmAutoConfiguration.java | 16 ++++++++++-- .../adapter/noop/NoOpAdapterLoggingTest.java | 4 +-- .../config/EcmGracefulDegradationTest.java | 12 ++++----- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpGenericAdapter.java b/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpGenericAdapter.java index 096250f..ae78f6d 100644 --- a/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpGenericAdapter.java +++ b/src/main/java/org/fireflyframework/ecm/adapter/noop/NoOpGenericAdapter.java @@ -36,11 +36,14 @@ *
      *
    • Methods returning {@link Mono}: Return empty Mono or error for modifications
    • *
    • Methods returning {@link Flux}: Return empty Flux
    • - *
    • Methods returning {@link Boolean}: Return false for existence checks, true for permissions
    • + *
    • Methods returning {@link Boolean}: Return false (deny by default) for ALL checks including permissions
    • *
    • Methods returning {@link String}: Return adapter name for getAdapterName(), empty for others
    • *
    • Other return types: Return null or appropriate defaults
    • *
    * + *

    Security Note: All permission/access checks return DENY by default. + * Configure a real adapter for production use.

    + * * @param the port interface type * @author Firefly Software Solutions Inc. * @version 1.0 @@ -121,12 +124,14 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl private Mono handleMonoReturn(String methodName) { String lowerMethodName = methodName.toLowerCase(); - // For boolean permission/access methods, return true (permissive default) - CHECK FIRST + // SECURITY: Permission/access methods DENY by default when no adapter configured if (lowerMethodName.startsWith("can") || lowerMethodName.contains("access") || lowerMethodName.contains("permission") || lowerMethodName.contains("allow")) { - return Mono.just(true); + log.warn("SECURITY: Permission check '{}' denied by NoOp {} adapter. " + + "Configure a real adapter for production use.", methodName, getAdapterType()); + return Mono.just(false); } // For boolean existence/status checks, return false @@ -170,23 +175,17 @@ private Mono handleMonoReturn(String methodName) { private boolean handleBooleanReturn(String methodName) { String lowerMethodName = methodName.toLowerCase(); - // For permission/access methods, return true (permissive default) + // SECURITY: Permission/access methods DENY by default if (lowerMethodName.startsWith("can") || lowerMethodName.contains("access") || lowerMethodName.contains("permission") || lowerMethodName.contains("allow")) { - return true; - } - - // For existence/status checks, return false - if (lowerMethodName.startsWith("exists") || - lowerMethodName.startsWith("has") || - lowerMethodName.startsWith("is") || - lowerMethodName.startsWith("contains")) { + log.warn("SECURITY: Permission check '{}' denied by NoOp {} adapter. " + + "Configure a real adapter for production use.", methodName, getAdapterType()); return false; } - // Default to false for other boolean methods + // For all other boolean methods, return false return false; } } diff --git a/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java b/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java index e3b42c2..4125f58 100644 --- a/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java +++ b/src/main/java/org/fireflyframework/ecm/config/EcmAutoConfiguration.java @@ -338,7 +338,13 @@ public FolderHierarchyPort folderHierarchyPort(EcmPortProvider portProvider, NoO @ConditionalOnProperty(prefix = "firefly.ecm.features", name = "permissions", havingValue = "true", matchIfMissing = true) public PermissionPort permissionPort(EcmPortProvider portProvider, NoOpAdapterFactory noOpAdapterFactory) { return portProvider.getPermissionPort() - .orElseGet(noOpAdapterFactory::createPermissionPort); + .orElseGet(() -> { + log.warn("========================================================================="); + log.warn("ECM PermissionPort is using NoOp adapter — ALL permission checks will"); + log.warn("DENY by default. Configure a real adapter for production use."); + log.warn("========================================================================="); + return noOpAdapterFactory.createPermissionPort(); + }); } /** @@ -361,7 +367,13 @@ public PermissionPort permissionPort(EcmPortProvider portProvider, NoOpAdapterFa @ConditionalOnProperty(prefix = "firefly.ecm.features", name = "security", havingValue = "true", matchIfMissing = true) public DocumentSecurityPort documentSecurityPort(EcmPortProvider portProvider, NoOpAdapterFactory noOpAdapterFactory) { return portProvider.getDocumentSecurityPort() - .orElseGet(noOpAdapterFactory::createDocumentSecurityPort); + .orElseGet(() -> { + log.warn("========================================================================="); + log.warn("ECM DocumentSecurityPort is using NoOp adapter — ALL security operations"); + log.warn("will be denied/no-op. Configure a real adapter for production use."); + log.warn("========================================================================="); + return noOpAdapterFactory.createDocumentSecurityPort(); + }); } /** diff --git a/src/test/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterLoggingTest.java b/src/test/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterLoggingTest.java index 9ffff85..f57ab77 100644 --- a/src/test/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterLoggingTest.java +++ b/src/test/java/org/fireflyframework/ecm/adapter/noop/NoOpAdapterLoggingTest.java @@ -145,9 +145,9 @@ void shouldLogWarningsForSecurityAdapterMethods() { UUID testDocumentId = UUID.randomUUID(); UUID testUserId = UUID.randomUUID(); - // Call a permission check method (should return true with warning) + // Call a permission check method (should return false with warning — deny by default) StepVerifier.create(adapter.canAccessDocument(testDocumentId, testUserId, "READ")) - .expectNext(true) + .expectNext(false) .verifyComplete(); // Call an encryption method and expect error diff --git a/src/test/java/org/fireflyframework/ecm/config/EcmGracefulDegradationTest.java b/src/test/java/org/fireflyframework/ecm/config/EcmGracefulDegradationTest.java index 809e891..6d8cad5 100644 --- a/src/test/java/org/fireflyframework/ecm/config/EcmGracefulDegradationTest.java +++ b/src/test/java/org/fireflyframework/ecm/config/EcmGracefulDegradationTest.java @@ -120,24 +120,24 @@ void shouldReturnEmptyResultsForQueryOperations() { } /** - * Verifies that no-op adapters return permissive defaults for security operations. + * Verifies that no-op adapters return deny by default for security operations. */ @Test - void shouldReturnPermissiveDefaultsForSecurityOperations() { + void shouldReturnDenyByDefaultForSecurityOperations() { UUID testDocumentId = UUID.randomUUID(); UUID testUserId = UUID.randomUUID(); - // Access checks should return true (permissive default) + // Access checks should return false (deny by default) StepVerifier.create(documentSecurityPort.canAccessDocument(testDocumentId, testUserId, "READ")) - .expectNext(true) + .expectNext(false) .verifyComplete(); StepVerifier.create(documentSecurityPort.canDeleteDocument(testDocumentId, testUserId)) - .expectNext(true) + .expectNext(false) .verifyComplete(); StepVerifier.create(documentSecurityPort.canModifyDocument(testDocumentId, testUserId)) - .expectNext(true) + .expectNext(false) .verifyComplete(); }