diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index f726368eba50..1ce13e0250b8 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -22,7 +22,7 @@ from open_webui.utils.access_control import has_access, has_permission from open_webui.env import SRC_LOG_LEVELS -from open_webui.utils.tools import get_tool_servers_data +from open_webui.utils.tools import can_access_tool, get_tool_servers_data log = logging.getLogger(__name__) @@ -380,6 +380,12 @@ async def delete_tools_by_id( async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user)): tools = Tools.get_tool_by_id(id) if tools: + if not can_access_tool(user, tools, "read"): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + try: valves = Tools.get_tool_valves_by_id(id) return valves @@ -406,6 +412,12 @@ async def get_tools_valves_spec_by_id( ): tools = Tools.get_tool_by_id(id) if tools: + if not can_access_tool(user, tools, "read"): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + if id in request.app.state.TOOLS: tools_module = request.app.state.TOOLS[id] else: diff --git a/backend/open_webui/test/util/test_tool_access.py b/backend/open_webui/test/util/test_tool_access.py new file mode 100644 index 000000000000..d4ac3cf7b062 --- /dev/null +++ b/backend/open_webui/test/util/test_tool_access.py @@ -0,0 +1,75 @@ +from types import SimpleNamespace + +from open_webui.utils import tools as tool_utils + + +def _user(id="user-1", role="user"): + return SimpleNamespace(id=id, role=role) + + +def test_get_tools_skips_private_database_tool_before_loading(monkeypatch): + private_tool = SimpleNamespace(user_id="owner", access_control={}, specs=[]) + + monkeypatch.setattr(tool_utils.Tools, "get_tool_by_id", lambda tool_id: private_tool) + monkeypatch.setattr(tool_utils, "has_access", lambda *args, **kwargs: False) + + def fail_load(*args, **kwargs): + raise AssertionError("unauthorized tool module should not be loaded") + + monkeypatch.setattr(tool_utils, "load_tool_module_by_id", fail_load) + + request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(TOOLS={}))) + + assert ( + tool_utils.get_tools( + request, + ["private-tool"], + _user(), + {"__user__": {}}, + ) + == {} + ) + + +def test_get_tools_skips_restricted_tool_server_before_using_credentials(monkeypatch): + monkeypatch.setattr(tool_utils.Tools, "get_tool_by_id", lambda tool_id: None) + monkeypatch.setattr(tool_utils, "has_access", lambda *args, **kwargs: False) + + request = SimpleNamespace( + app=SimpleNamespace( + state=SimpleNamespace( + config=SimpleNamespace( + TOOL_SERVER_CONNECTIONS=[ + { + "key": "server-secret", + "config": { + "access_control": {}, + }, + } + ] + ), + TOOL_SERVERS=[ + { + "idx": 0, + "url": "https://tool-server.example", + "specs": [ + { + "name": "sensitive_tool", + "parameters": {"properties": {}}, + } + ], + } + ], + ) + ) + ) + + assert ( + tool_utils.get_tools( + request, + ["server:0"], + _user(), + {"__user__": {}}, + ) + == {} + ) diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index dda2635ec70a..a7eaa1ff9d97 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -35,6 +35,7 @@ from open_webui.models.tools import Tools from open_webui.models.users import UserModel +from open_webui.utils.access_control import has_access from open_webui.utils.plugin import load_tool_module_by_id from open_webui.env import ( SRC_LOG_LEVELS, @@ -48,6 +49,24 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"]) +def can_access_tool(user: UserModel, tool, permission: str = "read") -> bool: + return ( + user.role == "admin" + or tool.user_id == user.id + or has_access(user.id, permission, tool.access_control) + ) + + +def can_access_tool_server( + user: UserModel, tool_server_connection: dict, permission: str = "read" +) -> bool: + return user.role == "admin" or has_access( + user.id, + permission, + tool_server_connection.get("config", {}).get("access_control", None), + ) + + def get_async_tool_function_and_apply_extra_params( function: Callable, extra_params: dict ) -> Callable[..., Awaitable]: @@ -80,6 +99,9 @@ def get_tools( tool_server_connection = ( request.app.state.config.TOOL_SERVER_CONNECTIONS[server_idx] ) + if not can_access_tool_server(user, tool_server_connection, "read"): + continue + tool_server_data = None for server in request.app.state.TOOL_SERVERS: if server["idx"] == server_idx: @@ -140,6 +162,9 @@ async def tool_function(**kwargs): else: continue else: + if not can_access_tool(user, tool, "read"): + continue + module = request.app.state.TOOLS.get(tool_id, None) if module is None: module, _ = load_tool_module_by_id(tool_id)