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; } 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( 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); +});