From 3bbef3bf143b7dfc2c64a25dd0a2b0516b7ad6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Thu, 15 Jan 2026 15:24:45 +0100 Subject: [PATCH 01/20] data protection: - use `Ecotone\DataProtection\Attribute\UsingSensitiveData` to define messages with sensitive data - use `Ecotone\DataProtection\Attribute\Sensitive` to mark properties with sensitive data - define encryption keys with `Ecotone\DataProtection\Configuration\DataProtectionConfiguration` - sensitive data will be encrypted right before its sended to queue and decrypted right after message is being retrieved from queue - data protection require JMSModule to be enabled --- composer.json | 7 ++ packages/DataProtection/.gitattributes | 7 ++ packages/DataProtection/.github/FUNDING.yml | 12 +++ .../.github/ISSUE_TEMPLATE/bug_report.md | 10 ++ packages/DataProtection/.gitignore | 9 ++ packages/DataProtection/LICENSE | 21 ++++ packages/DataProtection/LICENSE-ENTERPRISE | 3 + packages/DataProtection/README.md | 63 +++++++++++ packages/DataProtection/composer.json | 80 ++++++++++++++ packages/DataProtection/phpstan.neon | 4 + packages/DataProtection/phpunit.xml.dist | 20 ++++ .../src/Attribute/Sensitive.php | 9 ++ .../src/Attribute/UsingSensitiveData.php | 19 ++++ .../DataProtectionConfiguration.php | 37 +++++++ .../Configuration/DataProtectionModule.php | 88 +++++++++++++++ .../src/Obfuscator/MessageObfuscator.php | 44 ++++++++ .../src/Obfuscator/Obfuscator.php | 63 +++++++++++ .../src/OutboundDecryptionChannelBuilder.php | 35 ++++++ .../OutboundDecryptionChannelInterceptor.php | 37 +++++++ .../src/OutboundEncryptionChannelBuilder.php | 35 ++++++ .../OutboundEncryptionChannelInterceptor.php | 37 +++++++ .../FullyObfuscatedMessage.php | 18 ++++ .../PartiallyObfuscatedMessage.php | 21 ++++ .../TestCommandHandler.php | 22 ++++ .../tests/Fixture/ObfuscatedMessage.php | 15 +++ .../tests/Fixture/TestClass.php | 12 +++ .../DataProtection/tests/Fixture/TestEnum.php | 8 ++ .../ObfuscateAnnotatedMessagesTest.php | 74 +++++++++++++ .../tests/Unit/MessageObfuscatorTest.php | 101 ++++++++++++++++++ .../tests/Unit/ObfuscatorTest.php | 52 +++++++++ ...utboundSerializationChannelInterceptor.php | 10 +- .../src/Messaging/Config/ModuleClassList.php | 5 + .../Messaging/Config/ModulePackageList.php | 3 + .../AutoCollectionConversionService.php | 1 - .../DeserializingConverter.php | 6 -- packages/local_packages.json | 4 + 36 files changed, 980 insertions(+), 12 deletions(-) create mode 100644 packages/DataProtection/.gitattributes create mode 100644 packages/DataProtection/.github/FUNDING.yml create mode 100644 packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 packages/DataProtection/.gitignore create mode 100644 packages/DataProtection/LICENSE create mode 100644 packages/DataProtection/LICENSE-ENTERPRISE create mode 100644 packages/DataProtection/README.md create mode 100644 packages/DataProtection/composer.json create mode 100644 packages/DataProtection/phpstan.neon create mode 100644 packages/DataProtection/phpunit.xml.dist create mode 100644 packages/DataProtection/src/Attribute/Sensitive.php create mode 100644 packages/DataProtection/src/Attribute/UsingSensitiveData.php create mode 100644 packages/DataProtection/src/Configuration/DataProtectionConfiguration.php create mode 100644 packages/DataProtection/src/Configuration/DataProtectionModule.php create mode 100644 packages/DataProtection/src/Obfuscator/MessageObfuscator.php create mode 100644 packages/DataProtection/src/Obfuscator/Obfuscator.php create mode 100644 packages/DataProtection/src/OutboundDecryptionChannelBuilder.php create mode 100644 packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php create mode 100644 packages/DataProtection/src/OutboundEncryptionChannelBuilder.php create mode 100644 packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/TestClass.php create mode 100644 packages/DataProtection/tests/Fixture/TestEnum.php create mode 100644 packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php create mode 100644 packages/DataProtection/tests/Unit/MessageObfuscatorTest.php create mode 100644 packages/DataProtection/tests/Unit/ObfuscatorTest.php diff --git a/composer.json b/composer.json index 0d8c2ca14..5ba197ea2 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,9 @@ ], "Ecotone\\Amqp\\": "packages/Amqp/src", "Ecotone\\AnnotationFinder\\": "packages/Ecotone/src/AnnotationFinder/", + "Ecotone\\DataProtection\\": [ + "packages/DataProtection/src" + ], "Ecotone\\Dbal\\": [ "packages/Ecotone/src/Dbal/", "packages/Dbal/src" @@ -75,6 +78,9 @@ "Test\\Ecotone\\Amqp\\": [ "packages/Amqp/tests" ], + "Test\\Ecotone\\DataProtection\\": [ + "packages/DataProtection/tests" + ], "Test\\Ecotone\\Dbal\\": [ "packages/Dbal/tests" ], @@ -113,6 +119,7 @@ "php": "^8.2", "doctrine/dbal": "^3.9|^4.0", "doctrine/persistence": "^2.5|^3.4", + "defuse/php-encryption": "^2.4", "enqueue/amqp-lib": "^0.10.25", "enqueue/redis": "^0.10.9", "enqueue/sqs": "^0.10.15", diff --git a/packages/DataProtection/.gitattributes b/packages/DataProtection/.gitattributes new file mode 100644 index 000000000..5699823c5 --- /dev/null +++ b/packages/DataProtection/.gitattributes @@ -0,0 +1,7 @@ +tests/ export-ignore +.coveralls.yml export-ignore +.gitattributes export-ignore +.gitignore export-ignore +behat.yaml export-ignore +phpstan.neon export-ignore +phpunit.xml export-ignore \ No newline at end of file diff --git a/packages/DataProtection/.github/FUNDING.yml b/packages/DataProtection/.github/FUNDING.yml new file mode 100644 index 000000000..c7eaae65e --- /dev/null +++ b/packages/DataProtection/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [dgafka] +patreon: # Replace with a single Open Collective username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md b/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..2fc86f2cf --- /dev/null +++ b/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,10 @@ +--- +name: This is Read-Only repository +about: Report at ecotoneframework/ecotone-dev +title: '' +labels: '' +assignees: '' + +--- + +Report issue at [ecotone-dev](ecotoneframework/ecotone-dev) \ No newline at end of file diff --git a/packages/DataProtection/.gitignore b/packages/DataProtection/.gitignore new file mode 100644 index 000000000..18c159d80 --- /dev/null +++ b/packages/DataProtection/.gitignore @@ -0,0 +1,9 @@ +.idea/ +vendor/ +bin/ +tests/coverage +!tests/coverage/.gitkeep +file +.phpunit.result.cache +composer.lock +phpunit.xml diff --git a/packages/DataProtection/LICENSE b/packages/DataProtection/LICENSE new file mode 100644 index 000000000..82205508a --- /dev/null +++ b/packages/DataProtection/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2025 Dariusz Gafka + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +**Scope of the License** + +Apache-2.0 Licence applies to non Enterprise Functionalities of the Ecotone Framework. +Functionalities of the Ecotone Framework referred to as Enterprise functionalities, are not covered under the Apache-2.0 license. These functionalities are provided under a separate Enterprise License. +For details on the Enterprise License, please refer to the [LICENSE-ENTERPRISE](./LICENSE-ENTERPRISE) file. \ No newline at end of file diff --git a/packages/DataProtection/LICENSE-ENTERPRISE b/packages/DataProtection/LICENSE-ENTERPRISE new file mode 100644 index 000000000..fad1a5a8d --- /dev/null +++ b/packages/DataProtection/LICENSE-ENTERPRISE @@ -0,0 +1,3 @@ +Copyright (c) 2025 Dariusz Gafka + +Licence is available at [ecotone.tech/documents/ecotone_enterprise_licence.pdf](https://ecotone.tech/documents/ecotone_enterprise_licence.pdf) \ No newline at end of file diff --git a/packages/DataProtection/README.md b/packages/DataProtection/README.md new file mode 100644 index 000000000..a46452a91 --- /dev/null +++ b/packages/DataProtection/README.md @@ -0,0 +1,63 @@ +# This is Read Only Repository +To contribute make use of [Ecotone-Dev repository](https://github.com/ecotoneframework/ecotone-dev). + +

+ +

+ +![Github Actions](https://github.com/ecotoneFramework/ecotone-dev/actions/workflows/split-testing.yml/badge.svg) +[![Latest Stable Version](https://poser.pugx.org/ecotone/ecotone/v/stable)](https://packagist.org/packages/ecotone/ecotone) +[![License](https://poser.pugx.org/ecotone/ecotone/license)](https://packagist.org/packages/ecotone/ecotone) +[![Total Downloads](https://img.shields.io/packagist/dt/ecotone/ecotone)](https://packagist.org/packages/ecotone/ecotone) +[![PHP Version Require](https://img.shields.io/packagist/dependency-v/ecotone/ecotone/php.svg)](https://packagist.org/packages/ecotone/ecotone) + +The roots of Object Oriented Programming (OOP) were mainly about communication using Messages and logic encapsulation. +`Ecotone` aims to return to the origins of OOP, by providing tools which allows us to fully move the focus from Objects to Flows, from Data storage to Application Design, from Technicalities to Business logic. +Ecotone does that by making Messages first class-citizen in our Applications. + +Thanks to being Message-Driven at the foundation level, Ecotone provides architecture which is resilient and scalable by default, making it possible for Developers to focus on business problems instead of technical concerns. +Together with declarative configuration and higher level building blocks, it makes the system design explicit, easy to follow and change no matter of Developers experience. + +Visit main page [ecotone.tech](https://ecotone.tech) to learn more. + +> Ecotone can be used with [Symfony](https://docs.ecotone.tech/modules/symfony-ddd-cqrs-event-sourcing) and [Laravel](https://docs.ecotone.tech/modules/laravel-ddd-cqrs-event-sourcing) frameworks, or any other framework using [Ecotone Lite](https://docs.ecotone.tech/install-php-service-bus#install-ecotone-lite-no-framework). +> +## Getting started + +The quickstart [page](https://docs.ecotone.tech/quick-start) of the +[reference guide](https://docs.ecotone.tech) provides a starting point for using Ecotone. +Read more on the [Ecotone's Blog](https://blog.ecotone.tech). + +## AI-Friendly Documentation + +Ecotone provides AI-optimized documentation for use with AI assistants and code editors: + +- **MCP Server**: `https://docs.ecotone.tech/~gitbook/mcp` - [Install in VSCode](vscode:mcp/install?%7B%22name%22%3A%22Ecotone%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.ecotone.tech%2F~gitbook%2Fmcp%22%7D) +- **LLMs.txt**: [ecotone.tech/llms.txt](https://ecotone.tech/llms.txt) +- **Context7**: Available via [@upstash/context7-mcp](https://github.com/upstash/context7) + +Learn more: [AI Integration Guide](https://docs.ecotone.tech/other/ai-integration) + +## Feature requests and issue reporting + +Use [issue tracking system](https://github.com/ecotoneframework/ecotone-dev/issues) for new feature request and bugs. +Please verify that it's not already reported by someone else. + +## Contact + +If you want to talk or ask questions about Ecotone + +- [**Twitter**](https://twitter.com/EcotonePHP) +- **support@simplycodedsoftware.com** +- [**Community Channel**](https://discord.gg/GwM2BSuXeg) + +## Support Ecotone + +If you want to help building and improving Ecotone consider becoming a sponsor: + +- [Sponsor Ecotone](https://github.com/sponsors/dgafka) +- [Contribute to Ecotone](https://github.com/ecotoneframework/ecotone-dev). + +## Tags + +PHP, DDD, CQRS, Event Sourcing, Symfony, Laravel, Service Bus, Event Driven Architecture, SOA, Events, Commands diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json new file mode 100644 index 000000000..afc90acb1 --- /dev/null +++ b/packages/DataProtection/composer.json @@ -0,0 +1,80 @@ +{ + "name": "ecotone/data-protection", + "license": [ + "Apache-2.0", + "proprietary" + ], + "homepage": "https://docs.ecotone.tech/", + "forum": "https://discord.gg/GwM2BSuXeg", + "type": "library", + "minimum-stability": "dev", + "prefer-stable": true, + "authors": [ + { + "name": "Dariusz Gafka", + "email": "support@simplycodedsoftware.com" + } + ], + "keywords": ["ecotone", "Encryption", "OpenSSL", "Data Protection", "Data Obfuscation"], + "description": "Extends Ecotone with Data Protection features allowing to obfuscate messages with sensitive data.", + "autoload": { + "psr-4": { + "Ecotone\\DataProtection\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\Ecotone\\DataProtection\\": [ + "tests" + ] + } + }, + "require": { + "ext-openssl": "*", + "ecotone/ecotone": "~1.293.0", + "defuse/php-encryption": "^2.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.5|^10.5|^11.0", + "phpstan/phpstan": "^1.8", + "psr/container": "^2.0", + "wikimedia/composer-merge-plugin": "^2.1" + }, + "scripts": { + "tests:phpstan": "vendor/bin/phpstan", + "tests:phpunit": "vendor/bin/phpunit", + "tests:ci": [ + "@tests:phpstan", + "@tests:phpunit" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "1.62-dev" + }, + "ecotone": { + "repository": "DataProtection" + }, + "merge-plugin": { + "include": [ + "../local_packages.json" + ] + }, + "license-info": { + "Apache-2.0": { + "name": "Apache License 2.0", + "url": "https://github.com/ecotoneframework/ecotone-dev/blob/main/LICENSE", + "description": "Allows to use non Enterprise features of Ecotone. For more information please write to support@simplycodedsoftware.com" + }, + "proprietary": { + "name": "Enterprise License", + "description": "Allows to use Enterprise features of Ecotone. For more information please write to support@simplycodedsoftware.com" + } + } + }, + "config": { + "allow-plugins": { + "wikimedia/composer-merge-plugin": true + } + } +} diff --git a/packages/DataProtection/phpstan.neon b/packages/DataProtection/phpstan.neon new file mode 100644 index 000000000..672e0fa1f --- /dev/null +++ b/packages/DataProtection/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 1 + paths: + - src \ No newline at end of file diff --git a/packages/DataProtection/phpunit.xml.dist b/packages/DataProtection/phpunit.xml.dist new file mode 100644 index 000000000..fc3ebe4f7 --- /dev/null +++ b/packages/DataProtection/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + ./src + + + + + + + + tests + + + diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php new file mode 100644 index 000000000..dfc6bb81d --- /dev/null +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -0,0 +1,9 @@ +encryptionKeyName; + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php new file mode 100644 index 000000000..3dfbecc9e --- /dev/null +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -0,0 +1,37 @@ + $key], defaultKey: $key); + } + + public function withKey(string $name, Key $key, bool $asDefault = false): self + { + Assert::keyNotExists($this->keys, $name, sprintf('Encryption key name `%s` already exists', $name)); + + $config = clone $this; + $config->keys[$name] = $key; + + if ($asDefault) { + $config->defaultKey = $key; + } + + return $config; + } + + public function key(?string $name): Key + { + return $this->keys[$name] ?? $this->defaultKey; + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php new file mode 100644 index 000000000..da041823f --- /dev/null +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -0,0 +1,88 @@ +findAnnotatedClasses(UsingSensitiveData::class); + + foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { + /** @var UsingSensitiveData $attribute */ + $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); + + $reflectionClass = new \ReflectionClass($messageUsingSensitiveData); + $sensitiveProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => $property->getAttributes(Sensitive::class) !== []); + $scalarProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => Type::create($property->getType()->getName())->isScalar()); + + $obfuscators[$messageUsingSensitiveData] = [ + 'sensitive' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $sensitiveProperties), + 'scalar' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $scalarProperties), + 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), + ]; + } + + return new self($obfuscators); + } + + public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void + { + Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); + + $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); + + $obfuscators = array_map(static fn (array $config) => new Obfuscator($config['sensitive'], $config['scalar'], $dataProtectionConfiguration->key($config['encryptionKey'])), $this->obfuscators); + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); + + $pollableMessageChannels = ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects); + + foreach ($pollableMessageChannels as $pollableMessageChannel) { + $messagingConfiguration->registerChannelInterceptor( + new OutboundEncryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + ); + $messagingConfiguration->registerChannelInterceptor( + new OutboundDecryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + ); + } + } + + public function canHandle($extensionObject): bool + { + return $extensionObject instanceof DataProtectionConfiguration || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()); + } + + public function getModulePackageName(): string + { + return ModulePackageList::DATA_PROTECTION_PACKAGE; + } +} diff --git a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php new file mode 100644 index 000000000..5df5eca81 --- /dev/null +++ b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php @@ -0,0 +1,44 @@ + $obfuscators + */ + public function __construct(private array $obfuscators) + { + } + + public function encrypt(Message $message): string + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return $message->getPayload(); + } + + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + if (! array_key_exists($type, $this->obfuscators)) { + return $message->getPayload(); + } + + return $this->obfuscators[$type]->encrypt($message->getPayload()); + } + + public function decrypt(Message $message): string + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return $message->getPayload(); + } + + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + if (! array_key_exists($type, $this->obfuscators)) { + return $message->getPayload(); + } + + return $this->obfuscators[$type]->decrypt($message->getPayload()); + } +} diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php new file mode 100644 index 000000000..4c83c7846 --- /dev/null +++ b/packages/DataProtection/src/Obfuscator/Obfuscator.php @@ -0,0 +1,63 @@ +obfuscateAll = $sensitive === []; + } + + public function encrypt(string $json): string + { + $payload = json_decode($json, true); + $sensitiveParameters = $this->resolveSensitiveParameters($payload); + + foreach ($sensitiveParameters as $key) { + $value = in_array($key, $this->scalar) ? $payload[$key] : json_encode($payload[$key]); + + $payload[$key] = base64_encode(Crypto::encrypt($value, $this->encryptionKey)); + } + + return json_encode($payload); + } + + public function decrypt(string $json): string + { + $payload = json_decode($json, true); + $sensitiveParameters = $this->resolveSensitiveParameters($payload); + + foreach ($sensitiveParameters as $key) { + $value = Crypto::decrypt(base64_decode($payload[$key]), $this->encryptionKey); + + $payload[$key] = in_array($key, $this->scalar) ? $value : json_decode($value, true); + } + + return json_encode($payload); + } + + private function resolveSensitiveParameters(array $payload): array + { + if ($this->obfuscateAll) { + return array_keys($payload); + } + + return array_filter($this->sensitive, static fn (string $key) => array_key_exists($key, $payload)); + } + + public function getDefinition(): Definition + { + return Definition::createFor(self::class, [$this->sensitive, $this->scalar, $this->encryptionKey]); + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php new file mode 100644 index 000000000..ee6212a1a --- /dev/null +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -0,0 +1,35 @@ +relatedChannel; + } + + public function getPrecedence(): int + { + return PrecedenceChannelInterceptor::MESSAGE_SERIALIZATION + 1; + } + + public function compile(MessagingContainerBuilder $builder): Definition + { + return new Definition(OutboundDecryptionChannelInterceptor::class, [ + Reference::to(MessageObfuscator::class), + ]); + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php new file mode 100644 index 000000000..a6960eb7a --- /dev/null +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -0,0 +1,37 @@ +canHandle($message)) { + return $message; + } + + $payload = $this->messageObfuscator->decrypt($message); + + $preparedMessage = MessageBuilder::withPayload($payload) + ->setMultipleHeaders($message->getHeaders()->headers()) + ; + + return $preparedMessage->build(); + } + + private function canHandle(Message $message): bool + { + return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + } +} diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php new file mode 100644 index 000000000..b5cbc3e3f --- /dev/null +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -0,0 +1,35 @@ +relatedChannel; + } + + public function getPrecedence(): int + { + return PrecedenceChannelInterceptor::MESSAGE_SERIALIZATION - 1; + } + + public function compile(MessagingContainerBuilder $builder): Definition + { + return new Definition(OutboundEncryptionChannelInterceptor::class, [ + Reference::to(MessageObfuscator::class), + ]); + } +} diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php new file mode 100644 index 000000000..c6d06cf2c --- /dev/null +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -0,0 +1,37 @@ +canHandle($message)) { + return $message; + } + + $payload = $this->messageObfuscator->encrypt($message); + + $preparedMessage = MessageBuilder::withPayload($payload) + ->setMultipleHeaders($message->getHeaders()->headers()) + ; + + return $preparedMessage->build(); + } + + private function canHandle(Message $message): bool + { + return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php new file mode 100644 index 000000000..56a8c20dc --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php @@ -0,0 +1,18 @@ +key = Key::createNewRandomKey(); + } + + public function test_fully_obfuscated_message(): void + { + $ecotone = $this->bootstrapEcotone(); + + $ecotone->sendCommand( + new FullyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + } + + public function test_message_with_obfuscated_enum(): void + { + $ecotone = $this->bootstrapEcotone(); + + $ecotone->sendCommand( + new PartiallyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + } + + public function bootstrapEcotone(): FlowTestSupport + { + return EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + new TestCommandHandler(), + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) + ->withExtensionObjects([ + DataProtectionConfiguration::create('default', $this->key), + SimpleMessageChannelBuilder::createQueueChannel('test'), + ]) + ); + } +} diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php new file mode 100644 index 000000000..17c7642d7 --- /dev/null +++ b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php @@ -0,0 +1,101 @@ +message = MessageBuilder::withPayload(json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR)) + ->setHeader(MessageHeaders::TYPE_ID, ObfuscatedMessage::class) + ->build() + ; + } + + public function test_obfuscate_message_fully(): void + { + $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertNotEquals('value', $payload['bar']); + self::assertNotEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + } + + public function test_obfuscate_message_partially(): void + { + $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertNotEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } + + public function test_dont_obfuscate_unsupported_message(): void + { + $obfuscator = new Obfuscator(['foo', 'bar'], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([\stdClass::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } +} diff --git a/packages/DataProtection/tests/Unit/ObfuscatorTest.php b/packages/DataProtection/tests/Unit/ObfuscatorTest.php new file mode 100644 index 000000000..3007a60d8 --- /dev/null +++ b/packages/DataProtection/tests/Unit/ObfuscatorTest.php @@ -0,0 +1,52 @@ +payload = json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR); + } + + public function test_obfuscate_payload_fully(): void + { + $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); + + $encryptedPayload = $obfuscator->encrypt($this->payload); + $decryptedPayload = $obfuscator->decrypt($encryptedPayload); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertNotEquals('value', $payload['bar']); + self::assertNotEquals($this->payload, $encryptedPayload); + self::assertEquals($this->payload, $decryptedPayload); + } + + public function test_obfuscate_payload_partially(): void + { + $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); + + $encryptedPayload = $obfuscator->encrypt($this->payload); + $decryptedPayload = $obfuscator->decrypt($encryptedPayload); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertNotEquals($this->payload, $encryptedPayload); + self::assertEquals($this->payload, $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } +} diff --git a/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php b/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php index 51ee82584..c863f084c 100644 --- a/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php +++ b/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php @@ -16,7 +16,7 @@ /** * licence Apache-2.0 */ -final class OutboundSerializationChannelInterceptor extends AbstractChannelInterceptor implements ChannelInterceptor +final class OutboundSerializationChannelInterceptor extends AbstractChannelInterceptor { public function __construct( private OutboundMessageConverter $outboundMessageConverter, @@ -27,13 +27,13 @@ public function __construct( /** * @inheritDoc */ - public function preSend(Message $messageToConvert, MessageChannel $messageChannel): ?Message + public function preSend(Message $message, MessageChannel $messageChannel): ?Message { - if ($messageToConvert instanceof ErrorMessage) { - return $messageToConvert; + if ($message instanceof ErrorMessage) { + return $message; } - $outboundMessage = $this->outboundMessageConverter->prepare($messageToConvert, $this->conversionService); + $outboundMessage = $this->outboundMessageConverter->prepare($message, $this->conversionService); $preparedMessage = MessageBuilder::withPayload($outboundMessage->getPayload()) ->setMultipleHeaders($outboundMessage->getHeaders()); diff --git a/packages/Ecotone/src/Messaging/Config/ModuleClassList.php b/packages/Ecotone/src/Messaging/Config/ModuleClassList.php index 846295781..58c55f1fd 100644 --- a/packages/Ecotone/src/Messaging/Config/ModuleClassList.php +++ b/packages/Ecotone/src/Messaging/Config/ModuleClassList.php @@ -8,6 +8,7 @@ use Ecotone\Amqp\Configuration\RabbitConsumerModule; use Ecotone\Amqp\Publisher\AmqpMessagePublisherModule; use Ecotone\Amqp\Transaction\AmqpTransactionModule; +use Ecotone\DataProtection\Configuration\DataProtectionModule; use Ecotone\Dbal\Configuration\DbalConnectionModule; use Ecotone\Dbal\Configuration\DbalPublisherModule; use Ecotone\Dbal\Database\DatabaseSetupModule; @@ -204,4 +205,8 @@ class ModuleClassList public const KAFKA_MODULES = [ KafkaModule::class, ]; + + const DATA_PROTECTION_MODULES = [ + DataProtectionModule::class, + ]; } diff --git a/packages/Ecotone/src/Messaging/Config/ModulePackageList.php b/packages/Ecotone/src/Messaging/Config/ModulePackageList.php index 51adc667b..32b8e2e1d 100644 --- a/packages/Ecotone/src/Messaging/Config/ModulePackageList.php +++ b/packages/Ecotone/src/Messaging/Config/ModulePackageList.php @@ -15,6 +15,7 @@ final class ModulePackageList */ public const ASYNCHRONOUS_PACKAGE = 'asynchronous'; public const AMQP_PACKAGE = 'amqp'; + public const DATA_PROTECTION_PACKAGE = 'dataProtection'; public const DBAL_PACKAGE = 'dbal'; public const REDIS_PACKAGE = 'redis'; public const SQS_PACKAGE = 'sqs'; @@ -42,6 +43,7 @@ public static function getModuleClassesForPackage(string $packageName): array ModulePackageList::TEST_PACKAGE => ModuleClassList::TEST_MODULES, ModulePackageList::LARAVEL_PACKAGE => ModuleClassList::LARAVEL_MODULES, ModulePackageList::SYMFONY_PACKAGE => ModuleClassList::SYMFONY_MODULES, + ModulePackageList::DATA_PROTECTION_PACKAGE => ModuleClassList::DATA_PROTECTION_MODULES, default => throw ConfigurationException::create(sprintf('Given unknown package name %s. Available packages name are: %s', $packageName, implode(',', self::allPackages()))) }; } @@ -64,6 +66,7 @@ public static function allPackages(): array self::TRACING_PACKAGE, self::LARAVEL_PACKAGE, self::SYMFONY_PACKAGE, + self::DATA_PROTECTION_PACKAGE, ]; } diff --git a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php index 46b4f4964..0e38f7404 100644 --- a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php +++ b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php @@ -63,7 +63,6 @@ public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType public function canConvert(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool { if ($this->getConverter($sourceType, $sourceMediaType, $targetType, $targetMediaType)) { - ; return true; } if ($sourceType->isIterable() && $sourceType instanceof Type\GenericType diff --git a/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php b/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php index b0ed7d5d9..4a7e60674 100644 --- a/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php +++ b/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php @@ -20,9 +20,6 @@ */ class DeserializingConverter implements Converter { - /** - * @inheritDoc - */ public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType, ?ConversionService $conversionService = null) { $phpVar = unserialize(stripslashes($source)); @@ -36,9 +33,6 @@ public function convert($source, Type $sourceType, MediaType $sourceMediaType, T return $phpVar; } - /** - * @inheritDoc - */ public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool { return $sourceMediaType->isCompatibleWithParsed(MediaType::APPLICATION_X_PHP_SERIALIZED) diff --git a/packages/local_packages.json b/packages/local_packages.json index b97dbf8d1..bf61019b7 100644 --- a/packages/local_packages.json +++ b/packages/local_packages.json @@ -12,6 +12,10 @@ "type": "path", "url": "Dbal" }, + { + "type": "path", + "url": "DataProtection" + }, { "type": "path", "url": "Enqueue" From b18aaeadcf08355c1b79117bfd32ea69ec4e2d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 18:03:58 +0100 Subject: [PATCH 02/20] tests for Event and Command Handler --- .../src/AmqpBackedMessageChannelBuilder.php | 9 +- .../src/Attribute/UsingSensitiveData.php | 1 - .../DataProtectionConfiguration.php | 21 ++- .../Configuration/DataProtectionModule.php | 43 +++-- .../OutboundDecryptionChannelInterceptor.php | 5 +- .../OutboundEncryptionChannelInterceptor.php | 5 +- .../tests/Fixture/MessageReceiver.php | 18 ++ .../MessageWithSecondaryKeyEncryption.php | 14 ++ .../TestCommandHandler.php | 22 --- .../tests/Fixture/TestCommandHandler.php | 39 +++++ .../tests/Fixture/TestEventHandler.php | 40 +++++ .../ObfuscateAnnotatedMessagesTest.php | 157 ++++++++++++++++-- .../PollableChannelInterceptorAdapter.php | 2 +- 13 files changed, 313 insertions(+), 63 deletions(-) create mode 100644 packages/DataProtection/tests/Fixture/MessageReceiver.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/TestEventHandler.php diff --git a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php index 6fafaf28d..6277d8656 100644 --- a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php +++ b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php @@ -48,25 +48,20 @@ public static function create( ); } - private function getAmqpOutboundChannelAdapter(): AmqpOutboundChannelAdapterBuilder - { - return $this->outboundChannelAdapter; - } - /** * @deprecated use withPublisherConfirms * @TODO Ecotone 2.0 remove */ public function withPublisherAcknowledgments(bool $enabled): self { - $this->getAmqpOutboundChannelAdapter()->withPublisherConfirms($enabled); + $this->outboundChannelAdapter->withPublisherConfirms($enabled); return $this; } public function withPublisherConfirms(bool $enabled): self { - $this->getAmqpOutboundChannelAdapter()->withPublisherConfirms($enabled); + $this->outboundChannelAdapter->withPublisherConfirms($enabled); return $this; } diff --git a/packages/DataProtection/src/Attribute/UsingSensitiveData.php b/packages/DataProtection/src/Attribute/UsingSensitiveData.php index 4edd60cf0..ade3b3841 100644 --- a/packages/DataProtection/src/Attribute/UsingSensitiveData.php +++ b/packages/DataProtection/src/Attribute/UsingSensitiveData.php @@ -3,7 +3,6 @@ namespace Ecotone\DataProtection\Attribute; use Attribute; -use Ecotone\Messaging\Support\Assert; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class UsingSensitiveData diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php index 3dfbecc9e..8aed2dd99 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -7,13 +7,16 @@ class DataProtectionConfiguration { - private function __construct(private array $keys, private Key $defaultKey) + /** + * @param array $keys + */ + private function __construct(private array $keys, private string $defaultKey) { } public static function create(string $name, Key $key): self { - return new self(keys: [$name => $key], defaultKey: $key); + return new self(keys: [$name => $key], defaultKey: $name); } public function withKey(string $name, Key $key, bool $asDefault = false): self @@ -24,7 +27,7 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self $config->keys[$name] = $key; if ($asDefault) { - $config->defaultKey = $key; + $config->defaultKey = $name; } return $config; @@ -32,6 +35,16 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self public function key(?string $name): Key { - return $this->keys[$name] ?? $this->defaultKey; + return $this->keys[$name] ?? $this->keys[$this->defaultKey]; + } + + public function keyName(?string $name): string + { + return array_key_exists($name, $this->keys) ? $name : $this->defaultKey; + } + + public function keys(): array + { + return $this->keys; } } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index da041823f..0ff1de775 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -11,6 +11,7 @@ use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; +use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; use Ecotone\Messaging\Channel\MessageChannelWithSerializationBuilder; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver; @@ -19,6 +20,7 @@ use Ecotone\Messaging\Config\Container\Definition; 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; @@ -40,14 +42,10 @@ public static function create(AnnotationFinder $annotationRegistrationService, I foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { /** @var UsingSensitiveData $attribute */ $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); - - $reflectionClass = new \ReflectionClass($messageUsingSensitiveData); - $sensitiveProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => $property->getAttributes(Sensitive::class) !== []); - $scalarProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => Type::create($property->getType()->getName())->isScalar()); + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($messageUsingSensitiveData)); $obfuscators[$messageUsingSensitiveData] = [ - 'sensitive' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $sensitiveProperties), - 'scalar' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $scalarProperties), + 'properties' => $classDefinition->getProperties(), 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), ]; } @@ -58,15 +56,34 @@ public static function create(AnnotationFinder $annotationRegistrationService, I public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void { Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); + Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); + $jMSConverterConfiguration = ExtensionObjectResolver::resolveUnique(JMSConverterConfiguration::class, $extensionObjects, JMSConverterConfiguration::createWithDefaults()); - $obfuscators = array_map(static fn (array $config) => new Obfuscator($config['sensitive'], $config['scalar'], $dataProtectionConfiguration->key($config['encryptionKey'])), $this->obfuscators); - $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); - $pollableMessageChannels = ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects); + $isScalarProperty = static function (ClassPropertyDefinition $property) use ($jMSConverterConfiguration): bool { + $type = $property->getType(); + + return $type->isScalar() || ($type->isEnum() && $jMSConverterConfiguration->isEnumSupportEnabled()); + }; + + $obfuscators = array_map(function (array $config) use ($dataProtectionConfiguration, $isScalarProperty): Obfuscator { + $sensitiveProperties = array_map( + static fn (ClassPropertyDefinition $property): string => $property->getName(), + array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $property->hasAnnotation(Type::create(Sensitive::class))) + ); + $scalarProperties = array_map( + static fn (ClassPropertyDefinition $property): string => $property->getName(), + array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $isScalarProperty($property)) + ); + + return new Obfuscator($sensitiveProperties, $scalarProperties, $dataProtectionConfiguration->key($config['encryptionKey'])); + }, $this->obfuscators); + + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); - foreach ($pollableMessageChannels as $pollableMessageChannel) { + foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { $messagingConfiguration->registerChannelInterceptor( new OutboundEncryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) ); @@ -78,7 +95,11 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO public function canHandle($extensionObject): bool { - return $extensionObject instanceof DataProtectionConfiguration || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()); + return + $extensionObject instanceof DataProtectionConfiguration + || $extensionObject instanceof JMSConverterConfiguration + || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()) + ; } public function getModulePackageName(): string diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index a6960eb7a..15a0594a2 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -7,6 +7,7 @@ use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; +use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\Support\MessageBuilder; class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor @@ -32,6 +33,8 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? private function canHandle(Message $message): bool { - return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) + && MediaType::parseMediaType($message->getHeaders()->get(MessageHeaders::CONTENT_TYPE))->isCompatibleWith(MediaType::createApplicationJson()) + ; } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index c6d06cf2c..70b917f40 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -7,6 +7,7 @@ use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; +use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\Support\MessageBuilder; class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor @@ -32,6 +33,8 @@ public function preSend(Message $message, MessageChannel $messageChannel): ?Mess private function canHandle(Message $message): bool { - return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) + && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()) + ; } } diff --git a/packages/DataProtection/tests/Fixture/MessageReceiver.php b/packages/DataProtection/tests/Fixture/MessageReceiver.php new file mode 100644 index 000000000..65f2da33d --- /dev/null +++ b/packages/DataProtection/tests/Fixture/MessageReceiver.php @@ -0,0 +1,18 @@ +receivedMessage = $message; + } + + public function receivedMessage(): object + { + return $this->receivedMessage; + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php new file mode 100644 index 000000000..075f6c04f --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php @@ -0,0 +1,14 @@ +withReceivedMessage($message); + } + + #[CommandHandler(endpointId: 'test.PartiallyObfuscatedMessage')] + public function handlePartiallyObfuscatedMessage( + #[Payload] PartiallyObfuscatedMessage $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } + + #[CommandHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } +} diff --git a/packages/DataProtection/tests/Fixture/TestEventHandler.php b/packages/DataProtection/tests/Fixture/TestEventHandler.php new file mode 100644 index 000000000..4d3440948 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/TestEventHandler.php @@ -0,0 +1,40 @@ +withReceivedMessage($message); + } + + #[EventHandler(endpointId: 'test.PartiallyObfuscatedMessage')] + public function handlePartiallyObfuscatedMessage( + #[Payload] PartiallyObfuscatedMessage $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } + + #[EventHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } +} diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index 3ea3c00a5..ebc3b1737 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -2,72 +2,199 @@ namespace Test\Ecotone\DataProtection\Integration; +use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; +use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; +use Ecotone\Messaging\Channel\QueueChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; +use Ecotone\Messaging\MessageChannel; +use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnection; +use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnection; +use Interop\Amqp\AmqpConnectionFactory; use PHPUnit\Framework\TestCase; +use Test\Ecotone\Amqp\AmqpMessagingTestCase; +use Test\Ecotone\DataProtection\Fixture\MessageReceiver; use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\FullyObfuscatedMessage; +use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\MessageWithSecondaryKeyEncryption; use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\PartiallyObfuscatedMessage; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\TestCommandHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; +use Test\Ecotone\DataProtection\Fixture\TestCommandHandler; +use Test\Ecotone\DataProtection\Fixture\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestEnum; class ObfuscateAnnotatedMessagesTest extends TestCase { - private Key $key; + private Key $primaryKey; + private Key $secondaryKey; protected function setUp(): void { - $this->key = Key::createNewRandomKey(); + $this->primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); } - public function test_fully_obfuscated_message(): void + public function test_fully_obfuscated_command_handler_message(): void { - $ecotone = $this->bootstrapEcotone(); + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - new FullyObfuscatedMessage( + $messageSent = new FullyObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', ) ); + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + + $ecotone->sendCommand($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); } - public function test_message_with_obfuscated_enum(): void + public function test_partially_obfuscated_command_handler_message(): void { - $ecotone = $this->bootstrapEcotone(); + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - new PartiallyObfuscatedMessage( + $messageSent = new PartiallyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', $messagePayload['argument']); + + $ecotone->sendCommand($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_obfuscate_command_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + + $ecotone->sendCommand($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_fully_obfuscated_event_handler_message(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent( + $messageSent = new FullyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + + $ecotone->publishEvent($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_partially_obfuscated_event_handler_message(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent( + $messageSent = new PartiallyObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', ) ); + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', $messagePayload['argument']); + + $ecotone->publishEvent($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_obfuscate_event_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + + $ecotone->publishEvent($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + private function bootstrapEcotoneWithCommandHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport + { + return $this->bootstrapEcotone([TestCommandHandler::class], [new TestCommandHandler()], $messageChannel, $messageReceiver); + } + + private function bootstrapEcotoneWithEventHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport + { + return $this->bootstrapEcotone([TestEventHandler::class], [new TestEventHandler()], $messageChannel, $messageReceiver); } - public function bootstrapEcotone(): FlowTestSupport + private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( - containerOrAvailableServices: [ - new TestCommandHandler(), - ], + classesToResolve: $classesToResolve, + containerOrAvailableServices: array_merge([ + $receivedMessage, + AmqpConnectionFactory::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + AmqpExtConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + AmqpLibConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + ], $container), configuration: ServiceConfiguration::createWithDefaults() ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) ->withExtensionObjects([ - DataProtectionConfiguration::create('default', $this->key), - SimpleMessageChannelBuilder::createQueueChannel('test'), + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), ]) ); } diff --git a/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php b/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php index 0220c4fbf..a987e5e74 100644 --- a/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php +++ b/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php @@ -89,7 +89,7 @@ private function receiveFor(?PollingMetadata $pollingMetadata): ?Message } foreach ($this->sortedChannelInterceptors as $channelInterceptor) { - $channelInterceptor->postReceive($message, $this->messageChannel); + $message = $channelInterceptor->postReceive($message, $this->messageChannel); } return $message; From 5d0bd452fa1e5d1176f53616546fefb2788a77fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 18:08:26 +0100 Subject: [PATCH 03/20] add Enterprise license annotation --- packages/DataProtection/src/Attribute/Sensitive.php | 3 +++ packages/DataProtection/src/Attribute/UsingSensitiveData.php | 3 +++ .../src/Configuration/DataProtectionConfiguration.php | 3 +++ .../DataProtection/src/Configuration/DataProtectionModule.php | 3 +++ packages/DataProtection/src/Obfuscator/MessageObfuscator.php | 3 +++ packages/DataProtection/src/Obfuscator/Obfuscator.php | 3 +++ .../DataProtection/src/OutboundDecryptionChannelBuilder.php | 3 +++ .../src/OutboundDecryptionChannelInterceptor.php | 3 +++ .../DataProtection/src/OutboundEncryptionChannelBuilder.php | 3 +++ .../src/OutboundEncryptionChannelInterceptor.php | 3 +++ 10 files changed, 30 insertions(+) diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php index dfc6bb81d..6b1a07d38 100644 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -1,5 +1,8 @@ Date: Mon, 19 Jan 2026 22:56:22 +0100 Subject: [PATCH 04/20] - obfuscate full payload when message use sensitive data - allow to define sensitive headers --- .../src/Attribute/Sensitive.php | 12 -- .../src/Attribute/WithSensitiveHeader.php | 16 +++ .../src/Attribute/WithSensitiveHeaders.php | 18 +++ .../DataProtectionConfiguration.php | 5 - .../Configuration/DataProtectionModule.php | 51 +++++---- .../src/Obfuscator/MessageObfuscator.php | 73 +++++++++--- .../src/Obfuscator/Obfuscator.php | 66 ----------- .../src/OutboundDecryptionChannelBuilder.php | 10 +- .../OutboundDecryptionChannelInterceptor.php | 9 +- .../src/OutboundEncryptionChannelBuilder.php | 4 +- .../OutboundEncryptionChannelInterceptor.php | 9 +- .../tests/Fixture/MessageReceiver.php | 11 +- .../FullyObfuscatedMessage.php | 4 + .../PartiallyObfuscatedMessage.php | 6 +- .../tests/Fixture/TestCommandHandler.php | 10 +- .../tests/Fixture/TestEventHandler.php | 10 +- .../ObfuscateAnnotatedMessagesTest.php | 106 +++++++++++++----- .../tests/Unit/MessageObfuscatorTest.php | 75 +++---------- .../tests/Unit/ObfuscatorTest.php | 52 --------- .../src/Messaging/Handler/ClassDefinition.php | 23 +++- 20 files changed, 266 insertions(+), 304 deletions(-) delete mode 100644 packages/DataProtection/src/Attribute/Sensitive.php create mode 100644 packages/DataProtection/src/Attribute/WithSensitiveHeader.php create mode 100644 packages/DataProtection/src/Attribute/WithSensitiveHeaders.php delete mode 100644 packages/DataProtection/src/Obfuscator/Obfuscator.php delete mode 100644 packages/DataProtection/tests/Unit/ObfuscatorTest.php diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php deleted file mode 100644 index 6b1a07d38..000000000 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ /dev/null @@ -1,12 +0,0 @@ -headers, 'Header names should be all strings.'); + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php index 20b28b993..4b0c8286c 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -36,11 +36,6 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self return $config; } - public function key(?string $name): Key - { - return $this->keys[$name] ?? $this->keys[$this->defaultKey]; - } - public function keyName(?string $name): string { return array_key_exists($name, $this->keys) ? $name : $this->defaultKey; diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 13caeb1dd..dc37801e4 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -7,9 +7,11 @@ namespace Ecotone\DataProtection\Configuration; +use Defuse\Crypto\Key; use Ecotone\AnnotationFinder\AnnotationFinder; -use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; +use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; use Ecotone\DataProtection\Obfuscator\MessageObfuscator; use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; @@ -21,9 +23,9 @@ use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule; use Ecotone\Messaging\Config\Configuration; use Ecotone\Messaging\Config\Container\Definition; +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; @@ -43,13 +45,17 @@ public static function create(AnnotationFinder $annotationRegistrationService, I $messagesUsingSensitiveData = $annotationRegistrationService->findAnnotatedClasses(UsingSensitiveData::class); foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { - /** @var UsingSensitiveData $attribute */ - $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($messageUsingSensitiveData)); + $usingSensitiveDataAttribute = $classDefinition->getSingleClassAnnotation(Type::create(UsingSensitiveData::class)); + + $sensitiveHeaders = $classDefinition->findSingleClassAnnotation(Type::create(WithSensitiveHeaders::class))?->headers ?? []; + foreach ($classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) as $sensitiveHeader) { + $sensitiveHeaders[] = $sensitiveHeader->header; + } $obfuscators[$messageUsingSensitiveData] = [ - 'properties' => $classDefinition->getProperties(), 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), + 'sensitiveHeaders' => $sensitiveHeaders, ]; } @@ -62,29 +68,26 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); - $jMSConverterConfiguration = ExtensionObjectResolver::resolveUnique(JMSConverterConfiguration::class, $extensionObjects, JMSConverterConfiguration::createWithDefaults()); - - - $isScalarProperty = static function (ClassPropertyDefinition $property) use ($jMSConverterConfiguration): bool { - $type = $property->getType(); - - return $type->isScalar() || ($type->isEnum() && $jMSConverterConfiguration->isEnumSupportEnabled()); - }; - $obfuscators = array_map(function (array $config) use ($dataProtectionConfiguration, $isScalarProperty): Obfuscator { - $sensitiveProperties = array_map( - static fn (ClassPropertyDefinition $property): string => $property->getName(), - array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $property->hasAnnotation(Type::create(Sensitive::class))) - ); - $scalarProperties = array_map( - static fn (ClassPropertyDefinition $property): string => $property->getName(), - array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $isScalarProperty($property)) + foreach ($dataProtectionConfiguration->keys() as $encryptionKeyName => $key) { + $messagingConfiguration->registerServiceDefinition( + id: sprintf('ecotone.encryption.key.%s', $encryptionKeyName), + definition: new Definition( + Key::class, + [$key->saveToAsciiSafeString()], + 'loadFromAsciiSafeString' + ) ); + } - return new Obfuscator($sensitiveProperties, $scalarProperties, $dataProtectionConfiguration->key($config['encryptionKey'])); - }, $this->obfuscators); + $messageObfuscatorDefinition = new Definition(MessageObfuscator::class); + + foreach ($this->obfuscators as $messageClass => $config) { + $messageObfuscatorDefinition->addMethodCall('withKey', [$messageClass, Reference::to(sprintf('ecotone.encryption.key.%s', $dataProtectionConfiguration->keyName($config['encryptionKey'])))]); + $messageObfuscatorDefinition->addMethodCall('withSensitiveHeaders', [$messageClass, $config['sensitiveHeaders']]); + } - $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: $messageObfuscatorDefinition); foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { $messagingConfiguration->registerChannelInterceptor( diff --git a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php index e9ffa4b51..3e4a3a13e 100644 --- a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php +++ b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php @@ -5,43 +5,88 @@ */ namespace Ecotone\DataProtection\Obfuscator; +use Defuse\Crypto\Crypto; +use Defuse\Crypto\Key; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageHeaders; +use Ecotone\Messaging\Support\Assert; +use Ecotone\Messaging\Support\MessageBuilder; class MessageObfuscator { /** - * @param array $obfuscators + * @var array */ - public function __construct(private array $obfuscators) - { - } + private array $encryptionKeys = []; + + /** + * @var array> + */ + private array $sensitiveHeaders = []; - public function encrypt(Message $message): string + public function encrypt(Message $message): Message { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message->getPayload(); + return $message; } $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->obfuscators)) { - return $message->getPayload(); + if (! array_key_exists($type, $this->encryptionKeys)) { + return $message; } - return $this->obfuscators[$type]->encrypt($message->getPayload()); + $key = $this->encryptionKeys[$type]; + $encryptedPayload = base64_encode(Crypto::encrypt($message->getPayload(), $key)); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = base64_encode(Crypto::encrypt($headers[$sensitiveHeader], $key)); + } + } + + $preparedMessage = MessageBuilder::withPayload($encryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); } - public function decrypt(Message $message): string + public function decrypt(Message $message): Message { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message->getPayload(); + return $message; } $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->obfuscators)) { - return $message->getPayload(); + if (! array_key_exists($type, $this->encryptionKeys)) { + return $message; + } + + $key = $this->encryptionKeys[$type]; + $decryptedPayload = Crypto::decrypt(base64_decode($message->getPayload()), $key); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = Crypto::decrypt(base64_decode($headers[$sensitiveHeader]), $key); + } } - return $this->obfuscators[$type]->decrypt($message->getPayload()); + $preparedMessage = MessageBuilder::withPayload($decryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); + } + + public function withKey(string $messageClass, Key $key): void + { + $this->encryptionKeys[$messageClass] = $key; + } + + public function withSensitiveHeaders(string $messageClass, array $headers): void + { + Assert::allStrings($headers, sprintf('Headers for message class %s should be array of strings', $messageClass)); + + $this->sensitiveHeaders[$messageClass] = $headers; } } diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php deleted file mode 100644 index 98d41121a..000000000 --- a/packages/DataProtection/src/Obfuscator/Obfuscator.php +++ /dev/null @@ -1,66 +0,0 @@ -obfuscateAll = $sensitive === []; - } - - public function encrypt(string $json): string - { - $payload = json_decode($json, true); - $sensitiveParameters = $this->resolveSensitiveParameters($payload); - - foreach ($sensitiveParameters as $key) { - $value = in_array($key, $this->scalar) ? $payload[$key] : json_encode($payload[$key]); - - $payload[$key] = base64_encode(Crypto::encrypt($value, $this->encryptionKey)); - } - - return json_encode($payload); - } - - public function decrypt(string $json): string - { - $payload = json_decode($json, true); - $sensitiveParameters = $this->resolveSensitiveParameters($payload); - - foreach ($sensitiveParameters as $key) { - $value = Crypto::decrypt(base64_decode($payload[$key]), $this->encryptionKey); - - $payload[$key] = in_array($key, $this->scalar) ? $value : json_decode($value, true); - } - - return json_encode($payload); - } - - private function resolveSensitiveParameters(array $payload): array - { - if ($this->obfuscateAll) { - return array_keys($payload); - } - - return array_filter($this->sensitive, static fn (string $key) => array_key_exists($key, $payload)); - } - - public function getDefinition(): Definition - { - return Definition::createFor(self::class, [$this->sensitive, $this->scalar, $this->encryptionKey]); - } -} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php index af0f1a806..643fadb14 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -3,6 +3,7 @@ /** * licence Enterprise */ + namespace Ecotone\DataProtection; use Ecotone\DataProtection\Obfuscator\MessageObfuscator; @@ -14,9 +15,8 @@ class OutboundDecryptionChannelBuilder implements ChannelInterceptorBuilder { - public function __construct( - private string $relatedChannel - ) { + public function __construct(private string $relatedChannel) + { } public function relatedChannelName(): string @@ -31,8 +31,6 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundDecryptionChannelInterceptor::class, [ - Reference::to(MessageObfuscator::class), - ]); + return new Definition(OutboundDecryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); } } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index 28a566405..a638cb6bf 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -11,7 +11,6 @@ use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\MessageBuilder; class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor { @@ -25,13 +24,7 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? return $message; } - $payload = $this->messageObfuscator->decrypt($message); - - $preparedMessage = MessageBuilder::withPayload($payload) - ->setMultipleHeaders($message->getHeaders()->headers()) - ; - - return $preparedMessage->build(); + return $this->messageObfuscator->decrypt($message); } private function canHandle(Message $message): bool diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php index f3378c064..ce24c42ec 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -31,8 +31,6 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundEncryptionChannelInterceptor::class, [ - Reference::to(MessageObfuscator::class), - ]); + return new Definition(OutboundEncryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 4604c073f..2ba652023 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -11,7 +11,6 @@ use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\MessageBuilder; class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor { @@ -25,13 +24,7 @@ public function preSend(Message $message, MessageChannel $messageChannel): ?Mess return $message; } - $payload = $this->messageObfuscator->encrypt($message); - - $preparedMessage = MessageBuilder::withPayload($payload) - ->setMultipleHeaders($message->getHeaders()->headers()) - ; - - return $preparedMessage->build(); + return $this->messageObfuscator->encrypt($message); } private function canHandle(Message $message): bool diff --git a/packages/DataProtection/tests/Fixture/MessageReceiver.php b/packages/DataProtection/tests/Fixture/MessageReceiver.php index 65f2da33d..6530b67bc 100644 --- a/packages/DataProtection/tests/Fixture/MessageReceiver.php +++ b/packages/DataProtection/tests/Fixture/MessageReceiver.php @@ -5,14 +5,21 @@ class MessageReceiver { private ?object $receivedMessage = null; + private array $receivedHeaders = []; - public function withReceivedMessage(object $message): void + public function withReceived(object $message, array $headers): void { $this->receivedMessage = $message; + $this->receivedHeaders = $headers; } - public function receivedMessage(): object + public function receivedMessage(): ?object { return $this->receivedMessage; } + + public function receivedHeaders(): array + { + return $this->receivedHeaders; + } } diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php index 56a8c20dc..26fa7722d 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php @@ -3,10 +3,14 @@ namespace Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; +use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; #[UsingSensitiveData] +#[WithSensitiveHeaders(['foo', 'bar'])] +#[WithSensitiveHeader('fos')] class FullyObfuscatedMessage { public function __construct( diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php index eef1b87aa..fc1f7c2b8 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php @@ -2,18 +2,18 @@ namespace Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages; -use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; #[UsingSensitiveData] +#[WithSensitiveHeader('foo')] +#[WithSensitiveHeader('bar')] class PartiallyObfuscatedMessage { public function __construct( - #[Sensitive] public TestClass $class, - #[Sensitive] public TestEnum $enum, public string $argument ) { diff --git a/packages/DataProtection/tests/Fixture/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/TestCommandHandler.php index 5156dbaa5..2d8a87dfe 100644 --- a/packages/DataProtection/tests/Fixture/TestCommandHandler.php +++ b/packages/DataProtection/tests/Fixture/TestCommandHandler.php @@ -3,6 +3,7 @@ namespace Test\Ecotone\DataProtection\Fixture; use Ecotone\Messaging\Attribute\Asynchronous; +use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; @@ -16,24 +17,27 @@ class TestCommandHandler #[CommandHandler(endpointId: 'test.FullyObfuscatedMessage')] public function handleFullyObfuscatedMessage( #[Payload] FullyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[CommandHandler(endpointId: 'test.PartiallyObfuscatedMessage')] public function handlePartiallyObfuscatedMessage( #[Payload] PartiallyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[CommandHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] public function handleMessageWithSecondaryKeyEncryption( #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } } diff --git a/packages/DataProtection/tests/Fixture/TestEventHandler.php b/packages/DataProtection/tests/Fixture/TestEventHandler.php index 4d3440948..a2ab8c9f9 100644 --- a/packages/DataProtection/tests/Fixture/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/TestEventHandler.php @@ -3,6 +3,7 @@ namespace Test\Ecotone\DataProtection\Fixture; use Ecotone\Messaging\Attribute\Asynchronous; +use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; @@ -17,24 +18,27 @@ class TestEventHandler #[EventHandler(endpointId: 'test.FullyObfuscatedMessage')] public function handleFullyObfuscatedMessage( #[Payload] FullyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[EventHandler(endpointId: 'test.PartiallyObfuscatedMessage')] public function handlePartiallyObfuscatedMessage( #[Payload] PartiallyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[EventHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] public function handleMessageWithSecondaryKeyEncryption( #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } } diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index ebc3b1737..b26bcac76 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -48,19 +48,31 @@ public function test_fully_obfuscated_command_handler_message(): void 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', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->sendCommand($messageSent); + $ecotone->sendCommand($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_partially_obfuscated_command_handler_message(): void @@ -72,19 +84,31 @@ public function test_partially_obfuscated_command_handler_message(): void 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', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', $messagePayload['argument']); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->sendCommand($messageSent); + $ecotone->sendCommand($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_obfuscate_command_handler_message_with_non_default_key(): void @@ -93,9 +117,10 @@ public function test_obfuscate_command_handler_message_with_non_default_key(): v $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + self::assertEquals('{"argument":"value"}', $messagePayload); $ecotone->sendCommand($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); @@ -112,19 +137,31 @@ public function test_fully_obfuscated_event_handler_message(): void 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', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->publishEvent($messageSent); + $ecotone->publishEvent($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_partially_obfuscated_event_handler_message(): void @@ -136,19 +173,31 @@ public function test_partially_obfuscated_event_handler_message(): void 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', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', $messagePayload['argument']); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->publishEvent($messageSent); + $ecotone->publishEvent($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_obfuscate_event_handler_message_with_non_default_key(): void @@ -157,9 +206,10 @@ public function test_obfuscate_event_handler_message_with_non_default_key(): voi $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + self::assertEquals('{"argument":"value"}', $messagePayload); $ecotone->publishEvent($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php index 17c7642d7..49066d74a 100644 --- a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php +++ b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php @@ -17,6 +17,7 @@ class MessageObfuscatorTest extends TestCase { private Message $message; + private Message $messageWithoutTypeId; protected function setUp(): void { @@ -25,77 +26,27 @@ protected function setUp(): void 'bar' => 'value', ], JSON_THROW_ON_ERROR)) ->setHeader(MessageHeaders::TYPE_ID, ObfuscatedMessage::class) + ->setHeader('foo', 'bar') ->build() ; - } - - public function test_obfuscate_message_fully(): void - { - $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); - - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) - ->build() - ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertNotEquals('value', $payload['bar']); - self::assertNotEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - } - - public function test_obfuscate_message_partially(): void - { - $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) + $this->messageWithoutTypeId = MessageBuilder::withPayload(json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR)) + ->setHeader('foo', 'bar') ->build() ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertNotEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); } - public function test_dont_obfuscate_unsupported_message(): void + public function test_obfuscate_only_supported_message(): void { - $obfuscator = new Obfuscator(['foo', 'bar'], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([\stdClass::class => $obfuscator]); - - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) - ->build() - ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + $messageObfuscator = new MessageObfuscator(); - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + self::assertSame($this->message, $messageObfuscator->encrypt($this->message)); + self::assertSame($this->messageWithoutTypeId, $messageObfuscator->encrypt($this->messageWithoutTypeId)); - self::assertEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + self::assertSame($this->message, $messageObfuscator->decrypt($this->message)); + self::assertSame($this->messageWithoutTypeId, $messageObfuscator->decrypt($this->messageWithoutTypeId)); } } diff --git a/packages/DataProtection/tests/Unit/ObfuscatorTest.php b/packages/DataProtection/tests/Unit/ObfuscatorTest.php deleted file mode 100644 index 3007a60d8..000000000 --- a/packages/DataProtection/tests/Unit/ObfuscatorTest.php +++ /dev/null @@ -1,52 +0,0 @@ -payload = json_encode([ - 'foo' => 'value', - 'bar' => 'value', - ], JSON_THROW_ON_ERROR); - } - - public function test_obfuscate_payload_fully(): void - { - $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); - - $encryptedPayload = $obfuscator->encrypt($this->payload); - $decryptedPayload = $obfuscator->decrypt($encryptedPayload); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertNotEquals('value', $payload['bar']); - self::assertNotEquals($this->payload, $encryptedPayload); - self::assertEquals($this->payload, $decryptedPayload); - } - - public function test_obfuscate_payload_partially(): void - { - $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); - - $encryptedPayload = $obfuscator->encrypt($this->payload); - $decryptedPayload = $obfuscator->decrypt($encryptedPayload); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertNotEquals($this->payload, $encryptedPayload); - self::assertEquals($this->payload, $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); - } -} diff --git a/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php b/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php index 927a9e413..9c75584a6 100644 --- a/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php +++ b/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php @@ -151,13 +151,12 @@ public function getPropertiesWithAnnotation(Type $annotationClass): array /** * @return object[] */ - public function getClassAnnotations(): array + public function getClassAnnotations(?ObjectType $annotationType = null): array { - return $this->classAnnotations; - } + if ($annotationType === null) { + return $this->classAnnotations; + } - public function getSingleClassAnnotation(ObjectType $annotationType): object - { $foundAnnotations = []; foreach ($this->classAnnotations as $classAnnotation) { if ($annotationType->accepts($classAnnotation)) { @@ -165,6 +164,13 @@ public function getSingleClassAnnotation(ObjectType $annotationType): object } } + return $foundAnnotations; + } + + public function getSingleClassAnnotation(ObjectType $annotationType): object + { + $foundAnnotations = $this->getClassAnnotations($annotationType); + if (count($foundAnnotations) < 1) { throw InvalidArgumentException::create("Attribute {$annotationType} was not found for {$this}"); } @@ -175,6 +181,13 @@ public function getSingleClassAnnotation(ObjectType $annotationType): object return $foundAnnotations[0]; } + public function findSingleClassAnnotation(ObjectType $annotationType): ?object + { + $foundAnnotations = $this->getClassAnnotations($annotationType); + + return $foundAnnotations[0] ?? null; + } + public function isAnnotation(): bool { return $this->isAnnotation; From 0356036604debf6acacb73aadb98c55108850a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 22:57:42 +0100 Subject: [PATCH 05/20] bump ecotone version --- packages/DataProtection/composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index afc90acb1..5e56ed9fc 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -31,7 +31,8 @@ }, "require": { "ext-openssl": "*", - "ecotone/ecotone": "~1.293.0", + "ecotone/ecotone": "~1.295.0", + "ecotone/jms-converter": "~1.295.0", "defuse/php-encryption": "^2.4" }, "require-dev": { From f02870a1a8886cd1e7be7a6b25058d8395462a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 23:00:37 +0100 Subject: [PATCH 06/20] cleanups --- packages/DataProtection/tests/Unit/MessageObfuscatorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php index 49066d74a..54b1b67b4 100644 --- a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php +++ b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php @@ -2,9 +2,7 @@ namespace Test\Ecotone\DataProtection\Unit; -use Defuse\Crypto\Key; use Ecotone\DataProtection\Obfuscator\MessageObfuscator; -use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\Support\MessageBuilder; From 06bb11e8f36f56b03429b612edc282a03c9a7aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 23:13:24 +0100 Subject: [PATCH 07/20] tests cleanup --- .../src/AmqpBackedMessageChannelBuilder.php | 5 + ...catedMessage.php => ObfuscatedMessage.php} | 2 +- .../PartiallyObfuscatedMessage.php | 21 --- .../TestCommandHandler.php | 32 +++++ .../TestEventHandler.php | 32 +++++ .../tests/Fixture/TestCommandHandler.php | 43 ------ .../tests/Fixture/TestEventHandler.php | 44 ------- .../ObfuscateAnnotatedMessagesTest.php | 124 +++--------------- 8 files changed, 89 insertions(+), 214 deletions(-) rename packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/{FullyObfuscatedMessage.php => ObfuscatedMessage.php} (95%) delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/TestCommandHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/TestEventHandler.php diff --git a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php index 6277d8656..ec7a392e7 100644 --- a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php +++ b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php @@ -48,6 +48,11 @@ public static function create( ); } + private function getAmqpOutboundChannelAdapter(): AmqpOutboundChannelAdapterBuilder + { + return $this->outboundChannelAdapter; + } + /** * @deprecated use withPublisherConfirms * @TODO Ecotone 2.0 remove diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/ObfuscatedMessage.php similarity index 95% rename from packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php rename to packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/ObfuscatedMessage.php index 26fa7722d..c149f2bc7 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/ObfuscatedMessage.php @@ -11,7 +11,7 @@ #[UsingSensitiveData] #[WithSensitiveHeaders(['foo', 'bar'])] #[WithSensitiveHeader('fos')] -class FullyObfuscatedMessage +class ObfuscatedMessage { public function __construct( public TestClass $class, diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php deleted file mode 100644 index fc1f7c2b8..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php +++ /dev/null @@ -1,21 +0,0 @@ -withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.commandHandler.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php new file mode 100644 index 000000000..0d51fcc72 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php @@ -0,0 +1,32 @@ +withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.eventHandler.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/TestCommandHandler.php deleted file mode 100644 index 2d8a87dfe..000000000 --- a/packages/DataProtection/tests/Fixture/TestCommandHandler.php +++ /dev/null @@ -1,43 +0,0 @@ -withReceived($message, $headers); - } - - #[CommandHandler(endpointId: 'test.PartiallyObfuscatedMessage')] - public function handlePartiallyObfuscatedMessage( - #[Payload] PartiallyObfuscatedMessage $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } - - #[CommandHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/TestEventHandler.php b/packages/DataProtection/tests/Fixture/TestEventHandler.php deleted file mode 100644 index a2ab8c9f9..000000000 --- a/packages/DataProtection/tests/Fixture/TestEventHandler.php +++ /dev/null @@ -1,44 +0,0 @@ -withReceived($message, $headers); - } - - #[EventHandler(endpointId: 'test.PartiallyObfuscatedMessage')] - public function handlePartiallyObfuscatedMessage( - #[Payload] PartiallyObfuscatedMessage $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } - - #[EventHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index b26bcac76..eb600cbbe 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -14,20 +14,18 @@ use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; use Ecotone\Messaging\MessageChannel; -use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnection; -use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnection; -use Interop\Amqp\AmqpConnectionFactory; use PHPUnit\Framework\TestCase; -use Test\Ecotone\Amqp\AmqpMessagingTestCase; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\FullyObfuscatedMessage; use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\MessageWithSecondaryKeyEncryption; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\PartiallyObfuscatedMessage; +use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\ObfuscatedMessage; +use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\TestCommandHandler; +use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; -use Test\Ecotone\DataProtection\Fixture\TestCommandHandler; -use Test\Ecotone\DataProtection\Fixture\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestEnum; +/** + * @internal + */ class ObfuscateAnnotatedMessagesTest extends TestCase { private Key $primaryKey; @@ -39,48 +37,12 @@ protected function setUp(): void $this->secondaryKey = Key::createNewRandomKey(); } - public function test_fully_obfuscated_command_handler_message(): void + public function test_obfuscate_command_handler_message(): void { - $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - $messageSent = new FullyObfuscatedMessage( - 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', - ] - ); - - $channelMessage = $channel->receive(); - $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - - $ecotone->sendCommand($messageSent, metadata: $metadataSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); - } - - public function test_partially_obfuscated_command_handler_message(): void - { - $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone->sendCommand( - $messageSent = new PartiallyObfuscatedMessage( + $messageSent = new ObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -113,7 +75,7 @@ enum: TestEnum::FIRST, public function test_obfuscate_command_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); @@ -128,48 +90,12 @@ public function test_obfuscate_command_handler_message_with_non_default_key(): v self::assertEquals($messageSent, $messageReceiver->receivedMessage()); } - public function test_fully_obfuscated_event_handler_message(): void - { - $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone->publishEvent( - $messageSent = new FullyObfuscatedMessage( - 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', - ] - ); - - $channelMessage = $channel->receive(); - $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - - $ecotone->publishEvent($messageSent, metadata: $metadataSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); - } - - public function test_partially_obfuscated_event_handler_message(): void + public function test_obfuscate_event_handler_message(): void { - $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->publishEvent( - $messageSent = new PartiallyObfuscatedMessage( + $messageSent = new ObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -202,7 +128,7 @@ enum: TestEnum::FIRST, public function test_obfuscate_event_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); @@ -217,26 +143,14 @@ public function test_obfuscate_event_handler_message_with_non_default_key(): voi self::assertEquals($messageSent, $messageReceiver->receivedMessage()); } - private function bootstrapEcotoneWithCommandHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport - { - return $this->bootstrapEcotone([TestCommandHandler::class], [new TestCommandHandler()], $messageChannel, $messageReceiver); - } - - private function bootstrapEcotoneWithEventHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport - { - return $this->bootstrapEcotone([TestEventHandler::class], [new TestEventHandler()], $messageChannel, $messageReceiver); - } - - private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport + private function bootstrapEcotone(MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( - classesToResolve: $classesToResolve, - containerOrAvailableServices: array_merge([ + containerOrAvailableServices: [ $receivedMessage, - AmqpConnectionFactory::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), - AmqpExtConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), - AmqpLibConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), - ], $container), + new TestCommandHandler(), + new TestEventHandler(), + ], configuration: ServiceConfiguration::createWithDefaults() ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) From 12d51bbd4272585047105c0a40b9a7be45823ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 23 Jan 2026 21:43:46 +0100 Subject: [PATCH 08/20] allow to configure data protection on: - message - endpoint - channel --- docker-compose.yml | 2 - .../ChannelProtectionConfiguration.php | 47 +++ .../Configuration/DataProtectionModule.php | 107 ++++++- .../src/Configuration/ObfuscatorConfig.php | 23 ++ .../src/Obfuscator/MessageObfuscator.php | 92 ------ .../src/Obfuscator/Obfuscator.php | 53 ++++ .../src/OutboundDecryptionChannelBuilder.php | 19 +- .../OutboundDecryptionChannelInterceptor.php | 58 +++- .../src/OutboundEncryptionChannelBuilder.php | 14 +- .../OutboundEncryptionChannelInterceptor.php | 60 +++- .../tests/Fixture/MessageReceiver.php | 2 +- .../MessageWithSecondaryKeyEncryption.php | 16 + .../ObfuscatedMessage.php | 16 + .../TestCommandHandler.php | 52 +++ .../TestEventHandler.php | 53 ++++ .../ObfuscateChannel/ObfuscatedMessage.php | 16 + .../ObfuscateChannel/TestCommandHandler.php | 31 ++ .../ObfuscateChannel/TestEventHandler.php | 31 ++ .../ObfuscateAnnotatedEndpointsTest.php | 263 +++++++++++++++ .../ObfuscateAnnotatedMessagesTest.php | 111 +++---- .../Integration/ObfuscateChannelTest.php | 300 ++++++++++++++++++ .../tests/Unit/MessageObfuscatorTest.php | 50 --- .../src/Messaging/Handler/InterfaceToCall.php | 66 +++- 23 files changed, 1228 insertions(+), 254 deletions(-) create mode 100644 packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php create mode 100644 packages/DataProtection/src/Configuration/ObfuscatorConfig.php delete mode 100644 packages/DataProtection/src/Obfuscator/MessageObfuscator.php create mode 100644 packages/DataProtection/src/Obfuscator/Obfuscator.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/MessageWithSecondaryKeyEncryption.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateChannel/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php create mode 100644 packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php create mode 100644 packages/DataProtection/tests/Integration/ObfuscateChannelTest.php delete mode 100644 packages/DataProtection/tests/Unit/MessageObfuscatorTest.php diff --git a/docker-compose.yml b/docker-compose.yml index 52a5fe760..237f2e6b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: app: image: simplycodedsoftware/php:8.4.7 diff --git a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php new file mode 100644 index 000000000..81ddad29d --- /dev/null +++ b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php @@ -0,0 +1,47 @@ +channelName; + } + + public function obfuscatorConfig(): ObfuscatorConfig + { + return new ObfuscatorConfig($this->encryptionKey, $this->sensitiveHeaders); + } + + public function withSensitiveHeaders(array $sensitiveHeaders): self + { + Assert::allStrings($sensitiveHeaders, 'Sensitive Headers should be array of strings'); + + $config = clone $this; + $config->sensitiveHeaders = array_merge($this->sensitiveHeaders, $sensitiveHeaders); + + return $config; + } + + public function withSensitiveHeader(string $sensitiveHeader): self + { + $config = clone $this; + $config->sensitiveHeaders[] = $sensitiveHeader; + + return $config; + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index dc37801e4..d579d064e 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -12,12 +12,12 @@ use Ecotone\DataProtection\Attribute\UsingSensitiveData; use Ecotone\DataProtection\Attribute\WithSensitiveHeader; use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; -use Ecotone\DataProtection\Obfuscator\MessageObfuscator; use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; +use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Channel\MessageChannelWithSerializationBuilder; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule; @@ -26,21 +26,28 @@ use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ModuleReferenceSearchService; +use Ecotone\Messaging\Handler\InterfaceToCall; use Ecotone\Messaging\Handler\InterfaceToCallRegistry; use Ecotone\Messaging\Handler\Type; use Ecotone\Messaging\Support\Assert; +use Ecotone\Modelling\Attribute\CommandHandler; +use Ecotone\Modelling\Attribute\EventHandler; use stdClass; #[ModuleAnnotation] final class DataProtectionModule extends NoExternalConfigurationModule { - public function __construct(private array $obfuscators) - { + /** + * @param array $messageObfuscators + */ + public function __construct( + private array $messageObfuscators, + ) { } public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static { - $obfuscators = []; + $messageObfuscators = []; $messagesUsingSensitiveData = $annotationRegistrationService->findAnnotatedClasses(UsingSensitiveData::class); @@ -53,13 +60,49 @@ public static function create(AnnotationFinder $annotationRegistrationService, I $sensitiveHeaders[] = $sensitiveHeader->header; } - $obfuscators[$messageUsingSensitiveData] = [ - 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), - 'sensitiveHeaders' => $sensitiveHeaders, - ]; + $messageObfuscators[$messageUsingSensitiveData] = new ObfuscatorConfig($usingSensitiveDataAttribute->encryptionKeyName(), $sensitiveHeaders); + } + + $endpointsUsingSensitiveData = $annotationRegistrationService->findAnnotatedMethods(UsingSensitiveData::class); + + foreach ($endpointsUsingSensitiveData as $endpointUsingSensitiveData) { + $methodDefinition = $interfaceToCallRegistry->getFor($endpointUsingSensitiveData->getClassName(), $endpointUsingSensitiveData->getMethodName()); + + if (! $methodDefinition->hasAnnotation(CommandHandler::class) && ! $methodDefinition->hasAnnotation(EventHandler::class)) { + Assert::isTrue(false, 'Only CommandHandler and EventHandler can be annotated with UsingSensitiveData.'); + } + + $message = $methodDefinition->getFirstParameter(); + + if (array_key_exists($message->getTypeHint(), $messageObfuscators)) { + continue; + } + + if ($message->hasAnnotation(Payload::class)) { + $registerObfuscatorFor = $message->getTypeHint(); + } else { + $registerObfuscatorFor = self::resolveEndpointId($methodDefinition); + } + + $usingSensitiveDataAttribute = $methodDefinition->getSingleMethodAnnotationOf(Type::create(UsingSensitiveData::class)); + $sensitiveHeaders = $methodDefinition->findSingleMethodAnnotation(Type::create(WithSensitiveHeaders::class))?->headers ?? []; + foreach ($methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) as $sensitiveHeader) { + $sensitiveHeaders[] = $sensitiveHeader->header; + } + + $messageObfuscators[$registerObfuscatorFor] = new ObfuscatorConfig($usingSensitiveDataAttribute->encryptionKeyName(), $sensitiveHeaders); } - return new self($obfuscators); + return new self($messageObfuscators); + } + + private static function resolveEndpointId(InterfaceToCall $methodDefinition): string + { + if ($methodDefinition->hasAnnotation(CommandHandler::class)) { + return $methodDefinition->getSingleMethodAnnotationOf(Type::create(CommandHandler::class))->getEndpointId(); + } + + return $methodDefinition->getSingleMethodAnnotationOf(Type::create(EventHandler::class))->getEndpointId(); } public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void @@ -68,6 +111,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); + $channelProtectionConfigurations = ExtensionObjectResolver::resolve(ChannelProtectionConfiguration::class, $extensionObjects); foreach ($dataProtectionConfiguration->keys() as $encryptionKeyName => $key) { $messagingConfiguration->registerServiceDefinition( @@ -80,21 +124,51 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO ); } - $messageObfuscatorDefinition = new Definition(MessageObfuscator::class); + $channelObfuscatorReferences = $messageObfuscatorReferences = []; + foreach ($channelProtectionConfigurations as $channelProtectionConfiguration) { + $obfuscatorConfig = $channelProtectionConfiguration->obfuscatorConfig(); + $messagingConfiguration->registerServiceDefinition( + id: $id = sprintf('ecotone.encryption.obfuscator.%s', $channelProtectionConfiguration->channelName()), + definition: new Definition( + Obfuscator::class, + [ + Reference::to(sprintf('ecotone.encryption.key.%s', $obfuscatorConfig->encryptionKeyName($dataProtectionConfiguration))), + $obfuscatorConfig->sensitiveHeaders, + ], + ) + ); - foreach ($this->obfuscators as $messageClass => $config) { - $messageObfuscatorDefinition->addMethodCall('withKey', [$messageClass, Reference::to(sprintf('ecotone.encryption.key.%s', $dataProtectionConfiguration->keyName($config['encryptionKey'])))]); - $messageObfuscatorDefinition->addMethodCall('withSensitiveHeaders', [$messageClass, $config['sensitiveHeaders']]); + $channelObfuscatorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id); } - $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: $messageObfuscatorDefinition); + foreach($this->messageObfuscators as $messageClass => $obfuscatorConfig) { + $messagingConfiguration->registerServiceDefinition( + id: $id = sprintf('ecotone.encryption.obfuscator.%s', $messageClass), + definition: new Definition( + Obfuscator::class, + [ + Reference::to(sprintf('ecotone.encryption.key.%s', $obfuscatorConfig->encryptionKeyName($dataProtectionConfiguration))), + $obfuscatorConfig->sensitiveHeaders, + ], + ) + ); + $messageObfuscatorReferences[$messageClass] = Reference::to($id); + } foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { $messagingConfiguration->registerChannelInterceptor( - new OutboundEncryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + new OutboundEncryptionChannelBuilder( + relatedChannel: $pollableMessageChannel->getMessageChannelName(), + channelObfuscatorReference: $channelObfuscatorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, + messageObfuscatorReferences: $messageObfuscatorReferences, + ) ); $messagingConfiguration->registerChannelInterceptor( - new OutboundDecryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + new OutboundDecryptionChannelBuilder( + relatedChannel: $pollableMessageChannel->getMessageChannelName(), + channelObfuscatorReference: $channelObfuscatorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, + messageObfuscatorReferences: $messageObfuscatorReferences, + ) ); } } @@ -103,6 +177,7 @@ public function canHandle($extensionObject): bool { return $extensionObject instanceof DataProtectionConfiguration + || $extensionObject instanceof ChannelProtectionConfiguration || $extensionObject instanceof JMSConverterConfiguration || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()) ; diff --git a/packages/DataProtection/src/Configuration/ObfuscatorConfig.php b/packages/DataProtection/src/Configuration/ObfuscatorConfig.php new file mode 100644 index 000000000..b98bcb624 --- /dev/null +++ b/packages/DataProtection/src/Configuration/ObfuscatorConfig.php @@ -0,0 +1,23 @@ + $sensitiveHeaders + */ + public function __construct( + public ?string $encryptionKey, + 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/Obfuscator/MessageObfuscator.php b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php deleted file mode 100644 index 3e4a3a13e..000000000 --- a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php +++ /dev/null @@ -1,92 +0,0 @@ - - */ - private array $encryptionKeys = []; - - /** - * @var array> - */ - private array $sensitiveHeaders = []; - - public function encrypt(Message $message): Message - { - if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message; - } - - $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->encryptionKeys)) { - return $message; - } - - $key = $this->encryptionKeys[$type]; - $encryptedPayload = base64_encode(Crypto::encrypt($message->getPayload(), $key)); - $headers = $message->getHeaders()->headers(); - foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { - if (array_key_exists($sensitiveHeader, $headers)) { - $headers[$sensitiveHeader] = base64_encode(Crypto::encrypt($headers[$sensitiveHeader], $key)); - } - } - - $preparedMessage = MessageBuilder::withPayload($encryptedPayload) - ->setMultipleHeaders($headers) - ; - - return $preparedMessage->build(); - } - - public function decrypt(Message $message): Message - { - if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message; - } - - $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->encryptionKeys)) { - return $message; - } - - $key = $this->encryptionKeys[$type]; - $decryptedPayload = Crypto::decrypt(base64_decode($message->getPayload()), $key); - $headers = $message->getHeaders()->headers(); - foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { - if (array_key_exists($sensitiveHeader, $headers)) { - $headers[$sensitiveHeader] = Crypto::decrypt(base64_decode($headers[$sensitiveHeader]), $key); - } - } - - $preparedMessage = MessageBuilder::withPayload($decryptedPayload) - ->setMultipleHeaders($headers) - ; - - return $preparedMessage->build(); - } - - public function withKey(string $messageClass, Key $key): void - { - $this->encryptionKeys[$messageClass] = $key; - } - - public function withSensitiveHeaders(string $messageClass, array $headers): void - { - Assert::allStrings($headers, sprintf('Headers for message class %s should be array of strings', $messageClass)); - - $this->sensitiveHeaders[$messageClass] = $headers; - } -} diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php new file mode 100644 index 000000000..1c6026fad --- /dev/null +++ b/packages/DataProtection/src/Obfuscator/Obfuscator.php @@ -0,0 +1,53 @@ +sensitiveHeaders, 'Sensitive headers should be array of strings'); + } + + public function encrypt(Message $message): Message + { + $encryptedPayload = base64_encode(Crypto::encrypt($message->getPayload(), $this->encryptionKey)); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = base64_encode(Crypto::encrypt($headers[$sensitiveHeader], $this->encryptionKey)); + } + } + + $preparedMessage = MessageBuilder::withPayload($encryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); + } + + public function decrypt(Message $message): Message + { + $decryptedPayload = Crypto::decrypt(base64_decode($message->getPayload()), $this->encryptionKey); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = Crypto::decrypt(base64_decode($headers[$sensitiveHeader]), $this->encryptionKey); + } + } + + $preparedMessage = MessageBuilder::withPayload($decryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php index 643fadb14..3afbdc917 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -6,17 +6,20 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\MessageObfuscator; +use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\Messaging\Channel\ChannelInterceptorBuilder; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\MessagingContainerBuilder; use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\PrecedenceChannelInterceptor; -class OutboundDecryptionChannelBuilder implements ChannelInterceptorBuilder +readonly class OutboundDecryptionChannelBuilder implements ChannelInterceptorBuilder { - public function __construct(private string $relatedChannel) - { + public function __construct( + private string $relatedChannel, + private ?Reference $channelObfuscatorReference, + private array $messageObfuscatorReferences, + ) { } public function relatedChannelName(): string @@ -31,6 +34,12 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundDecryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); + return new Definition( + OutboundDecryptionChannelInterceptor::class, + [ + $this->channelObfuscatorReference, + $this->messageObfuscatorReferences, + ] + ); } } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index a638cb6bf..b7afe0397 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -5,32 +5,72 @@ */ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\MessageObfuscator; +use Ecotone\DataProtection\Obfuscator\Obfuscator; 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 { - public function __construct(private MessageObfuscator $messageObfuscator) - { + /** + * @param array $messageObfuscators + */ + public function __construct( + private readonly ?Obfuscator $channelObfuscator, + private readonly array $messageObfuscators, + ) { + Assert::allInstanceOfType($this->messageObfuscators, Obfuscator::class); } public function postReceive(Message $message, MessageChannel $messageChannel): ?Message { - if (! $this->canHandle($message)) { + if (! $message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { return $message; } - return $this->messageObfuscator->decrypt($message); + if ($messageObfuscator = $this->findMessageObfuscator($message)) { + return $messageObfuscator->decrypt($message); + } + + if ($routingObfuscator = $this->findRoutingObfuscator($message)) { + return $routingObfuscator->decrypt($message); + } + + if ($this->channelObfuscator) { + return $this->channelObfuscator->decrypt($message); + } + + return $message; + } + + private function findMessageObfuscator(Message $message): ?Obfuscator + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return null; + } + + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + + return $this->messageObfuscators[$type] ?? null; } - private function canHandle(Message $message): bool + private function findRoutingObfuscator(Message $message): ?Obfuscator { - return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) - && MediaType::parseMediaType($message->getHeaders()->get(MessageHeaders::CONTENT_TYPE))->isCompatibleWith(MediaType::createApplicationJson()) - ; + if (! $message->getHeaders()->containsKey(MessageHeaders::ROUTING_SLIP)) { + return null; + } + + $routingSlip = $message->getHeaders()->get(MessageHeaders::ROUTING_SLIP); + + foreach ($this->messageObfuscators as $routing => $obfuscator) { + if (str_starts_with($routingSlip, $routing)) { + return $obfuscator; + } + } + + return null; } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php index ce24c42ec..79e8c545b 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -5,17 +5,19 @@ */ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\MessageObfuscator; +use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\Messaging\Channel\ChannelInterceptorBuilder; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\MessagingContainerBuilder; use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\PrecedenceChannelInterceptor; -class OutboundEncryptionChannelBuilder implements ChannelInterceptorBuilder +readonly class OutboundEncryptionChannelBuilder implements ChannelInterceptorBuilder { public function __construct( private string $relatedChannel, + private ?Reference $channelObfuscatorReference, + private array $messageObfuscatorReferences, ) { } @@ -31,6 +33,12 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundEncryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); + return new Definition( + OutboundEncryptionChannelInterceptor::class, + [ + $this->channelObfuscatorReference, + $this->messageObfuscatorReferences, + ] + ); } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 2ba652023..8f36a54e8 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -5,32 +5,72 @@ */ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\MessageObfuscator; +use Ecotone\DataProtection\Obfuscator\Obfuscator; 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 { - public function __construct(private MessageObfuscator $messageObfuscator) - { + /** + * @param array $messageObfuscators + */ + public function __construct( + private readonly ?Obfuscator $channelObfuscator, + private readonly array $messageObfuscators, + ) { + Assert::allInstanceOfType($this->messageObfuscators, Obfuscator::class); } public function preSend(Message $message, MessageChannel $messageChannel): ?Message { - if (! $this->canHandle($message)) { - return $message; + if (! $message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { + return null; + } + + if ($messageObfuscator = $this->findMessageObfuscator($message)) { + return $messageObfuscator->encrypt($message); + } + + if ($routingObfuscator = $this->findRoutingObfuscator($message)) { + return $routingObfuscator->encrypt($message); + } + + if ($this->channelObfuscator) { + return $this->channelObfuscator->encrypt($message); + } + + return $message; + } + + private function findMessageObfuscator(Message $message): ?Obfuscator + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return null; } - return $this->messageObfuscator->encrypt($message); + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + + return $this->messageObfuscators[$type] ?? null; } - private function canHandle(Message $message): bool + private function findRoutingObfuscator(Message $message): ?Obfuscator { - return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) - && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()) - ; + if (! $message->getHeaders()->containsKey(MessageHeaders::ROUTING_SLIP)) { + return null; + } + + $routingSlip = $message->getHeaders()->get(MessageHeaders::ROUTING_SLIP); + + foreach ($this->messageObfuscators as $routing => $obfuscator) { + if (str_starts_with($routingSlip, $routing)) { + return $obfuscator; + } + } + + return null; } } diff --git a/packages/DataProtection/tests/Fixture/MessageReceiver.php b/packages/DataProtection/tests/Fixture/MessageReceiver.php index 6530b67bc..1a91a27d6 100644 --- a/packages/DataProtection/tests/Fixture/MessageReceiver.php +++ b/packages/DataProtection/tests/Fixture/MessageReceiver.php @@ -7,7 +7,7 @@ class MessageReceiver private ?object $receivedMessage = null; private array $receivedHeaders = []; - public function withReceived(object $message, array $headers): void + public function withReceived(?object $message, array $headers): void { $this->receivedMessage = $message; $this->receivedHeaders = $headers; diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/MessageWithSecondaryKeyEncryption.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/MessageWithSecondaryKeyEncryption.php new file mode 100644 index 000000000..9f38cd762 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/MessageWithSecondaryKeyEncryption.php @@ -0,0 +1,16 @@ +withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.commandHandler.MessageWithSecondaryKeyEncryption')] + #[UsingSensitiveData('secondary')] + #[WithSensitiveHeader('foo')] + #[WithSensitiveHeader('bar')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[CommandHandler(routingKey: 'command', endpointId: 'test.commandHandler.withRoutingKey')] + #[UsingSensitiveData] + #[WithSensitiveHeaders(['foo', 'bar'])] + #[WithSensitiveHeader('fos')] + public function handleRoutingKey( + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php new file mode 100644 index 000000000..c0a8519bd --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php @@ -0,0 +1,53 @@ +withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.eventHandler.MessageWithSecondaryKeyEncryption')] + #[UsingSensitiveData('secondary')] + #[WithSensitiveHeader('foo')] + #[WithSensitiveHeader('bar')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[EventHandler(listenTo: 'event', endpointId: 'test.eventHandler.withRoutingKey')] + #[UsingSensitiveData] + #[WithSensitiveHeaders(['foo', 'bar'])] + #[WithSensitiveHeader('fos')] + public function handleRoutingKey( + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php new file mode 100644 index 000000000..be353d320 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php @@ -0,0 +1,16 @@ +withReceived($message, $headers); + } + + #[CommandHandler(routingKey: 'command', endpointId: 'test.commandHandler.withRoutingKey')] + public function handleRoutingKey( + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php new file mode 100644 index 000000000..a97d052e2 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php @@ -0,0 +1,31 @@ +withReceived($message, $headers); + } + + #[EventHandler(listenTo: 'event', endpointId: 'test.eventHandler.withRoutingKey')] + public function handleRoutingKey( + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php new file mode 100644 index 000000000..0c7879264 --- /dev/null +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php @@ -0,0 +1,263 @@ +primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); + } + + public function test_obfuscate_command_handler_message(): void + { + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->sendCommand( + $messageSent = new ObfuscatedMessage( + 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'], 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_obfuscate_command_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->sendCommand( + $messageSent = new MessageWithSecondaryKeyEncryption( + 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_obfuscate_command_handler_channel_called_with_routing_key(): void + { + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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_obfuscate_event_handler_message(): void + { + $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent( + $messageSent = new ObfuscatedMessage( + 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', + ] + ); + + $channelMessage = $channel->receive(); + $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); + + $ecotone->publishEvent($messageSent, metadata: $metadataSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + $receivedHeaders = $messageReceiver->receivedHeaders(); + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); + } + + public function test_obfuscate_event_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->publishEvent( + $messageSent = new MessageWithSecondaryKeyEncryption( + 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_obfuscate_event_handler_channel_called_with_routing_key(): void + { + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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')); + } + + private function bootstrapEcotone(MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport + { + return EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + $receivedMessage, + new TestCommandHandler(), + new TestEventHandler(), + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints']) + ->withExtensionObjects([ + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ]) + ); + } +} diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index eb600cbbe..6366b8482 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -8,7 +8,6 @@ use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; -use Ecotone\Messaging\Channel\QueueChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; @@ -22,6 +21,7 @@ use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; +use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; /** * @internal @@ -39,7 +39,7 @@ protected function setUp(): void public function test_obfuscate_command_handler_message(): void { - $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( $messageSent = new ObfuscatedMessage( @@ -54,7 +54,18 @@ enum: TestEnum::FIRST, ] ); - $channelMessage = $channel->receive(); + $ecotone + ->sendCommand($messageSent, metadata: $metadataSent) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; + + $receivedHeaders = $messageReceiver->receivedHeaders(); + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); + + $channelMessage = $channel->getLastSentMessage(); $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); $messageHeaders = $channelMessage->getHeaders(); @@ -62,85 +73,75 @@ enum: TestEnum::FIRST, self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - - $ecotone->sendCommand($messageSent, metadata: $metadataSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_obfuscate_command_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - $channelMessage = $channel->receive(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); + $ecotone + ->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; - self::assertEquals('{"argument":"value"}', $messagePayload); + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - $ecotone->sendCommand($messageSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $messagePayload = Crypto::decrypt(base64_decode($channel->getLastSentMessage()->getPayload()), $this->secondaryKey); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('{"argument":"value"}', $messagePayload); } public function test_obfuscate_event_handler_message(): void { - $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->publishEvent( + $messageSent = new ObfuscatedMessage( + 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()) + ; - $ecotone->publishEvent( - $messageSent = new ObfuscatedMessage( - 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', - ] - ); + $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->receive(); + $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - - $ecotone->publishEvent($messageSent, metadata: $metadataSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); + 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_obfuscate_event_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')) + ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) + ; - $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - $channelMessage = $channel->receive(); + $channelMessage = $channel->getLastSentMessage(); $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); self::assertEquals('{"argument":"value"}', $messagePayload); - - $ecotone->publishEvent($messageSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); } private function bootstrapEcotone(MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport diff --git a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php new file mode 100644 index 000000000..ae46d3d3b --- /dev/null +++ b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php @@ -0,0 +1,300 @@ +primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); + } + + public function test_obfuscate_command_handler_channel_with_default_encryption_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->sendCommand( + $messageSent = new ObfuscatedMessage( + 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', + ] + ); + + $ecotone + ->sendCommand($messageSent, metadata: $metadataSent) + ->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'], 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_obfuscate_command_handler_channel_with_non_default_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test', 'secondary') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->sendCommand( + $messageSent = new ObfuscatedMessage( + 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', + ] + ); + + $ecotone + ->sendCommand($messageSent, metadata: $metadataSent) + ->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_obfuscate_command_handler_channel_called_with_routing_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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_obfuscate_event_handler_channel_with_default_encryption_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->publishEvent( + $messageSent = new ObfuscatedMessage( + 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'], 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_obfuscate_event_handler_channel_with_non_default_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test', 'secondary') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone + ->publishEvent( + $messageSent = new ObfuscatedMessage( + 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_obfuscate_event_handler_channel_called_with_routing_key(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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')); + } + + private function bootstrapEcotone(ChannelProtectionConfiguration $channelProtectionConfiguration, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport + { + return EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + $receivedMessage, + new TestCommandHandler(), + new TestEventHandler(), + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateChannel']) + ->withExtensionObjects([ + $channelProtectionConfiguration, + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ]) + ); + } +} diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php deleted file mode 100644 index 54b1b67b4..000000000 --- a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php +++ /dev/null @@ -1,50 +0,0 @@ -message = MessageBuilder::withPayload(json_encode([ - 'foo' => 'value', - 'bar' => 'value', - ], JSON_THROW_ON_ERROR)) - ->setHeader(MessageHeaders::TYPE_ID, ObfuscatedMessage::class) - ->setHeader('foo', 'bar') - ->build() - ; - - $this->messageWithoutTypeId = MessageBuilder::withPayload(json_encode([ - 'foo' => 'value', - 'bar' => 'value', - ], JSON_THROW_ON_ERROR)) - ->setHeader('foo', 'bar') - ->build() - ; - } - - public function test_obfuscate_only_supported_message(): void - { - $messageObfuscator = new MessageObfuscator(); - - self::assertSame($this->message, $messageObfuscator->encrypt($this->message)); - self::assertSame($this->messageWithoutTypeId, $messageObfuscator->encrypt($this->messageWithoutTypeId)); - - self::assertSame($this->message, $messageObfuscator->decrypt($this->message)); - self::assertSame($this->messageWithoutTypeId, $messageObfuscator->decrypt($this->messageWithoutTypeId)); - } -} diff --git a/packages/Ecotone/src/Messaging/Handler/InterfaceToCall.php b/packages/Ecotone/src/Messaging/Handler/InterfaceToCall.php index d2cb7013d..d4ccac7e4 100644 --- a/packages/Ecotone/src/Messaging/Handler/InterfaceToCall.php +++ b/packages/Ecotone/src/Messaging/Handler/InterfaceToCall.php @@ -138,6 +138,35 @@ public function hasAnnotation(ObjectType|string $className): bool return $this->hasMethodAnnotation($className) || $this->hasClassAnnotation($className); } + /** + * @return class-string|null + */ + public function findSingleAnnotation(Type $className): ?object + { + $annotation = $this->findSingleMethodAnnotation($className); + + if ($annotation === null) { + $annotation = $this->findSingleClassAnnotation($className); + } + + return $annotation; + } + + public function getSingleAnnotation(Type $className): object + { + $annotation = $this->findSingleMethodAnnotation($className); + + if ($annotation === null) { + $annotation = $this->findSingleClassAnnotation($className); + } + + if ($annotation !== null) { + return $annotation; + } + + throw InvalidArgumentException::create("Trying to retrieve not existing annotation {$className} for {$this}"); + } + /** * @return object[] */ @@ -161,11 +190,9 @@ public function getAnnotationsByImportanceOrder(ObjectType|string $className): a public function getSingleClassAnnotationOf(ObjectType|string $className): object { - $classNameType = ObjectType::from($className); - foreach ($this->getClassAnnotations() as $classAnnotation) { - if ($classNameType->accepts($classAnnotation)) { - return $classAnnotation; - } + $classAnnotation = $this->findSingleClassAnnotation($className); + if ($classAnnotation !== null) { + return $classAnnotation; } throw InvalidArgumentException::create("Trying to retrieve not existing class annotation {$className} for {$this}"); @@ -192,14 +219,31 @@ public function getClassAnnotationOf(ObjectType|string $className): array */ public function getSingleMethodAnnotationOf(ObjectType|string $className): object { - $classNameType = ObjectType::from($className); - foreach ($this->methodAnnotations as $methodAnnotation) { - if ($classNameType->accepts($methodAnnotation)) { - return $methodAnnotation; - } + $foundAnnotations = $this->getMethodAnnotationsOf($className); + + if (count($foundAnnotations) < 1) { + throw InvalidArgumentException::create("Attribute {$className} was not found for {$this}"); + } + + if (count($foundAnnotations) > 1) { + throw InvalidArgumentException::create("Looking for single attribute {$className}, however found more than one"); } - throw InvalidArgumentException::create("Trying to retrieve not existing method annotation {$className} for {$this}"); + return $foundAnnotations[0]; + } + + public function findSingleMethodAnnotation(ObjectType|string $className): ?object + { + $foundAnnotations = $this->getMethodAnnotationsOf($className); + + return $foundAnnotations[0] ?? null; + } + + public function findSingleClassAnnotation(ObjectType|string $className): ?object + { + $foundAnnotations = $this->getClassAnnotationOf($className); + + return $foundAnnotations[0] ?? null; } /** From 2e8aaf317069334afc5047a569f42f0a7c1e036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 23 Jan 2026 21:49:25 +0100 Subject: [PATCH 09/20] missing license annotation --- .../src/Configuration/ChannelProtectionConfiguration.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php index 81ddad29d..99a877f0f 100644 --- a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php @@ -4,6 +4,9 @@ use Ecotone\Messaging\Support\Assert; +/** + * licence Enterprise + */ class ChannelProtectionConfiguration { private array $sensitiveHeaders = []; From 647a732f6edee622540a6017552c611e4ac36f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Thu, 5 Feb 2026 22:15:38 +0100 Subject: [PATCH 10/20] API and general behavior changes: - solution provides obfuscation either for payload: either via Domain Message or entire channel - annotated Message will have precedence over channel configuration - obfuscating headers will be additional for message or default for channel as headers are not derivative from domain messages - annotating single endpoint, Ecotone will try to configure obfuscator for Message based on Payload --- .../src/Attribute/Sensitive.php | 13 + .../src/Attribute/UsingSensitiveData.php | 21 - .../src/Attribute/WithEncryptionKey.php | 23 + .../src/Attribute/WithSensitiveHeaders.php | 18 - .../ChannelProtectionConfiguration.php | 24 +- .../Configuration/DataProtectionModule.php | 123 ++-- .../src/Configuration/ObfuscatorConfig.php | 6 +- .../src/Obfuscator/Obfuscator.php | 27 +- .../OutboundDecryptionChannelInterceptor.php | 21 - .../OutboundEncryptionChannelInterceptor.php | 23 +- .../tests/Fixture/AnnotatedMessage.php | 16 + ...tatedMessageWithSecondaryEncryptionKey.php | 18 + .../AnnotatedMessageWithSensitiveHeaders.php | 20 + .../MessageWithSecondaryKeyEncryption.php | 16 - .../ObfuscatedMessage.php | 16 - .../TestCommandHandler.php | 52 -- .../TestEventHandler.php | 53 -- .../MessageWithSecondaryKeyEncryption.php | 14 - .../ObfuscatedMessage.php | 22 - .../TestCommandHandler.php | 32 - .../TestEventHandler.php | 32 - .../ObfuscateChannel/ObfuscatedMessage.php | 16 - .../ObfuscateChannel/TestCommandHandler.php | 11 +- .../ObfuscateChannel/TestEventHandler.php | 9 +- .../CommandHandlerCalledWithRoutingKey.php | 25 + .../CommandHandlerWithAnnotatedEndpoint.php | 29 + ...tedEndpointWithAlreadyAnnotatedMessage.php | 31 + ...atedEndpointWithSecondaryEncryptionKey.php | 30 + ...ndlerWithAnnotatedMethodWithoutPayload.php | 27 + .../CommandHandlerWithAnnotatedPayload.php | 28 + ...tatedPayloadWithSecondaryEncryptionKey.php | 29 + .../EventHandlerCalledWithRoutingKey.php | 25 + .../EventHandlerWithAnnotatedEndpoint.php | 29 + ...tedEndpointWithAlreadyAnnotatedMessage.php | 31 + ...atedEndpointWithSecondaryEncryptionKey.php | 30 + ...ndlerWithAnnotatedMethodWithoutPayload.php | 27 + .../EventHandlerWithAnnotatedPayload.php | 28 + ...tatedPayloadWithSecondaryEncryptionKey.php | 29 + .../ObfuscateMessages/TestCommandHandler.php | 44 ++ .../ObfuscateMessages/TestEventHandler.php | 44 ++ .../tests/Fixture/ObfuscatedMessage.php | 15 - .../tests/Fixture/SomeMessage.php | 13 + .../ObfuscateAnnotatedMessagesTest.php | 166 ----- .../Integration/ObfuscateChannelTest.php | 109 ++- .../Integration/ObfuscateEndpointsTest.php | 666 ++++++++++++++++++ ...intsTest.php => ObfuscateMessagesTest.php} | 224 ++++-- 46 files changed, 1615 insertions(+), 690 deletions(-) create mode 100644 packages/DataProtection/src/Attribute/Sensitive.php delete mode 100644 packages/DataProtection/src/Attribute/UsingSensitiveData.php create mode 100644 packages/DataProtection/src/Attribute/WithEncryptionKey.php delete mode 100644 packages/DataProtection/src/Attribute/WithSensitiveHeaders.php create mode 100644 packages/DataProtection/tests/Fixture/AnnotatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/AnnotatedMessageWithSecondaryEncryptionKey.php create mode 100644 packages/DataProtection/tests/Fixture/AnnotatedMessageWithSensitiveHeaders.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/MessageWithSecondaryKeyEncryption.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/ObfuscatedMessage.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestCommandHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/ObfuscatedMessage.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerCalledWithRoutingKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpoint.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedMethodWithoutPayload.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayload.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerCalledWithRoutingKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpoint.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedMethodWithoutPayload.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayload.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateMessages/TestEventHandler.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/SomeMessage.php delete mode 100644 packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php create mode 100644 packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php rename packages/DataProtection/tests/Integration/{ObfuscateAnnotatedEndpointsTest.php => ObfuscateMessagesTest.php} (54%) diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php new file mode 100644 index 000000000..93c0596de --- /dev/null +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -0,0 +1,13 @@ +encryptionKeyName; - } -} diff --git a/packages/DataProtection/src/Attribute/WithEncryptionKey.php b/packages/DataProtection/src/Attribute/WithEncryptionKey.php new file mode 100644 index 000000000..78c57a086 --- /dev/null +++ b/packages/DataProtection/src/Attribute/WithEncryptionKey.php @@ -0,0 +1,23 @@ +encryptionKey; + } +} diff --git a/packages/DataProtection/src/Attribute/WithSensitiveHeaders.php b/packages/DataProtection/src/Attribute/WithSensitiveHeaders.php deleted file mode 100644 index c75678fe7..000000000 --- a/packages/DataProtection/src/Attribute/WithSensitiveHeaders.php +++ /dev/null @@ -1,18 +0,0 @@ -headers, 'Header names should be all strings.'); - } -} diff --git a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php index 99a877f0f..58bb4f6e3 100644 --- a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php @@ -2,22 +2,22 @@ namespace Ecotone\DataProtection\Configuration; -use Ecotone\Messaging\Support\Assert; - /** * licence Enterprise */ class ChannelProtectionConfiguration { - private array $sensitiveHeaders = []; - - private function __construct(private string $channelName, private ?string $encryptionKey = null) - { + private function __construct( + private string $channelName, + private ?string $encryptionKey, + private bool $isPayloadSensitive, + private array $sensitiveHeaders, + ) { } - public static function create(string $channelName, ?string $encryptionKey = null): self + public static function create(string $channelName, ?string $encryptionKey = null, $isPayloadSensitive = true, array $sensitiveHeaders = []): self { - return new self($channelName, $encryptionKey); + return new self($channelName, $encryptionKey, $isPayloadSensitive, $sensitiveHeaders); } public function channelName(): string @@ -27,15 +27,13 @@ public function channelName(): string public function obfuscatorConfig(): ObfuscatorConfig { - return new ObfuscatorConfig($this->encryptionKey, $this->sensitiveHeaders); + return new ObfuscatorConfig($this->encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders); } - public function withSensitiveHeaders(array $sensitiveHeaders): self + public function withSensitivePayload(bool $isPayloadSensitive): self { - Assert::allStrings($sensitiveHeaders, 'Sensitive Headers should be array of strings'); - $config = clone $this; - $config->sensitiveHeaders = array_merge($this->sensitiveHeaders, $sensitiveHeaders); + $config->isPayloadSensitive = $isPayloadSensitive; return $config; } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index d579d064e..65f313ec9 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -8,16 +8,18 @@ namespace Ecotone\DataProtection\Configuration; use Defuse\Crypto\Key; +use Ecotone\AnnotationFinder\AnnotatedMethod; use Ecotone\AnnotationFinder\AnnotationFinder; -use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\Sensitive; +use Ecotone\DataProtection\Attribute\WithEncryptionKey; use Ecotone\DataProtection\Attribute\WithSensitiveHeader; -use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; -use Ecotone\Messaging\Attribute\Parameter\Payload; +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; @@ -34,75 +36,25 @@ use Ecotone\Modelling\Attribute\EventHandler; use stdClass; +use function Symfony\Component\DependencyInjection\Loader\Configurator\param; + #[ModuleAnnotation] final class DataProtectionModule extends NoExternalConfigurationModule { /** - * @param array $messageObfuscators + * @param array $obfuscatorConfigs */ - public function __construct( - private array $messageObfuscators, - ) { - } - - public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static + public function __construct(private array $obfuscatorConfigs) { - $messageObfuscators = []; - - $messagesUsingSensitiveData = $annotationRegistrationService->findAnnotatedClasses(UsingSensitiveData::class); - - foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { - $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($messageUsingSensitiveData)); - $usingSensitiveDataAttribute = $classDefinition->getSingleClassAnnotation(Type::create(UsingSensitiveData::class)); - - $sensitiveHeaders = $classDefinition->findSingleClassAnnotation(Type::create(WithSensitiveHeaders::class))?->headers ?? []; - foreach ($classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) as $sensitiveHeader) { - $sensitiveHeaders[] = $sensitiveHeader->header; - } - - $messageObfuscators[$messageUsingSensitiveData] = new ObfuscatorConfig($usingSensitiveDataAttribute->encryptionKeyName(), $sensitiveHeaders); - } - - $endpointsUsingSensitiveData = $annotationRegistrationService->findAnnotatedMethods(UsingSensitiveData::class); - - foreach ($endpointsUsingSensitiveData as $endpointUsingSensitiveData) { - $methodDefinition = $interfaceToCallRegistry->getFor($endpointUsingSensitiveData->getClassName(), $endpointUsingSensitiveData->getMethodName()); - - if (! $methodDefinition->hasAnnotation(CommandHandler::class) && ! $methodDefinition->hasAnnotation(EventHandler::class)) { - Assert::isTrue(false, 'Only CommandHandler and EventHandler can be annotated with UsingSensitiveData.'); - } - - $message = $methodDefinition->getFirstParameter(); - - if (array_key_exists($message->getTypeHint(), $messageObfuscators)) { - continue; - } - - if ($message->hasAnnotation(Payload::class)) { - $registerObfuscatorFor = $message->getTypeHint(); - } else { - $registerObfuscatorFor = self::resolveEndpointId($methodDefinition); - } - - $usingSensitiveDataAttribute = $methodDefinition->getSingleMethodAnnotationOf(Type::create(UsingSensitiveData::class)); - $sensitiveHeaders = $methodDefinition->findSingleMethodAnnotation(Type::create(WithSensitiveHeaders::class))?->headers ?? []; - foreach ($methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) as $sensitiveHeader) { - $sensitiveHeaders[] = $sensitiveHeader->header; - } - - $messageObfuscators[$registerObfuscatorFor] = new ObfuscatorConfig($usingSensitiveDataAttribute->encryptionKeyName(), $sensitiveHeaders); - } - - return new self($messageObfuscators); } - private static function resolveEndpointId(InterfaceToCall $methodDefinition): string + public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static { - if ($methodDefinition->hasAnnotation(CommandHandler::class)) { - return $methodDefinition->getSingleMethodAnnotationOf(Type::create(CommandHandler::class))->getEndpointId(); - } + $obfuscatorConfigs = self::resolveObfuscatorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), [], $interfaceToCallRegistry); + $obfuscatorConfigs = self::resolveObfuscatorConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(CommandHandler::class), $obfuscatorConfigs, $interfaceToCallRegistry); + $obfuscatorConfigs = self::resolveObfuscatorConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(EventHandler::class), $obfuscatorConfigs, $interfaceToCallRegistry); - return $methodDefinition->getSingleMethodAnnotationOf(Type::create(EventHandler::class))->getEndpointId(); + return new self($obfuscatorConfigs); } public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void @@ -133,6 +85,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO Obfuscator::class, [ Reference::to(sprintf('ecotone.encryption.key.%s', $obfuscatorConfig->encryptionKeyName($dataProtectionConfiguration))), + $obfuscatorConfig->isPayloadSensitive, $obfuscatorConfig->sensitiveHeaders, ], ) @@ -141,13 +94,14 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO $channelObfuscatorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id); } - foreach($this->messageObfuscators as $messageClass => $obfuscatorConfig) { + foreach ($this->obfuscatorConfigs as $messageClass => $obfuscatorConfig) { $messagingConfiguration->registerServiceDefinition( id: $id = sprintf('ecotone.encryption.obfuscator.%s', $messageClass), definition: new Definition( Obfuscator::class, [ Reference::to(sprintf('ecotone.encryption.key.%s', $obfuscatorConfig->encryptionKeyName($dataProtectionConfiguration))), + $obfuscatorConfig->isPayloadSensitive, $obfuscatorConfig->sensitiveHeaders, ], ) @@ -187,4 +141,47 @@ public function getModulePackageName(): string { return ModulePackageList::DATA_PROTECTION_PACKAGE; } + + private static function resolveObfuscatorConfigsFromAnnotatedClasses(array $sensitiveMessages, array $obfuscatorConfigs, InterfaceToCallRegistry $interfaceToCallRegistry): array + { + foreach ($sensitiveMessages as $message) { + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(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)) ?? []); + + $obfuscatorConfigs[$message] = new ObfuscatorConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders); + } + + return $obfuscatorConfigs; + } + + private static function resolveObfuscatorConfigsFromAnnotatedMethods(array $annotatedMethods, array $obfuscatorConfigs, 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(), $obfuscatorConfigs) + ) { + continue; + } + + $isPayloadSensitive = $payload->hasAnnotation(Sensitive::class) || $methodDefinition->hasAnnotation(Sensitive::class); + if (! $isPayloadSensitive) { + continue; + } + + $encryptionKey = $methodDefinition->findSingleMethodAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); + $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) ?? []); + + $obfuscatorConfigs[$payload->getTypeHint()] = new ObfuscatorConfig($encryptionKey, $isPayloadSensitive, $sensitiveHeaders); + } + + return $obfuscatorConfigs; + } } diff --git a/packages/DataProtection/src/Configuration/ObfuscatorConfig.php b/packages/DataProtection/src/Configuration/ObfuscatorConfig.php index b98bcb624..5a12848dd 100644 --- a/packages/DataProtection/src/Configuration/ObfuscatorConfig.php +++ b/packages/DataProtection/src/Configuration/ObfuscatorConfig.php @@ -1,5 +1,8 @@ sensitiveHeaders, 'Sensitive Headers should be array of strings'); } diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php index 1c6026fad..123938d08 100644 --- a/packages/DataProtection/src/Obfuscator/Obfuscator.php +++ b/packages/DataProtection/src/Obfuscator/Obfuscator.php @@ -1,5 +1,8 @@ sensitiveHeaders, 'Sensitive headers should be array of strings'); @@ -19,7 +23,12 @@ public function __construct( public function encrypt(Message $message): Message { - $encryptedPayload = base64_encode(Crypto::encrypt($message->getPayload(), $this->encryptionKey)); + $payload = $message->getPayload(); + + if ($this->isPayloadSensitive) { + $payload = base64_encode(Crypto::encrypt($payload, $this->encryptionKey)); + } + $headers = $message->getHeaders()->headers(); foreach ($this->sensitiveHeaders as $sensitiveHeader) { if (array_key_exists($sensitiveHeader, $headers)) { @@ -27,16 +36,19 @@ public function encrypt(Message $message): Message } } - $preparedMessage = MessageBuilder::withPayload($encryptedPayload) + return MessageBuilder::withPayload($payload) ->setMultipleHeaders($headers) + ->build() ; - - return $preparedMessage->build(); } public function decrypt(Message $message): Message { - $decryptedPayload = Crypto::decrypt(base64_decode($message->getPayload()), $this->encryptionKey); + $payload = $message->getPayload(); + if ($this->isPayloadSensitive) { + $payload = Crypto::decrypt(base64_decode($payload), $this->encryptionKey); + } + $headers = $message->getHeaders()->headers(); foreach ($this->sensitiveHeaders as $sensitiveHeader) { if (array_key_exists($sensitiveHeader, $headers)) { @@ -44,10 +56,9 @@ public function decrypt(Message $message): Message } } - $preparedMessage = MessageBuilder::withPayload($decryptedPayload) + return MessageBuilder::withPayload($payload) ->setMultipleHeaders($headers) + ->build() ; - - return $preparedMessage->build(); } } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index b7afe0397..de526e65b 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -35,10 +35,6 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? return $messageObfuscator->decrypt($message); } - if ($routingObfuscator = $this->findRoutingObfuscator($message)) { - return $routingObfuscator->decrypt($message); - } - if ($this->channelObfuscator) { return $this->channelObfuscator->decrypt($message); } @@ -56,21 +52,4 @@ private function findMessageObfuscator(Message $message): ?Obfuscator return $this->messageObfuscators[$type] ?? null; } - - private function findRoutingObfuscator(Message $message): ?Obfuscator - { - if (! $message->getHeaders()->containsKey(MessageHeaders::ROUTING_SLIP)) { - return null; - } - - $routingSlip = $message->getHeaders()->get(MessageHeaders::ROUTING_SLIP); - - foreach ($this->messageObfuscators as $routing => $obfuscator) { - if (str_starts_with($routingSlip, $routing)) { - return $obfuscator; - } - } - - return null; - } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 8f36a54e8..22b8e6d1e 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -28,17 +28,13 @@ public function __construct( public function preSend(Message $message, MessageChannel $messageChannel): ?Message { if (! $message->getHeaders()->getContentType()?->isCompatibleWith(MediaType::createApplicationJson())) { - return null; + return $message; } if ($messageObfuscator = $this->findMessageObfuscator($message)) { return $messageObfuscator->encrypt($message); } - if ($routingObfuscator = $this->findRoutingObfuscator($message)) { - return $routingObfuscator->encrypt($message); - } - if ($this->channelObfuscator) { return $this->channelObfuscator->encrypt($message); } @@ -56,21 +52,4 @@ private function findMessageObfuscator(Message $message): ?Obfuscator return $this->messageObfuscators[$type] ?? null; } - - private function findRoutingObfuscator(Message $message): ?Obfuscator - { - if (! $message->getHeaders()->containsKey(MessageHeaders::ROUTING_SLIP)) { - return null; - } - - $routingSlip = $message->getHeaders()->get(MessageHeaders::ROUTING_SLIP); - - foreach ($this->messageObfuscators as $routing => $obfuscator) { - if (str_starts_with($routingSlip, $routing)) { - return $obfuscator; - } - } - - return null; - } } diff --git a/packages/DataProtection/tests/Fixture/AnnotatedMessage.php b/packages/DataProtection/tests/Fixture/AnnotatedMessage.php new file mode 100644 index 000000000..6dc0962b4 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/AnnotatedMessage.php @@ -0,0 +1,16 @@ +withReceived($message, $headers); - } - - #[CommandHandler(endpointId: 'test.commandHandler.MessageWithSecondaryKeyEncryption')] - #[UsingSensitiveData('secondary')] - #[WithSensitiveHeader('foo')] - #[WithSensitiveHeader('bar')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } - - #[CommandHandler(routingKey: 'command', endpointId: 'test.commandHandler.withRoutingKey')] - #[UsingSensitiveData] - #[WithSensitiveHeaders(['foo', 'bar'])] - #[WithSensitiveHeader('fos')] - public function handleRoutingKey( - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived(null, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php deleted file mode 100644 index c0a8519bd..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedEndpoints/TestEventHandler.php +++ /dev/null @@ -1,53 +0,0 @@ -withReceived($message, $headers); - } - - #[EventHandler(endpointId: 'test.eventHandler.MessageWithSecondaryKeyEncryption')] - #[UsingSensitiveData('secondary')] - #[WithSensitiveHeader('foo')] - #[WithSensitiveHeader('bar')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } - - #[EventHandler(listenTo: 'event', endpointId: 'test.eventHandler.withRoutingKey')] - #[UsingSensitiveData] - #[WithSensitiveHeaders(['foo', 'bar'])] - #[WithSensitiveHeader('fos')] - public function handleRoutingKey( - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived(null, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php deleted file mode 100644 index 075f6c04f..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php +++ /dev/null @@ -1,14 +0,0 @@ -withReceived($message, $headers); - } - - #[CommandHandler(endpointId: 'test.commandHandler.MessageWithSecondaryKeyEncryption')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php deleted file mode 100644 index 0d51fcc72..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestEventHandler.php +++ /dev/null @@ -1,32 +0,0 @@ -withReceived($message, $headers); - } - - #[EventHandler(endpointId: 'test.eventHandler.MessageWithSecondaryKeyEncryption')] - public function handleMessageWithSecondaryKeyEncryption( - #[Payload] MessageWithSecondaryKeyEncryption $message, - #[Headers] array $headers, - #[Reference] MessageReceiver $messageReceiver, - ): void { - $messageReceiver->withReceived($message, $headers); - } -} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php deleted file mode 100644 index be353d320..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscateChannel/ObfuscatedMessage.php +++ /dev/null @@ -1,16 +0,0 @@ -withReceived($message, $headers); } - #[CommandHandler(routingKey: 'command', endpointId: 'test.commandHandler.withRoutingKey')] - public function handleRoutingKey( + #[CommandHandler(routingKey: 'command', endpointId: 'test.ObfuscateChannel.commandHandler.withoutPayload')] + public function withoutPayload( #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php index a97d052e2..3b8265726 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php @@ -8,20 +8,21 @@ use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\EventHandler; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; +use Test\Ecotone\DataProtection\Fixture\SomeMessage; #[Asynchronous('test')] class TestEventHandler { - #[EventHandler(endpointId: 'test.eventHandler.FullyObfuscatedMessage')] + #[EventHandler(endpointId: 'test.ObfuscateChannel.eventHandler.withPayload')] public function handleFullyObfuscatedMessage( - #[Payload] ObfuscatedMessage $message, - #[Headers] array $headers, + SomeMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { $messageReceiver->withReceived($message, $headers); } - #[EventHandler(listenTo: 'event', endpointId: 'test.eventHandler.withRoutingKey')] + #[EventHandler(listenTo: 'event', endpointId: 'test.ObfuscateChannel.eventHandler.withoutPayload')] public function handleRoutingKey( #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerCalledWithRoutingKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerCalledWithRoutingKey.php new file mode 100644 index 000000000..5dbc61b3a --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerCalledWithRoutingKey.php @@ -0,0 +1,25 @@ +withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpoint.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpoint.php new file mode 100644 index 000000000..e22ad7429 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpoint.php @@ -0,0 +1,29 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php new file mode 100644 index 000000000..e09c759d6 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php @@ -0,0 +1,31 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php new file mode 100644 index 000000000..f382eff34 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php @@ -0,0 +1,30 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedMethodWithoutPayload.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedMethodWithoutPayload.php new file mode 100644 index 000000000..e43565a06 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedMethodWithoutPayload.php @@ -0,0 +1,27 @@ +withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayload.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayload.php new file mode 100644 index 000000000..d3e9f4660 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayload.php @@ -0,0 +1,28 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php new file mode 100644 index 000000000..7727af39c --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php @@ -0,0 +1,29 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerCalledWithRoutingKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerCalledWithRoutingKey.php new file mode 100644 index 000000000..84291a1e3 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerCalledWithRoutingKey.php @@ -0,0 +1,25 @@ +withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpoint.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpoint.php new file mode 100644 index 000000000..a994e5df0 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpoint.php @@ -0,0 +1,29 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php new file mode 100644 index 000000000..f0ef48803 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage.php @@ -0,0 +1,31 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php new file mode 100644 index 000000000..daa20b722 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey.php @@ -0,0 +1,30 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedMethodWithoutPayload.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedMethodWithoutPayload.php new file mode 100644 index 000000000..58a213433 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedMethodWithoutPayload.php @@ -0,0 +1,27 @@ +withReceived(null, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayload.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayload.php new file mode 100644 index 000000000..f6feabe91 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayload.php @@ -0,0 +1,28 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php new file mode 100644 index 000000000..aace17a23 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadWithSecondaryEncryptionKey.php @@ -0,0 +1,29 @@ +withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestCommandHandler.php new file mode 100644 index 000000000..872ce0870 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestCommandHandler.php @@ -0,0 +1,44 @@ +withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.obfuscateAnnotatedMessages.commandHandler.AnnotatedMessageWithSecondaryEncryptionKey')] + public function handleAnnotatedMessageWithSecondaryEncryptionKey( + #[Payload] AnnotatedMessageWithSecondaryEncryptionKey $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[CommandHandler(endpointId: 'test.obfuscateAnnotatedMessages.commandHandler.AnnotatedMessageWithSensitiveHeaders')] + public function handleAnnotatedMessageWithSensitiveHeaders( + #[Payload] AnnotatedMessageWithSensitiveHeaders $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestEventHandler.php new file mode 100644 index 000000000..4fbca01e0 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateMessages/TestEventHandler.php @@ -0,0 +1,44 @@ +withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.obfuscateAnnotatedMessages.eventHandler.AnnotatedMessageWithSecondaryEncryptionKey')] + public function handleAnnotatedMessageWithSecondaryEncryptionKey( + #[Payload] AnnotatedMessageWithSecondaryEncryptionKey $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } + + #[EventHandler(endpointId: 'test.obfuscateAnnotatedMessages.eventHandler.AnnotatedMessageWithSensitiveHeaders')] + public function handleAnnotatedMessageWithSensitiveHeaders( + #[Payload] AnnotatedMessageWithSensitiveHeaders $message, + #[Headers] array $headers, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceived($message, $headers); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscatedMessage.php deleted file mode 100644 index be8da6782..000000000 --- a/packages/DataProtection/tests/Fixture/ObfuscatedMessage.php +++ /dev/null @@ -1,15 +0,0 @@ -primaryKey = Key::createNewRandomKey(); - $this->secondaryKey = Key::createNewRandomKey(); - } - - public function test_obfuscate_command_handler_message(): void - { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone->sendCommand( - $messageSent = new ObfuscatedMessage( - 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', - ] - ); - - $ecotone - ->sendCommand($messageSent, metadata: $metadataSent) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - } - - public function test_obfuscate_command_handler_message_with_non_default_key(): void - { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone - ->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - - $messagePayload = Crypto::decrypt(base64_decode($channel->getLastSentMessage()->getPayload()), $this->secondaryKey); - - self::assertEquals('{"argument":"value"}', $messagePayload); - } - - public function test_obfuscate_event_handler_message(): void - { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone - ->publishEvent( - $messageSent = new ObfuscatedMessage( - 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'], 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_obfuscate_event_handler_message_with_non_default_key(): void - { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone - ->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')) - ->run('test', ExecutionPollingMetadata::createWithTestingSetup()) - ; - - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - - $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - - self::assertEquals('{"argument":"value"}', $messagePayload); - } - - private function bootstrapEcotone(MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport - { - return EcotoneLite::bootstrapFlowTesting( - containerOrAvailableServices: [ - $receivedMessage, - new TestCommandHandler(), - new TestEventHandler(), - ], - configuration: ServiceConfiguration::createWithDefaults() - ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) - ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) - ->withExtensionObjects([ - DataProtectionConfiguration::create('primary', $this->primaryKey) - ->withKey('secondary', $this->secondaryKey), - SimpleMessageChannelBuilder::create('test', $messageChannel), - JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), - ]) - ); - } -} diff --git a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php index ae46d3d3b..039845850 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php @@ -16,9 +16,9 @@ use Ecotone\Messaging\MessageChannel; use PHPUnit\Framework\TestCase; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; -use Test\Ecotone\DataProtection\Fixture\ObfuscateChannel\ObfuscatedMessage; use Test\Ecotone\DataProtection\Fixture\ObfuscateChannel\TestCommandHandler; use Test\Ecotone\DataProtection\Fixture\ObfuscateChannel\TestEventHandler; +use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; @@ -40,14 +40,15 @@ protected function setUp(): void public function test_obfuscate_command_handler_channel_with_default_encryption_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - $messageSent = new ObfuscatedMessage( + $messageSent = new SomeMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -80,17 +81,62 @@ enum: TestEnum::FIRST, self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_obfuscate_command_handler_channel_with_default_encryption_key_and_no_sensitive_payload(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitivePayload(false) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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', + ] + ); + + $ecotone + ->sendCommand($messageSent, metadata: $metadataSent) + ->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(); + $messageHeaders = $channelMessage->getHeaders(); + + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $channelMessage->getPayload()); + 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_obfuscate_command_handler_channel_with_non_default_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test', 'secondary') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - $messageSent = new ObfuscatedMessage( + $messageSent = new SomeMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -126,7 +172,8 @@ enum: TestEnum::FIRST, public function test_obfuscate_command_handler_channel_called_with_routing_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; @@ -159,10 +206,48 @@ public function test_obfuscate_command_handler_channel_called_with_routing_key() self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_obfuscate_command_handler_channel_called_with_routing_key_and_no_sensitive_payload(): void + { + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') + ->withSensitivePayload(false) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') + ->withSensitiveHeader('fos') + ; + + $ecotone = $this->bootstrapEcotone($channelProtectionConfiguration, $channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $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'], 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_obfuscate_event_handler_channel_with_default_encryption_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; @@ -170,7 +255,7 @@ public function test_obfuscate_event_handler_channel_with_default_encryption_key $ecotone ->publishEvent( - $messageSent = new ObfuscatedMessage( + $messageSent = new SomeMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -203,7 +288,8 @@ enum: TestEnum::FIRST, public function test_obfuscate_event_handler_channel_with_non_default_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test', 'secondary') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; @@ -211,7 +297,7 @@ public function test_obfuscate_event_handler_channel_with_non_default_key(): voi $ecotone ->publishEvent( - $messageSent = new ObfuscatedMessage( + $messageSent = new SomeMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -244,7 +330,8 @@ enum: TestEnum::FIRST, public function test_obfuscate_event_handler_channel_called_with_routing_key(): void { $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test') - ->withSensitiveHeaders(['foo', 'bar']) + ->withSensitiveHeader('foo') + ->withSensitiveHeader('bar') ->withSensitiveHeader('fos') ; diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php new file mode 100644 index 000000000..bbc49e30d --- /dev/null +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -0,0 +1,666 @@ +primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); + } + + public function test_command_handler_with_annotated_endpoint(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + CommandHandlerWithAnnotatedEndpoint::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new CommandHandlerWithAnnotatedEndpoint(), + ], + 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->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')); + } + + public function test_command_handler_with_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_command_handler_with_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_command_handler_called_with_routing_key(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + CommandHandlerCalledWithRoutingKey::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new CommandHandlerCalledWithRoutingKey(), + ], + 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_command_handler_called_with_routing_key_will_use_channel_obfuscator(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + CommandHandlerCalledWithRoutingKey::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new CommandHandlerCalledWithRoutingKey(), + ], + 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_command_handler_with_annotated_method_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_command_handler_with_annotated_method_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_event_handler_with_annotated_endpoint(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + EventHandlerWithAnnotatedEndpoint::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new EventHandlerWithAnnotatedEndpoint(), + ], + 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->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')); + } + + public function test_event_handler_with_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_event_handler_with_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_event_handler_called_with_routing_key(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + EventHandlerCalledWithRoutingKey::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new EventHandlerCalledWithRoutingKey(), + ], + 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')); + } + + public function test_event_handler_called_with_routing_key_will_use_channel_obfuscator(): void + { + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + EventHandlerCalledWithRoutingKey::class, + ], + container: [ + $messageReceiver = new MessageReceiver(), + new EventHandlerCalledWithRoutingKey(), + ], + 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_event_handler_with_annotated_method_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_event_handler_with_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() + ->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/ObfuscateAnnotatedEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php similarity index 54% rename from packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php rename to packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php index 0c7879264..d21b116ae 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php @@ -4,27 +4,31 @@ use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; +use Ecotone\DataProtection\Configuration\ChannelProtectionConfiguration; use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; -use Ecotone\Messaging\Channel\QueueChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; use Ecotone\Messaging\MessageChannel; 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\MessageReceiver; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints\MessageWithSecondaryKeyEncryption; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints\ObfuscatedMessage; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints\TestCommandHandler; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints\TestEventHandler; +use Test\Ecotone\DataProtection\Fixture\ObfuscateMessages\TestCommandHandler; +use Test\Ecotone\DataProtection\Fixture\ObfuscateMessages\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; -class ObfuscateAnnotatedEndpointsTest extends TestCase +/** + * @internal + */ +class ObfuscateMessagesTest extends TestCase { private Key $primaryKey; private Key $secondaryKey; @@ -35,13 +39,23 @@ protected function setUp(): void $this->secondaryKey = Key::createNewRandomKey(); } - public function test_obfuscate_command_handler_message(): void + public function test_command_handler_with_obfuscate_annotated_message(): void { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessage::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') + ); $ecotone ->sendCommand( - $messageSent = new ObfuscatedMessage( + $messageSent = new AnnotatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -66,18 +80,31 @@ enum: TestEnum::FIRST, $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['foo'], $messageHeaders->get('foo')); + self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } public function test_obfuscate_command_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSecondaryEncryptionKey::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + extensionObjects: [ + ChannelProtectionConfiguration::create('test', encryptionKey: 'primary'), + ] + ); $ecotone ->sendCommand( - $messageSent = new MessageWithSecondaryKeyEncryption( + $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -102,18 +129,32 @@ enum: TestEnum::FIRST, $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['foo'], $messageHeaders->get('foo')); + self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } - public function test_obfuscate_command_handler_channel_called_with_routing_key(): void + public function test_obfuscate_command_handler_message_with_sensitive_headers(): void { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSensitiveHeaders::class, + TestCommandHandler::class, + ], + container: [ + new TestCommandHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') + ); $ecotone - ->sendCommandWithRoutingKey( - routingKey: 'command', + ->sendCommand( + $messageSent = new AnnotatedMessageWithSensitiveHeaders( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ), metadata: $metadataSent = [ 'foo' => 'secret-value', 'bar' => 'even-more-secret-value', @@ -124,63 +165,89 @@ public function test_obfuscate_command_handler_channel_called_with_routing_key() ; $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('[]', $messagePayload); + 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')); } - public function test_obfuscate_event_handler_message(): void + public function test_obfuscate_event_handler_with_annotated_message(): void { - $ecotone = $this->bootstrapEcotone($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); - - $ecotone->publishEvent( - $messageSent = new ObfuscatedMessage( - 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', - ] + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessage::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') ); - $channelMessage = $channel->receive(); + $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('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); - self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); - self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - - $ecotone->publishEvent($messageSent, metadata: $metadataSent); - $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); - - $receivedHeaders = $messageReceiver->receivedHeaders(); - self::assertEquals($messageSent, $messageReceiver->receivedMessage()); - self::assertEquals('secret-value', $receivedHeaders['foo']); - self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); - self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); + self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); + self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); + self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } public function test_obfuscate_event_handler_message_with_non_default_key(): void { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSecondaryEncryptionKey::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test'), + extensionObjects: [ + ChannelProtectionConfiguration::create('test', encryptionKey: 'primary'), + ] + ); $ecotone ->publishEvent( - $messageSent = new MessageWithSecondaryKeyEncryption( + $messageSent = new AnnotatedMessageWithSecondaryEncryptionKey( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', @@ -205,18 +272,32 @@ enum: TestEnum::FIRST, $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['foo'], $messageHeaders->get('foo')); + self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } - public function test_obfuscate_event_handler_channel_called_with_routing_key(): void + public function test_obfuscate_event_handler_message_with_sensitive_headers(): void { - $ecotone = $this->bootstrapEcotone($channel = TestQueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + $ecotone = $this->bootstrapEcotone( + classesToResolve: [ + AnnotatedMessageWithSensitiveHeaders::class, + TestEventHandler::class, + ], + container: [ + new TestEventHandler(), + $messageReceiver = new MessageReceiver(), + ], + messageChannel: $channel = TestQueueChannel::create('test') + ); $ecotone - ->publishEventWithRoutingKey( - routingKey: 'event', + ->publishEvent( + $messageSent = new AnnotatedMessageWithSensitiveHeaders( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ), metadata: $metadataSent = [ 'foo' => 'secret-value', 'bar' => 'even-more-secret-value', @@ -227,37 +308,42 @@ public function test_obfuscate_event_handler_channel_called_with_routing_key(): ; $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('[]', $messagePayload); + 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')); } - private function bootstrapEcotone(MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport + private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, array $extensionObjects = []): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( - containerOrAvailableServices: [ - $receivedMessage, - new TestCommandHandler(), - new TestEventHandler(), - ], + classesToResolve: $classesToResolve, + containerOrAvailableServices: $container, configuration: ServiceConfiguration::createWithDefaults() ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) - ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedEndpoints']) - ->withExtensionObjects([ - DataProtectionConfiguration::create('primary', $this->primaryKey) - ->withKey('secondary', $this->secondaryKey), - SimpleMessageChannelBuilder::create('test', $messageChannel), - JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), - ]) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) + ->withExtensionObjects( + array_merge( + [ + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ], + $extensionObjects, + ) + ) ); } } From 510c25f135de33f2d1b052d07dc791078490ed09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 16:56:11 +0100 Subject: [PATCH 11/20] code review changes check license ensure data protection is applied on pollable channels ensure ChannelInterceptor::postReceive() modifies message when defined --- .../Configuration/DataProtectionModule.php | 21 ++++++- ...ndHandlerWithAnnotatedPayloadAndHeader.php | 26 +++++++++ ...ntHandlerWithAnnotatedPayloadAndHeader.php | 27 +++++++++ .../Integration/ObfuscateChannelTest.php | 28 ++++++++++ .../Integration/ObfuscateEndpointsTest.php | 5 ++ .../Integration/ObfuscateMessagesTest.php | 2 + .../Integration/RequirementsCheckTest.php | 56 +++++++++++++++++++ .../src/Messaging/Config/Configuration.php | 8 +++ .../Config/MessagingSystemConfiguration.php | 5 ++ .../Unit/Channel/ChannelInterceptorTest.php | 10 +++- 10 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php create mode 100644 packages/DataProtection/tests/Integration/RequirementsCheckTest.php diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 65f313ec9..ae8969c3d 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -28,10 +28,10 @@ use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ModuleReferenceSearchService; -use Ecotone\Messaging\Handler\InterfaceToCall; 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; @@ -59,6 +59,8 @@ public static function create(AnnotationFinder $annotationRegistrationService, I public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void { + $this->verifyLicense($messagingConfiguration, $extensionObjects); + Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); @@ -78,6 +80,8 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO $channelObfuscatorReferences = $messageObfuscatorReferences = []; foreach ($channelProtectionConfigurations as $channelProtectionConfiguration) { + Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName()), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName())); + $obfuscatorConfig = $channelProtectionConfiguration->obfuscatorConfig(); $messagingConfiguration->registerServiceDefinition( id: $id = sprintf('ecotone.encryption.obfuscator.%s', $channelProtectionConfiguration->channelName()), @@ -110,6 +114,10 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO } foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { + if (! $pollableMessageChannel->isPollable()) { + continue; + } + $messagingConfiguration->registerChannelInterceptor( new OutboundEncryptionChannelBuilder( relatedChannel: $pollableMessageChannel->getMessageChannelName(), @@ -184,4 +192,15 @@ private static function resolveObfuscatorConfigsFromAnnotatedMethods(array $anno return $obfuscatorConfigs; } + + private function verifyLicense(Configuration $messagingConfiguration, array $extensionObjects): void + { + if ($messagingConfiguration->isRunningForEnterpriseLicence()) { + return; + } + + if (ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects)) { + throw LicensingException::create('Data Protection module is available only with Ecotone Enterprise Licence.'); + } + } } diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php new file mode 100644 index 000000000..ad5696b63 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php @@ -0,0 +1,26 @@ +withReceived($message, [$header]); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php new file mode 100644 index 000000000..c74295d6f --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php @@ -0,0 +1,27 @@ +withReceived($message, [$header]); + } +} diff --git a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php index 039845850..072dc6fad 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php @@ -9,11 +9,14 @@ use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; +use Ecotone\Messaging\Channel\DirectChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; use Ecotone\Messaging\MessageChannel; +use Ecotone\Messaging\Support\InvalidArgumentException; +use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; use Test\Ecotone\DataProtection\Fixture\ObfuscateChannel\TestCommandHandler; @@ -364,6 +367,30 @@ public function test_obfuscate_event_handler_channel_called_with_routing_key(): self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_obfuscate_non_pollable_channel(): void + { + $this->expectExceptionObject(InvalidArgumentException::create("`test` channel must be pollable channel to use Data Protection.")); + + $channelProtectionConfiguration = ChannelProtectionConfiguration::create('test'); + + EcotoneLite::bootstrapFlowTesting( + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\NonPollable']) + ->withExtensionObjects( + [ + $channelProtectionConfiguration, + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', new DirectChannel('test')), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), + ] + ) + ) + ; + } + private function bootstrapEcotone(ChannelProtectionConfiguration $channelProtectionConfiguration, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( @@ -373,6 +400,7 @@ private function bootstrapEcotone(ChannelProtectionConfiguration $channelProtect new TestEventHandler(), ], configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateChannel']) ->withExtensionObjects([ diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php index bbc49e30d..2eb8ebd9d 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -9,11 +9,13 @@ use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; +use Ecotone\Messaging\Channel\DirectChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; use Ecotone\Messaging\MessageChannel; +use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; @@ -27,9 +29,11 @@ use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedMethodWithoutPayload; +use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\NoPollableEventHandler; use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; +use Test\Ecotone\Messaging\Fixture\Handler\StatefulHandler; use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; /** @@ -652,6 +656,7 @@ private function bootstrapEcotone(array $classesToResolve, array $container, Mes 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([ diff --git a/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php index d21b116ae..2bdc80947 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php @@ -14,6 +14,7 @@ use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; use Ecotone\Messaging\MessageChannel; +use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\AnnotatedMessageWithSecondaryEncryptionKey; @@ -331,6 +332,7 @@ private function bootstrapEcotone(array $classesToResolve, array $container, Mes 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])) ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) ->withExtensionObjects( diff --git a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php new file mode 100644 index 000000000..370c2d516 --- /dev/null +++ b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php @@ -0,0 +1,56 @@ +expectException(LicensingException::class); + + EcotoneLite::bootstrapFlowTesting( + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) + ->withExtensionObjects([ + DataProtectionConfiguration::create('primary', Key::createNewRandomKey()), + ]) + ); + } + + public function test_module_require_data_protection_configuration(): void + { + $this->expectExceptionObject(InvalidArgumentException::create('Ecotone\DataProtection\Configuration\DataProtectionConfiguration was not found.')); + + EcotoneLite::bootstrapFlowTesting( + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) + ); + } + + public function test_module_require_jms_converter_configuration(): void + { + $this->expectExceptionObject(InvalidArgumentException::create('Ecotone\DataProtection\Configuration\DataProtectionConfiguration was not found.')); + + EcotoneLite::bootstrapFlowTesting( + configuration: ServiceConfiguration::createWithDefaults() + ->withLicenceKey(LicenceTesting::VALID_LICENCE) + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) + ); + } +} diff --git a/packages/Ecotone/src/Messaging/Config/Configuration.php b/packages/Ecotone/src/Messaging/Config/Configuration.php index ea68f6210..c9a2fd4c9 100644 --- a/packages/Ecotone/src/Messaging/Config/Configuration.php +++ b/packages/Ecotone/src/Messaging/Config/Configuration.php @@ -166,4 +166,12 @@ public function withExternalContainer(?ContainerInterface $externalContainer): C * @return array Map of referenceId => errorMessage */ public function getRequiredReferencesForValidation(): array; + + /** + * Checks if the channel is pollable. + * + * @param string $channelName + * @return bool + */ + public function isPollableChannel(string $channelName): bool; } diff --git a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php index 4b106d994..76997c756 100644 --- a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php +++ b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php @@ -1129,4 +1129,9 @@ public function isChannelDefinedFor(MessageHandlerBuilderWithOutputChannel|Messa return false; } + + public function isPollableChannel(string $channelName): bool + { + return $this->channelBuilders[$channelName]?->isPollable(); + } } diff --git a/packages/Ecotone/tests/Messaging/Unit/Channel/ChannelInterceptorTest.php b/packages/Ecotone/tests/Messaging/Unit/Channel/ChannelInterceptorTest.php index bf15c13d7..fe8056bfc 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Channel/ChannelInterceptorTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Channel/ChannelInterceptorTest.php @@ -108,13 +108,18 @@ public function test_intercepting_receiving_message_with_success() $queueChannel = QueueChannel::create(); $queueChannel->send($message); - $channelInterceptor = new TestChannelInterceptor(null, true, false, null); + $channelInterceptor = new TestChannelInterceptor( + returnMessageOnPreSend: null, + returnValueOnPreReceive: true, + returnValueOnAfterSendCompletion: false, + returnMessageOnPostReceive: $expectedReceivedMessage = MessageBuilder::withPayload('some2')->build() + ); $pollableChannel = new PollableChannelInterceptorAdapter( $queueChannel, [$channelInterceptor] ); - $pollableChannel->receive(); + $receivedMessage = $pollableChannel->receive(); $this->assertTrue($channelInterceptor->wasPreReceiveCalled()); $this->assertTrue($channelInterceptor->wasPostReceiveCalled()); @@ -122,6 +127,7 @@ public function test_intercepting_receiving_message_with_success() $this->assertSame($message, $channelInterceptor->getCapturedMessage()); $this->assertSame($queueChannel, $channelInterceptor->getCapturedChannel()); $this->assertNull($channelInterceptor->getCapturedException()); + $this->assertEquals($expectedReceivedMessage, $receivedMessage); } public function test_stopping_message_receiving() From cad5aed734b9d26b4050f72dcafe8cf8f2963c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 18:27:19 +0100 Subject: [PATCH 12/20] sensitive headers can be defined in parameters --- .../Configuration/DataProtectionModule.php | 6 ++ ...ndHandlerWithAnnotatedPayloadAndHeader.php | 6 +- ...ntHandlerWithAnnotatedPayloadAndHeader.php | 5 +- .../Integration/ObfuscateEndpointsTest.php | 86 +++++++++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index ae8969c3d..010e458d5 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -187,6 +187,12 @@ private static function resolveObfuscatorConfigsFromAnnotatedMethods(array $anno $encryptionKey = $methodDefinition->findSingleMethodAnnotation(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(); + } + } + $obfuscatorConfigs[$payload->getTypeHint()] = new ObfuscatorConfig($encryptionKey, $isPayloadSensitive, $sensitiveHeaders); } diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php index ad5696b63..5e10b496e 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/CommandHandlerWithAnnotatedPayloadAndHeader.php @@ -15,12 +15,14 @@ #[Asynchronous('test')] class CommandHandlerWithAnnotatedPayloadAndHeader { + #[WithSensitiveHeader('bar')] #[CommandHandler(endpointId: 'test.obfuscateAnnotatedEndpoints.commandHandler.annotatedMethod')] public function annotatedMethod( #[Sensitive] SomeMessage $message, - #[Sensitive] #[Header('foo')] string $header, + #[Sensitive] #[Header('foo')] string $foo, + #[Header('bar')] string $bar, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceived($message, [$header]); + $messageReceiver->withReceived($message, ['foo' => $foo, 'bar' => $bar]); } } diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php index c74295d6f..56ee97321 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php @@ -19,9 +19,10 @@ class EventHandlerWithAnnotatedPayloadAndHeader #[EventHandler(endpointId: 'test.obfuscateAnnotatedEndpoints.eventHandler.annotatedMethod')] public function annotatedMethod( #[Sensitive] SomeMessage $message, - #[Sensitive] #[Header('foo')] string $header, + #[Sensitive] #[Header('foo')] string $foo, + #[Header('bar')] string $bar, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceived($message, [$header]); + $messageReceiver->withReceived($message, ['foo' => $foo, 'bar' => $bar]); } } diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php index 2eb8ebd9d..8d1c80440 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -24,11 +24,13 @@ use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\CommandHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\CommandHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\CommandHandlerWithAnnotatedMethodWithoutPayload; +use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\CommandHandlerWithAnnotatedPayloadAndHeader; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerCalledWithRoutingKey; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpoint; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpointWithAlreadyAnnotatedMessage; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedMethodWithoutPayload; +use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedPayloadAndHeader; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\NoPollableEventHandler; use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; @@ -311,6 +313,48 @@ classesToResolve: [ self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_command_handler_with_annotated_method_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_command_handler_with_annotated_method_without_payload(): void { $ecotone = $this->bootstrapEcotone( @@ -611,6 +655,48 @@ classesToResolve: [ self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); } + public function test_event_handler_with_annotated_method_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->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_event_handler_with_annotated_method_without_payload(): void { $ecotone = $this->bootstrapEcotone( From 01be177afe55d8714e9638df607cd32f51c9d965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 18:41:37 +0100 Subject: [PATCH 13/20] encryption key can also be defined with payload parameter --- .../src/Configuration/DataProtectionModule.php | 3 +++ .../EventHandlerWithAnnotatedPayloadAndHeader.php | 3 ++- .../tests/Integration/ObfuscateEndpointsTest.php | 6 +++--- .../Ecotone/src/Messaging/Handler/InterfaceParameter.php | 6 ++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 010e458d5..0527bcc61 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -185,6 +185,9 @@ private static function resolveObfuscatorConfigsFromAnnotatedMethods(array $anno } $encryptionKey = $methodDefinition->findSingleMethodAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); + if ($encryptionKey === null) { + $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) { diff --git a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php index 56ee97321..ab1b8120d 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateEndpoints/EventHandlerWithAnnotatedPayloadAndHeader.php @@ -3,6 +3,7 @@ namespace Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints; use Ecotone\DataProtection\Attribute\Sensitive; +use Ecotone\DataProtection\Attribute\WithEncryptionKey; use Ecotone\DataProtection\Attribute\WithSensitiveHeader; use Ecotone\Messaging\Attribute\Asynchronous; use Ecotone\Messaging\Attribute\Parameter\Header; @@ -18,7 +19,7 @@ class EventHandlerWithAnnotatedPayloadAndHeader #[WithSensitiveHeader('bar')] #[EventHandler(endpointId: 'test.obfuscateAnnotatedEndpoints.eventHandler.annotatedMethod')] public function annotatedMethod( - #[Sensitive] SomeMessage $message, + #[Sensitive] #[WithEncryptionKey('secondary')] SomeMessage $message, #[Sensitive] #[Header('foo')] string $foo, #[Header('bar')] string $bar, #[Reference] MessageReceiver $messageReceiver, diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php index 8d1c80440..2614d1148 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -689,12 +689,12 @@ enum: TestEnum::FIRST, self::assertEquals($metadataSent['bar'], $receivedHeaders['bar']); $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $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->primaryKey)); - self::assertEquals($metadataSent['bar'], Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + 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_event_handler_with_annotated_method_without_payload(): void diff --git a/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php b/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php index 6897f7b14..2f44554e7 100644 --- a/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php +++ b/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php @@ -147,6 +147,12 @@ public function getAnnotationsOfType(string $type): array return $foundAnnotations; } + public function findSingleAnnotation(Type $annotationType): ?object + { + return array_find($this->annotations, fn ($annotation) => $annotationType->accepts($annotation)); + + } + /** * @return mixed|null */ From 8c1bd6720b731cc2e19556b8555ee031886c6935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 18:54:02 +0100 Subject: [PATCH 14/20] for stable API, payload parameter should have precedense --- .../DataProtection/src/Configuration/DataProtectionModule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 0527bcc61..b7358bf05 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -184,9 +184,9 @@ private static function resolveObfuscatorConfigsFromAnnotatedMethods(array $anno continue; } - $encryptionKey = $methodDefinition->findSingleMethodAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); + $encryptionKey = $payload->findSingleAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); if ($encryptionKey === null) { - $encryptionKey = $payload->findSingleAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); + $encryptionKey = $methodDefinition->findSingleMethodAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); } $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) ?? []); From 0b3c1e43eb01277d0a379f55288859255de883f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 18:57:29 +0100 Subject: [PATCH 15/20] skip on missing DataProtectionConfiguration instead of throwing an exception --- .../Configuration/DataProtectionModule.php | 5 ++++- .../Integration/RequirementsCheckTest.php | 22 ------------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index b7358bf05..791ef7aef 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -59,9 +59,12 @@ public static function create(AnnotationFinder $annotationRegistrationService, I public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void { + if (! ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects)) { + return; + } + $this->verifyLicense($messagingConfiguration, $extensionObjects); - Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); diff --git a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php index 370c2d516..4f4ca5cde 100644 --- a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php +++ b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php @@ -31,26 +31,4 @@ public function test_without_license_module_throws_an_exception(): void ]) ); } - - public function test_module_require_data_protection_configuration(): void - { - $this->expectExceptionObject(InvalidArgumentException::create('Ecotone\DataProtection\Configuration\DataProtectionConfiguration was not found.')); - - EcotoneLite::bootstrapFlowTesting( - configuration: ServiceConfiguration::createWithDefaults() - ->withLicenceKey(LicenceTesting::VALID_LICENCE) - ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) - ); - } - - public function test_module_require_jms_converter_configuration(): void - { - $this->expectExceptionObject(InvalidArgumentException::create('Ecotone\DataProtection\Configuration\DataProtectionConfiguration was not found.')); - - EcotoneLite::bootstrapFlowTesting( - configuration: ServiceConfiguration::createWithDefaults() - ->withLicenceKey(LicenceTesting::VALID_LICENCE) - ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::DATA_PROTECTION_PACKAGE])) - ); - } } From c38ef7e2734d8aeb1a96ee102dc4c0a1f623df8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 19:07:38 +0100 Subject: [PATCH 16/20] general cleanups --- .../DataProtection/src/Configuration/DataProtectionModule.php | 4 +--- .../DataProtection/src/OutboundDecryptionChannelBuilder.php | 1 - .../DataProtection/src/OutboundEncryptionChannelBuilder.php | 1 - .../tests/Fixture/ObfuscateChannel/TestCommandHandler.php | 1 - .../tests/Fixture/ObfuscateChannel/TestEventHandler.php | 1 - .../tests/Integration/ObfuscateEndpointsTest.php | 2 -- .../tests/Integration/RequirementsCheckTest.php | 2 -- 7 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 791ef7aef..94555b01e 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -211,8 +211,6 @@ private function verifyLicense(Configuration $messagingConfiguration, array $ext return; } - if (ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects)) { - throw LicensingException::create('Data Protection module is available only with Ecotone Enterprise Licence.'); - } + throw LicensingException::create('Data Protection module is available only with Ecotone Enterprise Licence.'); } } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php index 3afbdc917..e828db934 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -6,7 +6,6 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\Messaging\Channel\ChannelInterceptorBuilder; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\MessagingContainerBuilder; diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php index 79e8c545b..d7217e623 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -5,7 +5,6 @@ */ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\Messaging\Channel\ChannelInterceptorBuilder; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\MessagingContainerBuilder; diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestCommandHandler.php index 7c98d4d13..adaaedd39 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestCommandHandler.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestCommandHandler.php @@ -4,7 +4,6 @@ use Ecotone\Messaging\Attribute\Asynchronous; use Ecotone\Messaging\Attribute\Parameter\Headers; -use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; diff --git a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php index 3b8265726..6c735c240 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateChannel/TestEventHandler.php @@ -4,7 +4,6 @@ use Ecotone\Messaging\Attribute\Asynchronous; use Ecotone\Messaging\Attribute\Parameter\Headers; -use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\EventHandler; use Test\Ecotone\DataProtection\Fixture\MessageReceiver; diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php index 2614d1148..6ba2e7d8f 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -9,7 +9,6 @@ use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; -use Ecotone\Messaging\Channel\DirectChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; @@ -35,7 +34,6 @@ use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; -use Test\Ecotone\Messaging\Fixture\Handler\StatefulHandler; use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; /** diff --git a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php index 4f4ca5cde..f6132c9e4 100644 --- a/packages/DataProtection/tests/Integration/RequirementsCheckTest.php +++ b/packages/DataProtection/tests/Integration/RequirementsCheckTest.php @@ -9,9 +9,7 @@ use Ecotone\Lite\EcotoneLite; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; -use Ecotone\Messaging\Support\InvalidArgumentException; use Ecotone\Messaging\Support\LicensingException; -use Ecotone\Test\LicenceTesting; use PHPUnit\Framework\TestCase; /** From 393429f44bf2b024bd01e86e18acf6d4f7fcd71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 20:38:01 +0100 Subject: [PATCH 17/20] array_find not available with php 8.2 --- .../Ecotone/src/Messaging/Handler/InterfaceParameter.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php b/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php index 2f44554e7..f57820781 100644 --- a/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php +++ b/packages/Ecotone/src/Messaging/Handler/InterfaceParameter.php @@ -149,7 +149,12 @@ public function getAnnotationsOfType(string $type): array public function findSingleAnnotation(Type $annotationType): ?object { - return array_find($this->annotations, fn ($annotation) => $annotationType->accepts($annotation)); + foreach ($this->annotations as $annotation) { + if ($annotationType->accepts($annotation)) { + return $annotation; + } + } + return null; } From 9d60798e1c9a7ed8f63c7edbfc0af507a245daaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 21:33:25 +0100 Subject: [PATCH 18/20] bump requirement version --- packages/DataProtection/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index 5e56ed9fc..5d2e76819 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -31,8 +31,8 @@ }, "require": { "ext-openssl": "*", - "ecotone/ecotone": "~1.295.0", - "ecotone/jms-converter": "~1.295.0", + "ecotone/ecotone": "~1.298.0", + "ecotone/jms-converter": "~1.298.0", "defuse/php-encryption": "^2.4" }, "require-dev": { From c044c67e3664c2947693eedda9408d0f891fd620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Fri, 6 Feb 2026 21:48:51 +0100 Subject: [PATCH 19/20] remove lefover add DataProtection test suite to phpunit.xml.dist file --- .../DataProtection/src/Configuration/DataProtectionModule.php | 2 -- phpunit.xml.dist | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 94555b01e..32ed18673 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -36,8 +36,6 @@ use Ecotone\Modelling\Attribute\EventHandler; use stdClass; -use function Symfony\Component\DependencyInjection\Loader\Configurator\param; - #[ModuleAnnotation] final class DataProtectionModule extends NoExternalConfigurationModule { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1cd9dded0..dc3995aa4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,6 +11,7 @@ packages/Ecotone/src + packages/DataProtection/src packages/Dbal/src packages/PdoEventSourcing/src packages/JmsConverter/src @@ -39,6 +40,9 @@ packages/Ecotone/tests + + packages/DataProtection/tests + packages/Dbal/tests From b55df03cae5b8828620192fbe562741aaabc0c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Sat, 7 Feb 2026 13:06:51 +0100 Subject: [PATCH 20/20] pipeline fix --- packages/DataProtection/composer.json | 6 +-- .../Integration/ObfuscateChannelTest.php | 2 +- .../Integration/ObfuscateEndpointsTest.php | 3 +- .../Integration/ObfuscateMessagesTest.php | 2 +- .../DataProtection/tests/TestQueueChannel.php | 44 +++++++++++++++++++ 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 packages/DataProtection/tests/TestQueueChannel.php diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index 5d2e76819..3eaf70f64 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -43,7 +43,7 @@ }, "scripts": { "tests:phpstan": "vendor/bin/phpstan", - "tests:phpunit": "vendor/bin/phpunit", + "tests:phpunit": "vendor/bin/phpunit --no-coverage --testdox", "tests:ci": [ "@tests:phpstan", "@tests:phpunit" @@ -51,10 +51,10 @@ }, "extra": { "branch-alias": { - "dev-main": "1.62-dev" + "dev-main": "1.298.0-dev" }, "ecotone": { - "repository": "DataProtection" + "repository": "data-protection" }, "merge-plugin": { "include": [ diff --git a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php index 072dc6fad..fca82b3e6 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateChannelTest.php @@ -24,7 +24,7 @@ use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; -use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; +use Test\Ecotone\DataProtection\TestQueueChannel; /** * @internal diff --git a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php index 6ba2e7d8f..97c0ed4eb 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateEndpointsTest.php @@ -30,11 +30,10 @@ use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedEndpointWithSecondaryEncryptionKey; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedMethodWithoutPayload; use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\EventHandlerWithAnnotatedPayloadAndHeader; -use Test\Ecotone\DataProtection\Fixture\ObfuscateEndpoints\NoPollableEventHandler; use Test\Ecotone\DataProtection\Fixture\SomeMessage; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; -use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; +use Test\Ecotone\DataProtection\TestQueueChannel; /** * @internal diff --git a/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php index 2bdc80947..0c66c3ab1 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateMessagesTest.php @@ -24,7 +24,7 @@ use Test\Ecotone\DataProtection\Fixture\ObfuscateMessages\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; -use Test\Ecotone\Messaging\Unit\Channel\TestQueueChannel; +use Test\Ecotone\DataProtection\TestQueueChannel; /** * @internal diff --git a/packages/DataProtection/tests/TestQueueChannel.php b/packages/DataProtection/tests/TestQueueChannel.php new file mode 100644 index 000000000..49d505a8e --- /dev/null +++ b/packages/DataProtection/tests/TestQueueChannel.php @@ -0,0 +1,44 @@ +lastSentMessage = $message; + + parent::send($message); + } + + public function receive(): ?Message + { + return parent::receive(); + } + + public function getLastSentMessage(): ?Message + { + return $this->lastSentMessage; + } +}