Skip to content

Commit c3b3e7d

Browse files
fix property access when no hooks (#157)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Improved internal handling of property operations for better clarity and maintainability. * Updated internal labels for processing steps. * Enhanced access control logic to unconditionally deny access when specified. * **Tests** * Added tests to verify property access signals and enforce access control with security exceptions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Robert Landers <landers.robert@gmail.com>
1 parent 7cba4cb commit c3b3e7d

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

src/State/AbstractHistory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ protected function checkAccessControl(?Provenance $user, StateId $from, Reflecti
154154

155155
foreach ($controls as $accessControl) {
156156
if ($accessControl instanceof DenyAnyOperation) {
157+
if ($accessControl->fromType === null && $accessControl->fromId === null && $accessControl->fromRole === null && $accessControl->fromUser === null) {
158+
return false;
159+
}
160+
157161
if ($accessControl->fromUser && $user->userId === $accessControl->fromUser) {
158162
return false;
159163
}

src/State/EntityHistory.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
use Crell\Serde\Attributes\Field;
4949
use Generator;
5050
use Override;
51-
use PropertyHookType;
5251
use ReflectionAttribute;
5352
use ReflectionClass;
5453
use ReflectionException;
@@ -226,12 +225,12 @@ private function execute(Event $original, string $operation, array $input): Gene
226225
if (! $this->checkAccessControl($this->user, $this->from, ...$operationReflection->getAttributes(AccessControl::class, ReflectionAttribute::IS_INSTANCEOF))) {
227226
throw new SecurityException('Access denied');
228227
}
229-
$operationReflection = match ($operation) {
230-
'get' => $operationReflection->getHook(PropertyHookType::Get),
231-
'set' => $operationReflection->getHook(PropertyHookType::Set),
228+
$result = match ($operation) {
229+
'get' => $this->state->{$property},
230+
'set' => $this->state->{$property} = $input[0],
232231
default => throw new ReflectionException('Unknown operation'),
233232
};
234-
goto done;
233+
goto finalize;
235234
}
236235

237236
$operationReflection = $reflector->getMethod($operation);
@@ -281,6 +280,8 @@ private function execute(Event $original, string $operation, array $input): Gene
281280
}
282281
}
283282

283+
finalize:
284+
284285
if ($replyTo) {
285286
foreach ($replyTo as $reply) {
286287
yield WithPriority::high(

tests/Unit/EntityHistoryTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424

2525
// namespace Bottledcode\DurablePhp\Tests\Unit;
2626

27+
use Bottledcode\DurablePhp\Contexts\AuthContext\SecurityException;
2728
use Bottledcode\DurablePhp\Events\AwaitResult;
2829
use Bottledcode\DurablePhp\Events\RaiseEvent;
30+
use Bottledcode\DurablePhp\Events\TaskCompleted;
2931
use Bottledcode\DurablePhp\Events\WithEntity;
3032
use Bottledcode\DurablePhp\Events\WithLock;
3133
use Bottledcode\DurablePhp\State\EntityState;
@@ -193,3 +195,69 @@ public function signal(): void
193195
$finalResult = processEvent($secondResult[1], $otherEntity->applyRaiseEvent(...));
194196
expect($finalResult)->toBeEmpty();
195197
});
198+
199+
it('can get a property value using get signal', function (): void {
200+
// Create an entity state with a property
201+
$history = getEntityHistory(
202+
new class extends EntityState {
203+
public string $testProperty = 'test value';
204+
},
205+
);
206+
$history->from = StateId::fromInstance(OrchestrationInstance('test', 'test'));
207+
208+
// Create a signal to get the property value
209+
$event = RaiseEvent::forOperation('$testProperty::get', []);
210+
$event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event);
211+
212+
// Process the event
213+
$result = processEvent($event, $history->applyRaiseEvent(...));
214+
215+
// Verify the result contains a TaskCompleted event with the property value
216+
expect($result)->toHaveCount(1);
217+
expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class);
218+
expect($result[0]->getInnerEvent()->getInnerEvent()->result)->toBe(['value' => 'test value']);
219+
});
220+
221+
it('can set a property value using set signal', function (): void {
222+
// Create an entity state with a property
223+
$history = getEntityHistory(
224+
new class extends EntityState {
225+
public string $testProperty = 'initial value';
226+
},
227+
);
228+
$history->from = StateId::fromInstance(OrchestrationInstance('test', 'test'));
229+
230+
// Create a signal to set the property value
231+
$event = RaiseEvent::forOperation('$testProperty::set', ['new value']);
232+
$event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event);
233+
234+
// Process the event
235+
$result = processEvent($event, $history->applyRaiseEvent(...));
236+
237+
// Verify the property was updated
238+
expect($history->getState()->testProperty)->toBe('new value');
239+
240+
// Verify the result contains a TaskCompleted event
241+
expect($result)->toHaveCount(1);
242+
expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class);
243+
});
244+
245+
it('handles access control for property signals', function (): void {
246+
// Create a mock class with an AccessControl attribute on a property
247+
$from = StateId::fromInstance(OrchestrationInstance('test', 'test'));
248+
$mockClass = new class extends EntityState {
249+
#[Bottledcode\DurablePhp\State\Attributes\DenyAnyOperation(fromType: 'test')]
250+
public string $restrictedProperty = 'restricted value';
251+
252+
public string $publicProperty = 'public value';
253+
};
254+
255+
$history = getEntityHistory($mockClass);
256+
$history->from = $from;
257+
258+
// Try to access the restricted property
259+
$restrictedEvent = RaiseEvent::forOperation('$restrictedProperty::get', []);
260+
261+
// This should throw a SecurityException, which is caught in the execute method
262+
expect(fn() => processEvent($restrictedEvent, $history->applyRaiseEvent(...)))->toThrow(SecurityException::class);
263+
});

0 commit comments

Comments
 (0)