Skip to content

Commit 5b1d928

Browse files
Merge pull request #56630 from nextcloud/carl/workflowengine-hardening
refactor(workflowengine): Check if class is correct
2 parents 8e7bdab + 7541afa commit 5b1d928

File tree

2 files changed

+62
-30
lines changed

2 files changed

+62
-30
lines changed

apps/workflowengine/lib/Manager.php

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -422,16 +422,18 @@ public function deleteOperation(int $id, ScopeContext $scopeContext): bool {
422422
* @param array $events
423423
*/
424424
protected function validateEvents(string $entity, array $events, IOperation $operation): void {
425+
/** @psalm-suppress TaintedCallable newInstance is not called */
426+
$reflection = new \ReflectionClass($entity);
427+
if ($entity !== IEntity::class && !in_array(IEntity::class, $reflection->getInterfaceNames())) {
428+
throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
429+
}
430+
425431
try {
426432
$instance = $this->container->get($entity);
427433
} catch (ContainerExceptionInterface $e) {
428434
throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
429435
}
430436

431-
if (!$instance instanceof IEntity) {
432-
throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
433-
}
434-
435437
if (empty($events)) {
436438
if (!$operation instanceof IComplexOperation) {
437439
throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
@@ -458,17 +460,23 @@ protected function validateEvents(string $entity, array $events, IOperation $ope
458460
* @throws \UnexpectedValueException
459461
*/
460462
public function validateOperation(string $class, string $name, array $checks, string $operation, ScopeContext $scope, string $entity, array $events): void {
463+
if (strlen($operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
464+
throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
465+
}
466+
467+
/** @psalm-suppress TaintedCallable newInstance is not called */
468+
$reflection = new \ReflectionClass($class);
469+
if ($class !== IOperation::class && !in_array(IOperation::class, $reflection->getInterfaceNames())) {
470+
throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]) . join(', ', $reflection->getInterfaceNames()));
471+
}
472+
461473
try {
462474
/** @var IOperation $instance */
463475
$instance = $this->container->get($class);
464476
} catch (ContainerExceptionInterface $e) {
465477
throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
466478
}
467479

468-
if (!($instance instanceof IOperation)) {
469-
throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
470-
}
471-
472480
if (!$instance->isAvailableForScope($scope->getScope())) {
473481
throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
474482
}
@@ -479,38 +487,35 @@ public function validateOperation(string $class, string $name, array $checks, st
479487
throw new \UnexpectedValueException($this->l->t('At least one check needs to be provided'));
480488
}
481489

482-
if (strlen($operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
483-
throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
484-
}
485-
486490
$instance->validateOperation($name, $checks, $operation);
487491

488492
foreach ($checks as $check) {
489493
if (!is_string($check['class'])) {
490494
throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
491495
}
492496

497+
if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
498+
throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
499+
}
500+
501+
$reflection = new \ReflectionClass($check['class']);
502+
if ($check['class'] !== ICheck::class && !in_array(ICheck::class, $reflection->getInterfaceNames())) {
503+
throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
504+
}
505+
493506
try {
494507
/** @var ICheck $instance */
495508
$instance = $this->container->get($check['class']);
496509
} catch (ContainerExceptionInterface) {
497510
throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
498511
}
499512

500-
if (!($instance instanceof ICheck)) {
501-
throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
502-
}
503-
504513
if (!empty($instance->supportedEntities())
505514
&& !in_array($entity, $instance->supportedEntities())
506515
) {
507516
throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
508517
}
509518

510-
if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
511-
throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
512-
}
513-
514519
$instance->validateCheck($check['operator'], $check['value']);
515520
}
516521
}

apps/workflowengine/tests/ManagerTest.php

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\WorkflowEngine\Manager;
1313
use OCP\AppFramework\QueryException;
1414
use OCP\AppFramework\Services\IAppConfig;
15+
use OCP\EventDispatcher\Event;
1516
use OCP\EventDispatcher\IEventDispatcher;
1617
use OCP\Files\Events\Node\NodeCreatedEvent;
1718
use OCP\Files\IRootFolder;
@@ -31,11 +32,42 @@
3132
use OCP\WorkflowEngine\IEntityEvent;
3233
use OCP\WorkflowEngine\IManager;
3334
use OCP\WorkflowEngine\IOperation;
35+
use OCP\WorkflowEngine\IRuleMatcher;
3436
use PHPUnit\Framework\MockObject\MockObject;
3537
use Psr\Container\ContainerInterface;
3638
use Psr\Log\LoggerInterface;
3739
use Test\TestCase;
3840

41+
class TestAdminOp implements IOperation {
42+
public function getDisplayName(): string {
43+
return 'Admin';
44+
}
45+
46+
public function getDescription(): string {
47+
return '';
48+
}
49+
50+
public function getIcon(): string {
51+
return '';
52+
}
53+
54+
public function isAvailableForScope(int $scope): bool {
55+
return true;
56+
}
57+
58+
public function validateOperation(string $name, array $checks, string $operation): void {
59+
}
60+
61+
public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
62+
}
63+
}
64+
65+
class TestUserOp extends TestAdminOp {
66+
public function getDisplayName(): string {
67+
return 'User';
68+
}
69+
}
70+
3971
/**
4072
* Class ManagerTest
4173
*
@@ -393,19 +425,19 @@ public function testUpdateOperation(): void {
393425
$opId1 = $this->invokePrivate(
394426
$this->manager,
395427
'insertOperation',
396-
['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
428+
[TestAdminOp::class, 'Test01', [11, 22], 'foo', $entity, []]
397429
);
398430
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
399431

400432
$opId2 = $this->invokePrivate(
401433
$this->manager,
402434
'insertOperation',
403-
['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
435+
[TestUserOp::class, 'Test02', [33, 22], 'bar', $entity, []]
404436
);
405437
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
406438

407-
$check1 = ['class' => 'OCA\WFE\C22', 'operator' => 'eq', 'value' => 'asdf'];
408-
$check2 = ['class' => 'OCA\WFE\C33', 'operator' => 'eq', 'value' => 23456];
439+
$check1 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 'asdf'];
440+
$check2 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 23456];
409441

410442
/** @noinspection PhpUnhandledExceptionInspection */
411443
$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
@@ -667,11 +699,6 @@ public function testValidateOperationDataLengthError(): void {
667699
->method('getScope')
668700
->willReturn(IManager::SCOPE_ADMIN);
669701

670-
$operationMock->expects($this->once())
671-
->method('isAvailableForScope')
672-
->with(IManager::SCOPE_ADMIN)
673-
->willReturn(true);
674-
675702
$operationMock->expects($this->never())
676703
->method('validateOperation');
677704

@@ -719,7 +746,7 @@ public function testValidateOperationScopeNotAvailable(): void {
719746
'operator' => 'is',
720747
'value' => 'barfoo',
721748
];
722-
$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
749+
$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES - 1, 'FooBar');
723750

724751
$operationMock = $this->createMock(IOperation::class);
725752
$entityMock = $this->createMock(IEntity::class);

0 commit comments

Comments
 (0)