Add ownership checks to global task endpoints

- Restrict GET /api/tasks and POST /api/tasks/stop/{task_id} to admin-only
- Add new scoped POST /api/tasks/chat/{chat_id}/stop endpoint with ownership
  check so regular users can stop their own chat tasks
- Allow admins to access the scoped chat task endpoints alongside owners
- Update frontend to use the new scoped stop endpoint when a chatId is available

https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
This commit is contained in:
Claude 2026-04-06 14:03:44 +00:00 • committed by DrMelone
parent f6b85700ea
commit b42d132778
3 changed files with 55 additions and 5 deletions

View file

@ -565,6 +565,7 @@ from open_webui.tasks import (
list_task_ids_by_item_id,
create_task,
stop_task,
stop_item_tasks,
list_tasks,
) # Import from tasks.py
@ -2012,7 +2013,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user=De
@app.post('/api/tasks/stop/{task_id}')
async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_verified_user)):
async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)):
try:
result = await stop_task(request.app.state.redis, task_id)
return result
@ -2021,14 +2022,14 @@ async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_ve
@app.get('/api/tasks')
async def list_tasks_endpoint(request: Request, user=Depends(get_verified_user)):
async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)):
return {'tasks': await list_tasks(request.app.state.redis)}
@app.get('/api/tasks/chat/{chat_id}')
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
chat = await Chats.get_chat_by_id(chat_id)
if chat is None or chat.user_id != user.id:
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
return {'task_ids': []}
task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id)
@ -2037,6 +2038,15 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
return {'task_ids': task_ids}
@app.post('/api/tasks/chat/{chat_id}/stop')
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id(chat_id)
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
result = await stop_item_tasks(request.app.state.redis, chat_id)
return result
##################################
#
# Config Endpoints

View file

@ -273,6 +273,38 @@ export const stopTask = async (token: string, id: string) => {
return res;
};
export const stopTasksByChatId = async (token: string, chat_id: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}/stop`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.error(err);
if ('detail' in err) {
error = err.detail;
} else {
error = err;
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
let error = null;

View file

@ -86,6 +86,7 @@
chatAction,
generateMoACompletion,
stopTask,
stopTasksByChatId,
getTaskIdsByChatId
} from '$lib/apis';
import { getTools } from '$lib/apis/tools';
@ -2459,11 +2460,18 @@
const stopResponse = async (processQueue = true) => {
if (taskIds) {
for (const taskId of taskIds) {
const res = await stopTask(localStorage.token, taskId).catch((error) => {
if ($chatId) {
await stopTasksByChatId(localStorage.token, $chatId).catch((error) => {
toast.error(`${error}`);
return null;
});
} else {
for (const taskId of taskIds) {
const res = await stopTask(localStorage.token, taskId).catch((error) => {
toast.error(`${error}`);
return null;
});
}
}
taskIds = null;