Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow user flows when the acting user is legitimate, but not its owner #18134

Merged
merged 5 commits into from
Nov 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/workflowengine/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ function ($event) use ($eventName, $operationClass, $entityClass) {
/** @var IOperation $operation */
$operation = $this->getContainer()->query($operationClass);

$ruleMatcher->setEntity($entity);
$ruleMatcher->setOperation($operation);

if ($event instanceof Event) {
$entity->prepareRuleMatcher($ruleMatcher, $eventName, $event);
$operation->onEvent($eventName, $event, $ruleMatcher);
Expand Down
160 changes: 133 additions & 27 deletions apps/workflowengine/lib/Entity/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,27 @@

namespace OCA\WorkflowEngine\Entity;

use OCA\WorkflowEngine\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\GenericEvent;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Share\IManager as ShareManager;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\MapperEvent;
use OCP\WorkflowEngine\EntityContext\IDisplayText;
use OCP\WorkflowEngine\GenericEntityEvent;
use OCP\WorkflowEngine\IEntity;
use OCP\WorkflowEngine\IRuleMatcher;
use Symfony\Component\EventDispatcher\GenericEvent;

class File implements IEntity {
class File implements IEntity, IDisplayText {

private const EVENT_NAMESPACE = '\OCP\Files::';

/** @var IL10N */
protected $l10n;
Expand All @@ -46,12 +54,34 @@ class File implements IEntity {
protected $root;
/** @var ILogger */
protected $logger;
/** @var string */
protected $eventName;
/** @var Event */
protected $event;
/** @var ShareManager */
private $shareManager;
/** @var IUserSession */
private $userSession;
/** @var ISystemTagManager */
private $tagManager;


public function __construct(IL10N $l10n, IURLGenerator $urlGenerator, IRootFolder $root, ILogger $logger) {
public function __construct(
IL10N $l10n,
IURLGenerator $urlGenerator,
IRootFolder $root,
ILogger $logger,
ShareManager $shareManager,
IUserSession $userSession,
ISystemTagManager $tagManager
) {
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
$this->root = $root;
$this->logger = $logger;
$this->shareManager = $shareManager;
$this->userSession = $userSession;
$this->tagManager = $tagManager;
}

public function getName(): string {
Expand All @@ -63,14 +93,13 @@ public function getIcon(): string {
}

public function getEvents(): array {
$namespace = '\OCP\Files::';
return [
new GenericEntityEvent($this->l10n->t('File created'), $namespace . 'postCreate'),
new GenericEntityEvent($this->l10n->t('File updated'), $namespace . 'postWrite'),
new GenericEntityEvent($this->l10n->t('File renamed'), $namespace . 'postRename'),
new GenericEntityEvent($this->l10n->t('File deleted'), $namespace . 'postDelete'),
new GenericEntityEvent($this->l10n->t('File accessed'), $namespace . 'postTouch'),
new GenericEntityEvent($this->l10n->t('File copied'), $namespace . 'postCopy'),
new GenericEntityEvent($this->l10n->t('File created'), self::EVENT_NAMESPACE . 'postCreate'),
new GenericEntityEvent($this->l10n->t('File updated'), self::EVENT_NAMESPACE . 'postWrite'),
new GenericEntityEvent($this->l10n->t('File renamed'), self::EVENT_NAMESPACE . 'postRename'),
new GenericEntityEvent($this->l10n->t('File deleted'), self::EVENT_NAMESPACE . 'postDelete'),
new GenericEntityEvent($this->l10n->t('File accessed'), self::EVENT_NAMESPACE . 'postTouch'),
new GenericEntityEvent($this->l10n->t('File copied'), self::EVENT_NAMESPACE . 'postCopy'),
new GenericEntityEvent($this->l10n->t('Tag assigned'), MapperEvent::EVENT_ASSIGN),
];
}
Expand All @@ -79,27 +108,104 @@ public function prepareRuleMatcher(IRuleMatcher $ruleMatcher, string $eventName,
if (!$event instanceof GenericEvent && !$event instanceof MapperEvent) {
return;
}
switch ($eventName) {
case 'postCreate':
case 'postWrite':
case 'postDelete':
case 'postTouch':
$ruleMatcher->setEntitySubject($this, $event->getSubject());
break;
case 'postRename':
case 'postCopy':
$ruleMatcher->setEntitySubject($this, $event->getSubject()[1]);
break;
$this->eventName = $eventName;
$this->event = $event;
try {
$node = $this->getNode();
$ruleMatcher->setEntitySubject($this, $node);
$ruleMatcher->setFileInfo($node->getStorage(), $node->getPath());
} catch (NotFoundException $e) {
// pass
}
}

public function isLegitimatedForUserId(string $uid): bool {
try {
$node = $this->getNode();
if($node->getOwner()->getUID() === $uid) {
return true;
}
$acl = $this->shareManager->getAccessList($node, true, true);
return array_key_exists($uid, $acl['users']);
} catch (NotFoundException $e) {
return false;
}
}

/**
* @throws NotFoundException
*/
protected function getNode(): Node {
if (!$this->event instanceof GenericEvent && !$this->event instanceof MapperEvent) {
throw new NotFoundException();
}
switch ($this->eventName) {
case self::EVENT_NAMESPACE . 'postCreate':
case self::EVENT_NAMESPACE . 'postWrite':
case self::EVENT_NAMESPACE . 'postDelete':
case self::EVENT_NAMESPACE . 'postTouch':
return $this->event->getSubject();
case self::EVENT_NAMESPACE . 'postRename':
case self::EVENT_NAMESPACE . 'postCopy':
return $this->event->getSubject()[1];
case MapperEvent::EVENT_ASSIGN:
if (!$event instanceof MapperEvent || $event->getObjectType() !== 'files') {
break;
if (!$this->event instanceof MapperEvent || $this->event->getObjectType() !== 'files') {
throw new NotFoundException();
}
$nodes = $this->root->getById((int)$event->getObjectId());
$nodes = $this->root->getById((int)$this->event->getObjectId());
if (is_array($nodes) && !empty($nodes)) {
$node = array_shift($nodes);
$ruleMatcher->setEntitySubject($this, $node);
return array_shift($nodes);
}
break;
}
throw new NotFoundException();
}

public function getDisplayText(int $verbosity = 0): string {
$user = $this->userSession->getUser();
try {
$node = $this->getNode();
} catch (NotFoundException $e) {
return '';
}

$options = [
$user ? $user->getDisplayName() : $this->t('Someone'),
$node->getName()
];

switch ($this->eventName) {
case self::EVENT_NAMESPACE . 'postCreate':
return $this->l10n->t('%s created %s', $options);
case self::EVENT_NAMESPACE . 'postWrite':
return $this->l10n->t('%s modified %s', $options);
case self::EVENT_NAMESPACE . 'postDelete':
return $this->l10n->t('%s deleted %s', $options);
case self::EVENT_NAMESPACE . 'postTouch':
return $this->l10n->t('%s accessed %s', $options);
case self::EVENT_NAMESPACE . 'postRename':
return $this->l10n->t('%s renamed %s', $options);
case self::EVENT_NAMESPACE . 'postCopy':
return $this->l10n->t('%s copied %s', $options);
case MapperEvent::EVENT_ASSIGN:
$tagNames = [];
if($this->event instanceof MapperEvent) {
$tagIDs = $this->event->getTags();
$tagObjects = $this->tagManager->getTagsByIds($tagIDs);
foreach ($tagObjects as $systemTag) {
/** @var ISystemTag $systemTag */
if($systemTag->isUserVisible()) {
$tagNames[] = $systemTag->getName();
}
}
}
$filename = array_pop($options);
$tagString = implode(', ', $tagNames);
if($tagString === '') {
return '';
}
array_push($options, $tagString, $filename);
return $this->l10n->t('%s assigned %s to %s', $options);
}
}
}
28 changes: 28 additions & 0 deletions apps/workflowengine/lib/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,32 @@ public function getAllConfiguredEvents() {
return $operations;
}

public function getAllConfiguredScopesForOperation(string $operationClass): array {
static $scopesByOperation = [];
if (isset($scopesByOperation[$operationClass])) {
return $scopesByOperation[$operationClass];
}

$query = $this->connection->getQueryBuilder();

$query->selectDistinct('s.type')
->addSelect('s.value')
->from('flow_operations', 'o')
->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));

$query->setParameters(['operationClass' => $operationClass]);
$result = $query->execute();

$scopesByOperation[$operationClass] = [];
while ($row = $result->fetch()) {
$scope = new ScopeContext($row['type'], $row['value']);
$scopesByOperation[$operationClass][$scope->getHash()] = $scope;
}

return $scopesByOperation[$operationClass];
}

public function getAllOperations(ScopeContext $scopeContext): array {
if(isset($this->operations[$scopeContext->getHash()])) {
return $this->operations[$scopeContext->getHash()];
Expand All @@ -160,6 +186,8 @@ public function getAllOperations(ScopeContext $scopeContext): array {
$query = $this->connection->getQueryBuilder();

$query->select('o.*')
->selectAlias('s.type', 'scope_type')
->selectAlias('s.value', 'scope_actor_id')
->from('flow_operations', 'o')
->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
->where($query->expr()->eq('s.type', $query->createParameter('scope')));
Expand Down
57 changes: 53 additions & 4 deletions apps/workflowengine/lib/Service/RuleMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
use OCP\WorkflowEngine\IEntityCheck;
use OCP\WorkflowEngine\IFileCheck;
use OCP\WorkflowEngine\IManager;
use OCP\WorkflowEngine\IOperation;
use OCP\WorkflowEngine\IRuleMatcher;
use RuntimeException;

class RuleMatcher implements IRuleMatcher {

Expand All @@ -52,8 +54,17 @@ class RuleMatcher implements IRuleMatcher {
protected $fileInfo = [];
/** @var IL10N */
protected $l;

public function __construct(IUserSession $session, IServerContainer $container, IL10N $l, Manager $manager) {
/** @var IOperation */
protected $operation;
/** @var IEntity */
protected $entity;

public function __construct(
IUserSession $session,
IServerContainer $container,
IL10N $l,
Manager $manager
) {
$this->session = $session;
$this->manager = $manager;
$this->container = $container;
Expand All @@ -65,11 +76,38 @@ public function setFileInfo(IStorage $storage, string $path): void {
$this->fileInfo['path'] = $path;
}


public function setEntitySubject(IEntity $entity, $subject): void {
$this->contexts[get_class($entity)] = [$entity, $subject];
}

public function setOperation(IOperation $operation): void {
if($this->operation !== null) {
throw new RuntimeException('This method must not be called more than once');
}
$this->operation = $operation;
}

public function setEntity(IEntity $entity): void {
if($this->entity !== null) {
throw new RuntimeException('This method must not be called more than once');
}
$this->entity = $entity;
}

public function getEntity(): IEntity {
if($this->entity === null) {
throw new \LogicException('Entity was not set yet');
}
return $this->entity;
}

public function getFlows(bool $returnFirstMatchingOperationOnly = true): array {
if(!$this->operation) {
throw new RuntimeException('Operation is not set');
}
return $this->getMatchingOperations(get_class($this->operation), $returnFirstMatchingOperationOnly);
}

public function getMatchingOperations(string $class, bool $returnFirstMatchingOperationOnly = true): array {
$scopes[] = new ScopeContext(IManager::SCOPE_ADMIN);
$user = $this->session->getUser();
Expand All @@ -82,6 +120,17 @@ public function getMatchingOperations(string $class, bool $returnFirstMatchingOp
$operations = array_merge($operations, $this->manager->getOperations($class, $scope));
}

$additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class);
foreach ($additionalScopes as $hash => $scopeCandidate) {
/** @var ScopeContext $scopeCandidate */
if ($scopeCandidate->getScope() !== IManager::SCOPE_USER) {
continue;
}
if ($this->entity->isLegitimatedForUserId($scopeCandidate->getScopeId())) {
$operations = array_merge($operations, $this->manager->getOperations($class, $scopeCandidate));
}
}

$matches = [];
foreach ($operations as $operation) {
$checkIds = json_decode($operation['checks'], true);
Expand Down Expand Up @@ -117,7 +166,7 @@ public function check(array $check) {

if ($checkInstance instanceof IFileCheck) {
if (empty($this->fileInfo)) {
throw new \RuntimeException('Must set file info before running the check');
throw new RuntimeException('Must set file info before running the check');
}
$checkInstance->setFileInfo($this->fileInfo['storage'], $this->fileInfo['path']);
} elseif ($checkInstance instanceof IEntityCheck) {
Expand Down
6 changes: 5 additions & 1 deletion apps/workflowengine/tests/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use OCP\IServerContainer;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use OCP\WorkflowEngine\ICheck;
use OCP\WorkflowEngine\IEntity;
use OCP\WorkflowEngine\IManager;
Expand Down Expand Up @@ -283,7 +284,10 @@ public function testUpdateOperation() {
$this->l,
$this->createMock(IURLGenerator::class),
$this->createMock(IRootFolder::class),
$this->createMock(ILogger::class)
$this->createMock(ILogger::class),
$this->createMock(\OCP\Share\IManager::class),
$this->createMock(IUserSession::class),
$this->createMock(ISystemTagManager::class)
])
->setMethodsExcept(['getEvents'])
->getMock();
Expand Down
4 changes: 4 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@
'OCP\\User\\Backend\\ISetPasswordBackend' => $baseDir . '/lib/public/User/Backend/ISetPasswordBackend.php',
'OCP\\User\\Events\\PostLoginEvent' => $baseDir . '/lib/public/User/Events/PostLoginEvent.php',
'OCP\\Util' => $baseDir . '/lib/public/Util.php',
'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php',
blizzz marked this conversation as resolved.
Show resolved Hide resolved
'OCP\\WorkflowEngine\\EntityContext\\IDisplayText' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayText.php',
'OCP\\WorkflowEngine\\EntityContext\\IIcon' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IIcon.php',
'OCP\\WorkflowEngine\\EntityContext\\IUrl' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IUrl.php',
'OCP\\WorkflowEngine\\GenericEntityEvent' => $baseDir . '/lib/public/WorkflowEngine/GenericEntityEvent.php',
'OCP\\WorkflowEngine\\ICheck' => $baseDir . '/lib/public/WorkflowEngine/ICheck.php',
'OCP\\WorkflowEngine\\IComplexOperation' => $baseDir . '/lib/public/WorkflowEngine/IComplexOperation.php',
Expand Down
4 changes: 4 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\User\\Backend\\ISetPasswordBackend' => __DIR__ . '/../../..' . '/lib/public/User/Backend/ISetPasswordBackend.php',
'OCP\\User\\Events\\PostLoginEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PostLoginEvent.php',
'OCP\\Util' => __DIR__ . '/../../..' . '/lib/public/Util.php',
'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php',
'OCP\\WorkflowEngine\\EntityContext\\IDisplayText' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayText.php',
'OCP\\WorkflowEngine\\EntityContext\\IIcon' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IIcon.php',
'OCP\\WorkflowEngine\\EntityContext\\IUrl' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IUrl.php',
'OCP\\WorkflowEngine\\GenericEntityEvent' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/GenericEntityEvent.php',
'OCP\\WorkflowEngine\\ICheck' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/ICheck.php',
'OCP\\WorkflowEngine\\IComplexOperation' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/IComplexOperation.php',
Expand Down
Loading