Skip to content

Deleting a board that has file attachments breaks the filesystem for every board member (unhandled DoesNotExistException in SetupManager) #8433

Description

@YaSoC

Describe the bug

If a Deck board is deleted (soft-deleted to trash) while any of its cards still has a file attached, every user who had access to that board loses access to their entire filesystem until the board is permanently purged by DeleteCron.

DeckShareProvider is queried by Files_Sharing\MountProvider during SetupManager::setupForUser(). It returns shares belonging to cards of the deleted board, then asks PermissionService for the board's permissions — which refuses to load a deleted board and throws DoesNotExistException. Nothing catches it, so the exception propagates out of filesystem setup itself.

The result is HTTP 500 on every request that touches the filesystem — not just Deck: Files/WebDAV, the Dashboard widgets, Talk attachments and avatars, Collabora/WOPI save callbacks, previews, and the trashbin/versions background jobs. Even occ user:info fails, because it calls getStorageInfo().

This is effectively a self-inflicted denial of service that any regular user can trigger by deleting their own board.

Steps to reproduce

  1. As user A, create a board, a list and a card.
  2. Attach a file from Files to that card (this creates an oc_share row with share_type = 12).
  3. Share the board with user B (this creates the child row with share_type = 13 for B).
  4. As user A, delete the board (normal delete — it goes to the Deck trash, deleted_at is set).
  5. As user B (and as A), open Files.

Expected behaviour

Deleting a board should not affect unrelated parts of the product. Shares belonging to cards of a deleted board should simply not be returned by the share provider, and a failure inside one mount provider should not abort filesystem setup.

Actual behaviour

Files shows "This folder is unavailable, please try again later or contact the administration" (the generic HTTP 500 branch of humanizeWebDAVError()). The Dashboard photos widget shows "Could not load photos folder". Collabora shows
"The document cannot be saved." All of these are the same 500.

The instance log fills with:

Exception: OCP\AppFramework\Db\DoesNotExistException
Message: Did expect one result but found none when executing: query
"SELECT * FROM *PREFIX*deck_boards WHERE (id = :dcValue2) AND (deleted_at = :dcValue1) ORDER BY

A convenient way to confirm which users are affected, without a browser:

occ files:mount:list --cached-only # works — the cached mount data is intact
occ files:mount:list # fails — this one runs setupFS
occ files:mount:list # works

Stack trace

Abridged, from a PROPFIND /remote.php/dav/files// request:

remote.php:152
OCA\DAV\Server->exec → Sabre\DAV\Server->httpPropFind
OCA\DAV\Connector\Sabre\Directory->getChildren
OC\Files\Node\Folder->getDirectoryListing
OC\Files\SetupManager->setupForUser (lib/private/Files/SetupManager.php:492)
OC\Files\Config\MountProviderCollection->addMountForUser (lib/private/Files/SetupManager.php:288)
OCA\Files_Sharing\MountProvider->getMountsForUser (apps/files_sharing/lib/MountProvider.php:45
OCA\Files_Sharing\MountProvider->getSuperSharesForUser (apps/files_sharing/lib/MountProvider.php:60)
OC\Share20\Manager->getSharedWith (lib/private/Share20/Manager.php:1335)
OCA\Deck\Sharing\DeckShareProvider->getSharedWith (lib/Sharing/DeckShareProvider.php:727)
OCA\Deck\Sharing\DeckShareProvider->_getSharedWith (lib/Sharing/DeckShareProvider.php:860)
OCA\Deck\Sharing\DeckShareProvider->resolveSharesForRecipient (lib/Sharing/DeckShareProvider.php:678)
OCA\Deck\Sharing\DeckShareProvider->applyBoardPermission (lib/Sharing/DeckShareProvider.php:275)
OCA\Deck\Service\PermissionService->checkPermission (lib/Service/PermissionService.php:158)
OCA\Deck\Service\PermissionService->getPermissions (lib/Service/PermissionService.php:84)
OCA\Deck\Service\PermissionService->getBoard (lib/Service/PermissionService.php:201)
OCA\Deck\Db\BoardMapper->find (lib/Db/BoardMapper.php:64)
OCP\AppFramework\Db\QBMapper->findEntity (lib/public/AppFramework/Db/QBMapper.php:284)
✗ DoesNotExistException

Analysis

Three things combine, each harmless on its own:

  1. The share query filters deleted cards but not deleted boards. DeckShareProvider::_getSharedWith() jo → deck_boards and applies only:

$qb->andWhere($qb->expr()->eq('dc.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))

There is no equivalent condition on db.deleted_at, although the deck_boards table is already joined as d is itself still deleted_at = 0, so the share passes the filter.

  1. The permission check then refuses to load that board. PermissionService::checkPermission() has the s

public function checkPermission(?IPermissionMapper $mapper, $id, int $permission, $userId = null,
bool $allowDeletedCard = false, bool $allowDeletedBoard = false): bool

applyBoardPermission() passes true for $allowDeletedCard but leaves $allowDeletedBoard at false, so BoardMapper::find() builds eq('deleted_at', 0) and the deleted board is treated as non-existent.

  1. The exception escapes. applyBoardPermission() only catches NoPermissionException:

private function applyBoardPermission($share, $permissions, $userId) {
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_EDIT, $userId, true);
} catch (NoPermissionException $e) { ... }

DoesNotExistException is not caught anywhere up the chain, so it leaves SetupManager::setupForUser() an

Worth noting: BoardService::deleteUndo() gets this right and explicitly passes the named argument —

$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE, allowDeleted

— so the parameter exists and is used correctly elsewhere. It simply is not considered on the share-prot behaviour is the opposite: skip the share entirely.

Suggested fix

Primary — exclude deleted boards from the share query. In _getSharedWith(), alongside the existing card

$qb->andWhere($qb->expr()->eq('db.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))

A deleted board should not grant file access at all, so filtering at the source is the semantically coroardIds() appears to have the same omission and is likely worth checking too.

Secondary, as defence in depth — do not let one provider abort filesystem setup. Catching DoesNotExistEsion() (or resolveSharesForRecipient()) and skipping the offending share would ensure that any futureinconsistency in Deck data degrades to "one attachment missing" rather than "no filesystem at all". Arguably the core MountProviderCollection should also isolate provider failures, but that is a separate discussion.

Impact and severity

  • Triggered by an ordinary user action, on their own board, with no special privileges.
  • Affects every recipient of the affected shares, i.e. everyone who had access to the board.
  • Additionally affects users who merely open a file shared by an affected user: SharedStorage::init() calls setupForUser() for the owner, so the exception arrives from someone else's filesystem.
  • Background jobs (trashbin and versions expiry) fail for all users on the instance while the condition
  • Duration is trashRetentionHours (default 5 hours), after which DeleteCron purges the board and the symptom disappears on its own — at the cost of the board and all its cards being destroyed permanently.

Recovery is unusually hard for a non-technical user. There is no UI listing deleted boards: the only ways back are the transient "Undo" toast shown immediately after deletion, or a manual POST
/index.php/apps/deck/boards//deleteUndo. In our case the board was silently purged before anyone re, so the data was lost. A visible trash view for boards, or at least a warning when deleting a board that still has attachments, would help a lot here.

Workaround

Detach all files from a board's cards before deleting the board. Archiving a board instead of deleting is safe — archived is a separate column and does not affect board lookup. Deleting individual cards is also safe, since dc.deleted_at
is filtered.

Note for administrators: raising trashRetentionHours is counterproductive while this bug is present, bewindow by exactly that amount.

Server configuration
Deck version: 1.18.4
Nextcloud version: 34.0.1.2
Installation: Nextcloud All-in-One (Docker)
Database: PostgreSQL 18.6
PHP - as shipped in the AIO image
Primary storage: local filesystem (no object storage, files_external disabled)
Relevant apps: groupfolders 22.0.6, spreed 24.0.4

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions