Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/open_webui/routers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
75 changes: 75 additions & 0 deletions backend/open_webui/test/util/test_tool_access.py
Original file line number Diff line number Diff line change
@@ -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__": {}},
)
== {}
)
25 changes: 25 additions & 0 deletions backend/open_webui/utils/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down