Merge pull request #36820 from nextcloud/backport/36814/stable20

[stable20] Validate the scope when validating operations
pull/37191/head
John Molakvoæ 1 year ago committed by GitHub
commit fbc5007e0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -181,6 +181,13 @@ class Manager implements IManager {
return $scopesByOperation[$operationClass]; return $scopesByOperation[$operationClass];
} }
try {
/** @var IOperation $operation */
$operation = $this->container->query($operationClass);
} catch (QueryException $e) {
return [];
}
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
$query->selectDistinct('s.type') $query->selectDistinct('s.type')
@ -195,6 +202,11 @@ class Manager implements IManager {
$scopesByOperation[$operationClass] = []; $scopesByOperation[$operationClass] = [];
while ($row = $result->fetch()) { while ($row = $result->fetch()) {
$scope = new ScopeContext($row['type'], $row['value']); $scope = new ScopeContext($row['type'], $row['value']);
if (!$operation->isAvailableForScope((int) $row['type'])) {
continue;
}
$scopesByOperation[$operationClass][$scope->getHash()] = $scope; $scopesByOperation[$operationClass][$scope->getHash()] = $scope;
} }
@ -224,6 +236,17 @@ class Manager implements IManager {
$this->operations[$scopeContext->getHash()] = []; $this->operations[$scopeContext->getHash()] = [];
while ($row = $result->fetch()) { while ($row = $result->fetch()) {
try {
/** @var IOperation $operation */
$operation = $this->container->query($row['class']);
} catch (QueryException $e) {
continue;
}
if (!$operation->isAvailableForScope((int) $row['scope_type'])) {
continue;
}
if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) { if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
$this->operations[$scopeContext->getHash()][$row['class']] = []; $this->operations[$scopeContext->getHash()][$row['class']] = [];
} }
@ -302,7 +325,7 @@ class Manager implements IManager {
string $entity, string $entity,
array $events array $events
) { ) {
$this->validateOperation($class, $name, $checks, $operation, $entity, $events); $this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
$this->connection->beginTransaction(); $this->connection->beginTransaction();
@ -374,7 +397,7 @@ class Manager implements IManager {
throw new \DomainException('Target operation not within scope'); throw new \DomainException('Target operation not within scope');
}; };
$row = $this->getOperation($id); $row = $this->getOperation($id);
$this->validateOperation($row['class'], $name, $checks, $operation, $entity, $events); $this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
$checkIds = []; $checkIds = [];
try { try {
@ -474,9 +497,12 @@ class Manager implements IManager {
* @param string $name * @param string $name
* @param array[] $checks * @param array[] $checks
* @param string $operation * @param string $operation
* @param ScopeContext $scope
* @param string $entity
* @param array $events
* @throws \UnexpectedValueException * @throws \UnexpectedValueException
*/ */
public function validateOperation($class, $name, array $checks, $operation, string $entity, array $events) { public function validateOperation($class, $name, array $checks, $operation, ScopeContext $scope, string $entity, array $events) {
try { try {
/** @var IOperation $instance */ /** @var IOperation $instance */
$instance = $this->container->query($class); $instance = $this->container->query($class);
@ -488,6 +514,10 @@ class Manager implements IManager {
throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class])); throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
} }
if (!$instance->isAvailableForScope($scope->getScope())) {
throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
}
$this->validateEvents($entity, $events, $instance); $this->validateEvents($entity, $events, $instance);
if (count($checks) === 0) { if (count($checks) === 0) {

@ -25,6 +25,7 @@ use OC\L10N\L10N;
use OCA\WorkflowEngine\Entity\File; use OCA\WorkflowEngine\Entity\File;
use OCA\WorkflowEngine\Helper\ScopeContext; use OCA\WorkflowEngine\Helper\ScopeContext;
use OCA\WorkflowEngine\Manager; use OCA\WorkflowEngine\Manager;
use OCP\AppFramework\QueryException;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\IConfig; use OCP\IConfig;
@ -194,6 +195,32 @@ class ManagerTest extends TestCase {
$userScope = $this->buildScope('jackie'); $userScope = $this->buildScope('jackie');
$entity = File::class; $entity = File::class;
$adminOperation = $this->createMock(IOperation::class);
$adminOperation->expects($this->any())
->method('isAvailableForScope')
->willReturnMap([
[IManager::SCOPE_ADMIN, true],
[IManager::SCOPE_USER, false],
]);
$userOperation = $this->createMock(IOperation::class);
$userOperation->expects($this->any())
->method('isAvailableForScope')
->willReturnMap([
[IManager::SCOPE_ADMIN, false],
[IManager::SCOPE_USER, true],
]);
$this->container->expects($this->any())
->method('query')
->willReturnCallback(function ($className) use ($adminOperation, $userOperation) {
switch ($className) {
case 'OCA\WFE\TestAdminOp':
return $adminOperation;
case 'OCA\WFE\TestUserOp':
return $userOperation;
}
});
$opId1 = $this->invokePrivate( $opId1 = $this->invokePrivate(
$this->manager, $this->manager,
'insertOperation', 'insertOperation',
@ -214,6 +241,13 @@ class ManagerTest extends TestCase {
); );
$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]); $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
$opId4 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestAdminOp', 'Test04', [41, 10, 4], 'NoBar', $entity, []]
);
$this->invokePrivate($this->manager, 'addScope', [$opId4, $userScope]);
$adminOps = $this->manager->getAllOperations($adminScope); $adminOps = $this->manager->getAllOperations($adminScope);
$userOps = $this->manager->getAllOperations($userScope); $userOps = $this->manager->getAllOperations($userScope);
@ -264,6 +298,25 @@ class ManagerTest extends TestCase {
); );
$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]); $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
$operation = $this->createMock(IOperation::class);
$operation->expects($this->any())
->method('isAvailableForScope')
->willReturnMap([
[IManager::SCOPE_ADMIN, true],
[IManager::SCOPE_USER, true],
]);
$this->container->expects($this->any())
->method('query')
->willReturnCallback(function ($className) use ($operation) {
switch ($className) {
case 'OCA\WFE\TestOp':
return $operation;
case 'OCA\WFE\OtherTestOp':
throw new QueryException();
}
});
$adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope); $adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
$userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope); $userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
@ -283,11 +336,20 @@ class ManagerTest extends TestCase {
$userScope = $this->buildScope('jackie'); $userScope = $this->buildScope('jackie');
$entity = File::class; $entity = File::class;
$operationMock = $this->createMock(IOperation::class);
$operationMock->expects($this->any())
->method('isAvailableForScope')
->withConsecutive(
[IManager::SCOPE_ADMIN],
[IManager::SCOPE_USER]
)
->willReturn(true);
$this->container->expects($this->any()) $this->container->expects($this->any())
->method('query') ->method('query')
->willReturnCallback(function ($class) { ->willReturnCallback(function ($class) use ($operationMock) {
if (substr($class, -2) === 'Op') { if (substr($class, -2) === 'Op') {
return $this->createMock(IOperation::class); return $operationMock;
} elseif ($class === File::class) { } elseif ($class === File::class) {
return $this->getMockBuilder(File::class) return $this->getMockBuilder(File::class)
->setConstructorArgs([ ->setConstructorArgs([
@ -448,6 +510,16 @@ class ManagerTest extends TestCase {
$entityMock = $this->createMock(IEntity::class); $entityMock = $this->createMock(IEntity::class);
$eventEntityMock = $this->createMock(IEntityEvent::class); $eventEntityMock = $this->createMock(IEntityEvent::class);
$checkMock = $this->createMock(ICheck::class); $checkMock = $this->createMock(ICheck::class);
$scopeMock = $this->createMock(ScopeContext::class);
$scopeMock->expects($this->any())
->method('getScope')
->willReturn(IManager::SCOPE_ADMIN);
$operationMock->expects($this->once())
->method('isAvailableForScope')
->with(IManager::SCOPE_ADMIN)
->willReturn(true);
$operationMock->expects($this->once()) $operationMock->expects($this->once())
->method('validateOperation') ->method('validateOperation')
@ -484,7 +556,7 @@ class ManagerTest extends TestCase {
} }
}); });
$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', IEntity::class, ['MyEvent']); $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
} }
public function testValidateOperationCheckInputLengthError() { public function testValidateOperationCheckInputLengthError() {
@ -498,6 +570,16 @@ class ManagerTest extends TestCase {
$entityMock = $this->createMock(IEntity::class); $entityMock = $this->createMock(IEntity::class);
$eventEntityMock = $this->createMock(IEntityEvent::class); $eventEntityMock = $this->createMock(IEntityEvent::class);
$checkMock = $this->createMock(ICheck::class); $checkMock = $this->createMock(ICheck::class);
$scopeMock = $this->createMock(ScopeContext::class);
$scopeMock->expects($this->any())
->method('getScope')
->willReturn(IManager::SCOPE_ADMIN);
$operationMock->expects($this->once())
->method('isAvailableForScope')
->with(IManager::SCOPE_ADMIN)
->willReturn(true);
$operationMock->expects($this->once()) $operationMock->expects($this->once())
->method('validateOperation') ->method('validateOperation')
@ -535,7 +617,7 @@ class ManagerTest extends TestCase {
}); });
try { try {
$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', IEntity::class, ['MyEvent']); $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
} catch (\UnexpectedValueException $e) { } catch (\UnexpectedValueException $e) {
$this->assertSame('The provided check value is too long', $e->getMessage()); $this->assertSame('The provided check value is too long', $e->getMessage());
} }
@ -553,6 +635,16 @@ class ManagerTest extends TestCase {
$entityMock = $this->createMock(IEntity::class); $entityMock = $this->createMock(IEntity::class);
$eventEntityMock = $this->createMock(IEntityEvent::class); $eventEntityMock = $this->createMock(IEntityEvent::class);
$checkMock = $this->createMock(ICheck::class); $checkMock = $this->createMock(ICheck::class);
$scopeMock = $this->createMock(ScopeContext::class);
$scopeMock->expects($this->any())
->method('getScope')
->willReturn(IManager::SCOPE_ADMIN);
$operationMock->expects($this->once())
->method('isAvailableForScope')
->with(IManager::SCOPE_ADMIN)
->willReturn(true);
$operationMock->expects($this->never()) $operationMock->expects($this->never())
->method('validateOperation'); ->method('validateOperation');
@ -589,9 +681,73 @@ class ManagerTest extends TestCase {
}); });
try { try {
$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, IEntity::class, ['MyEvent']); $this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
} catch (\UnexpectedValueException $e) { } catch (\UnexpectedValueException $e) {
$this->assertSame('The provided operation data is too long', $e->getMessage()); $this->assertSame('The provided operation data is too long', $e->getMessage());
} }
} }
public function testValidateOperationScopeNotAvailable() {
$check = [
'class' => ICheck::class,
'operator' => 'is',
'value' => 'barfoo',
];
$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
$operationMock = $this->createMock(IOperation::class);
$entityMock = $this->createMock(IEntity::class);
$eventEntityMock = $this->createMock(IEntityEvent::class);
$checkMock = $this->createMock(ICheck::class);
$scopeMock = $this->createMock(ScopeContext::class);
$scopeMock->expects($this->any())
->method('getScope')
->willReturn(IManager::SCOPE_ADMIN);
$operationMock->expects($this->once())
->method('isAvailableForScope')
->with(IManager::SCOPE_ADMIN)
->willReturn(false);
$operationMock->expects($this->never())
->method('validateOperation');
$entityMock->expects($this->any())
->method('getEvents')
->willReturn([$eventEntityMock]);
$eventEntityMock->expects($this->any())
->method('getEventName')
->willReturn('MyEvent');
$checkMock->expects($this->any())
->method('supportedEntities')
->willReturn([IEntity::class]);
$checkMock->expects($this->never())
->method('validateCheck');
$this->container->expects($this->any())
->method('query')
->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
switch ($className) {
case IOperation::class:
return $operationMock;
case IEntity::class:
return $entityMock;
case IEntityEvent::class:
return $eventEntityMock;
case ICheck::class:
return $checkMock;
default:
return $this->createMock($className);
}
});
try {
$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
} catch (\UnexpectedValueException $e) {
$this->assertSame('Operation OCP\WorkflowEngine\IOperation is invalid', $e->getMessage());
}
}
} }

Loading…
Cancel
Save