Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/split-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ jobs:
overall_exit_code=1
fi

# Run SQLite pass (skip for PdoEventSourcing - SQLite event store not supported)
if [[ "$dir" != *"PdoEventSourcing"* ]]; then
# Run SQLite pass (skip for PdoEventSourcing and DataProtection - SQLite event store not supported)
if [[ ! "$dir" =~ (PdoEventSourcing|DataProtection) ]]; then
local sqlite_db="/tmp/ecotone_${slug}_test.db"
local sqlite_db_secondary="/tmp/ecotone_${slug}_test_b.db"
# Use 4 slashes for absolute paths: sqlite:// + / + /path = sqlite:////path
Expand Down
1 change: 1 addition & 0 deletions packages/DataProtection/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"paragonie/random_compat": "^2.0"
},
"require-dev": {
"ecotone/pdo-event-sourcing": "~1.300.2",
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^2.1",
"wikimedia/composer-merge-plugin": "^2.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/DataProtection/src/Attribute/Sensitive.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PARAMETER)]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Sensitive
{
}
17 changes: 0 additions & 17 deletions packages/DataProtection/src/Attribute/WithSensitiveHeader.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
/**
* licence Enterprise
*/
class ChannelProtectionConfiguration
readonly class ChannelProtectionConfiguration
{
private function __construct(
private string $channelName,
private ?string $encryptionKey,
private bool $isPayloadSensitive,
private array $sensitiveHeaders,
public string $channelName,
public ?string $encryptionKey,
public bool $isPayloadSensitive,
public array $sensitiveHeaders,
) {
}

Expand All @@ -20,29 +20,18 @@ public static function create(string $channelName, ?string $encryptionKey = null
return new self($channelName, $encryptionKey, $isPayloadSensitive, $sensitiveHeaders);
}

public function channelName(): string
public function withEncryptionKey(string $encryptionKey): self
{
return $this->channelName;
}

public function messageEncryptionConfig(): MessageEncryptionConfig
{
return new MessageEncryptionConfig($this->encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders);
return self::create($this->channelName, $encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders);
}

public function withSensitivePayload(bool $isPayloadSensitive): self
{
$config = clone $this;
$config->isPayloadSensitive = $isPayloadSensitive;

return $config;
return self::create($this->channelName, $this->encryptionKey, $isPayloadSensitive, $this->sensitiveHeaders);
}

public function withSensitiveHeader(string $sensitiveHeader): self
{
$config = clone $this;
$config->sensitiveHeaders[] = $sensitiveHeader;

return $config;
return self::create($this->channelName, $this->encryptionKey, $this->isPayloadSensitive, array_merge($this->sensitiveHeaders, [$sensitiveHeader]));
}
}
126 changes: 49 additions & 77 deletions packages/DataProtection/src/Configuration/DataProtectionModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@

namespace Ecotone\DataProtection\Configuration;

use Ecotone\AnnotationFinder\AnnotatedMethod;
use Ecotone\AnnotationFinder\AnnotationFinder;
use Ecotone\DataProtection\Attribute\Sensitive;
use Ecotone\DataProtection\Attribute\WithEncryptionKey;
use Ecotone\DataProtection\Attribute\WithSensitiveHeader;
use Ecotone\DataProtection\Conversion\DataProtectionConversionServiceDecorator;
use Ecotone\DataProtection\Encryption\Key;
use Ecotone\DataProtection\MessageEncryption\MessageEncryptor;
use Ecotone\DataProtection\OutboundDecryptionChannelBuilder;
use Ecotone\DataProtection\OutboundEncryptionChannelBuilder;
use Ecotone\DataProtection\Protector\ChannelProtector;
use Ecotone\DataProtection\Protector\DataProtector;
use Ecotone\JMSConverter\JMSConverterConfiguration;
use Ecotone\Messaging\Attribute\ModuleAnnotation;
use Ecotone\Messaging\Attribute\Parameter\Header;
use Ecotone\Messaging\Attribute\Parameter\Headers;
use Ecotone\Messaging\Channel\MessageChannelWithSerializationBuilder;
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver;
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule;
Expand All @@ -28,12 +26,11 @@
use Ecotone\Messaging\Config\Container\Reference;
use Ecotone\Messaging\Config\ModulePackageList;
use Ecotone\Messaging\Config\ModuleReferenceSearchService;
use Ecotone\Messaging\Handler\ClassPropertyDefinition;
use Ecotone\Messaging\Handler\InterfaceToCallRegistry;
use Ecotone\Messaging\Handler\Type;
use Ecotone\Messaging\Support\Assert;
use Ecotone\Messaging\Support\LicensingException;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\EventHandler;
use stdClass;

#[ModuleAnnotation]
Expand All @@ -43,19 +40,17 @@ final class DataProtectionModule extends NoExternalConfigurationModule
final public const KEY_SERVICE_ID_FORMAT = 'ecotone.encryption.key.%s';

/**
* @param array<MessageEncryptionConfig> $encryptionConfigs
* @param array<DataProtectorConfig> $dataProtectorConfigs
*/
public function __construct(private array $encryptionConfigs)
public function __construct(private array $dataProtectorConfigs)
{
}

public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static
{
$encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry);
$encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(CommandHandler::class), $encryptionConfigs, $interfaceToCallRegistry);
$encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(EventHandler::class), $encryptionConfigs, $interfaceToCallRegistry);

return new self($encryptionConfigs);
return new self(
dataProtectorConfigs: self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry)
);
}

public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void
Expand All @@ -82,58 +77,59 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
);
}

$channelEncryptorReferences = $messageEncryptorReferences = [];
$channelProtectorReferences = [];
foreach ($channelProtectionConfigurations as $channelProtectionConfiguration) {
Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName()), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName()));
Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName));

$encryptionConfig = $channelProtectionConfiguration->messageEncryptionConfig();
$messagingConfiguration->registerServiceDefinition(
id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName()),
id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName),
definition: new Definition(
MessageEncryptor::class,
ChannelProtector::class,
[
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))),
$encryptionConfig->isPayloadSensitive,
$encryptionConfig->sensitiveHeaders,
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $dataProtectionConfiguration->keyName($channelProtectionConfiguration->encryptionKey))),
$channelProtectionConfiguration->isPayloadSensitive,
$channelProtectionConfiguration->sensitiveHeaders,
],
)
);

$channelEncryptorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id);
$channelProtectorReferences[$channelProtectionConfiguration->channelName] = Reference::to($id);
}

foreach ($this->encryptionConfigs as $messageClass => $encryptionConfig) {
$messagingConfiguration->registerServiceDefinition(
id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $messageClass),
definition: new Definition(
MessageEncryptor::class,
[
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))),
$encryptionConfig->isPayloadSensitive,
$encryptionConfig->sensitiveHeaders,
],
)
$conversionServiceDecorator = new Definition(DataProtectionConversionServiceDecorator::class);
foreach ($this->dataProtectorConfigs as $protectorConfig) {
$conversionServiceDecorator->addMethodCall(
'withDataProtector',
[
$protectorConfig->supportedType,
new Definition(
DataProtector::class,
[
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))),
$protectorConfig->sensitiveProperties,
$protectorConfig->scalarProperties,
],
),
]
);
$messageEncryptorReferences[$messageClass] = Reference::to($id);
}
$messagingConfiguration->registerConversionServiceDecorator($conversionServiceDecorator);

foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) {
if (! $pollableMessageChannel->isPollable()) {
if (! $pollableMessageChannel->isPollable() || ! array_key_exists($pollableMessageChannel->getMessageChannelName(), $channelProtectorReferences)) {
continue;
}

$messagingConfiguration->registerChannelInterceptor(
new OutboundEncryptionChannelBuilder(
relatedChannel: $pollableMessageChannel->getMessageChannelName(),
channelEncryptorReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null,
messageEncryptorReferences: $messageEncryptorReferences,
channelProtectorReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()],
)
);
$messagingConfiguration->registerChannelInterceptor(
new OutboundDecryptionChannelBuilder(
relatedChannel: $pollableMessageChannel->getMessageChannelName(),
channelEncryptionReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null,
messageEncryptionReferences: $messageEncryptorReferences,
channelProtectorReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()],
)
);
}
Expand All @@ -154,53 +150,29 @@ public function getModulePackageName(): string
return ModulePackageList::DATA_PROTECTION_PACKAGE;
}

private static function resolveEncryptionConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
{
$encryptionConfigs = [];
$dataEncryptorConfigs = [];
foreach ($sensitiveMessages as $message) {
$classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($message));
$classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message));
$encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey();
$sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) ?? []);

$encryptionConfigs[$message] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders);
}

return $encryptionConfigs;
}

private static function resolveEncryptionConfigsFromAnnotatedMethods(array $annotatedMethods, array $encryptionConfigs, InterfaceToCallRegistry $interfaceToCallRegistry): array
{
/** @var AnnotatedMethod $method */
foreach ($annotatedMethods as $method) {
$methodDefinition = $interfaceToCallRegistry->getFor($method->getClassName(), $method->getMethodName());
$payload = $methodDefinition->getFirstParameter();

if (
$payload->hasAnnotation(Header::class)
|| $payload->hasAnnotation(Headers::class)
|| $payload->hasAnnotation(Reference::class)
|| array_key_exists($payload->getTypeHint(), $encryptionConfigs)
) {
continue;
$sensitiveProperties = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class));
if ($sensitiveProperties === []) {
$sensitiveProperties = $classDefinition->getProperties();
}

$isPayloadSensitive = $payload->hasAnnotation(Sensitive::class);
if (! $isPayloadSensitive) {
continue;
}
$scalarProperties = array_values(array_filter($sensitiveProperties, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar()));

$encryptionKey = $payload->findSingleAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey();
$sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) ?? []);
foreach ($methodDefinition->getInterfaceParameters() as $parameter) {
if ($parameter->hasAnnotation(Header::class) && $parameter->hasAnnotation(Sensitive::class)) {
$sensitiveHeaders[] = $parameter->getName();
}
}
$mapper = static fn (ClassPropertyDefinition $property): string => $property->getName();

$sensitiveProperties = array_map($mapper, $sensitiveProperties);
$scalarProperties = array_map($mapper, $scalarProperties);

$encryptionConfigs[$payload->getTypeHint()] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders);
$dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties);
}

return $encryptionConfigs;
return $dataEncryptorConfigs;
}

private function verifyLicense(Configuration $messagingConfiguration): void
Expand Down
31 changes: 31 additions & 0 deletions packages/DataProtection/src/Configuration/DataProtectorConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/**
* licence Enterprise
*/

namespace Ecotone\DataProtection\Configuration;

use Ecotone\Messaging\Handler\Type;
use Ecotone\Messaging\Support\Assert;

final readonly class DataProtectorConfig
{
/**
* @param array<string> $sensitiveProperties
*/
public function __construct(
public Type $supportedType,
public ?string $encryptionKey,
public array $sensitiveProperties,
public array $scalarProperties,
) {
Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings');
Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings');
}

public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string
{
return $dataProtectionConfiguration->keyName($this->encryptionKey);
}
}

This file was deleted.

Loading