From 4a9200bcbce0f2790d31c57168024b979954f41c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 12:01:04 +0200 Subject: [PATCH 1/3] fix property access when no hooks Signed-off-by: Robert Landers --- src/State/EntityHistory.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/State/EntityHistory.php b/src/State/EntityHistory.php index aca00e33..ee8c4528 100644 --- a/src/State/EntityHistory.php +++ b/src/State/EntityHistory.php @@ -48,7 +48,6 @@ use Crell\Serde\Attributes\Field; use Generator; use Override; -use PropertyHookType; use ReflectionAttribute; use ReflectionClass; use ReflectionException; @@ -226,12 +225,12 @@ private function execute(Event $original, string $operation, array $input): Gene if (! $this->checkAccessControl($this->user, $this->from, ...$operationReflection->getAttributes(AccessControl::class, ReflectionAttribute::IS_INSTANCEOF))) { throw new SecurityException('Access denied'); } - $operationReflection = match ($operation) { - 'get' => $operationReflection->getHook(PropertyHookType::Get), - 'set' => $operationReflection->getHook(PropertyHookType::Set), + $result = match ($operation) { + 'get' => $this->state->{$property}, + 'set' => $this->state->{$property} = $input[0], default => throw new ReflectionException('Unknown operation'), }; - goto done; + goto finalize; } $operationReflection = $reflector->getMethod($operation); @@ -281,6 +280,8 @@ private function execute(Event $original, string $operation, array $input): Gene } } + finalize: + if ($replyTo) { foreach ($replyTo as $reply) { yield WithPriority::high( From 98717b27f506d4eb194d53351bd6cc2ffc0dce4b Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 12:31:38 +0200 Subject: [PATCH 2/3] add tests Signed-off-by: Robert Landers --- tests/Unit/EntityHistoryTest.php | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/Unit/EntityHistoryTest.php b/tests/Unit/EntityHistoryTest.php index 5f1e14fc..f4d5723e 100644 --- a/tests/Unit/EntityHistoryTest.php +++ b/tests/Unit/EntityHistoryTest.php @@ -24,8 +24,10 @@ // namespace Bottledcode\DurablePhp\Tests\Unit; +use Bottledcode\DurablePhp\Contexts\AuthContext\SecurityException; use Bottledcode\DurablePhp\Events\AwaitResult; use Bottledcode\DurablePhp\Events\RaiseEvent; +use Bottledcode\DurablePhp\Events\TaskCompleted; use Bottledcode\DurablePhp\Events\WithEntity; use Bottledcode\DurablePhp\Events\WithLock; use Bottledcode\DurablePhp\State\EntityState; @@ -193,3 +195,69 @@ public function signal(): void $finalResult = processEvent($secondResult[1], $otherEntity->applyRaiseEvent(...)); expect($finalResult)->toBeEmpty(); }); + +it('can get a property value using get signal', function (): void { + // Create an entity state with a property + $history = getEntityHistory( + new class extends EntityState { + public string $testProperty = 'test value'; + }, + ); + $history->from = StateId::fromInstance(OrchestrationInstance('test', 'test')); + + // Create a signal to get the property value + $event = RaiseEvent::forOperation('$testProperty::get', []); + $event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event); + + // Process the event + $result = processEvent($event, $history->applyRaiseEvent(...)); + + // Verify the result contains a TaskCompleted event with the property value + expect($result)->toHaveCount(1); + expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class); + expect($result[0]->getInnerEvent()->getInnerEvent()->result)->toBe(['value' => 'test value']); +}); + +it('can set a property value using set signal', function (): void { + // Create an entity state with a property + $history = getEntityHistory( + new class extends EntityState { + public string $testProperty = 'initial value'; + }, + ); + $history->from = StateId::fromInstance(OrchestrationInstance('test', 'test')); + + // Create a signal to set the property value + $event = RaiseEvent::forOperation('$testProperty::set', ['new value']); + $event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event); + + // Process the event + $result = processEvent($event, $history->applyRaiseEvent(...)); + + // Verify the property was updated + expect($history->getState()->testProperty)->toBe('new value'); + + // Verify the result contains a TaskCompleted event + expect($result)->toHaveCount(1); + expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class); +}); + +it('handles access control for property signals', function (): void { + // Create a mock class with an AccessControl attribute on a property + $from = StateId::fromInstance(OrchestrationInstance('test', 'test')); + $mockClass = new class extends EntityState { + #[Bottledcode\DurablePhp\State\Attributes\DenyAnyOperation(fromType: 'test')] + public string $restrictedProperty = 'restricted value'; + + public string $publicProperty = 'public value'; + }; + + $history = getEntityHistory($mockClass); + $history->from = $from; + + // Try to access the restricted property + $restrictedEvent = RaiseEvent::forOperation('$restrictedProperty::get', []); + + // This should throw a SecurityException, which is caught in the execute method + expect(fn() => processEvent($restrictedEvent, $history->applyRaiseEvent(...)))->toThrow(SecurityException::class); +}); From e19d2516da0807228d66a67f3a29c22790c620e7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 12:35:03 +0200 Subject: [PATCH 3/3] make possible to deny all operations Signed-off-by: Robert Landers --- src/State/AbstractHistory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/State/AbstractHistory.php b/src/State/AbstractHistory.php index 4b2c3190..f5de7413 100644 --- a/src/State/AbstractHistory.php +++ b/src/State/AbstractHistory.php @@ -154,6 +154,10 @@ protected function checkAccessControl(?Provenance $user, StateId $from, Reflecti foreach ($controls as $accessControl) { if ($accessControl instanceof DenyAnyOperation) { + if ($accessControl->fromType === null && $accessControl->fromId === null && $accessControl->fromRole === null && $accessControl->fromUser === null) { + return false; + } + if ($accessControl->fromUser && $user->userId === $accessControl->fromUser) { return false; }