Skip to content

Commit 691cd8d

Browse files
committed
Merge branch 'v2-c' into knowledge_workflow
2 parents 35b8a34 + f341930 commit 691cd8d

File tree

17 files changed

+493
-82
lines changed

17 files changed

+493
-82
lines changed

apps/common/utils/tool_code.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import socket
88
import subprocess
99
import sys
10+
import signal
11+
import time
1012
import uuid_utils.compat as uuid
1113
from common.utils.logger import maxkb_logger
1214
from django.utils.translation import gettext_lazy as _
@@ -84,13 +86,14 @@ def exec_code(self, code_str, keywords, function_name=None):
8486
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
8587
_exec_code = f"""
8688
try:
87-
import sys, json, base64, builtins
89+
import os, sys, json, base64, builtins
8890
path_to_exclude = ['/opt/py3/lib/python3.11/site-packages', '/opt/maxkb-app/apps']
8991
sys.path = [p for p in sys.path if p not in path_to_exclude]
9092
sys.path += {python_paths}
9193
locals_v={'{}'}
9294
keywords={keywords}
9395
globals_v={'{}'}
96+
os.environ.clear()
9497
exec({dedent(code_str)!a}, globals_v, locals_v)
9598
f_name, f = {action_function}
9699
for local in locals_v:
@@ -182,16 +185,14 @@ def generate_mcp_server_code(self, code_str, params):
182185
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
183186
code = self._generate_mcp_server_code(code_str, params)
184187
return f"""
185-
import os
186-
import sys
187-
import logging
188+
import os, sys, logging
188189
logging.basicConfig(level=logging.WARNING)
189190
logging.getLogger("mcp").setLevel(logging.ERROR)
190191
logging.getLogger("mcp.server").setLevel(logging.ERROR)
191-
192192
path_to_exclude = ['/opt/py3/lib/python3.11/site-packages', '/opt/maxkb-app/apps']
193193
sys.path = [p for p in sys.path if p not in path_to_exclude]
194194
sys.path += {python_paths}
195+
os.environ.clear()
195196
exec({dedent(code)!a})
196197
"""
197198

@@ -223,24 +224,40 @@ def get_tool_mcp_config(self, code, params):
223224
return tool_config
224225

225226
def _exec_sandbox(self, _code):
226-
kwargs = {'cwd': BASE_DIR}
227-
kwargs['env'] = {
227+
kwargs = {'cwd': BASE_DIR, 'env': {
228228
'LD_PRELOAD': self.sandbox_so_path,
229-
}
229+
}}
230230
maxkb_logger.debug(f"Sandbox execute code: {_code}")
231231
compressed_and_base64_encoded_code_str = base64.b64encode(gzip.compress(_code.encode())).decode()
232+
cmd = [
233+
'su', '-s', python_directory, '-c',
234+
f'import base64,gzip; exec(gzip.decompress(base64.b64decode(\'{compressed_and_base64_encoded_code_str}\')).decode())',
235+
self.user
236+
]
232237
try:
233-
subprocess_result = subprocess.run(
234-
['su', '-s', python_directory, '-c',
235-
f'import base64,gzip; exec(gzip.decompress(base64.b64decode(\'{compressed_and_base64_encoded_code_str}\')).decode())',
236-
self.user],
238+
proc = subprocess.Popen(
239+
cmd,
240+
stdout=subprocess.PIPE,
241+
stderr=subprocess.PIPE,
237242
text=True,
238-
capture_output=True,
239-
timeout=self.process_timeout_seconds,
240-
**kwargs)
243+
**kwargs,
244+
start_new_session=True
245+
)
246+
proc.wait(timeout=self.process_timeout_seconds)
247+
return subprocess.CompletedProcess(
248+
proc.args,
249+
proc.returncode,
250+
proc.stdout.read(),
251+
proc.stderr.read()
252+
)
241253
except subprocess.TimeoutExpired:
254+
pgid = os.getpgid(proc.pid)
255+
os.killpg(pgid, signal.SIGTERM) #温和终止
256+
time.sleep(1) #留出短暂时间让进程清理
257+
if proc.poll() is None: #如果仍未终止,强制终止
258+
os.killpg(pgid, signal.SIGKILL)
259+
proc.wait()
242260
raise Exception(_("Sandbox process execution timeout, consider increasing MAXKB_SANDBOX_PYTHON_PROCESS_TIMEOUT_SECONDS."))
243-
return subprocess_result
244261

245262
def validate_mcp_transport(self, code_str):
246263
servers = json.loads(code_str)

apps/oss/views/file.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# coding=utf-8
22
import base64
3+
import ipaddress
4+
import socket
5+
from urllib.parse import urlparse
36

47
import requests
58
from django.utils.translation import gettext_lazy as _
@@ -83,7 +86,17 @@ class GetUrlView(APIView):
8386
)
8487
def get(self, request: Request):
8588
url = request.query_params.get('url')
86-
response = requests.get(url)
89+
parsed = validate_url(url)
90+
91+
response = requests.get(
92+
url,
93+
timeout=3,
94+
allow_redirects=False
95+
)
96+
final_host = urlparse(response.url).hostname
97+
if is_private_ip(final_host):
98+
raise ValueError("Blocked unsafe redirect to internal host")
99+
87100
# 返回状态码 响应内容大小 响应的contenttype 还有字节流
88101
content_type = response.headers.get('Content-Type', '')
89102
# 根据内容类型决定如何处理
@@ -99,3 +112,43 @@ def get(self, request: Request):
99112
'Content-Type': content_type,
100113
'content': content,
101114
})
115+
116+
117+
def is_private_ip(host: str) -> bool:
118+
"""检测 IP 是否属于内网、环回、云 metadata 的危险地址"""
119+
try:
120+
ip = ipaddress.ip_address(socket.gethostbyname(host))
121+
return (
122+
ip.is_private or
123+
ip.is_loopback or
124+
ip.is_reserved or
125+
ip.is_link_local or
126+
ip.is_multicast
127+
)
128+
except Exception:
129+
return True
130+
131+
132+
def validate_url(url: str):
133+
"""验证 URL 是否安全"""
134+
if not url:
135+
raise ValueError("URL is required")
136+
137+
parsed = urlparse(url)
138+
139+
# 仅允许 http / https
140+
if parsed.scheme not in ("http", "https"):
141+
raise ValueError("Only http and https are allowed")
142+
143+
host = parsed.hostname
144+
path = parsed.path
145+
146+
# 域名不能为空
147+
if not host:
148+
raise ValueError("Invalid URL")
149+
150+
# 禁止访问内部、保留、环回、云 metadata
151+
if is_private_ip(host):
152+
raise ValueError("Access to internal IP addresses is blocked")
153+
154+
return parsed

apps/system_manage/serializers/user_resource_permission.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010
import os
1111

12+
from django.contrib.postgres.fields import ArrayField
1213
from django.core.cache import cache
1314
from django.db import models
1415
from django.db.models import QuerySet, Q, TextField
@@ -343,10 +344,13 @@ def get_queryset(self, instance, is_x_pack_ee: bool):
343344
"role": models.CharField(),
344345
"role_setting.type": models.CharField(),
345346
"user_role_relation.workspace_id": models.CharField(),
347+
'tmp.type_list': ArrayField(models.CharField()),
348+
'tmp.role_name_list_str': models.CharField()
346349

347350
}))
348351
nick_name = instance.get('nick_name')
349352
username = instance.get('username')
353+
role_name = instance.get('role')
350354
permission = instance.get('permission')
351355
query_p_list = [None if p == "NOT_AUTH" else p for p in permission]
352356

@@ -375,15 +379,31 @@ def get_queryset(self, instance, is_x_pack_ee: bool):
375379
**{"u.id__in": QuerySet(workspace_user_role_mapping_model).filter(
376380
workspace_id=self.data.get('workspace_id')).values("user_id")})
377381
if is_x_pack_ee:
378-
user_query_set = user_query_set.filter(
379-
**{'role_setting.type': "USER", 'user_role_relation.workspace_id': self.data.get('workspace_id')})
382+
user_query_set = user_query_set.filter(**{
383+
"tmp.type_list__contains": ["USER"]
384+
})
385+
role_name_and_type_query_set = QuerySet(model=get_dynamics_model({
386+
'user_role_relation.workspace_id': models.CharField(),
387+
})).filter(**{
388+
"user_role_relation.workspace_id": self.data.get('workspace_id'),
389+
})
390+
if role_name:
391+
user_query_set = user_query_set.filter(
392+
**{'tmp.role_name_list_str__icontains': str(role_name)}
393+
)
394+
395+
return {
396+
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
397+
'user_query_set': user_query_set,
398+
'role_name_and_type_query_set': role_name_and_type_query_set
399+
}
380400
else:
381401
user_query_set = user_query_set.filter(
382402
**{'role': "USER"})
383-
return {
384-
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
385-
'user_query_set': user_query_set
386-
}
403+
return {
404+
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
405+
'user_query_set': user_query_set
406+
}
387407

388408
def list(self, instance, with_valid=True):
389409
if with_valid:
Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
11
SELECT
2-
distinct(u.id),
2+
DISTINCT u.id,
33
u.nick_name,
44
u.username,
5-
case
6-
when
7-
wurp."permission" is null then 'NOT_AUTH'
8-
else wurp."permission"
9-
end
5+
tmp.role_name_list AS role_name,
6+
CASE
7+
WHEN wurp."permission" IS NULL THEN 'NOT_AUTH'
8+
ELSE wurp."permission"
9+
END AS permission
1010
FROM
1111
public."user" u
1212
LEFT JOIN (
1313
SELECT
14-
user_id ,
15-
(case
16-
when auth_type = 'ROLE'
17-
and 'ROLE' = any( permission_list) then 'ROLE'
18-
when auth_type = 'RESOURCE_PERMISSION_GROUP'
19-
and 'MANAGE'= any(permission_list) then 'MANAGE'
20-
when auth_type = 'RESOURCE_PERMISSION_GROUP'
21-
and 'VIEW' = any( permission_list) then 'VIEW'
22-
else null
23-
end) as "permission"
14+
user_id,
15+
CASE
16+
WHEN auth_type = 'ROLE'
17+
AND 'ROLE' = ANY(permission_list) THEN 'ROLE'
18+
WHEN auth_type = 'RESOURCE_PERMISSION_GROUP'
19+
AND 'MANAGE' = ANY(permission_list) THEN 'MANAGE'
20+
WHEN auth_type = 'RESOURCE_PERMISSION_GROUP'
21+
AND 'VIEW' = ANY(permission_list) THEN 'VIEW'
22+
ELSE NULL
23+
END AS "permission"
2424
FROM
2525
workspace_user_resource_permission
26-
${workspace_user_resource_permission_query_set}
27-
) wurp
28-
ON
29-
u.id = wurp.user_id
30-
left join user_role_relation user_role_relation
31-
on user_role_relation.user_id = u.id
32-
left join role_setting role_setting
33-
on role_setting.id = user_role_relation.role_id
26+
${workspace_user_resource_permission_query_set}
27+
) wurp ON u.id = wurp.user_id
28+
LEFT JOIN (
29+
SELECT
30+
ARRAY_AGG(role_setting.role_name) AS role_name_list,
31+
ARRAY_AGG(role_setting.role_name)::text AS role_name_list_str,
32+
ARRAY_AGG(role_setting.type) AS type_list,
33+
user_role_relation.user_id
34+
FROM user_role_relation user_role_relation
35+
LEFT JOIN role_setting role_setting
36+
ON role_setting.id = user_role_relation.role_id
37+
${role_name_and_type_query_set}
38+
GROUP BY
39+
user_role_relation.user_id) tmp
40+
ON u.id = tmp.user_id
3441
${user_query_set}

apps/system_manage/views/user_resource_permission.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def get(self, request: Request, workspace_id: str, target: str, resource: str, c
196196
return result.success(ResourceUserPermissionSerializer(
197197
data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, }
198198
).page({'username': request.query_params.get("username"),
199+
'role': request.query_params.get("role"),
199200
'nick_name': request.query_params.get("nick_name"),
200201
'permission': request.query_params.getlist("permission[]")}, current_page, page_size,
201202
))

apps/tools/serializers/tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ def get_field_value(debug_field_list, name, is_required):
431431

432432
@staticmethod
433433
def convert_value(name: str, value: str, _type: str, is_required: bool):
434-
if not is_required and value is None:
434+
if not is_required and (value is None or (isinstance(value, str) and len(value.strip()) == 0)):
435435
return None
436436
try:
437437
if _type == 'int':

0 commit comments

Comments
 (0)