diff --git a/src/EntityList/Commands/Wizards/InstanceWizardCommand.php b/src/EntityList/Commands/Wizards/InstanceWizardCommand.php index 873680619..abd31ee40 100644 --- a/src/EntityList/Commands/Wizards/InstanceWizardCommand.php +++ b/src/EntityList/Commands/Wizards/InstanceWizardCommand.php @@ -57,6 +57,11 @@ protected function initialDataForStep(string $step, mixed $instanceId): array return []; } + public function authorizeForStep(string $step, mixed $instanceId): bool + { + return true; + } + abstract protected function executeFirstStep(mixed $instanceId, array $data): array; abstract protected function buildFormFieldsForFirstStep(FieldsContainer $formFields): void; diff --git a/src/EntityList/Commands/Wizards/IsEntityWizardCommand.php b/src/EntityList/Commands/Wizards/IsEntityWizardCommand.php index a962f39dc..e10b5d059 100644 --- a/src/EntityList/Commands/Wizards/IsEntityWizardCommand.php +++ b/src/EntityList/Commands/Wizards/IsEntityWizardCommand.php @@ -54,4 +54,9 @@ protected function initialDataForStep(string $step): array { return []; } + + public function authorizeForStep(string $step): bool + { + return true; + } } diff --git a/src/EntityList/Commands/Wizards/IsWizardCommand.php b/src/EntityList/Commands/Wizards/IsWizardCommand.php index d0354a1ec..930f7e245 100644 --- a/src/EntityList/Commands/Wizards/IsWizardCommand.php +++ b/src/EntityList/Commands/Wizards/IsWizardCommand.php @@ -38,9 +38,9 @@ protected function toStep(string $step): array ]; } - protected function extractStepFromRequest(): ?string + public function extractStepFromRequest(): ?string { - if ($step = request()->get('command_step')) { + if ($step = request()->input('command_step')) { [$step, $this->key] = explode(':', $step); return $step; diff --git a/src/Http/Controllers/Api/Commands/ApiDashboardCommandController.php b/src/Http/Controllers/Api/Commands/ApiDashboardCommandController.php index 4fad2a167..8c5cbab26 100644 --- a/src/Http/Controllers/Api/Commands/ApiDashboardCommandController.php +++ b/src/Http/Controllers/Api/Commands/ApiDashboardCommandController.php @@ -2,6 +2,7 @@ namespace Code16\Sharp\Http\Controllers\Api\Commands; +use Code16\Sharp\Dashboard\Commands\DashboardWizardCommand; use Code16\Sharp\Dashboard\SharpDashboard; use Code16\Sharp\Data\Commands\CommandFormData; use Code16\Sharp\Exceptions\Auth\SharpAuthorizationException; @@ -51,14 +52,17 @@ public function update(string $globalFilter, string $entityKey, string $commandK protected function getDashboardCommandHandler(SharpDashboard $dashboard, string $commandKey) { - if ($handler = $dashboard->findDashboardCommandHandler($commandKey)) { - $handler->buildCommandConfig(); + $commandHandler = $dashboard->findDashboardCommandHandler($commandKey); + $commandHandler->buildCommandConfig(); - if (! $handler->authorize()) { - throw new SharpAuthorizationException(); - } + $authorized = $commandHandler instanceof DashboardWizardCommand && ($step = $commandHandler->extractStepFromRequest()) + ? $commandHandler->authorizeForStep($step) + : $commandHandler->authorize(); + + if (! $authorized) { + throw new SharpAuthorizationException(); } - return $handler; + return $commandHandler; } } diff --git a/src/Http/Controllers/Api/Commands/HandlesEntityCommand.php b/src/Http/Controllers/Api/Commands/HandlesEntityCommand.php index bf926d3a3..6b978cddb 100644 --- a/src/Http/Controllers/Api/Commands/HandlesEntityCommand.php +++ b/src/Http/Controllers/Api/Commands/HandlesEntityCommand.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Http\Controllers\Api\Commands; use Code16\Sharp\EntityList\Commands\EntityCommand; +use Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand; use Code16\Sharp\EntityList\SharpEntityList; use Code16\Sharp\Exceptions\Auth\SharpAuthorizationException; @@ -13,7 +14,11 @@ protected function getEntityCommandHandler(SharpEntityList $list, string $comman $commandHandler = $list->findEntityCommandHandler($commandKey); $commandHandler->buildCommandConfig(); - if (! $commandHandler->authorize()) { + $authorized = $commandHandler instanceof EntityWizardCommand && ($step = $commandHandler->extractStepFromRequest()) + ? $commandHandler->authorizeForStep($step) + : $commandHandler->authorize(); + + if (! $authorized) { throw new SharpAuthorizationException(); } diff --git a/src/Http/Controllers/Api/Commands/HandlesInstanceCommand.php b/src/Http/Controllers/Api/Commands/HandlesInstanceCommand.php index 08418f98c..a3d72b848 100644 --- a/src/Http/Controllers/Api/Commands/HandlesInstanceCommand.php +++ b/src/Http/Controllers/Api/Commands/HandlesInstanceCommand.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Http\Controllers\Api\Commands; use Code16\Sharp\EntityList\Commands\InstanceCommand; +use Code16\Sharp\EntityList\Commands\Wizards\InstanceWizardCommand; use Code16\Sharp\EntityList\SharpEntityList; use Code16\Sharp\Exceptions\Auth\SharpAuthorizationException; use Code16\Sharp\Show\SharpShow; @@ -17,7 +18,11 @@ protected function getInstanceCommandHandler( $commandHandler = $commandContainer->findInstanceCommandHandler($commandKey); $commandHandler->buildCommandConfig(); - if (! $commandHandler->authorize() || ! $commandHandler->authorizeFor($instanceId)) { + $authorized = $commandHandler instanceof InstanceWizardCommand && ($step = $commandHandler->extractStepFromRequest()) + ? $commandHandler->authorizeForStep($step, $instanceId) + : $commandHandler->authorize() && $commandHandler->authorizeFor($instanceId); + + if (! $authorized) { throw new SharpAuthorizationException(); } diff --git a/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php index fe78d81dd..4bd711a73 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php @@ -303,6 +303,104 @@ protected function executeStepNextStep(array $data): array ]); }); +it('authorize() is only called for firstStep', function () { + $authorizeForStep = true; + + fakeListFor('person', new class($authorizeForStep) extends PersonList + { + public function __construct(public bool &$authorizeForStep) {} + + protected function getEntityCommands(): ?array + { + return [ + 'wizard' => new class($this->authorizeForStep) extends EntityWizardCommand + { + public function __construct(public bool &$authorizeForStep) {} + + protected function getKey(): string + { + return 'test-key'; + } + + public function label(): ?string + { + return 'my command'; + } + + public function authorize(): bool + { + return false; + } + + public function authorizeForStep(string $step): bool + { + return $this->authorizeForStep; + } + + public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('name')); + } + + protected function executeFirstStep(array $data): array + { + $this->validate($data, ['name' => 'required']); + + return $this->toStep('next-step'); + } + + public function buildFormFieldsForStepNextStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('age')); + } + + protected function executeStepNextStep(array $data): array + { + return $this->reload(); + } + }, + ]; + } + }); + + // First post step 1... + $this + ->postJson( + route('code16.sharp.api.list.command.entity', [ + 'globalFilter' => 'root', + 'entityKey' => 'person', + 'commandKey' => 'wizard', + ]), + ['data' => ['name' => 'test']], + ) + ->assertForbidden(); + + // Then post step 2 but authorized (default) + $this + ->postJson( + route('code16.sharp.api.list.command.entity', [ + 'entityKey' => 'person', + 'commandKey' => 'wizard', + 'command_step' => 'next-step:test-key', + ]), + ['data' => ['age' => '22']], + ) + ->assertOk(); + + // Post 2 but disallowed + $authorizeForStep = false; + $this + ->postJson( + route('code16.sharp.api.list.command.entity', [ + 'entityKey' => 'person', + 'commandKey' => 'wizard', + 'command_step' => 'next-step:test-key', + ]), + ['data' => ['age' => '22']], + ) + ->assertForbidden(); +}); + it('allows to define a global method for step execution', function () { fakeListFor('person', new class() extends PersonList { diff --git a/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php index bd954f4ea..5222cbe3a 100644 --- a/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php @@ -309,6 +309,106 @@ protected function executeStepNextStep($instanceId, array $data): array ]); }); +it('authorize() is only called for firstStep', function () { + $authorizeForStep = true; + + fakeListFor('person', new class($authorizeForStep) extends PersonList + { + public function __construct(public bool &$authorizeForStep) {} + + protected function getInstanceCommands(): ?array + { + return [ + 'wizard' => new class($this->authorizeForStep) extends InstanceWizardCommand + { + public function __construct(public bool &$authorizeForStep) {} + + protected function getKey(): string + { + return 'test-key'; + } + + public function label(): ?string + { + return 'my command'; + } + + public function authorizeFor(mixed $instanceId): bool + { + return false; + } + + public function authorizeForStep(string $step, mixed $instanceId): bool + { + return $this->authorizeForStep; + } + + public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('name')); + } + + protected function executeFirstStep($instanceId, array $data): array + { + $this->validate($data, ['name' => 'required']); + + return $this->toStep('next-step'); + } + + public function buildFormFieldsForStepNextStep(FieldsContainer $formFields): void + { + $formFields->addField(SharpFormTextField::make('age')); + } + + protected function executeStepNextStep($instanceId, array $data): array + { + return $this->reload(); + } + }, + ]; + } + }); + + // First post step 1... + $this + ->postJson( + route('code16.sharp.api.list.command.instance', [ + 'globalFilter' => 'root', + 'entityKey' => 'person', + 'commandKey' => 'wizard', + 'instanceId' => 1]), + ['data' => ['name' => 'test']], + ) + ->assertForbidden(); + + // Then post step 2 should be authorized (default)... + $this + ->postJson( + route('code16.sharp.api.list.command.instance', [ + 'entityKey' => 'person', + 'commandKey' => 'wizard', + 'instanceId' => 1, + 'command_step' => 'next-step:test-key', + ]), + ['data' => ['age' => '22']], + ) + ->assertOk(); + + // Post 2 but disallowed + $authorizeForStep = false; + $this + ->postJson( + route('code16.sharp.api.list.command.instance', [ + 'entityKey' => 'person', + 'commandKey' => 'wizard', + 'instanceId' => 1, + 'command_step' => 'next-step:test-key', + ]), + ['data' => ['age' => '22']], + ) + ->assertForbidden(); +}); + it('allows to define a global method for step execution', function () { fakeListFor('person', new class() extends PersonList {