diff --git a/composer.json b/composer.json index 0bbfd8e8a..8ae6d7b92 100644 --- a/composer.json +++ b/composer.json @@ -177,6 +177,9 @@ "enqueue/dsn": "^0.10.27", "yoast/phpunit-polyfills": "^4.0.0" }, + "suggest": { + "ext-simplexml": "Required if application/xml is used as serialization media type" + }, "conflict": { "symfony/doctrine-messenger": ">7.0.5 < 7.1.0", "enqueue/dbal": "*" diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index 293cf88ff..cf9b186ea 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -48,6 +48,9 @@ "phpstan/phpstan": "^2.1", "wikimedia/composer-merge-plugin": "^2.1" }, + "suggest": { + "ext-simplexml": "Required if application/xml is used as serialization media type" + }, "scripts": { "tests:phpstan": "vendor/bin/phpstan", "tests:phpunit": [ diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php index b443b98a5..b38421a71 100644 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -9,6 +9,9 @@ use Attribute; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] -class Sensitive +readonly class Sensitive { + public function __construct(public string $sensitiveName = '') + { + } } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 11459194c..9a10e63fa 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -15,6 +15,8 @@ use Ecotone\DataProtection\Conversion\DataProtectionConversionServiceDecorator; use Ecotone\DataProtection\Conversion\JsonDecryptionConverter; use Ecotone\DataProtection\Conversion\JsonEncryptionConverter; +use Ecotone\DataProtection\Conversion\XMLDecryptionConverter; +use Ecotone\DataProtection\Conversion\XMLEncryptionConverter; use Ecotone\DataProtection\Conversion\XPhpDecryptionConverter; use Ecotone\DataProtection\Conversion\XPhpEncryptionConverter; use Ecotone\DataProtection\Encryption\Key; @@ -24,6 +26,7 @@ use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule; use Ecotone\Messaging\Config\Configuration; +use Ecotone\Messaging\Config\ConfigurationException; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ModulePackageList; @@ -50,8 +53,11 @@ public function __construct(private array $dataProtectorConfigs) public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static { + $dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses([], $annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry); + $dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses($dataProtectorConfigs, $annotationRegistrationService->findClassesWithAnnotatedProperties(Sensitive::class), $interfaceToCallRegistry); + return new self( - dataProtectorConfigs: self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry) + dataProtectorConfigs: $dataProtectorConfigs ); } @@ -103,7 +109,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO } $converters = []; - $encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class]; + $encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class, XMLEncryptionConverter::class, XMLDecryptionConverter::class]; foreach ($this->dataProtectorConfigs as $protectorConfig) { foreach ($encryptionConverters as $converterClass) { $converters[] = new Definition( @@ -113,6 +119,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))), $protectorConfig->sensitiveProperties, $protectorConfig->scalarProperties, + $protectorConfig->sensitivePropertyNames, ] ); } @@ -152,29 +159,43 @@ public function getModulePackageName(): string return ModulePackageList::DATA_PROTECTION_PACKAGE; } - private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array + private static function resolveProtectorConfigsFromAnnotatedClasses(array $dataProtectorConfigs, array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array { - $dataEncryptorConfigs = []; foreach ($sensitiveMessages as $message) { + if (array_key_exists($message, $dataProtectorConfigs)) { + continue; + } + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message)); + $isClassSensitive = $classDefinition->findSingleClassAnnotation(Type::create(Sensitive::class)) !== null; $encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); - $sensitiveProperties = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class)); - if ($sensitiveProperties === []) { - $sensitiveProperties = $classDefinition->getProperties(); + $propertiesToProtect = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class)); + if ($propertiesToProtect !== [] && $isClassSensitive) { + throw ConfigurationException::create('#[Sensitive] attribute can be used only on class level, not on property level.'); } - $scalarProperties = array_values(array_filter($sensitiveProperties, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar())); + if ($propertiesToProtect === []) { + $propertiesToProtect = $classDefinition->getProperties(); + } + + $scalarProperties = array_values(array_filter($propertiesToProtect, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar())); + + $nameMapper = static fn (ClassPropertyDefinition $property): string => $property->getName(); + $sensitiveNameMapper = static function (ClassPropertyDefinition $property): string { + $name = $property->findAnnotation(Type::create(Sensitive::class))?->sensitiveName ?? ''; - $mapper = static fn (ClassPropertyDefinition $property): string => $property->getName(); + return $name ?: $property->getName(); + }; - $sensitiveProperties = array_map($mapper, $sensitiveProperties); - $scalarProperties = array_map($mapper, $scalarProperties); + $sensitiveProperties = array_map($nameMapper, $propertiesToProtect); + $scalarProperties = array_map($nameMapper, $scalarProperties); + $sensitivePropertyNames = array_combine($sensitiveProperties, array_map($sensitiveNameMapper, $propertiesToProtect)); - $dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties); + $dataProtectorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties, sensitivePropertyNames: $sensitivePropertyNames); } - return $dataEncryptorConfigs; + return $dataProtectorConfigs; } private function verifyLicense(Configuration $messagingConfiguration): void diff --git a/packages/DataProtection/src/Configuration/DataProtectorConfig.php b/packages/DataProtection/src/Configuration/DataProtectorConfig.php index 99f459fe6..728623996 100644 --- a/packages/DataProtection/src/Configuration/DataProtectorConfig.php +++ b/packages/DataProtection/src/Configuration/DataProtectorConfig.php @@ -19,9 +19,11 @@ public function __construct( public ?string $encryptionKey, public array $sensitiveProperties, public array $scalarProperties, + public array $sensitivePropertyNames, ) { Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings'); Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings'); + Assert::allStrings($this->sensitivePropertyNames, 'Sensitive Properties custom names should be array of strings'); } public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string diff --git a/packages/DataProtection/src/Conversion/AbstractDataProtectionConverter.php b/packages/DataProtection/src/Conversion/AbstractDataProtectionConverter.php new file mode 100644 index 000000000..bc7efaeac --- /dev/null +++ b/packages/DataProtection/src/Conversion/AbstractDataProtectionConverter.php @@ -0,0 +1,24 @@ +sensitiveProperties as $property) { + $propertyKey = $this->sensitivePropertyNames[$property] ?? $property; + if (! array_key_exists($propertyKey, $data)) { + continue; + } + + $data[$propertyKey] = Crypto::decrypt($data[$propertyKey], $this->encryptionKey); + + if (! in_array($propertyKey, $this->scalarProperties, true)) { + $data[$propertyKey] = json_decode($data[$propertyKey], true); + } + } + + return $data; + } +} diff --git a/packages/DataProtection/src/Conversion/AbstractEncryptionConverter.php b/packages/DataProtection/src/Conversion/AbstractEncryptionConverter.php new file mode 100644 index 000000000..885d4b502 --- /dev/null +++ b/packages/DataProtection/src/Conversion/AbstractEncryptionConverter.php @@ -0,0 +1,31 @@ +sensitiveProperties as $property) { + $propertyKey = $this->sensitivePropertyNames[$property] ?? $property; + if (! array_key_exists($propertyKey, $data)) { + continue; + } + + if (! in_array($propertyKey, $this->scalarProperties, true)) { + $data[$propertyKey] = json_encode($data[$propertyKey]); + } + + $data[$propertyKey] = Crypto::encrypt($data[$propertyKey], $this->encryptionKey); + } + + return $data; + } +} diff --git a/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php b/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php index 0fca93594..a9fe510a5 100644 --- a/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php +++ b/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php @@ -27,13 +27,13 @@ public function decorate(ConversionService $conversionService): void public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType, Type $targetPHPType, MediaType $targetMediaType) { if ($this->dataProtectionConversionService->canConvert($sourcePHPType, $encryptedSourceMediaType = $sourceMediaType->addParameter('encrypted', 'true'), $targetPHPType, $targetMediaType)) { - $source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType); + $source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType, $this->innerConversionService); } - $source = $this->innerConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType); + $source = $this->innerConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType, $this->innerConversionService); if ($this->dataProtectionConversionService->canConvert($sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType = $targetMediaType->addParameter('encrypted', 'true'))) { - $source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType); + $source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType, $this->innerConversionService); } return $source; diff --git a/packages/DataProtection/src/Conversion/JsonDecryptionConverter.php b/packages/DataProtection/src/Conversion/JsonDecryptionConverter.php index 911a7de03..26f1492ba 100644 --- a/packages/DataProtection/src/Conversion/JsonDecryptionConverter.php +++ b/packages/DataProtection/src/Conversion/JsonDecryptionConverter.php @@ -13,31 +13,12 @@ /** * licence Enterprise */ -readonly class JsonDecryptionConverter implements Converter +class JsonDecryptionConverter extends AbstractDecryptionConverter { - public function __construct( - private Type $supportedType, - private Key $encryptionKey, - private array $sensitiveProperties, - private array $scalarProperties, - ) { - } - public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType) { $data = json_decode($source, true); - - foreach ($this->sensitiveProperties as $property) { - if (! array_key_exists($property, $data)) { - continue; - } - - $data[$property] = Crypto::decrypt($data[$property], $this->encryptionKey); - - if (! in_array($property, $this->scalarProperties, true)) { - $data[$property] = json_decode($data[$property], true); - } - } + $data = $this->decrypt($data); return json_encode($data); } diff --git a/packages/DataProtection/src/Conversion/JsonEncryptionConverter.php b/packages/DataProtection/src/Conversion/JsonEncryptionConverter.php index dbc9387ec..ecd57b849 100644 --- a/packages/DataProtection/src/Conversion/JsonEncryptionConverter.php +++ b/packages/DataProtection/src/Conversion/JsonEncryptionConverter.php @@ -4,40 +4,18 @@ namespace Ecotone\DataProtection\Conversion; -use Ecotone\DataProtection\Encryption\Crypto; -use Ecotone\DataProtection\Encryption\Key; -use Ecotone\Messaging\Conversion\Converter; use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Handler\Type; /** * licence Enterprise */ -readonly class JsonEncryptionConverter implements Converter +class JsonEncryptionConverter extends AbstractEncryptionConverter { - public function __construct( - private Type $supportedType, - private Key $encryptionKey, - private array $sensitiveProperties, - private array $scalarProperties, - ) { - } - public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType) { $data = json_decode($source, true); - - foreach ($this->sensitiveProperties as $property) { - if (! array_key_exists($property, $data)) { - continue; - } - - if (! in_array($property, $this->scalarProperties, true)) { - $data[$property] = json_encode($data[$property]); - } - - $data[$property] = Crypto::encrypt($data[$property], $this->encryptionKey); - } + $data = $this->encrypt($data); return json_encode($data); } diff --git a/packages/DataProtection/src/Conversion/XMLDecryptionConverter.php b/packages/DataProtection/src/Conversion/XMLDecryptionConverter.php new file mode 100644 index 000000000..8353c8cfe --- /dev/null +++ b/packages/DataProtection/src/Conversion/XMLDecryptionConverter.php @@ -0,0 +1,30 @@ +decrypt($data); + + return XmlHelper::arrayToXml($data); + } + + public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool + { + return $targetType->acceptType($this->supportedType) && $sourceType->isString() && $sourceMediaType->isCompatibleWith(MediaType::createApplicationXml()) && $sourceMediaType->hasParameter('encrypted'); + } +} diff --git a/packages/DataProtection/src/Conversion/XMLEncryptionConverter.php b/packages/DataProtection/src/Conversion/XMLEncryptionConverter.php new file mode 100644 index 000000000..14c8bea5a --- /dev/null +++ b/packages/DataProtection/src/Conversion/XMLEncryptionConverter.php @@ -0,0 +1,31 @@ +encrypt($data); + + return XmlHelper::arrayToXml($data); + } + + public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool + { + return $sourceType->acceptType($this->supportedType) && $targetType->isString() && $targetMediaType->isCompatibleWith(MediaType::createApplicationXml()) && $targetMediaType->hasParameter('encrypted'); + } +} diff --git a/packages/DataProtection/src/Conversion/XPhpDecryptionConverter.php b/packages/DataProtection/src/Conversion/XPhpDecryptionConverter.php index 6f4802bd5..880bac501 100644 --- a/packages/DataProtection/src/Conversion/XPhpDecryptionConverter.php +++ b/packages/DataProtection/src/Conversion/XPhpDecryptionConverter.php @@ -13,31 +13,11 @@ /** * licence Enterprise */ -class XPhpDecryptionConverter implements Converter +class XPhpDecryptionConverter extends AbstractDecryptionConverter { - public function __construct( - private Type $supportedType, - private Key $encryptionKey, - private array $sensitiveProperties, - private array $scalarProperties, - ) { - } - public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType) { - foreach ($this->sensitiveProperties as $property) { - if (! array_key_exists($property, $source)) { - continue; - } - - $source[$property] = Crypto::decrypt($source[$property], $this->encryptionKey); - - if (! in_array($property, $this->scalarProperties, true)) { - $source[$property] = json_decode($source[$property], true); - } - } - - return $source; + return $this->decrypt($source); } public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool diff --git a/packages/DataProtection/src/Conversion/XPhpEncryptionConverter.php b/packages/DataProtection/src/Conversion/XPhpEncryptionConverter.php index caf94516a..8c50af323 100644 --- a/packages/DataProtection/src/Conversion/XPhpEncryptionConverter.php +++ b/packages/DataProtection/src/Conversion/XPhpEncryptionConverter.php @@ -13,31 +13,11 @@ /** * licence Enterprise */ -class XPhpEncryptionConverter implements Converter +class XPhpEncryptionConverter extends AbstractEncryptionConverter { - public function __construct( - private Type $supportedType, - private Key $encryptionKey, - private array $sensitiveProperties, - private array $scalarProperties, - ) { - } - public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType) { - foreach ($this->sensitiveProperties as $property) { - if (! array_key_exists($property, $source)) { - continue; - } - - if (! in_array($property, $this->scalarProperties, true)) { - $source[$property] = json_encode($source[$property]); - } - - $source[$property] = Crypto::encrypt($source[$property], $this->encryptionKey); - } - - return $source; + return $this->encrypt($source); } public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool diff --git a/packages/DataProtection/src/Conversion/XmlHelper.php b/packages/DataProtection/src/Conversion/XmlHelper.php new file mode 100644 index 000000000..411c7e5ad --- /dev/null +++ b/packages/DataProtection/src/Conversion/XmlHelper.php @@ -0,0 +1,45 @@ +'); + + self::addToXml($array, $xml); + + return $xml->asXML(); + } + + public static function xmlToArray(string $xmlString): array + { + $xml = simplexml_load_string($xmlString, 'SimpleXMLElement', LIBXML_NOCDATA); + + return json_decode(json_encode($xml, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR); + } + + private static function addToXml(array $array, SimpleXMLElement $xml): void + { + foreach ($array as $key => $value) { + if (is_array($value)) { + $label = $xml->addChild($key); + self::addToXml($value, $label); + } else { + $xml->addChild($key, $value); + } + } + } +} diff --git a/packages/DataProtection/tests/Fixture/AnnotatedClassWithAnnotatedProperty.php b/packages/DataProtection/tests/Fixture/AnnotatedClassWithAnnotatedProperty.php new file mode 100644 index 000000000..17d5cd7df --- /dev/null +++ b/packages/DataProtection/tests/Fixture/AnnotatedClassWithAnnotatedProperty.php @@ -0,0 +1,16 @@ + $object->sensitiveProperty, + 'bar' => $object->nonSensitiveProperty, + ]; + } + + #[Converter] + public function convertTo(array $payload): MessageWithCustomConverter + { + return new MessageWithCustomConverter( + sensitiveProperty: $payload['foo'], + nonSensitiveProperty: $payload['bar'], + ); + } +} diff --git a/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestCommandHandler.php index be5d74f2b..308558285 100644 --- a/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestCommandHandler.php +++ b/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestCommandHandler.php @@ -9,8 +9,10 @@ use Ecotone\Modelling\Attribute\CommandHandler; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSecondaryEncryptionKey; -use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveProperties; +use Test\Ecotone\DataProtection\Fixture\MessageWithSensitiveProperties; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; +use Test\Ecotone\DataProtection\Fixture\MessageWithCustomConverter; +use Test\Ecotone\DataProtection\Fixture\MessageWithSensitiveProperty; #[Asynchronous('test')] class TestCommandHandler @@ -35,7 +37,25 @@ public function handleAnnotatedMessageWithSecondaryEncryptionKey( #[CommandHandler(endpointId: 'test.EncryptAnnotatedMessages.commandHandler.AnnotatedMessageWithSensitiveProperties')] public function handleAnnotatedMessageWithSensitiveProperties( - #[Payload] AnnotatedMessageWithSensitiveProperties $message, + #[Payload] MessageWithSensitiveProperties $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.EncryptAnnotatedMessages.commandHandler.MessageWithSensitiveProperty')] + public function handleMessageWithSensitiveProperty( + #[Payload] MessageWithSensitiveProperty $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.EncryptAnnotatedMessages.commandHandler.MessageWithCustomConverter')] + public function handleMessageWithCustomConverter( + #[Payload] MessageWithCustomConverter $message, #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { diff --git a/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php b/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php index 7f6bd734f..252c5b974 100644 --- a/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php @@ -9,8 +9,10 @@ use Ecotone\Modelling\Attribute\EventHandler; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSecondaryEncryptionKey; -use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveProperties; +use Test\Ecotone\DataProtection\Fixture\MessageWithSensitiveProperties; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; +use Test\Ecotone\DataProtection\Fixture\MessageWithCustomConverter; +use Test\Ecotone\DataProtection\Fixture\MessageWithSensitiveProperty; #[Asynchronous('test')] class TestEventHandler @@ -35,7 +37,25 @@ public function handleAnnotatedMessageWithSecondaryEncryptionKey( #[EventHandler(endpointId: 'test.EncryptAnnotatedMessages.eventHandler.AnnotatedMessageWithSensitiveProperties')] public function handleAnnotatedMessageWithSensitiveProperties( - #[Payload] AnnotatedMessageWithSensitiveProperties $message, + #[Payload] MessageWithSensitiveProperties $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.EncryptAnnotatedMessages.eventHandler.MessageWithSensitiveProperty')] + public function handleMessageWithSensitiveProperty( + #[Payload] MessageWithSensitiveProperty $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.EncryptAnnotatedMessages.eventHandler.MessageWithCustomConverter')] + public function handleMessageWithCustomConverter( + #[Payload] MessageWithCustomConverter $message, #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { diff --git a/packages/DataProtection/tests/Fixture/MessageWithCustomConverter.php b/packages/DataProtection/tests/Fixture/MessageWithCustomConverter.php new file mode 100644 index 000000000..69cf95105 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/MessageWithCustomConverter.php @@ -0,0 +1,17 @@ +receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->primaryKey) ); } @@ -100,7 +103,7 @@ classesToResolve: [ self::assertEquals($messageSent, $messageReceiver->receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->secondaryKey), + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->secondaryKey), ); } @@ -108,7 +111,7 @@ public function test_protect_commands_using_property_annotation(): void { $ecotone = $this->bootstrapEcotone( classesToResolve: [ - AnnotatedMessageWithSensitiveProperties::class, + MessageWithSensitiveProperties::class, TestCommandHandler::class, ], container: [ @@ -120,7 +123,7 @@ classesToResolve: [ $ecotone ->sendCommand( - $messageSent = new AnnotatedMessageWithSensitiveProperties( + $messageSent = new MessageWithSensitiveProperties( sensitiveObject: new TestClass('value', TestEnum::FIRST), sensitiveEnum: TestEnum::FIRST, property: 'value', @@ -133,7 +136,63 @@ classesToResolve: [ self::assertEquals($messageSent, $messageReceiver->receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","property":"value","sensitiveProperty":"sensitive value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->primaryKey) + ); + } + + public function test_protect_commands_using_attribute_on_property(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithSensitiveProperty::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->sendCommand($messageSent = new MessageWithSensitiveProperty(sensitiveProperty: 'sensitive-value', nonSensitiveProperty: 'non-sensitive-value')) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('{"sensitiveProperty":"sensitive-value","nonSensitiveProperty":"non-sensitive-value"}', $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveProperty'], $this->primaryKey)); + } + + public function test_protect_commands_with_custom_converters(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithCustomConverter::class, + CustomConverter::class, + TestCommandHandler::class, + ], + container: [ + new CustomConverter(), + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->sendCommand( + $messageSent = new MessageWithCustomConverter( + sensitiveProperty: 'sensitive value', + nonSensitiveProperty: 'non-sensitive value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"foo":"\"sensitive value\"","bar":"non-sensitive value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['foo'], $this->primaryKey) ); } @@ -165,7 +224,7 @@ classesToResolve: [ self::assertEquals($messageSent, $messageReceiver->receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->primaryKey) ); } @@ -173,7 +232,7 @@ public function test_protect_events_using_property_annotation(): void { $ecotone = $this->bootstrapEcotone( classesToResolve: [ - AnnotatedMessageWithSensitiveProperties::class, + MessageWithSensitiveProperties::class, TestEventHandler::class, ], container: [ @@ -185,7 +244,7 @@ classesToResolve: [ $ecotone ->publishEvent( - $messageSent = new AnnotatedMessageWithSensitiveProperties( + $messageSent = new MessageWithSensitiveProperties( sensitiveObject: new TestClass('value', TestEnum::FIRST), sensitiveEnum: TestEnum::FIRST, property: 'value', @@ -198,7 +257,40 @@ classesToResolve: [ self::assertEquals($messageSent, $messageReceiver->receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","property":"value","sensitiveProperty":"sensitive value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->primaryKey) + ); + } + + public function test_protect_events_with_custom_converters(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithCustomConverter::class, + CustomConverter::class, + TestEventHandler::class, + ], + container: [ + new CustomConverter(), + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->publishEvent( + $messageSent = new MessageWithCustomConverter( + sensitiveProperty: 'sensitive value', + nonSensitiveProperty: 'non-sensitive value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"foo":"\"sensitive value\"","bar":"non-sensitive value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['foo'], $this->primaryKey) ); } @@ -230,8 +322,31 @@ classesToResolve: [ self::assertEquals($messageSent, $messageReceiver->receivedMessage()); self::assertEquals( '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', - $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->secondaryKey) + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveObject', 'sensitiveEnum', 'sensitiveProperty'], $this->secondaryKey) + ); + } + + public function test_protect_events_using_attribute_on_property(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithSensitiveProperty::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), ); + + $ecotone + ->publishEvent($messageSent = new MessageWithSensitiveProperty(sensitiveProperty: 'sensitive-value', nonSensitiveProperty: 'non-sensitive-value')) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('{"sensitiveProperty":"sensitive-value","nonSensitiveProperty":"non-sensitive-value"}', $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), ['sensitiveProperty'], $this->primaryKey)); } private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, array $extensionObjects = []): FlowTestSupport @@ -257,15 +372,11 @@ classesToResolve: $classesToResolve, ); } - private function decryptChannelMessagePayload(string $payload, Key $primaryKey): string + private function decryptChannelMessagePayload(string $payload, array $encryptedProperties, Key $primaryKey): string { $payload = json_decode($payload, true); - foreach ($payload as $key => $value) { - try { - $payload[$key] = Crypto::decrypt($value, $primaryKey); - } catch (CryptoException) { // in some cases property is not encrypted - $payload[$key] = $value; - } + foreach ($encryptedProperties as $key) { + $payload[$key] = Crypto::decrypt($payload[$key], $primaryKey); } return json_encode($payload); diff --git a/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesXMLTest.php b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesXMLTest.php new file mode 100644 index 000000000..4882cf64e --- /dev/null +++ b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesXMLTest.php @@ -0,0 +1,276 @@ +primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); + } + + public function test_protect_commands_using_message_annotations(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessage::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') + ); + + $ecotone + ->sendCommand( + $messageSent = new AnnotatedMessage( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + ); + } + + public function test_protect_commands_using_non_default_key(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSecondaryEncryptionKey::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->sendCommand( + $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ) + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->secondaryKey), + ); + } + + public function test_protect_commands_using_property_annotation(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithSensitiveProperties::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->sendCommand( + $messageSent = new MessageWithSensitiveProperties( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + property: 'value', + sensitiveProperty: 'sensitive value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","property":"value","sensitiveProperty":"sensitive value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + ); + } + + public function test_protect_events_using_message_annotations(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessage::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') + ); + + $ecotone + ->publishEvent( + $messageSent = new AnnotatedMessage( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + ); + } + + public function test_protect_events_using_property_annotation(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + MessageWithSensitiveProperties::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->publishEvent( + $messageSent = new MessageWithSensitiveProperties( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + property: 'value', + sensitiveProperty: 'sensitive value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","property":"value","sensitiveProperty":"sensitive value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + ); + } + + public function test_protect_events_using_non_default_key(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSecondaryEncryptionKey::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + ); + + $ecotone + ->publishEvent( + $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ), + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->secondaryKey) + ); + } + + private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, array $extensionObjects = []): FlowTestSupport + { + return EcotoneLite::bootstrapFlowTesting( + classesToResolve: $classesToResolve, + containerOrAvailableServices: $container, + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::CORE_PACKAGE, ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\EncryptAnnotatedMessages']) + ->withDefaultSerializationMediaType(MediaType::APPLICATION_XML) + ->withExtensionObjects( + array_merge( + [ + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ], + $extensionObjects, + ) + ) + ); + } + + private function decryptChannelMessagePayload(string $payload, Key $encryptionKey): string + { + $payload = XmlHelper::xmlToArray($payload); + foreach ($payload as $key => $value) { + try { + $payload[$key] = Crypto::decrypt($value, $encryptionKey); + } catch (CryptoException) { // in some cases property is not encrypted + $payload[$key] = $value; + } + } + + return json_encode($payload); + } +} diff --git a/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationXMLTest.php b/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationXMLTest.php index 739e64dd9..f3850aef2 100644 --- a/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationXMLTest.php +++ b/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationXMLTest.php @@ -4,7 +4,9 @@ use Ecotone\DataProtection\Configuration\ChannelProtectionConfiguration; use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; +use Ecotone\DataProtection\Conversion\XmlHelper; use Ecotone\DataProtection\Encryption\Crypto; +use Ecotone\DataProtection\Encryption\Exception\CryptoException; use Ecotone\DataProtection\Encryption\Key; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; @@ -27,6 +29,7 @@ use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; use Test\Ecotone\DataProtection\TestQueueChannel; +use Throwable; /** * @internal @@ -331,18 +334,11 @@ classesToResolve: [AnnotatedMessage::class], $channelMessagePayload = Crypto::decrypt($channelMessage->getPayload(), $this->secondaryKey); $expectedPayload = << - - - - - - - - + +valuefirstfirstvalue - XML; - self::assertEquals($expectedPayload, $channelMessagePayload); +XML; + self::assertEquals($expectedPayload, $this->decryptChannelMessagePayload($channelMessagePayload, $this->primaryKey)); $messageHeaders = $channelMessage->getHeaders(); self::assertEquals($metadataSent['foo'], Crypto::decrypt($messageHeaders->get('foo'), $this->secondaryKey)); @@ -570,18 +566,11 @@ classesToResolve: [AnnotatedMessage::class], $channelMessagePayload = Crypto::decrypt($channelMessage->getPayload(), $this->secondaryKey); $expectedPayload = << - - - - - - - - + +valuefirstfirstvalue - XML; - self::assertEquals($expectedPayload, $channelMessagePayload); +XML; + self::assertEquals($expectedPayload, $this->decryptChannelMessagePayload($channelMessagePayload, $this->primaryKey)); $messageHeaders = $channelMessage->getHeaders(); self::assertEquals($metadataSent['foo'], Crypto::decrypt($messageHeaders->get('foo'), $this->secondaryKey)); @@ -625,4 +614,22 @@ classesToResolve: $classesToResolve, ]) ); } + + private function decryptChannelMessagePayload(string $payload, Key $encryptionKey): string + { + $payload = XmlHelper::xmlToArray($payload); + foreach ($payload as $key => $value) { + try { + $payload[$key] = Crypto::decrypt($value, $encryptionKey); + } catch (CryptoException) { // in some cases property is not encrypted + $payload[$key] = $value; + } + try { + $payload[$key] = json_decode($payload[$key], true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable) { // in some cases property is not json encoded + } + } + + return XmlHelper::arrayToXml($payload); + } } diff --git a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php index d9982d346..94527799c 100644 --- a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php +++ b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php @@ -7,10 +7,13 @@ use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; use Ecotone\DataProtection\Encryption\Key; use Ecotone\Lite\EcotoneLite; +use Ecotone\Messaging\Config\ConfigurationException; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Support\LicensingException; +use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; +use Test\Ecotone\DataProtection\Fixture\AnnotatedClassWithAnnotatedProperty; /** * @internal @@ -29,4 +32,19 @@ public function test_without_license_module_throws_an_exception(): void ]) ); } + + public function test_using_sensitive_attribute_on_both_class_and_property_throws_an_exception(): void + { + $this->expectException(ConfigurationException::class); + + EcotoneLite::bootstrapFlowTesting( + classesToResolve: [AnnotatedClassWithAnnotatedProperty::class], + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) + ->withExtensionObjects([ + DataProtectionConfiguration::create('primary', Key::createNewRandomKey()), + ]) + ); + } } diff --git a/packages/Ecotone/src/AnnotationFinder/AnnotationFinder.php b/packages/Ecotone/src/AnnotationFinder/AnnotationFinder.php index ddfa87bc4..1c7680cf0 100644 --- a/packages/Ecotone/src/AnnotationFinder/AnnotationFinder.php +++ b/packages/Ecotone/src/AnnotationFinder/AnnotationFinder.php @@ -36,4 +36,9 @@ public function findAnnotatedMethods(string $methodAnnotationClassName): array; */ public function getAttributeForClass(string $className, string $attributeClassName): object; public function findAttributeForClass(string $className, string $attributeClassName): ?object; + + /** + * @return class-string[] + */ + public function findClassesWithAnnotatedProperties(string $annotationClassName): array; } diff --git a/packages/Ecotone/src/AnnotationFinder/FileSystem/FileSystemAnnotationFinder.php b/packages/Ecotone/src/AnnotationFinder/FileSystem/FileSystemAnnotationFinder.php index fb3721937..54227dcd8 100644 --- a/packages/Ecotone/src/AnnotationFinder/FileSystem/FileSystemAnnotationFinder.php +++ b/packages/Ecotone/src/AnnotationFinder/FileSystem/FileSystemAnnotationFinder.php @@ -53,6 +53,11 @@ class FileSystemAnnotationFinder implements AnnotationFinder * @var object[][] */ private array $cachedClassAnnotations = []; + + /** + * @var array> + */ + private array $cachedClassesWithAnnotatedProperties = []; private AnnotationResolver $annotationResolver; private IsAbstract $isAbstractAnnotation; @@ -216,6 +221,29 @@ public function findAnnotatedClasses(string $annotationClassName): array return $classesWithAnnotations; } + public function findClassesWithAnnotatedProperties(string $annotationClassName): array + { + if (isset($this->cachedClassesWithAnnotatedProperties[$annotationClassName])) { + return $this->cachedClassesWithAnnotatedProperties[$annotationClassName]; + } + + $this->cachedClassesWithAnnotatedProperties[$annotationClassName] = []; + foreach ($this->registeredClasses as $class) { + $reflection = new ReflectionClass($class); + foreach ($reflection->getProperties() as $property) { + $propertyAnnotations = $this->annotationResolver->getAnnotationsForProperty($class, $property->getName()); + + foreach ($propertyAnnotations as $propertyAnnotation) { + if ($propertyAnnotation instanceof $annotationClassName) { + $this->cachedClassesWithAnnotatedProperties[$annotationClassName][] = $class; + } + } + } + } + + return $this->cachedClassesWithAnnotatedProperties[$annotationClassName]; + } + private function hasAnnotation(string $className, string $annotationClassNameToFind): bool { $annotationsForClass = $this->getAnnotationsForClass($className); diff --git a/packages/Ecotone/src/AnnotationFinder/InMemory/InMemoryAnnotationFinder.php b/packages/Ecotone/src/AnnotationFinder/InMemory/InMemoryAnnotationFinder.php index 6a723ae42..e9a736059 100644 --- a/packages/Ecotone/src/AnnotationFinder/InMemory/InMemoryAnnotationFinder.php +++ b/packages/Ecotone/src/AnnotationFinder/InMemory/InMemoryAnnotationFinder.php @@ -199,6 +199,11 @@ public function findAnnotatedClasses(string $annotationClassName): array return $classes; } + public function findClassesWithAnnotatedProperties(string $annotationClassName): array + { + return []; + } + /** * @inheritDoc */ diff --git a/packages/Ecotone/src/Messaging/Config/Annotation/ModuleConfiguration/BasicMessagingModule.php b/packages/Ecotone/src/Messaging/Config/Annotation/ModuleConfiguration/BasicMessagingModule.php index 45f94c748..404081043 100644 --- a/packages/Ecotone/src/Messaging/Config/Annotation/ModuleConfiguration/BasicMessagingModule.php +++ b/packages/Ecotone/src/Messaging/Config/Annotation/ModuleConfiguration/BasicMessagingModule.php @@ -49,6 +49,7 @@ use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\NullableMessageChannel; use Ecotone\Modelling\MessageHandling\MetadataPropagator\MessageHeadersPropagatorInterceptor; +use Ramsey\Uuid\UuidInterface; #[ModuleAnnotation] /** @@ -93,7 +94,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO $messagingConfiguration->registerMessageChannel(SimpleMessageChannelBuilder::createPublishSubscribeChannel(MessageHeaders::ERROR_CHANNEL)); $messagingConfiguration->registerMessageChannel(SimpleMessageChannelBuilder::createPublishSubscribeChannel(OnConsumerStop::CONSUMER_STOP_CHANNEL_NAME)); $messagingConfiguration->registerMessageChannel(SimpleMessageChannelBuilder::create(NullableMessageChannel::CHANNEL_NAME, NullableMessageChannel::create())); - if (interface_exists(\Ramsey\Uuid\UuidInterface::class)) { + if (interface_exists(UuidInterface::class)) { $messagingConfiguration->registerConverter(new Definition(UuidToStringConverter::class)); $messagingConfiguration->registerConverter(new Definition(StringToUuidConverter::class)); } diff --git a/packages/Ecotone/src/Messaging/Handler/ClassPropertyDefinition.php b/packages/Ecotone/src/Messaging/Handler/ClassPropertyDefinition.php index 43396983b..e28ad16ff 100644 --- a/packages/Ecotone/src/Messaging/Handler/ClassPropertyDefinition.php +++ b/packages/Ecotone/src/Messaging/Handler/ClassPropertyDefinition.php @@ -109,6 +109,17 @@ public function getAnnotation(Type $annotationClass): object throw InvalidArgumentException::create("Annotation {$annotationClass} was not found for property {$this}"); } + public function findAnnotation(Type $annotationClass): ?object + { + foreach ($this->annotations as $annotation) { + if ($annotationClass->accepts($annotation)) { + return $annotation; + } + } + + return null; + } + /** * @return string */