Skip to content
Closed
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
4 changes: 2 additions & 2 deletions apps/application/flow/step_node/loop_node/i_loop_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ def is_valid(self, *, raise_exception=False):
if loop_type == 'ARRAY':
array = self.data.get('array')
if array is None or len(array) == 0:
message = _('{field}, this field is required.', field='array')
message = _('{field}, this field is required.').format(field='array')
raise AppApiException(500, message)
elif loop_type == 'NUMBER':
number = self.data.get('number')
if number is None:
message = _('{field}, this field is required.', field='number')
message = _('{field}, this field is required.').format(field='number')
raise AppApiException(500, message)


Expand Down
87 changes: 87 additions & 0 deletions apps/application/serializers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,76 @@
from application.serializers.common import update_resource_mapping_by_application


def get_bound_tool_ids(instance: Dict) -> List[str]:
"""
收集应用配置(含工作流节点)中引用的所有工具id,用于绑定前的权限校验
"""
tool_ids = set()
for key in ("tool_ids", "skill_tool_ids", "mcp_tool_ids"):
for tool_id in (instance.get(key) or []):
tool_ids.add(str(tool_id))
if instance.get("mcp_tool_id"):
tool_ids.add(str(instance.get("mcp_tool_id")))

def walk(work_flow):
if not work_flow:
return
for node in work_flow.get("nodes", []) or []:
node_data = (node.get("properties") or {}).get("node_data") or {}
for key in ("tool_lib_id", "mcp_tool_id"):
if node_data.get(key):
tool_ids.add(str(node_data.get(key)))
for key in ("mcp_tool_ids", "tool_ids", "skill_tool_ids"):
for tool_id in (node_data.get(key) or []):
tool_ids.add(str(tool_id))
if node.get("type") == "loop-node":
walk(node_data.get("loop_body"))

walk(instance.get("work_flow"))
return list(tool_ids)


def get_authorized_tool_ids(user_id: str, workspace_id: str, tool_ids: List[str]) -> List[str]:
"""
返回 tool_ids 中当前用户被授权绑定/使用的工具id。
工作空间管理员默认拥有全部工具权限;其他用户必须在 workspace_user_resource_permission
中存在针对该工具的显式授权记录(默认拒绝)。
"""
if not tool_ids:
return []
tool_ids = list({str(t) for t in tool_ids})
if is_workspace_manage(user_id, workspace_id):
return tool_ids
granted_tool_ids = {
str(permission.target)
for permission in QuerySet(WorkspaceUserResourcePermission).filter(
workspace_id=workspace_id,
user_id=user_id,
auth_target_type=AuthTargetType.TOOL.value,
target__in=tool_ids,
)
if "VIEW" in permission.permission_list or "ROLE" in permission.permission_list
}
return [tool_id for tool_id in tool_ids if tool_id in granted_tool_ids]


def validate_bound_tool_permissions(user_id: str, workspace_id: str, instance: Dict):
"""
校验应用/工作流中绑定的工具,当前用户是否都有权限使用,防止低权限成员
绑定自己被禁止访问的工具,并通过应用/工作流执行绕过工具的单独授权控制。
"""
tool_ids = get_bound_tool_ids(instance)
if not tool_ids:
return
authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, tool_ids))
unauthorized_tool_ids = [tool_id for tool_id in tool_ids if tool_id not in authorized_tool_ids]
if unauthorized_tool_ids:
message = lazy_format(
_("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids)
)
raise AppApiException(403, str(message))


def get_base_node_work_flow(work_flow):
node_list = work_flow.get("nodes")
base_node_list = [node for node in node_list if node.get("id") == "base-node"]
Expand Down Expand Up @@ -623,6 +693,7 @@ def insert_workflow(self, instance: Dict):
workspace_id = self.data.get("workspace_id")
wq = ApplicationCreateSerializer.WorkflowRequest(data=instance)
wq.is_valid(raise_exception=True)
validate_bound_tool_permissions(user_id, workspace_id, instance)
application_model = wq.to_application_model(user_id, workspace_id, instance)
application_model.save()
# 插入认证信息
Expand Down Expand Up @@ -703,6 +774,20 @@ def import_(self, instance: dict, is_import_tool, with_valid=True):
if not exits_tool_id_list.__contains__(tool.get("id"))
and not exits_tool_id_list.__contains__(generate_uuid((tool.get("id") + workspace_id or "")))
]
# 导入包内新建的工具由导入者本人持有,无需校验;仅需校验绑定到已存在工具的引用
existing_bound_tool_ids = [
tool_id for tool_id in get_bound_tool_ids(application) if tool_id not in update_tool_map
]
if existing_bound_tool_ids:
authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, existing_bound_tool_ids))
unauthorized_tool_ids = [
tool_id for tool_id in existing_bound_tool_ids if tool_id not in authorized_tool_ids
]
if unauthorized_tool_ids:
message = lazy_format(
_("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids)
)
raise AppApiException(403, str(message))
application_model = self.to_application(application, workspace_id, user_id, update_tool_map, folder_id)
tool_model_list = [self.to_tool(f, workspace_id, user_id) for f in tool_list]
application_model.save()
Expand Down Expand Up @@ -1190,6 +1275,8 @@ def edit(self, instance: Dict, with_valid=True):
if "work_flow_template" in instance:
return self.update_template_workflow(instance, application)

validate_bound_tool_permissions(self.data.get("user_id"), self.data.get("workspace_id"), instance)

if instance.get("model_id") is None or len(instance.get("model_id")) == 0:
application.model_id = None
else:
Expand Down
3 changes: 3 additions & 0 deletions apps/chat/mcp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import uuid_utils.compat as uuid
from django.db.models import QuerySet
from django.utils import timezone

from application.models import ApplicationApiKey, Application, ChatUserType, ChatSourceChoices
from chat.serializers.chat import ChatSerializers
Expand All @@ -13,6 +14,8 @@ def __init__(self, auth_header):
app_key = QuerySet(ApplicationApiKey).filter(secret_key=auth_header, is_active=True).first()
if not app_key:
raise PermissionError("Invalid API Key")
if app_key.is_permanent is False and app_key.expire_time < timezone.now():
raise PermissionError("API Key is expired")

self.application = QuerySet(Application).filter(id=app_key.application_id, is_publish=True).first()
if not self.application:
Expand Down
2 changes: 1 addition & 1 deletion apps/common/utils/tool_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def _set_resource_limit():
)
return subprocess_result
except subprocess.TimeoutExpired:
raise Exception(_(f"Process execution timed out after {_process_limit_timeout_seconds} seconds."))
raise Exception(_("Process execution timed out after {} seconds.").format(_process_limit_timeout_seconds))

def validate_mcp_transport(self, code_str):
servers = json.loads(code_str)
Expand Down
3 changes: 2 additions & 1 deletion apps/knowledge/serializers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ def is_valid(self, *, raise_exception=False):
if not query_set.exists():
raise AppApiException(500, _("Knowledge id does not exist"))
document_id = self.data.get("document_id")
if not QuerySet(Document).filter(id=document_id).exists():
knowledge_id = self.data.get("knowledge_id")
if not QuerySet(Document).filter(id=document_id, knowledge_id=knowledge_id).exists():
raise AppApiException(500, _("document id not exist"))

def export(self, with_valid=True):
Expand Down
12 changes: 10 additions & 2 deletions apps/knowledge/serializers/paragraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ def is_valid(self, *, raise_exception=False):
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
raise AppApiException(500, _("Knowledge id does not exist"))
if not QuerySet(Paragraph).filter(id=self.data.get("paragraph_id")).exists():
if not QuerySet(Paragraph).filter(
id=self.data.get("paragraph_id"),
document_id=self.data.get("document_id"),
knowledge_id=self.data.get("knowledge_id"),
).exists():
raise AppApiException(500, _("Paragraph id does not exist"))

def list(self, with_valid=False):
Expand Down Expand Up @@ -209,7 +213,11 @@ def is_valid(self, *, raise_exception=True):
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
raise AppApiException(500, _("Knowledge id does not exist"))
if not QuerySet(Paragraph).filter(id=self.data.get("paragraph_id")).exists():
if not QuerySet(Paragraph).filter(
id=self.data.get("paragraph_id"),
document_id=self.data.get("document_id"),
knowledge_id=self.data.get("knowledge_id"),
).exists():
raise AppApiException(500, _("Paragraph id does not exist"))

@staticmethod
Expand Down
4 changes: 2 additions & 2 deletions apps/locales/en_US/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -4255,7 +4255,7 @@ msgstr ""
#: apps/models_provider/impl/openai_model_provider/credential/tti.py:29
#: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29
msgid ""
" \n"
"\n"
"By default, images are produced in standard quality, but with DALL·E 3 you "
"can set quality: \"hd\" to enhance detail. Square, standard quality images "
"are generated fastest.\n"
Expand Down Expand Up @@ -4781,7 +4781,7 @@ msgstr ""

#: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44
msgid ""
" \n"
"\n"
"Code Llama Instruct is a fine-tuned version of Code Llama's instructions, "
"designed to perform specific tasks.\n"
" "
Expand Down
6 changes: 3 additions & 3 deletions apps/locales/zh_CN/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ msgstr "字段仅支持自定义|引用"

#: apps/application/flow/step_node/function_node/i_function_node.py:40
msgid "{field}, this field is required."
msgstr "{field_label} 字段是必填项"
msgstr "{field} 字段是必填项"

#: apps/application/flow/step_node/function_node/i_function_node.py:46
msgid "function"
Expand Down Expand Up @@ -4328,7 +4328,7 @@ msgstr ""
#: apps/models_provider/impl/openai_model_provider/credential/tti.py:29
#: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29
msgid ""
" \n"
"\n"
"By default, images are produced in standard quality, but with DALL·E 3 you "
"can set quality: \"hd\" to enhance detail. Square, standard quality images "
"are generated fastest.\n"
Expand Down Expand Up @@ -4896,7 +4896,7 @@ msgstr "Code Llama 是一个专门用于代码生成的语言模型。"

#: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44
msgid ""
" \n"
"\n"
"Code Llama Instruct is a fine-tuned version of Code Llama's instructions, "
"designed to perform specific tasks.\n"
" "
Expand Down
9 changes: 6 additions & 3 deletions apps/locales/zh_Hant/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ msgstr "欄位僅支持自定義|引用"

#: apps/application/flow/step_node/function_node/i_function_node.py:40
msgid "{field}, this field is required."
msgstr "{field_label} 欄位是必填項"
msgstr "{field} 欄位是必填項"

#: apps/application/flow/step_node/function_node/i_function_node.py:46
msgid "function"
Expand Down Expand Up @@ -4328,7 +4328,7 @@ msgstr ""
#: apps/models_provider/impl/openai_model_provider/credential/tti.py:29
#: apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py:29
msgid ""
" \n"
"\n"
"By default, images are produced in standard quality, but with DALL·E 3 you "
"can set quality: \"hd\" to enhance detail. Square, standard quality images "
"are generated fastest.\n"
Expand Down Expand Up @@ -4896,7 +4896,7 @@ msgstr "Code Llama 是一個專門用於代碼生成的語言模型。"

#: apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py:44
msgid ""
" \n"
"\n"
"Code Llama Instruct is a fine-tuned version of Code Llama's instructions, "
"designed to perform specific tasks.\n"
" "
Expand Down Expand Up @@ -8629,6 +8629,9 @@ msgstr "API KEY"
msgid "Download"
msgstr "下載"

msgid "User"
msgstr "用戶"

msgid "Delete personal system API_KEY"
msgstr "删除個人系統API KEY"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def text_to_speech(self, text):
if audio_hex:
audio = bytes.fromhex(audio_hex)
else:
raise Exception('Failed to get audio data from response' + str(response.text))
raise Exception('Failed to get audio data from response: ' + str(response.text))
else:
from dashscope.audio.tts_v2 import SpeechSynthesizer
synthesizer = SpeechSynthesizer(model=self.model, **self.params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class DockerAITTIModelParams(BaseForm):
)

quality = forms.SingleSelect(
TooltipLabel(_('Picture quality'), _('''
TooltipLabel(_('Picture quality'), _('''
By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest.
''')),
required=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class OpenAITTIModelParams(BaseForm):
)

quality = forms.SingleSelect(
TooltipLabel(_('Picture quality'), _('''
TooltipLabel(_('Picture quality'), _('''
By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest.
''')),
required=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SiliconCloudTTIModelParams(BaseForm):
)

quality = forms.SingleSelect(
TooltipLabel(_('Picture quality'), _('''
TooltipLabel(_('Picture quality'), _('''
By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest.
''')),
required=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
),
ModelInfo(
'code-llama-instruct',
_('''
_('''
Code Llama Instruct is a fine-tuned version of Code Llama's instructions, designed to perform specific tasks.
'''),
ModelTypeConst.LLM,
Expand Down
Loading
Loading