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

[stable27] Fix deleted card/board issues #5442

Merged
merged 6 commits into from
Jan 12, 2024
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
7 changes: 7 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}

- name: Checkout activity
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
repository: nextcloud/activity
ref: ${{ matrix.server-versions }}
path: apps/activity

- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/[email protected]
with:
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"@test:integration"
],
"test:unit": "vendor/bin/phpunit -c tests/phpunit.xml",
"test:integration": "vendor/bin/phpunit -c tests/phpunit.integration.xml && cd tests/integration && ./run.sh"
"test:integration": "vendor/bin/phpunit -c tests/phpunit.integration.xml",
"test:api": "cd tests/integration && ./run.sh"
},
"autoload-dev": {
"psr-4": {
Expand Down
21 changes: 21 additions & 0 deletions lib/Activity/ActivityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\PermissionService;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
Expand Down Expand Up @@ -554,4 +555,24 @@ private function findDetailsForAcl($aclId) {
'board' => $board
];
}

public function canSeeCardActivity(int $cardId): bool {
try {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$card = $this->cardMapper->find($cardId);
return $card->getDeletedAt() === 0;
} catch (NoPermissionException $e) {
return false;
}
}

public function canSeeBoardActivity(int $boardId): bool {
try {
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
$board = $this->boardMapper->find($boardId);
return $board->getDeletedAt() === 0;
} catch (NoPermissionException $e) {
return false;
}
}
}
6 changes: 6 additions & 0 deletions lib/Activity/DeckProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public function parse($language, IEvent $event, IEvent $previousEvent = null): I
$event->setAuthor($author);
}
if ($event->getObjectType() === ActivityManager::DECK_OBJECT_BOARD) {
if (!$this->activityManager->canSeeBoardActivity($event->getObjectId())) {
throw new \InvalidArgumentException();
}
if (isset($subjectParams['board']) && $event->getObjectName() === '') {
$event->setObject($event->getObjectType(), $event->getObjectId(), $subjectParams['board']['title']);
}
Expand All @@ -125,6 +128,9 @@ public function parse($language, IEvent $event, IEvent $previousEvent = null): I
}

if (isset($subjectParams['card']) && $event->getObjectType() === ActivityManager::DECK_OBJECT_CARD) {
if (!$this->activityManager->canSeeCardActivity($event->getObjectId())) {
throw new \InvalidArgumentException();
}
if ($event->getObjectName() === '') {
$event->setObject($event->getObjectType(), $event->getObjectId(), $subjectParams['card']['title']);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/Db/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
* @method int getLastModified()
* @method int getCreatedAt()
* @method bool getArchived()
* @method int getDeletedAt()
* @method void setDeletedAt(int $deletedAt)
* @method bool getNotified()
*
* @method void setLabels(Label[] $labels)
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/BoardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ public function addAcl($boardId, $type, $participant, $edit, $share, $manage) {
$newAcl = $this->aclMapper->insert($acl);

$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE, [], $this->userId);
$this->notificationHelper->sendBoardShared((int)$boardId, $acl);
$this->notificationHelper->sendBoardShared($boardId, $acl);
$this->boardMapper->mapAcl($newAcl);
$this->changeHelper->boardChanged($boardId);

Expand Down
6 changes: 3 additions & 3 deletions lib/Service/CardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public function delete($id) {
public function update($id, $title, $stackId, $type, $owner, $description = '', $order = 0, $duedate = null, $deletedAt = null, $archived = null) {
$this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order'));

$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT, allowDeletedCard: true);
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT);

if ($this->boardService->isArchived($this->cardMapper, $id)) {
Expand All @@ -306,9 +306,9 @@ public function update($id, $title, $stackId, $type, $owner, $description = '',
}

if ($card->getDeletedAt() !== 0) {
if ($deletedAt === null) {
if ($deletedAt === null || $deletedAt > 0) {
// Only allow operations when restoring the card
throw new StatusException('Operation not allowed. This card was deleted.');
throw new NoPermissionException('Operation not allowed. This card was deleted.');
}
}

Expand Down
15 changes: 4 additions & 11 deletions lib/Service/CommentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private function get(int $cardId, int $commentId): IComment {
throw new NotFoundException('No comment found.');
}
if ($comment->getParentId() !== '0') {
$this->permissionService->checkPermission($this->cardMapper, $comment->getParentId(), Acl::PERMISSION_READ);
$this->permissionService->checkPermission($this->cardMapper, (int)$comment->getParentId(), Acl::PERMISSION_READ);
}

return $comment;
Expand All @@ -113,24 +113,17 @@ public function getFormatted(int $cardId, int $commentId): array {
}

/**
* @param string $cardId
* @param string $message
* @param string $replyTo
* @return DataResponse
* @throws BadRequestException
* @throws NotFoundException|NoPermissionException
*/
public function create(string $cardId, string $message, string $replyTo = '0'): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
public function create(int $cardId, string $message, string $replyTo = '0'): DataResponse {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);

// Check if parent is a comment on the same card
if ($replyTo !== '0') {
try {
$comment = $this->commentsManager->get($replyTo);
if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) {
if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || (int)$comment->getObjectId() !== $cardId) {
throw new CommentNotFoundException();
}
} catch (CommentNotFoundException $e) {
Expand All @@ -139,7 +132,7 @@ public function create(string $cardId, string $message, string $replyTo = '0'):
}

try {
$comment = $this->commentsManager->create('users', $this->userId, Application::COMMENT_ENTITY_TYPE, $cardId);
$comment = $this->commentsManager->create('users', $this->userId, Application::COMMENT_ENTITY_TYPE, (string)$cardId);
$comment->setMessage($message);
$comment->setVerb('comment');
$comment->setParentId($replyTo);
Expand Down
19 changes: 13 additions & 6 deletions lib/Service/PermissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\User;
use OCA\Deck\NoPermissionException;
Expand Down Expand Up @@ -107,8 +108,9 @@ public function getPermissions($boardId, ?string $userId = null) {
return $cached;
}

$board = $this->getBoard($boardId);
$owner = $this->userIsBoardOwner($boardId, $userId);
$acls = $this->aclMapper->findAll($boardId);
$acls = $board->getDeletedAt() === 0 ? $this->aclMapper->findAll($boardId) : [];
$permissions = [
Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ, $userId),
Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT, $userId),
Expand Down Expand Up @@ -142,13 +144,10 @@ public function matchPermissions(Board $board) {
/**
* check permissions for replacing dark magic middleware
*
* @param $mapper IPermissionMapper|null null if $id is a boardId
* @param $id int unique identifier of the Entity
* @param $permission int
* @return bool
* @param numeric $id
* @throws NoPermissionException
*/
public function checkPermission($mapper, $id, $permission, $userId = null): bool {
public function checkPermission(?IPermissionMapper $mapper, $id, int $permission, $userId = null, bool $allowDeletedCard = false): bool {
$boardId = $id;
if ($mapper instanceof IPermissionMapper && !($mapper instanceof BoardMapper)) {
$boardId = $mapper->findBoardId($id);
Expand All @@ -160,6 +159,14 @@ public function checkPermission($mapper, $id, $permission, $userId = null): bool

$permissions = $this->getPermissions($boardId, $userId);
if ($permissions[$permission] === true) {

if (!$allowDeletedCard && $mapper instanceof CardMapper) {
$card = $mapper->find($id);
if ($card->getDeletedAt() > 0) {
throw new NoPermissionException('Card is deleted');
}
}

return true;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Sharing/ShareAPIHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ private function parseDate(string $expireDate): \DateTime {
*/
public function canAccessShare(IShare $share, string $user): bool {
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_READ, $user);
$this->permissionService->checkPermission($this->cardMapper, (int)$share->getSharedWith(), Acl::PERMISSION_READ, $user);
} catch (NoPermissionException $e) {
return false;
}
Expand Down
34 changes: 34 additions & 0 deletions tests/integration/features/acl.feature
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,37 @@ Feature: acl
| property | value |
| title | Double shared board |


Scenario: Deleted board is inaccessible to share recipients
Given acting as user "user0"
When creates a board with example content
And remember the last card as "user0-card"
When post a comment with content "hello comment" on the card
And uploads an attachment to the last used card
And remember the last attachment as "user0-attachment"
And shares the board with user "user1"
Then the HTTP status code should be "200"
And delete the board

Given acting as user "user1"
When fetching the attachments for the card "user0-card"
Then the response should have a status code 403

When get the comments on the card
Then the response should have a status code 403

When update a comment with content "hello deleted" on the card
Then the response should have a status code 403

When delete the comment on the card
Then the response should have a status code 403
# 644
When post a comment with content "hello deleted" on the card
Then the response should have a status code 403

When get the card details
Then the response should have a status code 403
When fetching the attachment "user0-attachment" for the card "user0-card"
Then the response should have a status code 403
When deleting the attachment "user0-attachment" for the card "user0-card"
Then the response should have a status code 403
10 changes: 10 additions & 0 deletions tests/integration/features/bootstrap/AttachmentContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,14 @@ public function fetchingTheAttachmentForTheCard($attachmentReference, $cardRefer

$this->requestContext->sendPlainRequest('GET', '/index.php/apps/deck/cards/' . $cardId . '/attachment/file:' . $attachmentId);
}

/**
* @When fetching the attachments for the card :cardReference
*/
public function fetchingTheAttachmentsForTheCard($cardReference) {
$cardId = $this->boardContext->getRememberedCard($cardReference)['id'] ?? null;
Assert::assertNotNull($cardId, 'Card needs to be available');

$this->requestContext->sendPlainRequest('GET', '/index.php/apps/deck/cards/' . $cardId . '/attachments');
}
}
50 changes: 45 additions & 5 deletions tests/integration/features/bootstrap/BoardContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class BoardContext implements Context {
/** @var array last card response */
private $card = null;
private array $storedCards = [];
private ?array $activities = null;

/** @var ServerContext */
private $serverContext;
private ServerContext $serverContext;

/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
Expand Down Expand Up @@ -204,7 +204,9 @@ public function setTheDescriptionTo($description) {
['description' => $description]
));
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
if ($this->requestContext->getResponse()->getStatusCode() === 200) {
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
}

/**
Expand All @@ -216,7 +218,9 @@ public function setCardAttribute($attribute, $value) {
[$attribute => $value]
));
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
if ($this->requestContext->getResponse()->getStatusCode() === 200) {
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
}

/**
Expand All @@ -227,7 +231,9 @@ public function getCard() {
$this->card
));
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
if ($this->requestContext->getResponse()->getStatusCode() === 200) {
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
}

/**
Expand Down Expand Up @@ -282,4 +288,38 @@ public function rememberTheLastCardAs($arg1) {
public function getRememberedCard($arg1) {
return $this->storedCards[$arg1] ?? null;
}

/**
* @Given /^delete the card$/
*/
public function deleteTheCard() {
$this->requestContext->sendJSONrequest('DELETE', '/index.php/apps/deck/cards/' . $this->card['id']);
$this->card['deletedAt'] = time();
}

/**
* @Given /^delete the board/
*/
public function deleteTheBoard() {
$this->requestContext->sendJSONrequest('DELETE', '/index.php/apps/deck/boards/' . $this->board['id']);
}


/**
* @Given /^get the activities for the last card$/
*/
public function getActivitiesForTheLastCard() {
$card = $this->getLastUsedCard();
$this->requestContext->sendOCSRequest('GET', '/apps/activity/api/v2/activity/filter?format=json&type=deck&since=0&object_type=deck_card&object_id=' . $card['id'] . '&limit=50');
$this->activities = json_decode((string)$this->getResponse()->getBody(), true)['ocs']['data'] ?? null;
}

/**
* @Then the fetched activities should have :count entries
*/
public function theFetchedActivitiesShouldHaveEntries($count) {
Assert::assertEquals($count, count($this->activities ?? []));
}


}
Loading
Loading