diff --git a/.github/workflows/split-testing.yml b/.github/workflows/split-testing.yml index 40f9daed8..63c7d295c 100644 --- a/.github/workflows/split-testing.yml +++ b/.github/workflows/split-testing.yml @@ -209,8 +209,8 @@ jobs: overall_exit_code=1 fi - # Run SQLite pass (skip for PdoEventSourcing - SQLite event store not supported) - if [[ "$dir" != *"PdoEventSourcing"* ]]; then + # Run SQLite pass (skip for PdoEventSourcing and DataProtection - SQLite event store not supported) + if [[ ! "$dir" =~ (PdoEventSourcing|DataProtection) ]]; then local sqlite_db="/tmp/ecotone_${slug}_test.db" local sqlite_db_secondary="/tmp/ecotone_${slug}_test_b.db" # Use 4 slashes for absolute paths: sqlite:// + / + /path = sqlite:////path diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index 31974d4f5..19729a1df 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -43,6 +43,7 @@ "paragonie/random_compat": "^2.0" }, "require-dev": { + "ecotone/pdo-event-sourcing": "~1.300.2", "phpunit/phpunit": "^11.0", "phpstan/phpstan": "^2.1", "wikimedia/composer-merge-plugin": "^2.1" diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php index b4dd1f2bd..b443b98a5 100644 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -8,7 +8,7 @@ use Attribute; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PARAMETER)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] class Sensitive { } diff --git a/packages/DataProtection/src/Attribute/WithSensitiveHeader.php b/packages/DataProtection/src/Attribute/WithSensitiveHeader.php deleted file mode 100644 index 24b50d951..000000000 --- a/packages/DataProtection/src/Attribute/WithSensitiveHeader.php +++ /dev/null @@ -1,17 +0,0 @@ -channelName; - } - - public function messageEncryptionConfig(): MessageEncryptionConfig - { - return new MessageEncryptionConfig($this->encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders); + return self::create($this->channelName, $encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders); } public function withSensitivePayload(bool $isPayloadSensitive): self { - $config = clone $this; - $config->isPayloadSensitive = $isPayloadSensitive; - - return $config; + return self::create($this->channelName, $this->encryptionKey, $isPayloadSensitive, $this->sensitiveHeaders); } public function withSensitiveHeader(string $sensitiveHeader): self { - $config = clone $this; - $config->sensitiveHeaders[] = $sensitiveHeader; - - return $config; + return self::create($this->channelName, $this->encryptionKey, $this->isPayloadSensitive, array_merge($this->sensitiveHeaders, [$sensitiveHeader])); } } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index dfff37927..6992058d1 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -7,19 +7,17 @@ namespace Ecotone\DataProtection\Configuration; -use Ecotone\AnnotationFinder\AnnotatedMethod; use Ecotone\AnnotationFinder\AnnotationFinder; use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\WithEncryptionKey; -use Ecotone\DataProtection\Attribute\WithSensitiveHeader; +use Ecotone\DataProtection\Conversion\DataProtectionConversionServiceDecorator; use Ecotone\DataProtection\Encryption\Key; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; +use Ecotone\DataProtection\Protector\ChannelProtector; +use Ecotone\DataProtection\Protector\DataProtector; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; -use Ecotone\Messaging\Attribute\Parameter\Header; -use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Channel\MessageChannelWithSerializationBuilder; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule; @@ -28,12 +26,11 @@ use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ModuleReferenceSearchService; +use Ecotone\Messaging\Handler\ClassPropertyDefinition; use Ecotone\Messaging\Handler\InterfaceToCallRegistry; use Ecotone\Messaging\Handler\Type; use Ecotone\Messaging\Support\Assert; use Ecotone\Messaging\Support\LicensingException; -use Ecotone\Modelling\Attribute\CommandHandler; -use Ecotone\Modelling\Attribute\EventHandler; use stdClass; #[ModuleAnnotation] @@ -43,19 +40,17 @@ final class DataProtectionModule extends NoExternalConfigurationModule final public const KEY_SERVICE_ID_FORMAT = 'ecotone.encryption.key.%s'; /** - * @param array $encryptionConfigs + * @param array $dataProtectorConfigs */ - public function __construct(private array $encryptionConfigs) + public function __construct(private array $dataProtectorConfigs) { } public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static { - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry); - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(CommandHandler::class), $encryptionConfigs, $interfaceToCallRegistry); - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(EventHandler::class), $encryptionConfigs, $interfaceToCallRegistry); - - return new self($encryptionConfigs); + return new self( + dataProtectorConfigs: self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry) + ); } public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void @@ -82,58 +77,59 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO ); } - $channelEncryptorReferences = $messageEncryptorReferences = []; + $channelProtectorReferences = []; foreach ($channelProtectionConfigurations as $channelProtectionConfiguration) { - Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName()), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName())); + Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName)); - $encryptionConfig = $channelProtectionConfiguration->messageEncryptionConfig(); $messagingConfiguration->registerServiceDefinition( - id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName()), + id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName), definition: new Definition( - MessageEncryptor::class, + ChannelProtector::class, [ - Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))), - $encryptionConfig->isPayloadSensitive, - $encryptionConfig->sensitiveHeaders, + Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $dataProtectionConfiguration->keyName($channelProtectionConfiguration->encryptionKey))), + $channelProtectionConfiguration->isPayloadSensitive, + $channelProtectionConfiguration->sensitiveHeaders, ], ) ); - $channelEncryptorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id); + $channelProtectorReferences[$channelProtectionConfiguration->channelName] = Reference::to($id); } - foreach ($this->encryptionConfigs as $messageClass => $encryptionConfig) { - $messagingConfiguration->registerServiceDefinition( - id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $messageClass), - definition: new Definition( - MessageEncryptor::class, - [ - Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))), - $encryptionConfig->isPayloadSensitive, - $encryptionConfig->sensitiveHeaders, - ], - ) + $conversionServiceDecorator = new Definition(DataProtectionConversionServiceDecorator::class); + foreach ($this->dataProtectorConfigs as $protectorConfig) { + $conversionServiceDecorator->addMethodCall( + 'withDataProtector', + [ + $protectorConfig->supportedType, + new Definition( + DataProtector::class, + [ + Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))), + $protectorConfig->sensitiveProperties, + $protectorConfig->scalarProperties, + ], + ), + ] ); - $messageEncryptorReferences[$messageClass] = Reference::to($id); } + $messagingConfiguration->registerConversionServiceDecorator($conversionServiceDecorator); foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { - if (! $pollableMessageChannel->isPollable()) { + if (! $pollableMessageChannel->isPollable() || ! array_key_exists($pollableMessageChannel->getMessageChannelName(), $channelProtectorReferences)) { continue; } $messagingConfiguration->registerChannelInterceptor( new OutboundEncryptionChannelBuilder( relatedChannel: $pollableMessageChannel->getMessageChannelName(), - channelEncryptorReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, - messageEncryptorReferences: $messageEncryptorReferences, + channelProtectorReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()], ) ); $messagingConfiguration->registerChannelInterceptor( new OutboundDecryptionChannelBuilder( relatedChannel: $pollableMessageChannel->getMessageChannelName(), - channelEncryptionReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, - messageEncryptionReferences: $messageEncryptorReferences, + channelProtectorReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()], ) ); } @@ -154,53 +150,29 @@ public function getModulePackageName(): string return ModulePackageList::DATA_PROTECTION_PACKAGE; } - private static function resolveEncryptionConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array + private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array { - $encryptionConfigs = []; + $dataEncryptorConfigs = []; foreach ($sensitiveMessages as $message) { - $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($message)); + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message)); $encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); - $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) ?? []); - - $encryptionConfigs[$message] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders); - } - return $encryptionConfigs; - } - - private static function resolveEncryptionConfigsFromAnnotatedMethods(array $annotatedMethods, array $encryptionConfigs, InterfaceToCallRegistry $interfaceToCallRegistry): array - { - /** @var AnnotatedMethod $method */ - foreach ($annotatedMethods as $method) { - $methodDefinition = $interfaceToCallRegistry->getFor($method->getClassName(), $method->getMethodName()); - $payload = $methodDefinition->getFirstParameter(); - - if ( - $payload->hasAnnotation(Header::class) - || $payload->hasAnnotation(Headers::class) - || $payload->hasAnnotation(Reference::class) - || array_key_exists($payload->getTypeHint(), $encryptionConfigs) - ) { - continue; + $sensitiveProperties = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class)); + if ($sensitiveProperties === []) { + $sensitiveProperties = $classDefinition->getProperties(); } - $isPayloadSensitive = $payload->hasAnnotation(Sensitive::class); - if (! $isPayloadSensitive) { - continue; - } + $scalarProperties = array_values(array_filter($sensitiveProperties, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar())); - $encryptionKey = $payload->findSingleAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); - $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) ?? []); - foreach ($methodDefinition->getInterfaceParameters() as $parameter) { - if ($parameter->hasAnnotation(Header::class) && $parameter->hasAnnotation(Sensitive::class)) { - $sensitiveHeaders[] = $parameter->getName(); - } - } + $mapper = static fn (ClassPropertyDefinition $property): string => $property->getName(); + + $sensitiveProperties = array_map($mapper, $sensitiveProperties); + $scalarProperties = array_map($mapper, $scalarProperties); - $encryptionConfigs[$payload->getTypeHint()] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders); + $dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties); } - return $encryptionConfigs; + return $dataEncryptorConfigs; } private function verifyLicense(Configuration $messagingConfiguration): void diff --git a/packages/DataProtection/src/Configuration/DataProtectorConfig.php b/packages/DataProtection/src/Configuration/DataProtectorConfig.php new file mode 100644 index 000000000..99f459fe6 --- /dev/null +++ b/packages/DataProtection/src/Configuration/DataProtectorConfig.php @@ -0,0 +1,31 @@ + $sensitiveProperties + */ + public function __construct( + public Type $supportedType, + public ?string $encryptionKey, + public array $sensitiveProperties, + public array $scalarProperties, + ) { + Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings'); + Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings'); + } + + public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string + { + return $dataProtectionConfiguration->keyName($this->encryptionKey); + } +} diff --git a/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php b/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php deleted file mode 100644 index a768afe6c..000000000 --- a/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - $sensitiveHeaders - */ - public function __construct( - public ?string $encryptionKey, - public bool $isPayloadSensitive, - public array $sensitiveHeaders, - ) { - Assert::allStrings($this->sensitiveHeaders, 'Sensitive Headers should be array of strings'); - } - - public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string - { - return $dataProtectionConfiguration->keyName($this->encryptionKey); - } -} diff --git a/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php b/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php new file mode 100644 index 000000000..57c7a851d --- /dev/null +++ b/packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php @@ -0,0 +1,85 @@ +dataProtectors[$type->toString()] = $dataProtector; + } + + public function decorate(ConversionService $conversionService): void + { + $this->innerConversionService = $conversionService; + } + + public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType, Type $targetPHPType, MediaType $targetMediaType) + { + if ($this->expectProtectedData($targetPHPType)) { + $source = $this->decrypt($source, $this->getDataProtector($targetPHPType), $sourcePHPType->isCompatibleWith(Type::array())); + } + + $source = $this->innerConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType); + + if ($this->expectProtectedData($sourcePHPType)) { + $source = $this->encrypt($source, $this->getDataProtector($sourcePHPType), $targetPHPType->isCompatibleWith(Type::array())); + } + + return $source; + } + + public function canConvert(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool + { + return $this->innerConversionService->canConvert($sourceType, $sourceMediaType, $targetType, $targetMediaType); + } + + private function expectProtectedData(Type $type): bool + { + return array_key_exists($type->toString(), $this->dataProtectors); + } + + private function getDataProtector(Type $targetPHPType): DataProtector + { + return $this->dataProtectors[$targetPHPType->toString()]; + } + + private function decrypt($source, DataProtector $dataProtector, bool $handleArray) + { + if ($handleArray) { + $source = json_encode($source); + } + + $source = $dataProtector->decrypt($source); + + return $handleArray ? json_decode($source, true) : $source; + } + + private function encrypt($source, DataProtector $dataProtector, bool $handleArray) + { + if ($handleArray) { + $source = json_encode($source); + } + + $source = $dataProtector->encrypt($source); + + return $handleArray ? json_decode($source, true) : $source; + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php index d45d76ee5..892ce8110 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -14,11 +14,8 @@ readonly class OutboundDecryptionChannelBuilder implements ChannelInterceptorBuilder { - public function __construct( - private string $relatedChannel, - private ?Reference $channelEncryptionReference, - private array $messageEncryptionReferences, - ) { + public function __construct(private string $relatedChannel, private Reference $channelProtectorReference) + { } public function relatedChannelName(): string @@ -36,8 +33,7 @@ public function compile(MessagingContainerBuilder $builder): Definition return new Definition( OutboundDecryptionChannelInterceptor::class, [ - $this->channelEncryptionReference, - $this->messageEncryptionReferences, + $this->channelProtectorReference, ] ); } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index 5540564b9..db78833d9 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -6,51 +6,24 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; +use Ecotone\DataProtection\Protector\ChannelProtector; use Ecotone\Messaging\Channel\AbstractChannelInterceptor; use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; -use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\Assert; class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor { - /** - * @param array $messageEncryptors - */ - public function __construct( - private readonly ?MessageEncryptor $channelEncryptor, - private readonly array $messageEncryptors, - ) { - Assert::allInstanceOfType($this->messageEncryptors, MessageEncryptor::class); + public function __construct(private readonly ChannelProtector $channelProtector) + { } public function postReceive(Message $message, MessageChannel $messageChannel): ?Message { - if (! $message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { - return $message; - } - - if ($messageEncryptor = $this->findMessageEncryptor($message)) { - return $messageEncryptor->decrypt($message); - } - - if ($this->channelEncryptor) { - return $this->channelEncryptor->decrypt($message); + if ($message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { + return $this->channelProtector->decrypt($message); } return $message; } - - private function findMessageEncryptor(Message $message): ?MessageEncryptor - { - if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return null; - } - - $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - - return $this->messageEncryptors[$type] ?? null; - } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php index d05db8157..5baf80a54 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -14,11 +14,8 @@ readonly class OutboundEncryptionChannelBuilder implements ChannelInterceptorBuilder { - public function __construct( - private string $relatedChannel, - private ?Reference $channelEncryptorReference, - private array $messageEncryptorReferences, - ) { + public function __construct(private string $relatedChannel, private Reference $channelProtectorReference) + { } public function relatedChannelName(): string @@ -36,8 +33,7 @@ public function compile(MessagingContainerBuilder $builder): Definition return new Definition( OutboundEncryptionChannelInterceptor::class, [ - $this->channelEncryptorReference, - $this->messageEncryptorReferences, + $this->channelProtectorReference, ] ); } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 27da47cb2..1736a1b75 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -6,51 +6,24 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; +use Ecotone\DataProtection\Protector\ChannelProtector; use Ecotone\Messaging\Channel\AbstractChannelInterceptor; use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; -use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\Assert; class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor { - /** - * @param array $messageEncryptors - */ - public function __construct( - private readonly ?MessageEncryptor $channelEncryptor, - private readonly array $messageEncryptors, - ) { - Assert::allInstanceOfType($this->messageEncryptors, MessageEncryptor::class); + public function __construct(private readonly ?ChannelProtector $channelProtector) + { } public function preSend(Message $message, MessageChannel $messageChannel): ?Message { - if (! $message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { - return $message; - } - - if ($messageEncryptor = $this->findMessageEncryptor($message)) { - return $messageEncryptor->encrypt($message); - } - - if ($this->channelEncryptor) { - return $this->channelEncryptor->encrypt($message); + if ($message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { + return $this->channelProtector->encrypt($message); } return $message; } - - private function findMessageEncryptor(Message $message): ?MessageEncryptor - { - if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return null; - } - - $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - - return $this->messageEncryptors[$type] ?? null; - } } diff --git a/packages/DataProtection/src/MessageEncryption/MessageEncryptor.php b/packages/DataProtection/src/Protector/ChannelProtector.php similarity index 95% rename from packages/DataProtection/src/MessageEncryption/MessageEncryptor.php rename to packages/DataProtection/src/Protector/ChannelProtector.php index 619583753..d7f9e61ed 100644 --- a/packages/DataProtection/src/MessageEncryption/MessageEncryptor.php +++ b/packages/DataProtection/src/Protector/ChannelProtector.php @@ -4,7 +4,7 @@ * licence Enterprise */ -namespace Ecotone\DataProtection\MessageEncryption; +namespace Ecotone\DataProtection\Protector; use Ecotone\DataProtection\Encryption\Crypto; use Ecotone\DataProtection\Encryption\Key; @@ -12,7 +12,7 @@ use Ecotone\Messaging\Support\Assert; use Ecotone\Messaging\Support\MessageBuilder; -readonly class MessageEncryptor +readonly class ChannelProtector { public function __construct( private Key $encryptionKey, diff --git a/packages/DataProtection/src/Protector/DataProtector.php b/packages/DataProtection/src/Protector/DataProtector.php new file mode 100644 index 000000000..7af2a22c8 --- /dev/null +++ b/packages/DataProtection/src/Protector/DataProtector.php @@ -0,0 +1,59 @@ +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] = base64_encode(Crypto::encrypt($source[$property], $this->encryptionKey)); + } + + return json_encode($source); + } + + public function decrypt(string $source): string + { + $source = json_decode($source, true); + + foreach ($this->sensitiveProperties as $property) { + if (! array_key_exists($property, $source)) { + continue; + } + + $source[$property] = Crypto::decrypt(base64_decode($source[$property]), $this->encryptionKey); + + if (! in_array($property, $this->scalarProperties, true)) { + $source[$property] = json_decode($source[$property], true); + } + } + + return json_encode($source); + } +} diff --git a/packages/DataProtection/tests/Fixture/AnnotatedMessage.php b/packages/DataProtection/tests/Fixture/AnnotatedMessage.php index 6dc0962b4..a8a9aa57b 100644 --- a/packages/DataProtection/tests/Fixture/AnnotatedMessage.php +++ b/packages/DataProtection/tests/Fixture/AnnotatedMessage.php @@ -8,9 +8,9 @@ class AnnotatedMessage { public function __construct( - public TestClass $class, - public TestEnum $enum, - public string $argument + public TestClass $sensitiveObject, + public TestEnum $sensitiveEnum, + public string $sensitiveProperty ) { } } diff --git a/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSecondaryEncryptionKey.php index 8a32559ab..72dea609b 100644 --- a/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSecondaryEncryptionKey.php +++ b/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSecondaryEncryptionKey.php @@ -10,9 +10,9 @@ class AnnotatedMessageWithSecondaryEncryptionKey { public function __construct( - public TestClass $class, - public TestEnum $enum, - public string $argument + public TestClass $sensitiveObject, + public TestEnum $sensitiveEnum, + public string $sensitiveProperty ) { } } diff --git a/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSensitiveHeaders.php b/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSensitiveHeaders.php deleted file mode 100644 index 97e11c5b0..000000000 --- a/packages/DataProtection/tests/Fixture/AnnotatedMessageWithSensitiveHeaders.php +++ /dev/null @@ -1,20 +0,0 @@ -withReceived($message, $headers); } - #[CommandHandler(endpointId: 'test.EncryptAnnotatedMessages.commandHandler.AnnotatedMessageWithSensitiveHeaders')] - public function handleAnnotatedMessageWithSensitiveHeaders( - #[Payload] AnnotatedMessageWithSensitiveHeaders $message, + #[CommandHandler(endpointId: 'test.EncryptAnnotatedMessages.commandHandler.AnnotatedMessageWithSensitiveProperties')] + public function handleAnnotatedMessageWithSensitiveProperties( + #[Payload] AnnotatedMessageWithSensitiveProperties $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 860aa02b4..7f6bd734f 100644 --- a/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/EncryptAnnotatedMessages/TestEventHandler.php @@ -9,7 +9,7 @@ use Ecotone\Modelling\Attribute\EventHandler; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSecondaryEncryptionKey; -use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveHeaders; +use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveProperties; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; #[Asynchronous('test')] @@ -33,9 +33,9 @@ public function handleAnnotatedMessageWithSecondaryEncryptionKey( $messageReceiver->withReceived($message, $headers); } - #[EventHandler(endpointId: 'test.EncryptAnnotatedMessages.eventHandler.AnnotatedMessageWithSensitiveHeaders')] - public function handleAnnotatedMessageWithSensitiveHeaders( - #[Payload] AnnotatedMessageWithSensitiveHeaders $message, + #[EventHandler(endpointId: 'test.EncryptAnnotatedMessages.eventHandler.AnnotatedMessageWithSensitiveProperties')] + public function handleAnnotatedMessageWithSensitiveProperties( + #[Payload] AnnotatedMessageWithSensitiveProperties $message, #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php deleted file mode 100644 index 5ffa4a97d..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php +++ /dev/null @@ -1,29 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php deleted file mode 100644 index b5013ba96..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php +++ /dev/null @@ -1,28 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedMethodWithoutPayload.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedMethodWithoutPayload.php deleted file mode 100644 index 8137cebea..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedMethodWithoutPayload.php +++ /dev/null @@ -1,25 +0,0 @@ -withReceived(null, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayload.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayload.php deleted file mode 100644 index f41786ac7..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayload.php +++ /dev/null @@ -1,28 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadAndHeader.php deleted file mode 100644 index ccb45c1a9..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadAndHeader.php +++ /dev/null @@ -1,27 +0,0 @@ -withReceived($message, ['foo' => $foo, 'bar' => $bar]); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php deleted file mode 100644 index 8b2936d37..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php +++ /dev/null @@ -1,29 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php deleted file mode 100644 index cbb62c20d..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php +++ /dev/null @@ -1,29 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php deleted file mode 100644 index d818107b9..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php +++ /dev/null @@ -1,28 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedMethodWithoutPayload.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedMethodWithoutPayload.php deleted file mode 100644 index 2242141e0..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedMethodWithoutPayload.php +++ /dev/null @@ -1,25 +0,0 @@ -withReceived(null, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayload.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayload.php deleted file mode 100644 index ff445f345..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayload.php +++ /dev/null @@ -1,28 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadAndHeader.php deleted file mode 100644 index 5d860339a..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadAndHeader.php +++ /dev/null @@ -1,28 +0,0 @@ -withReceived($message, ['foo' => $foo, 'bar' => $bar]); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php deleted file mode 100644 index 954058533..000000000 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithAnnotatedEndpoint/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php +++ /dev/null @@ -1,29 +0,0 @@ -withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestCommandHandler.php index 946ec3e84..9bdd583be 100644 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestCommandHandler.php +++ b/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestCommandHandler.php @@ -6,6 +6,7 @@ use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; +use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; use Test\Ecotone\DataProtection\Fixture\SomeMessage; @@ -28,4 +29,13 @@ public function withoutPayload( ): void { $messageReceiver->withReceived(null, $headers); } + + #[CommandHandler(endpointId: 'test.EncryptMessagesWithChannelConfiguration.commandHandler.handleAnnotatedMessage')] + public function handleAnnotatedMessage( + AnnotatedMessage $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } } diff --git a/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestEventHandler.php b/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestEventHandler.php index 1a39a8d6a..6ec23d74c 100644 --- a/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/EncryptMessagesWithChannelConfiguration/TestEventHandler.php @@ -6,6 +6,7 @@ use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\EventHandler; +use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; use Test\Ecotone\DataProtection\Fixture\SomeMessage; @@ -13,7 +14,7 @@ class TestEventHandler { #[EventHandler(endpointId: 'test.EncryptMessagesWithChannelConfiguration.eventHandler.withPayload')] - public function handleFullyObfuscatedMessage( + public function withPayload( SomeMessage $message, #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, @@ -22,10 +23,19 @@ public function handleFullyObfuscatedMessage( } #[EventHandler(listenTo: 'event', endpointId: 'test.EncryptMessagesWithChannelConfiguration.eventHandler.withoutPayload')] - public function handleRoutingKey( + public function withoutPayload( #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { $messageReceiver->withReceived(null, $headers); } + + #[EventHandler(endpointId: 'test.EncryptMessagesWithChannelConfiguration.eventHandler.handleAnnotatedMessage')] + public function handleAnnotatedMessage( + AnnotatedMessage $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } } diff --git a/packages/DataProtection/tests/Fixture/PersistingSensitiveEvents/AggregateEvent.php b/packages/DataProtection/tests/Fixture/PersistingSensitiveEvents/AggregateEvent.php new file mode 100644 index 000000000..a4d6fceaa --- /dev/null +++ b/packages/DataProtection/tests/Fixture/PersistingSensitiveEvents/AggregateEvent.php @@ -0,0 +1,21 @@ +id = $event->id; + } +} diff --git a/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php index f0a586031..bd3936928 100644 --- a/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php @@ -2,9 +2,9 @@ namespace Test\Ecotone\DataProtection\Integration; -use Ecotone\DataProtection\Configuration\ChannelProtectionConfiguration; use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; use Ecotone\DataProtection\Encryption\Crypto; +use Ecotone\DataProtection\Encryption\Exception\CryptoException; use Ecotone\DataProtection\Encryption\Key; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; @@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSecondaryEncryptionKey; -use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveHeaders; +use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSensitiveProperties; use Test\Ecotone\DataProtection\Fixture\EncryptAnnotatedMessages\TestCommandHandler; use Test\Ecotone\DataProtection\Fixture\EncryptAnnotatedMessages\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; @@ -57,33 +57,19 @@ classesToResolve: [ $ecotone ->sendCommand( $messageSent = new AnnotatedMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + 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 @@ -98,89 +84,57 @@ classesToResolve: [ $messageReceiver = new MessageReceiver(), ], messageChannel: $channel = TestQueueChannel::create('test'), - extensionObjects: [ - ChannelProtectionConfiguration::create('test', encryptionKey: 'primary'), - ] ); $ecotone ->sendCommand( $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ) ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->secondaryKey), + ); } - public function test_protect_commands_with_sensitive_headers(): void + public function test_protect_commands_using_property_annotation(): void { $ecotone = $this->bootstrapEcotone( classesToResolve: [ - AnnotatedMessageWithSensitiveHeaders::class, + AnnotatedMessageWithSensitiveProperties::class, TestCommandHandler::class, ], container: [ new TestCommandHandler(), $messageReceiver = new MessageReceiver(), ], - messageChannel: $channel = TestQueueChannel::create('test') + messageChannel: $channel = TestQueueChannel::create('test'), ); $ecotone ->sendCommand( - $messageSent = new AnnotatedMessageWithSensitiveHeaders( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', + $messageSent = new AnnotatedMessageWithSensitiveProperties( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + property: 'value', + sensitiveProperty: 'sensitive value', ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - self::assertArrayNotHasKey('fos', $receivedHeaders); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - self::assertFalse($messageHeaders->containsKey('fos')); + 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 @@ -200,40 +154,26 @@ classesToResolve: [ $ecotone ->publishEvent( $messageSent = new AnnotatedMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + self::assertEquals( + '{"sensitiveObject":"{\"argument\":\"value\",\"enum\":\"first\"}","sensitiveEnum":"\"first\"","sensitiveProperty":"value"}', + $this->decryptChannelMessagePayload($channel->getLastSentMessage()->getPayload(), $this->primaryKey) + ); } - public function test_protect_events_using_non_default_key(): void + public function test_protect_events_using_property_annotation(): void { $ecotone = $this->bootstrapEcotone( classesToResolve: [ - AnnotatedMessageWithSecondaryEncryptionKey::class, + AnnotatedMessageWithSensitiveProperties::class, TestEventHandler::class, ], container: [ @@ -241,89 +181,57 @@ classesToResolve: [ $messageReceiver = new MessageReceiver(), ], messageChannel: $channel = TestQueueChannel::create('test'), - extensionObjects: [ - ChannelProtectionConfiguration::create('test', encryptionKey: 'primary'), - ] ); $ecotone ->publishEvent( - $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', + $messageSent = new AnnotatedMessageWithSensitiveProperties( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + property: 'value', + sensitiveProperty: 'sensitive value', ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + 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_with_sensitive_headers(): void + public function test_protect_events_using_non_default_key(): void { $ecotone = $this->bootstrapEcotone( classesToResolve: [ - AnnotatedMessageWithSensitiveHeaders::class, + AnnotatedMessageWithSecondaryEncryptionKey::class, TestEventHandler::class, ], container: [ new TestEventHandler(), $messageReceiver = new MessageReceiver(), ], - messageChannel: $channel = TestQueueChannel::create('test') + messageChannel: $channel = TestQueueChannel::create('test'), ); $ecotone ->publishEvent( - $messageSent = new AnnotatedMessageWithSensitiveHeaders( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', + $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] ) ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) ; - $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - self::assertArrayNotHasKey('fos', $receivedHeaders); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - self::assertFalse($messageHeaders->containsKey('fos')); + 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 @@ -348,4 +256,18 @@ classesToResolve: $classesToResolve, ) ); } + + private function decryptChannelMessagePayload(string $payload, Key $primaryKey): string + { + $payload = json_decode($payload, true); + foreach ($payload as $key => $value) { + try { + $payload[$key] = Crypto::decrypt(base64_decode($value), $primaryKey); + } catch (CryptoException) { // in some cases property is not encrypted + $payload[$key] = $value; + } + } + + return json_encode($payload); + } } diff --git a/packages/DataProtection/tests/Integration/EncryptMessagesWithAnnotatedEndpointTest.php b/packages/DataProtection/tests/Integration/EncryptMessagesWithAnnotatedEndpointTest.php deleted file mode 100644 index f145cfcbd..000000000 --- a/packages/DataProtection/tests/Integration/EncryptMessagesWithAnnotatedEndpointTest.php +++ /dev/null @@ -1,496 +0,0 @@ -primaryKey = Key::createNewRandomKey(); - $this->secondaryKey = Key::createNewRandomKey(); - } - - public function test_protect_commands_using_annotated_endpoint_with_already_annotated_message(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - AnnotatedMessage::class, - CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->sendCommand( - $messageSent = new AnnotatedMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protect_commands_using_annotated_endpoint_and_secondary_encryption_key(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->sendCommand( - $messageSent = new SomeMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->secondaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->secondaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protecting_commands_using_annotated_endpoint_without_payload_will_use_channel_obfuscator(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - CommandHandlerWithAnnotatedMethodWithoutPayload::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new CommandHandlerWithAnnotatedMethodWithoutPayload(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - extensionObjects: [ - ChannelProtectionConfiguration::create('test')->withSensitiveHeader('foo')->withSensitiveHeader('bar'), - ] - ); - - $ecotone - ->sendCommandWithRoutingKey( - routingKey: 'command', - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('[]', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protect_commands_using_annotated_endpoint_with_annotated_payload_and_header(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - CommandHandlerWithAnnotatedPayloadAndHeader::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new CommandHandlerWithAnnotatedPayloadAndHeader(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->sendCommand( - $messageSent = new SomeMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - } - - public function test_protect_commands_using_annotated_endpoint_without_payload(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - CommandHandlerWithAnnotatedMethodWithoutPayload::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new CommandHandlerWithAnnotatedMethodWithoutPayload(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->sendCommandWithRoutingKey( - routingKey: 'command', - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('[]', $channelMessage->getPayload()); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protecting_events_using_annotated_endpoint_with_already_annotated_message(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - AnnotatedMessage::class, - EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->publishEvent( - $messageSent = new AnnotatedMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protect_events_using_annotated_endpoint_and_secondary_encryption_key(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->publishEvent( - $messageSent = new SomeMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->secondaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->secondaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protecting_events_using_annotated_endpoint_without_payload_will_use_channel_obfuscator(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - EventHandlerWithAnnotatedMethodWithoutPayload::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new EventHandlerWithAnnotatedMethodWithoutPayload(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - extensionObjects: [ - ChannelProtectionConfiguration::create('test')->withSensitiveHeader('foo')->withSensitiveHeader('bar'), - ] - ); - - $ecotone - ->publishEventWithRoutingKey( - routingKey: 'event', - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('[]', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - public function test_protect_events_using_annotated_endpoint_with_annotated_payload_and_header(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - EventHandlerWithAnnotatedPayloadAndHeader::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new EventHandlerWithAnnotatedPayloadAndHeader(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->publishEvent( - $messageSent = new SomeMessage( - class: new TestClass('value', TestEnum::FIRST), - enum: TestEnum::FIRST, - argument: 'value', - ), - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); - self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->secondaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->secondaryKey)); - } - - public function test_protect_events_using_annotated_method_without_payload(): void - { - $ecotone = $this->bootstrapEcotone( - classesToResolve: [ - EventHandlerWithAnnotatedMethodWithoutPayload::class, - ], - container: [ - $messageReceiver = new MessageReceiver(), - new EventHandlerWithAnnotatedMethodWithoutPayload(), - ], - messageChannel: $channel = TestQueueChannel::create('test'), - ); - - $ecotone - ->publishEventWithRoutingKey( - routingKey: 'event', - metadata: $metadataSent = [ - 'foo' => 'secret-value', - 'bar' => 'even-more-secret-value', - 'baz' => 'non-sensitive-value', - ] - ) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); - self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); - self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); - - $channelMessage = $channel->getLastSentMessage(); - $messageHeaders = $channelMessage->getHeaders(); - - self::assertEquals('[]', $channelMessage->getPayload()); - self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); - self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); - self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); - } - - 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::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) - ->withExtensionObjects( - array_merge([ - DataProtectionConfiguration::create('primary', $this->primaryKey) - ->withKey('secondary', $this->secondaryKey), - SimpleMessageChannelBuilder::create('test', $messageChannel), - JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), - ], $extensionObjects) - ) - ); - } -} diff --git a/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationTest.php b/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationTest.php index 16ba385b9..dc500ab19 100644 --- a/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationTest.php +++ b/packages/DataProtection/tests/Integration/EncryptMessagesWithChannelConfigurationTest.php @@ -18,6 +18,7 @@ use Ecotone\Messaging\Support\InvalidArgumentException; use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; +use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\EncryptMessagesWithChannelConfiguration\TestCommandHandler; use Test\Ecotone\DataProtection\Fixture\EncryptMessagesWithChannelConfiguration\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; @@ -246,6 +247,62 @@ public function test_protect_commands_using_channel_configuration_with_default_e self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_protect_annotated_commands_using_channel_configuration_with_default_encryption_key_and_routing_key_with_no_sensitive_payload(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withEncryptionKey('secondary') + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone( + channelProtectionConfiguration: $channelProtectionConfiguration, + messageChannel: $channel = TestQueueChannel::create('test'), + receivedMessage: $messageReceiver = new MessageReceiver(), + classesToResolve: [AnnotatedMessage::class], + ); + + $ecotone + ->sendCommand( + command: $messageSent = new AnnotatedMessage( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + // assert channel message + $channelMessage = $channel->getLastSentMessage(); + $channelMessagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); + $channelMessagePayload = json_decode($channelMessagePayload, true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveObject']), $this->primaryKey)); + self::assertEquals('"first"', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveEnum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveProperty']), $this->primaryKey)); + $messageHeaders = $channelMessage->getHeaders(); + + self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->secondaryKey)); + self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->secondaryKey)); + self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + self::assertFalse($messageHeaders->containsKey('fos'), 'encryption should not add additional headers'); + + + // assert received message + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + $receivedHeaders = $messageReceiver->receivedHeaders(); + self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); + self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); + self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); + } + public function test_protect_events_using_channel_configuration_with_default_encryption_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') @@ -391,9 +448,70 @@ public function test_protecting_messages_with_non_pollable_channel_is_not_possib ; } - private function bootstrapEcotone(ChannelProtectionConfiguration $channelProtectionConfiguration, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport + public function test_protect_annotated_events_using_channel_configuration_with_default_encryption_key_and_routing_key_with_no_sensitive_payload(): void { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withEncryptionKey('secondary') + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone( + channelProtectionConfiguration: $channelProtectionConfiguration, + messageChannel: $channel = TestQueueChannel::create('test'), + receivedMessage: $messageReceiver = new MessageReceiver(), + classesToResolve: [AnnotatedMessage::class], + ); + + $ecotone + ->publishEvent( + event: $messageSent = new AnnotatedMessage( + sensitiveObject: new TestClass('value', TestEnum::FIRST), + sensitiveEnum: TestEnum::FIRST, + sensitiveProperty: 'value', + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] + ) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + // assert channel message + $channelMessage = $channel->getLastSentMessage(); + $channelMessagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); + $channelMessagePayload = json_decode($channelMessagePayload, true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveObject']), $this->primaryKey)); + self::assertEquals('"first"', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveEnum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($channelMessagePayload['sensitiveProperty']), $this->primaryKey)); + $messageHeaders = $channelMessage->getHeaders(); + + self::assertEquals($metadataSent['foo'], Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->secondaryKey)); + self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->secondaryKey)); + self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); + self::assertFalse($messageHeaders->containsKey('fos'), 'encryption should not add additional headers'); + + + // assert received message + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + $receivedHeaders = $messageReceiver->receivedHeaders(); + self::assertEquals($metadataSent['foo'], $receivedHeaders['foo']); + self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); + self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); + } + + private function bootstrapEcotone( + ChannelProtectionConfiguration $channelProtectionConfiguration, + MessageChannel $messageChannel, + MessageReceiver $receivedMessage, + array $classesToResolve = [], + ): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( + classesToResolve: $classesToResolve, containerOrAvailableServices: [ $receivedMessage, new TestCommandHandler(), diff --git a/packages/DataProtection/tests/Integration/EncryptStoredEventsTest.php b/packages/DataProtection/tests/Integration/EncryptStoredEventsTest.php new file mode 100644 index 000000000..b39b3d0ea --- /dev/null +++ b/packages/DataProtection/tests/Integration/EncryptStoredEventsTest.php @@ -0,0 +1,110 @@ +connectionFactory = new DbalConnectionFactory(getenv('DATABASE_DSN') ? getenv('DATABASE_DSN') : 'pgsql://ecotone:secret@127.0.0.1:5432/ecotone'); + $this->connection = $this->connectionFactory->establishConnection(); + + self::clearDataTables($this->connection); + } + + protected function tearDown(): void + { + $this->connection->close(); + } + + public function test_sensitive_events_will_be_stored_encrypted(): void + { + $encryptionKey = Key::createNewRandomKey(); + $ecotone = EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + DbalConnectionFactory::class => $this->connectionFactory, + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::EVENT_SOURCING_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\PersistingSensitiveEvents']) + ->withExtensionObjects( + array_merge( + [ + EventSourcingConfiguration::createWithDefaults(), + DataProtectionConfiguration::create('primary', $encryptionKey), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ], + ) + ), + addInMemoryEventSourcedRepository: false, + ); + $ecotone->withEventsFor( + '123', + SomeAggregate::class, + $storedEvents = [ + new AggregateEvent('123', 'sensitive', TestEnum::FIRST, new TestClass('sensitive', TestEnum::FIRST)), + new AggregateEvent('123', 'another sensitive', TestEnum::FIRST, new TestClass('another sensitive', TestEnum::FIRST)), + new AggregateEvent('123', 'most sensitive', TestEnum::FIRST, new TestClass('most sensitive', TestEnum::FIRST)), + ] + ); + + $this->streamName = $this->connection->fetchOne('select stream_name from event_streams where real_stream_name = ?', [SomeAggregate::class]); + + $storedPayloads = $this->connection->fetchFirstColumn(sprintf('select payload from %s', $this->streamName)); + foreach ($storedPayloads as $payload) { + $payload = json_decode($payload, true); + + // decryption should not throw any exception + Crypto::decrypt(base64_decode($payload['sensitiveValue']), $encryptionKey); + Crypto::decrypt(base64_decode($payload['sensitiveEnum']), $encryptionKey); + Crypto::decrypt(base64_decode($payload['sensitiveObject']), $encryptionKey); + } + + self::assertEquals($storedEvents, array_map(static fn (Event $storedEvent) => $storedEvent->getPayload(), $ecotone->getEventStreamEvents(SomeAggregate::class))); + } + + private static function clearDataTables(Connection $connection): void + { + foreach (self::getSchemaManager($connection)->listTableNames() as $tableName) { + $sql = 'DROP TABLE ' . $tableName; + + $connection->executeQuery($sql); + } + } + + protected static function getSchemaManager(Connection $connection): AbstractSchemaManager + { + // Handle both DBAL 3.x (getSchemaManager) and 4.x (createSchemaManager) + return method_exists($connection, 'getSchemaManager') ? $connection->getSchemaManager() : $connection->createSchemaManager(); + } +} diff --git a/packages/Ecotone/src/Messaging/Config/Configuration.php b/packages/Ecotone/src/Messaging/Config/Configuration.php index c9a2fd4c9..5909273bc 100644 --- a/packages/Ecotone/src/Messaging/Config/Configuration.php +++ b/packages/Ecotone/src/Messaging/Config/Configuration.php @@ -8,6 +8,7 @@ use Ecotone\Messaging\Channel\MessageChannelBuilder; use Ecotone\Messaging\Config\Container\CompilableBuilder; use Ecotone\Messaging\Config\Container\Compiler\CompilerPass; +use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Endpoint\ChannelAdapterConsumerBuilder; use Ecotone\Messaging\Endpoint\MessageHandlerConsumerBuilder; @@ -129,6 +130,8 @@ public function registerConsoleCommand(ConsoleCommandConfiguration $consoleComma */ public function registerConverter(CompilableBuilder $converterBuilder): Configuration; + public function registerConversionServiceDecorator(Definition $conversionServiceDecoratorBuilder): Configuration; + /** * @param string $referenceName * @return Configuration diff --git a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php index 76997c756..88e1a6aeb 100644 --- a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php +++ b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php @@ -129,6 +129,7 @@ final class MessagingSystemConfiguration implements Configuration * @var CompilableBuilder[] */ private array $converterBuilders = []; + private ?Definition $conversionServiceDecoratorBuilder = null; /** * @var string[] */ @@ -831,6 +832,13 @@ public function registerConverter(CompilableBuilder $converterBuilder): Configur return $this; } + public function registerConversionServiceDecorator(Definition $conversionServiceDecoratorBuilder): Configuration + { + $this->conversionServiceDecoratorBuilder = $conversionServiceDecoratorBuilder; + + return $this; + } + /** * @inheritDoc */ @@ -913,11 +921,18 @@ public function process(ContainerBuilder $builder): void // TODO: some service configuration should be handled at runtime. Here they are all cached in the container // $messagingBuilder->register('config.defaultSerializationMediaType', MediaType::parseMediaType($this->applicationConfiguration->getDefaultSerializationMediaType())); - $converters = []; + $converters = $dataEncryptors = []; foreach ($this->converterBuilders as $converterBuilder) { $converters[] = $converterBuilder->compile($messagingBuilder); } - $messagingBuilder->register(ConversionService::REFERENCE_NAME, new Definition(AutoCollectionConversionService::class, ['converters' => $converters])); + + $messagingBuilder->register(ConversionService::REFERENCE_NAME, $conversionService = new Definition(AutoCollectionConversionService::class, ['converters' => $converters])); + if ($this->conversionServiceDecoratorBuilder !== null) { + $this->conversionServiceDecoratorBuilder->addMethodCall('decorate', [$conversionService]); + + $messagingBuilder->replace(ConversionService::REFERENCE_NAME, $this->conversionServiceDecoratorBuilder); + } + $channelInterceptorsByImportance = $this->channelInterceptorBuilders; $channelInterceptorsByChannelName = []; diff --git a/packages/Ecotone/src/Messaging/Conversion/MediaType.php b/packages/Ecotone/src/Messaging/Conversion/MediaType.php index ea1c62d6d..951ad1c90 100644 --- a/packages/Ecotone/src/Messaging/Conversion/MediaType.php +++ b/packages/Ecotone/src/Messaging/Conversion/MediaType.php @@ -30,6 +30,7 @@ final class MediaType implements DefinedObject public const IMAGE_GIF = 'image/gif'; public const APPLICATION_XML = 'application/xml'; public const APPLICATION_JSON = 'application/json'; + public const APPLICATION_JSON_ENCRYPTED = 'application/json+encrypted'; public const APPLICATION_FORM_URLENCODED = 'application/x-www-form-urlencoded'; public const APPLICATION_ATOM_XML = 'application/atom+xml'; public const APPLICATION_XHTML_XML = 'application/xhtml+xml'; @@ -86,6 +87,15 @@ public static function createApplicationJson(): self return self::parseMediaType(self::APPLICATION_JSON); } + /** + * @return MediaType + * @throws \Ecotone\Messaging\MessagingException + */ + public static function createApplicationJsonEncrypted(): self + { + return self::parseMediaType(self::APPLICATION_JSON_ENCRYPTED); + } + /** * @return MediaType * @throws \Ecotone\Messaging\MessagingException diff --git a/packages/PdoEventSourcing/src/Prooph/EcotoneEventStoreProophWrapper.php b/packages/PdoEventSourcing/src/Prooph/EcotoneEventStoreProophWrapper.php index 6bb764ffb..5d788bc02 100644 --- a/packages/PdoEventSourcing/src/Prooph/EcotoneEventStoreProophWrapper.php +++ b/packages/PdoEventSourcing/src/Prooph/EcotoneEventStoreProophWrapper.php @@ -139,24 +139,23 @@ private function convertToProophMetadataMatcher(EcotoneMetadataMatcher $ecotoneM private function convertToEcotoneEvents(Iterator $streamEvents, bool $deserialize): array { $events = []; - $sourcePHPType = Type::array(); - $PHPMediaType = MediaType::createApplicationXPHP(); - /** @var ProophMessage $event */ - while ($event = $streamEvents->current()) { + /** @var ProophMessage $proophEvent */ + while ($proophEvent = $streamEvents->current()) { try { - $eventName = Type::create($this->eventMapper->mapNameToEventType($event->messageName())); + $eventName = Type::create($this->eventMapper->mapNameToEventType($proophEvent->messageName())); } catch (TypeDefinitionException $e) { // Fallback to using the message name as is if we find an unknown event type (deleted class etc.) - $eventName = $event->messageName(); + $eventName = $proophEvent->messageName(); } + $events[] = Event::createWithType( - $eventName, - $deserialize ? $this->conversionService->convert($event->payload(), $sourcePHPType, $PHPMediaType, $eventName, $PHPMediaType) : $event->payload(), - array_merge( + eventType: $eventName, + event: $deserialize ? $this->conversionService->convert($proophEvent->payload(), Type::array(), MediaType::createApplicationXPHP(), $eventName, MediaType::createApplicationXPHP()) : $proophEvent->payload(), + metadata: array_merge( [ MessageHeaders::REVISION => 1, ], - $event->metadata() + $proophEvent->metadata() ) );