fix(tasks): prevent projectless task visibility leaks (#1066)
Fixes task metrics queries that could show private projectless tasks in another user's Today/dashboard lists. The issue happened because dashboard-specific Op.or filters could overwrite the task visibility Op.or condition when query objects were combined with object spread. This addresses issue #1063 where tasks created from Inbox, Telegram, or directly in the web app could appear for other users when they were not assigned to a shared project. Changes: - Combined task visibility filters with dashboard filters using Op.and - Prevented metrics Op.or conditions from overwriting permission filters - Preserved access for owned, directly shared, and shared-project tasks - Added regression tests for tasks_in_progress and suggested_tasks leaks Fixes #1063
This commit is contained in:
parent
57a6e558f3
commit
9edbe142b6
2 changed files with 114 additions and 39 deletions
|
|
@ -26,10 +26,14 @@ function isTaskInTodayPlan(task) {
|
|||
async function countTotalOpenTasks(visibleTasksWhere) {
|
||||
return await Task.count({
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
[Op.and]: [
|
||||
visibleTasksWhere,
|
||||
{
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
|
|
@ -39,11 +43,15 @@ async function countTasksPendingOverMonth(visibleTasksWhere) {
|
|||
const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
return await Task.count({
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
created_at: { [Op.lt]: oneMonthAgo },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
[Op.and]: [
|
||||
visibleTasksWhere,
|
||||
{
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
created_at: { [Op.lt]: oneMonthAgo },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
|
|
@ -53,15 +61,23 @@ async function fetchTasksInProgress(visibleTasksWhere) {
|
|||
const now = new Date();
|
||||
return await Task.findAll({
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: { [Op.in]: [Task.STATUS.IN_PROGRESS, 'in_progress'] },
|
||||
// Exclude tasks deferred to the future
|
||||
[Op.or]: [
|
||||
{ defer_until: null },
|
||||
{ defer_until: { [Op.lte]: now } },
|
||||
[Op.and]: [
|
||||
visibleTasksWhere,
|
||||
{
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.IN_PROGRESS, 'in_progress'],
|
||||
},
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
},
|
||||
// Exclude tasks deferred to the future
|
||||
{
|
||||
[Op.or]: [
|
||||
{ defer_until: null },
|
||||
{ defer_until: { [Op.lte]: now } },
|
||||
],
|
||||
},
|
||||
],
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
},
|
||||
include: getTaskIncludeConfigLight(),
|
||||
order: [
|
||||
|
|
@ -280,15 +296,22 @@ async function fetchNonProjectTasks(
|
|||
limit = null
|
||||
) {
|
||||
const exclusionIds = [...excludedTaskIds, ...somedayTaskIds];
|
||||
const filters = {
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
[Op.or]: [{ project_id: null }, { project_id: '' }],
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
};
|
||||
|
||||
if (exclusionIds.length > 0) {
|
||||
filters.id = { [Op.notIn]: exclusionIds };
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
[Op.or]: [{ project_id: null }, { project_id: '' }],
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
[Op.and]: [visibleTasksWhere, filters],
|
||||
},
|
||||
include: getTaskIncludeConfigLight(),
|
||||
order: [
|
||||
|
|
@ -298,10 +321,6 @@ async function fetchNonProjectTasks(
|
|||
],
|
||||
};
|
||||
|
||||
if (exclusionIds.length > 0) {
|
||||
queryOptions.where.id = { [Op.notIn]: exclusionIds };
|
||||
}
|
||||
|
||||
if (limit && Number.isInteger(limit)) {
|
||||
queryOptions.limit = limit;
|
||||
}
|
||||
|
|
@ -316,15 +335,22 @@ async function fetchProjectTasks(
|
|||
limit = null
|
||||
) {
|
||||
const exclusionIds = [...excludedTaskIds, ...somedayTaskIds];
|
||||
const filters = {
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
project_id: { [Op.not]: null, [Op.ne]: '' },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
};
|
||||
|
||||
if (exclusionIds.length > 0) {
|
||||
filters.id = { [Op.notIn]: exclusionIds };
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
project_id: { [Op.not]: null, [Op.ne]: '' },
|
||||
parent_task_id: null,
|
||||
recurring_parent_id: null,
|
||||
[Op.and]: [visibleTasksWhere, filters],
|
||||
},
|
||||
include: getTaskIncludeConfigLight(),
|
||||
order: [
|
||||
|
|
@ -334,10 +360,6 @@ async function fetchProjectTasks(
|
|||
],
|
||||
};
|
||||
|
||||
if (exclusionIds.length > 0) {
|
||||
queryOptions.where.id = { [Op.notIn]: exclusionIds };
|
||||
}
|
||||
|
||||
if (limit && Number.isInteger(limit)) {
|
||||
queryOptions.limit = limit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,4 +96,57 @@ describe('Tasks Permissions', () => {
|
|||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it("GET /api/tasks/metrics should not include another user's projectless in-progress task", async () => {
|
||||
const myTask = await Task.create({
|
||||
name: 'My In Progress Task',
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.IN_PROGRESS,
|
||||
});
|
||||
const otherTask = await Task.create({
|
||||
name: 'Other In Progress Task',
|
||||
user_id: otherUser.id,
|
||||
status: Task.STATUS.IN_PROGRESS,
|
||||
});
|
||||
|
||||
const res = await agent.get('/api/tasks/metrics');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const taskIds = res.body.tasks_in_progress.map((task) => task.id);
|
||||
expect(taskIds).toContain(myTask.id);
|
||||
expect(taskIds).not.toContain(otherTask.id);
|
||||
});
|
||||
|
||||
it("GET /api/tasks/metrics should not suggest another user's projectless task", async () => {
|
||||
await Task.bulkCreate([
|
||||
{
|
||||
name: 'My Task 1',
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
},
|
||||
{
|
||||
name: 'My Task 2',
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
},
|
||||
{
|
||||
name: 'My Task 3',
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
},
|
||||
]);
|
||||
const otherTask = await Task.create({
|
||||
name: 'Other Suggested Task',
|
||||
user_id: otherUser.id,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
const res = await agent.get('/api/tasks/metrics');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const suggestedTaskIds = res.body.suggested_tasks.map(
|
||||
(task) => task.id
|
||||
);
|
||||
expect(suggestedTaskIds).not.toContain(otherTask.id);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue