Skip to content

fix: Authorize a task update in the service layer on the stored task and its loaded status - EXO-90711 - #658

Merged
boubaker merged 5 commits into
feature/maintenancefrom
fix/update-task-authorize-stored-task
Oct 5, 2026
Merged

boubaker merged 5 commits into
feature/maintenancefrom
fix/update-task-authorize-stored-task

Conversation

@boubaker

@boubaker boubaker commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Symptom: PUT /tasks/{id} authorized the update on a task built from the request body: a status posted on a task without a project was copied onto it before the edit check, so the project participants the body declared decided the permission; the update applied to the id the body carried, and a status was trusted as posted, so a task could be moved to a project the user cannot view.

Cause: the checks lived in TaskRestService#updateTaskById and read the body instead of the stored task and the stored status, while TaskService#updateTask(TaskDto) checks nothing.

Fix: the checks move to the Service layer, in TaskService#updateTask(long taskId, TaskDto task, Identity identity):

  • the task is loaded by the path id (ObjectNotFoundException → 404) and the edit permission is checked on it (IllegalAccessException → 403); the update applies to that task whatever id the body carries;
  • a posted status is loaded by its id (IllegalArgumentException("task.status.notFound") → 400); a status of another project requires the view permission on that project (→ 403); a status change inside the task's project, and an update without a status, keep their behaviour;
  • createdBy and createdTime are kept from the stored task: the creator is an edit-permission input.

updateTaskById only delegates and maps the exceptions. updateTask(TaskDto) is unchanged for the internal callers (MCP tool, listeners); REST contract and UI are unchanged.

Behaviour change, deliberate: the edit check is now TaskUtil.hasEditPermission(TaskService, TaskDto, Identity), the rule of TaskAclPlugin EDIT, which grants the task creator; the previous ConversationState variant did not. Until the rest of TaskRestService moves to the same check, a creator who is neither assignee, coworker nor project member can update a task through this endpoint while GET /tasks/{id}, clone, delete, labels and updateCompleted still refuse them.

Tests: TaskServiceTest#testUpdateTaskAuthorizesOnTheStoredTaskAndTheLoadedStatus pins each guard (each one, reverted, fails it) and the update without a status; TestTaskRestService#testUpdateTaskById pins the exception → status mapping. services: 225 tests green.

Knowledge: to follow — the eng-standards PR recording the task update authorization in domains/task.md (§6 states the ACL is checked by callers only), opened before this PR leaves draft.

This change is classified N1 (computed on f06a87ec8) — its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver.

🤖 Generated with Claude Code

@boubaker boubaker changed the title fix: Authorize a task update on the stored task and its resolved status EXO-TBD fix: Authorize a task update in the service layer on the stored task and its loaded status - EXO-TBD Sep 28, 2026
@boubaker

Copy link
Copy Markdown
Member Author

Self-review close-out — two reviewer rounds by an independent reviewer agent, head f06a87ec8.

# Finding Status
🟡1 An update without a status was not pinned ✅ Fixed — 47d883f1e, scenario on a personal task without a status; the reject/lookup mutants fail it
🟡2 The Identity-based edit check grants the task creator on PUT /tasks/{id} (TaskAclPlugin EDIT rule), sibling endpoints do not yet ➖ Kept by the developer as an alignment, declared in the PR body
🟡3 createdBy / createdTime taken from the body while the creator is an edit-permission input ✅ Fixed — f06a87ec8, kept from the stored task; dropping either setter fails the test
🟡4 PR body, title and commits out of step with the head; EXO-TBD; Knowledge: value ⏳ Body and title rewritten; the Tribe id and the Knowledge: PR are set before the PR leaves draft
🟢5 The broad IllegalArgumentException → 400 catch can carry an engine message ➖ Waived by the developer: no normal-flow trigger
🟢6 Pre-existing self-assigned projectService field in TaskServiceImpl ➖ Out of scope, untouched

Verified conform: the edit permission is checked on the task loaded by the path id, and the path id is the one updated (StorageUtil.taskToEntity loads the entity by the DTO id); the status is loaded by its id and a cross-project move is authorized on the reloaded project, never on a DTO's participants; check order existence → ACL → validation → move ACL matches the Service exception contract; the lazy ExoContainerContext lookups avoid the ProjectService ↔ TaskService Kernel cycle; the MCP tool and updateTask(TaskDto) are unchanged; every frontend caller sends a stored status with its id. services: 225 tests green; each guard mutation-verified.

This change is classified N1 (computed on f06a87ec8) — its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone; author ≠ approver.

@boubaker boubaker changed the title fix: Authorize a task update in the service layer on the stored task and its loaded status - EXO-TBD fix: Authorize a task update in the service layer on the stored task and its loaded status - EXO-90711 Sep 28, 2026
@boubaker
boubaker requested a review from ahamdi September 28, 2026 16:31
@boubaker
boubaker marked this pull request as ready for review September 28, 2026 16:32
ahamdi
ahamdi previously approved these changes Sep 29, 2026
@exo-swf
exo-swf dismissed ahamdi’s stale review September 30, 2026 23:18

The merge-base changed after approval.

@exo-swf
exo-swf force-pushed the feature/maintenance branch 2 times, most recently from 5f3a58d to 2a5a860 Compare October 1, 2026 23:18
boubaker and others added 5 commits October 5, 2026 13:58
…us - EXO-TBD

updateTaskById checks the edit permission on the stored task, applies
the update to the task of the path, and loads a posted status by its
id, checking the view permission on its project when the task changes
project; an unknown status is a bad request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TaskService#updateTask(taskId, task, identity) holds the checks the
REST endpoint made: the edit permission on the stored task, the update
applied to that task, the posted status loaded by its id (an unknown one
is an IllegalArgumentException) and the view permission on its project
when the task changes project. updateTaskById delegates to it and maps
its exceptions to 404, 403 and 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The creator is an edit permission input, so updateTask(taskId, task,
identity) keeps createdBy and createdTime from the stored task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boubaker
boubaker force-pushed the fix/update-task-authorize-stored-task branch from eaefff4 to fc90207 Compare October 5, 2026 12:58
@boubaker
boubaker requested a review from Jihed525 October 5, 2026 12:59
@boubaker

boubaker commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Rebased on new feature/maintenance

@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

@boubaker
boubaker merged commit 9aede63 into feature/maintenance Oct 5, 2026
9 checks passed
@boubaker
boubaker deleted the fix/update-task-authorize-stored-task branch October 5, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants