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

[stable24] Fix deleted card/board issues #5449

Merged
merged 11 commits into from Jan 12, 2024
19 changes: 16 additions & 3 deletions .github/workflows/fixup.yml
Expand Up @@ -3,18 +3,31 @@
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization

name: Pull request checks
name: Block fixup and squash commits

on: pull_request
on:
pull_request:
types: [opened, ready_for_review, reopened, synchronize]

permissions:
contents: read

concurrency:
group: fixup-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
commit-message-check:
if: github.event.pull_request.draft == false

permissions:
pull-requests: write
name: Block fixup and squash commits

runs-on: ubuntu-latest

steps:
- name: Run check
uses: xt0rted/block-autosquash-commits-action@v2
uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
18 changes: 13 additions & 5 deletions .github/workflows/integration.yml
Expand Up @@ -61,17 +61,25 @@ 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/setup-php@2.18.0
uses: shivammathur/setup-php@2.25.4
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql,
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, apcu
ini-values:
apc.enable_cli=on
coverage: none

- name: Set up PHPUnit
- name: Set up dependencies
working-directory: apps/${{ env.APP_NAME }}
run: composer i
run: composer i --no-dev

- name: Set up Nextcloud
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/phpunit.yml
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php-versions: ['7.4', '8.0', '8.1']
php-versions: ['7.4', '8.0']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['stable24']

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -42,7 +42,8 @@
"@test:integration"
],
"test:unit": "phpunit -c tests/phpunit.xml",
"test:integration": "phpunit -c tests/phpunit.integration.xml && cd tests/integration && ./run.sh"
"test:integration": "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
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 @@ -549,4 +550,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
Expand Up @@ -111,6 +111,9 @@ public function parse($language, IEvent $event, IEvent $previousEvent = null) {
$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) {
}

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: 1 addition & 1 deletion lib/Service/BoardService.php
Expand Up @@ -471,7 +471,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
Expand Up @@ -264,7 +264,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, null, true);
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT);

if ($this->boardService->isArchived($this->cardMapper, $id)) {
Expand All @@ -276,9 +276,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
Expand Up @@ -83,24 +83,17 @@ public function list(string $cardId, int $limit = 20, int $offset = 0): DataResp
}

/**
* @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 @@ -109,7 +102,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 Expand Up @@ -145,7 +138,7 @@ public function update(string $cardId, string $commentId, string $message): Data
throw new NoPermissionException('Only authors are allowed to edit their comment.');
}
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);
}

$comment->setMessage($message);
Expand Down
39 changes: 25 additions & 14 deletions lib/Service/PermissionService.php
Expand Up @@ -29,6 +29,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 @@ -97,21 +98,26 @@ public function __construct(
* @param $boardId
* @return bool|array
*/
public function getPermissions($boardId) {
public function getPermissions($boardId, ?string $userId = null) {
if ($userId === null) {
$userId = $this->userId;
}

if ($cached = $this->permissionCache->get($boardId)) {
return $cached;
}

$owner = $this->userIsBoardOwner($boardId);
$acls = $this->aclMapper->findAll($boardId);
$board = $this->getBoard($boardId);
$owner = $this->userIsBoardOwner($boardId, $userId);
$acls = $board->getDeletedAt() === 0 ? $this->aclMapper->findAll($boardId) : [];
$permissions = [
Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ),
Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT),
Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE),
Acl::PERMISSION_SHARE => ($owner || $this->userCan($acls, Acl::PERMISSION_SHARE))
&& (!$this->shareManager->sharingDisabledForUser($this->userId))
];
$this->permissionCache->set($boardId, $permissions);
$this->permissionCache->set((string)$boardId, $permissions);
return $permissions;
}

Expand All @@ -137,13 +143,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) {
public function checkPermission($mapper, $id, $permission, $userId = null, bool $allowDeletedCard = false) {
$boardId = $id;
if ($mapper instanceof IPermissionMapper && !($mapper instanceof BoardMapper)) {
$boardId = $mapper->findBoardId($id);
Expand All @@ -157,12 +160,20 @@ public function checkPermission($mapper, $id, $permission, $userId = null) {
throw new NoPermissionException('Permission denied');
}

if ($this->userIsBoardOwner($boardId, $userId)) {
return true;
}

try {
$acls = $this->getBoard($boardId)->getAcl() ?? [];
$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;
}

$acls = $this->getBoard((int)$boardId)->getAcl() ?? [];
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
Expand Down
2 changes: 1 addition & 1 deletion lib/Sharing/ShareAPIHelper.php
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
50 changes: 50 additions & 0 deletions tests/integration/features/acl.feature
Expand Up @@ -90,3 +90,53 @@ Feature: acl
And the current user should not have "edit" permissions on the board
And the current user should have "share" permissions on the board
And the current user should not have "manage" permissions on the board

Scenario: Share a board multiple times
Given Logging in using web as "user0"
And creates a board named "Double shared board" with color "ff0000"
And shares the board with user "user1"
And shares the board with group "group1"
And creates a board named "Single shared board" with color "00ff00"
And shares the board with user "user1"
When Logging in using web as "user1"
And fetching the board list
Then the response should have a status code "200"
And the response should be a list of objects
And the response should contain an element with the properties
| 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
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');
}
}