Tie workflows to the board owner instead of their creator
Build package / php-lint (8.4) (push) Successful in 2m22s
Build package / php-lint (8.5) (push) Successful in 57s
Build package / xml-lint (push) Successful in 18s
Build package / unit-tests (push) Successful in 1m5s
Build package / package (push) Successful in 1m21s

A workflow now belongs to whoever owns its board. The settings page
lists, edits and deletes workflows by board ownership alone, using
Deck's BoardMapper::findAllByOwner(), a single owner query that never
returns shared boards and carries no frozen uid. A transferred board's
workflows therefore show up for the new owner immediately.

The background job still needs one user to run as and mail, so it
looks up the board owner before every run and stores it in user_id
when it changed. Archived boards stay listed so their workflows keep
a name, but are no longer offered or accepted as a target.
This commit is contained in:
Patrick Niebeling committed 2026-09-30 09:38:11 +02:00
1 parent ada16cb101
commit e84123654e
6 files changed
+185 -72

No files matched your search

+57 -21
View File
@@ -38,9 +38,14 @@ class WorkflowController extends OCSController {
return $user->getUID();
}
/**
* Every workflow on a board the user owns, no matter who created it: a
* workflow belongs to its board's owner, and follows the board when
* Deck transfers it.
*/
#[NoAdminRequired]
public function index(): DataResponse {
$workflows = $this->workflowMapper->findAllForUser($this->currentUserId());
$workflows = $this->workflowMapper->findAllForBoards($this->ownedBoardIds());
return new DataResponse(array_map(static fn (Workflow $w) => $w->jsonSerialize(), $workflows));
}
@@ -85,14 +90,12 @@ class WorkflowController extends OCSController {
bool $notifyEmail = false,
bool $enabled = true,
): DataResponse {
try {
$workflow = $this->workflowMapper->findForUser($id, $this->currentUserId());
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
}
$workflow = $this->findOwnWorkflow($id);
$this->validate($title, $boardId, $sourceStackId, $targetStackId);
// Kept in step with the board owner so the job runs as, and mails,
// the right user; validate() made sure the editor owns the board.
$workflow->setUserId($this->currentUserId());
$this->applyFields($workflow, $title, $boardId, $sourceStackId, $targetStackId, $filterUserIds, $filterLabelIds, $notifyEmail, $enabled);
$workflow = $this->workflowMapper->update($workflow);
@@ -101,12 +104,7 @@ class WorkflowController extends OCSController {
#[NoAdminRequired]
public function destroy(int $id): DataResponse {
try {
$workflow = $this->workflowMapper->findForUser($id, $this->currentUserId());
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
}
$workflow = $this->findOwnWorkflow($id);
$this->workflowMapper->delete($workflow);
return new DataResponse([]);
}
@@ -114,7 +112,7 @@ class WorkflowController extends OCSController {
#[NoAdminRequired]
public function boards(): DataResponse {
$this->assertDeck();
return new DataResponse($this->deckService->listBoardsForCurrentUser($this->currentUserId()));
return new DataResponse($this->ownedBoards());
}
#[NoAdminRequired]
@@ -135,6 +133,40 @@ class WorkflowController extends OCSController {
return new DataResponse($this->deckService->listParticipants($boardId));
}
private function findOwnWorkflow(int $id): Workflow {
try {
$workflow = $this->workflowMapper->findById($id);
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
}
if (!in_array($workflow->getBoardId(), $this->ownedBoardIds(), true)) {
// Same answer as for a missing id: don't reveal other users' workflows.
throw new OCSNotFoundException('Workflow not found');
}
return $workflow;
}
/**
* @return array<int, array{id: int, title: string, archived: bool}>
*/
private function ownedBoards(): array {
$userId = $this->currentUserId();
$this->assertDeck();
try {
return $this->deckService->listOwnedBoards($userId);
} catch (DeckUnavailableException $e) {
throw new OCSPreconditionFailedException('Could not load your Deck boards: ' . $e->getMessage());
}
}
/**
* @return int[]
*/
private function ownedBoardIds(): array {
return array_map(static fn (array $board) => $board['id'], $this->ownedBoards());
}
private function assertDeck(): void {
$user = $this->userSession->getUser();
if ($user === null) {
@@ -162,13 +194,17 @@ class WorkflowController extends OCSController {
$this->assertDeck();
// Only boards the user owns are listed, so this also enforces the
// owner rule.
$boardIds = array_map(
static fn (array $board) => (int)$board['id'],
$this->deckService->listBoardsForCurrentUser($this->currentUserId()),
);
if (!in_array($boardId, $boardIds, true)) {
throw new OCSBadRequestException('The selected board does not exist or is not owned by you');
// owner rule. Archived boards stay listed (their workflows must stay
// visible) but are no target for a new or changed workflow.
$board = null;
foreach ($this->ownedBoards() as $candidate) {
if ($candidate['id'] === $boardId) {
$board = $candidate;
break;
}
}
if ($board === null || $board['archived']) {
throw new OCSBadRequestException('The selected board does not exist, is archived or is not owned by you');
}
$stackIds = array_map(