fix: Add ownership checks to global task endpoints (#23454)
* 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
* Handle temporary (local:) chat IDs in scoped task endpoints
Temporary chats use local:<socketId> as chat_id which doesn't exist in
the DB. The scoped endpoints now skip ownership checks for local: IDs
(they aren't enumerable) and use {chat_id:path} to handle the colon in
the URL path.
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Verify session ownership for local: chat IDs and URL-encode chat_id
- For local:<socketId> chat IDs, look up the socket's owner in
SESSION_POOL and verify it matches the requesting user (or admin)
- URL-encode chat_id in frontend fetch calls to handle special
characters (colon in local: IDs) safely
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,7 @@ from open_webui.socket.main import (
|
||||
periodic_session_pool_cleanup,
|
||||
get_event_emitter,
|
||||
get_models_in_use,
|
||||
get_user_id_from_session_pool,
|
||||
)
|
||||
from open_webui.routers import (
|
||||
analytics,
|
||||
@@ -566,6 +567,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
|
||||
|
||||
@@ -1974,7 +1976,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
|
||||
@@ -1983,15 +1985,21 @@ 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}')
|
||||
@app.get('/api/tasks/chat/{chat_id:path}')
|
||||
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:
|
||||
return {'task_ids': []}
|
||||
if chat_id.startswith('local:'):
|
||||
socket_id = chat_id[len('local:'):]
|
||||
owner_id = get_user_id_from_session_pool(socket_id)
|
||||
if owner_id != user.id and user.role != 'admin':
|
||||
return {'task_ids': []}
|
||||
else:
|
||||
chat = await Chats.get_chat_by_id(chat_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)
|
||||
|
||||
@@ -1999,6 +2007,21 @@ 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:path}/stop')
|
||||
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
||||
if chat_id.startswith('local:'):
|
||||
socket_id = chat_id[len('local:'):]
|
||||
owner_id = get_user_id_from_session_pool(socket_id)
|
||||
if owner_id != user.id and user.role != 'admin':
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
|
||||
else:
|
||||
chat = await 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
|
||||
|
||||
+33
-1
@@ -273,10 +273,42 @@ 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/${encodeURIComponent(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;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}`, {
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
chatAction,
|
||||
generateMoACompletion,
|
||||
stopTask,
|
||||
stopTasksByChatId,
|
||||
getTaskIdsByChatId
|
||||
} from '$lib/apis';
|
||||
import { getTools } from '$lib/apis/tools';
|
||||
@@ -2464,11 +2465,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;
|
||||
|
||||
Reference in New Issue
Block a user