Skip to content
Open
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
87 changes: 75 additions & 12 deletions bkflow/apigw/serializers/credential.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making
蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available.
Expand All @@ -20,28 +19,92 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers

from bkflow.space.credential import BkAppCredential
from bkflow.space.credential import CredentialDispatcher
from bkflow.space.models import Credential, CredentialScopeLevel
from bkflow.space.serializers import CredentialScopeSerializer


class CredentialSerializer(serializers.ModelSerializer):
create_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
update_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")

def to_representation(self, instance):
data = super().to_representation(instance)
credential = CredentialDispatcher(credential_type=instance.type, data=instance.content)
if credential:
data["content"] = credential.display_value()
else:
data["content"] = {}

return data

class Meta:
model = Credential
fields = "__all__"


class CreateCredentialSerializer(serializers.Serializer):
name = serializers.CharField(help_text=_("凭证名称"), max_length=32, required=True)
desc = serializers.CharField(help_text=_("凭证描述"), max_length=32, required=False)
desc = serializers.CharField(help_text=_("凭证描述"), max_length=128, required=False)
type = serializers.CharField(help_text=_("凭证类型"), max_length=32, required=True)
content = serializers.JSONField(help_text=_("凭证内容"), required=True)
scope_level = serializers.ChoiceField(
help_text=_("作用域级别"),
required=False,
default=CredentialScopeLevel.NONE.value,
choices=Credential.CREDENTIAL_SCOPE_LEVEL_CHOICES,
)
scopes = serializers.ListField(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 异常处理过宽: except Exception 会捕获所有异常,包括系统错误。建议改为具体异常类型如 except ValidationError

child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False, default=list
)

def validate(self, attrs):
# 动态验证content根据type
credential_type = attrs.get("type")
content = attrs.get("content")

def validate_content(self, value):
content_ser = BkAppCredential.BkAppSerializer(data=value)
content_ser.is_valid(raise_exception=True)
return value
if attrs.get("scope_level") == CredentialScopeLevel.PART.value and not attrs.get("scopes"):
raise serializers.ValidationError(_("作用域不能为空"))

try:
credential = CredentialDispatcher(credential_type, data=content)
credential.validate_data()
except Exception as e:
raise serializers.ValidationError({"content": str(e)})

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

return attrs


class UpdateCredentialSerializer(serializers.Serializer):
name = serializers.CharField(help_text=_("凭证名称"), max_length=32, required=False)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 异常处理问题: 同样需要捕获具体异常类型,避免掩盖意外错误

desc = serializers.CharField(help_text=_("凭证描述"), max_length=32, required=False)
desc = serializers.CharField(help_text=_("凭证描述"), max_length=128, required=False)
type = serializers.CharField(help_text=_("凭证类型"), max_length=32, required=False)
content = serializers.JSONField(help_text=_("凭证内容"), required=False)
scope_level = serializers.ChoiceField(
help_text=_("作用域级别"),
required=False,
default=CredentialScopeLevel.NONE.value,
choices=Credential.CREDENTIAL_SCOPE_LEVEL_CHOICES,
)
scopes = serializers.ListField(child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False)

def validate(self, attrs):
if attrs.get("scope_level") == CredentialScopeLevel.PART.value and not attrs.get("scopes"):
raise serializers.ValidationError(_("作用域不能为空"))

# 如果提供了type和content,需要验证content
if "content" in attrs:
# 如果有type字段使用type,否则需要从实例获取
credential_type = attrs.get("type")
if not credential_type and hasattr(self, "instance"):
credential_type = self.instance.type

if credential_type:
content = attrs.get("content")
try:
credential = CredentialDispatcher(credential_type, data=content)
credential.validate_data()
except Exception as e:
raise serializers.ValidationError({"content": str(e)})

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

def validate_content(self, value):
content_ser = BkAppCredential.BkAppSerializer(data=value)
content_ser.is_valid(raise_exception=True)
return value
return attrs
3 changes: 3 additions & 0 deletions bkflow/apigw/serializers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class CreateTaskSerializer(serializers.Serializer):
creator = serializers.CharField(help_text=_("创建者"), max_length=USER_NAME_MAX_LENGTH, required=True)
description = serializers.CharField(help_text=_("任务描述"), required=False)
constants = serializers.JSONField(help_text=_("任务启动参数"), required=False, default={})
credentials = serializers.JSONField(help_text=_("任务凭证"), required=False, default={})


class TaskMockDataSerializer(serializers.Serializer):
Expand All @@ -48,6 +49,7 @@ class CreateMockTaskBaseSerializer(serializers.Serializer):
mock_data = TaskMockDataSerializer(help_text=_("Mock 数据"), default=TaskMockDataSerializer())
description = serializers.CharField(help_text=_("任务描述"), required=False)
constants = serializers.JSONField(help_text=_("任务启动参数"), default={})
credentials = serializers.JSONField(help_text=_("任务凭证"), required=False, default={})


class CreateMockTaskWithPipelineTreeSerializer(CreateMockTaskBaseSerializer):
Expand Down Expand Up @@ -86,6 +88,7 @@ class CreateTaskWithoutTemplateSerializer(serializers.Serializer):
scope_value = serializers.CharField(help_text=_("任务范围值"), max_length=128, required=False)
description = serializers.CharField(help_text=_("任务描述"), required=False)
constants = serializers.JSONField(help_text=_("任务启动参数"), required=False, default={})
credentials = serializers.JSONField(help_text=_("任务凭证"), required=False, default={})
pipeline_tree = serializers.JSONField(help_text=_("任务树"), required=True)
notify_config = serializers.JSONField(help_text=_("通知配置"), required=False, default={})

Expand Down
2 changes: 2 additions & 0 deletions bkflow/apigw/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from bkflow.apigw.views.operate_task_node import operate_task_node
from bkflow.apigw.views.renew_space_config import renew_space_config
from bkflow.apigw.views.revoke_token import revoke_token
from bkflow.apigw.views.update_credential import update_credential
from bkflow.apigw.views.update_template import update_template
from bkflow.apigw.views.validate_pipeline_tree import validate_pipeline_tree

Expand All @@ -73,6 +74,7 @@
url(r"^space/(?P<space_id>\d+)/create_task_without_template/$", create_task_without_template),
url(r"^space/(?P<space_id>\d+)/validate_pipeline_tree/$", validate_pipeline_tree),
url(r"^space/(?P<space_id>\d+)/create_credential/$", create_credential),
url(r"^space/(?P<space_id>\d+)/credential/(?P<credential_id>\d+)/$", update_credential),
url(r"^space/(?P<space_id>\d+)/get_task_list/$", get_task_list),
url(r"^space/(?P<space_id>\d+)/task/(?P<task_id>\d+)/get_task_detail/$", get_task_detail),
url(r"^space/(?P<space_id>\d+)/task/(?P<task_id>\d+)/get_task_states/$", get_task_states),
Expand Down
38 changes: 34 additions & 4 deletions bkflow/apigw/views/create_credential.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making
蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available.
Expand All @@ -21,12 +20,13 @@

from apigw_manager.apigw.decorators import apigw_require
from blueapps.account.decorators import login_exempt
from django.db import transaction
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from bkflow.apigw.decorators import check_jwt_and_space, return_json_response
from bkflow.apigw.serializers.credential import CreateCredentialSerializer
from bkflow.space.models import Credential
from bkflow.space.models import Credential, CredentialScope, CredentialScopeLevel


@login_exempt
Expand All @@ -36,9 +36,39 @@
@check_jwt_and_space
@return_json_response
def create_credential(request, space_id):
"""
创建凭证

:param request: HTTP 请求对象
:param space_id: 空间ID
:return: 创建的凭证信息
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 语法错误: 此行代码不完整 if scope_l,缺少完整的条件判断,会导致 Python 语法错误

data = json.loads(request.body)
ser = CreateCredentialSerializer(data=data)
ser.is_valid(raise_exception=True)
# 序列化器已经检查过是否存在了
credential = Credential.create_credential(**ser.data, space_id=space_id, creator=request.user.username)

# 提取作用域数据
credential_data = dict(ser.validated_data)
scopes = credential_data.pop("scopes", [])
scope_level = credential_data.pop("scope_level", None)

# 创建凭证和作用域
with transaction.atomic():
# 序列化器已经检查过是否存在了
credential = Credential.create_credential(
**credential_data, space_id=space_id, creator=request.user.username, scope_level=scope_level
)

# 创建凭证作用域
if scope_level == CredentialScopeLevel.PART.value and scopes:
scope_objects = [
CredentialScope(
credential_id=credential.id,
scope_type=scope.get("scope_type"),
scope_value=scope.get("scope_value"),
)
for scope in scopes
]
CredentialScope.objects.bulk_create(scope_objects)

return credential.display_json()
8 changes: 8 additions & 0 deletions bkflow/apigw/views/create_mock_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from bkflow.constants import TaskTriggerMethod
from bkflow.contrib.api.collections.task import TaskComponentClient
from bkflow.exceptions import ValidationError
from bkflow.space.credential.resolver import resolve_credentials
from bkflow.template.models import Template


Expand Down Expand Up @@ -91,6 +92,13 @@ def create_mock_task(request, space_id):
{"notify_config": template.notify_config or DEFAULT_NOTIFY_CONFIG}
)

# 处理凭证:解析并验证凭证,然后合并到pipeline_tree
credentials = create_task_data.get("credentials", {})
if credentials:
resolved_credentials = resolve_credentials(credentials, space_id, template.scope_type, template.scope_value)
# 将解析后的凭证合并到pipeline_tree
create_task_data["pipeline_tree"].setdefault("credentials", {}).update(resolved_credentials)

client = TaskComponentClient(space_id=space_id)
result = client.create_task(create_task_data)
return result
8 changes: 8 additions & 0 deletions bkflow/apigw/views/create_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from bkflow.constants import TaskTriggerMethod, WebhookEventType, WebhookScopeType
from bkflow.contrib.api.collections.task import TaskComponentClient
from bkflow.exceptions import ValidationError
from bkflow.space.credential.resolver import resolve_credentials
from bkflow.template.models import Template
from bkflow.utils.trace import CallFrom, trace_view

Expand Down Expand Up @@ -68,6 +69,13 @@ def create_task(request, space_id):
{"notify_config": template.notify_config or DEFAULT_NOTIFY_CONFIG}
)

# 处理凭证:解析并验证凭证,然后合并到pipeline_tree
credentials = create_task_data.get("credentials", {})
if credentials:
resolved_credentials = resolve_credentials(credentials, space_id, template.scope_type, template.scope_value)
# 将解析后的凭证合并到pipeline_tree
create_task_data["pipeline_tree"].setdefault("credentials", {}).update(resolved_credentials)

client = TaskComponentClient(space_id=space_id)
result = client.create_task(create_task_data)

Expand Down
13 changes: 13 additions & 0 deletions bkflow/apigw/views/create_task_without_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from bkflow.apigw.serializers.task import CreateTaskWithoutTemplateSerializer
from bkflow.constants import TaskTriggerMethod
from bkflow.contrib.api.collections.task import TaskComponentClient
from bkflow.space.credential.resolver import resolve_credentials


@login_exempt
Expand All @@ -50,6 +51,18 @@ def create_task_without_template(request, space_id):
notify_config = create_task_data.pop("notify_config", {}) or DEFAULT_NOTIFY_CONFIG
create_task_data.setdefault("extra_info", {}).update({"notify_config": notify_config})

# 处理凭证:解析并验证凭证,然后合并到pipeline_tree
credentials = create_task_data.get("credentials", {})
if credentials:
resolved_credentials = resolve_credentials(
credentials,
space_id,
create_task_data.get("scope_type"),
create_task_data.get("scope_value"),
)
# 将解析后的凭证合并到pipeline_tree
create_task_data["pipeline_tree"].setdefault("credentials", {}).update(resolved_credentials)

client = TaskComponentClient(space_id=space_id)
result = client.create_task(create_task_data)
return result
89 changes: 89 additions & 0 deletions bkflow/apigw/views/update_credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
TencentBlueKing is pleased to support the open source community by making
蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available.
Copyright (C) 2024 THL A29 Limited,
a Tencent company. All rights reserved.
Licensed under the MIT License (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the
specific language governing permissions and limitations under the License.

We undertake not to change the open source license (MIT license) applicable

to the current version of the project delivered to anyone in the future.
"""
import json

from apigw_manager.apigw.decorators import apigw_require
from blueapps.account.decorators import login_exempt
from django.db import transaction
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from bkflow.apigw.decorators import check_jwt_and_space, return_json_response
from bkflow.apigw.serializers.credential import UpdateCredentialSerializer
from bkflow.exceptions import ValidationError
from bkflow.space.models import Credential, CredentialScope


@login_exempt
@csrf_exempt
@require_http_methods(["PUT", "PATCH"])
@apigw_require
@check_jwt_and_space
@return_json_response
def update_credential(request, space_id, credential_id):
"""
更新凭证

:param request: HTTP 请求对象
:param space_id: 空间ID
:param credential_id: 凭证ID
:return: 更新后的凭证信息
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 事务完整性: 更新凭证和作用域应在同一事务中,建议使用 with transaction.atomic() 包裹整个更新逻辑

data = json.loads(request.body)
ser = UpdateCredentialSerializer(data=data)
ser.is_valid(raise_exception=True)

try:
credential = Credential.objects.get(id=credential_id, space_id=space_id, is_deleted=False)
except Credential.DoesNotExist:
raise ValidationError(_("凭证不存在: space_id={}, credential_id={}").format(space_id, credential_id))

with transaction.atomic():
# 更新凭证基本信息
credential_data = dict(ser.validated_data)
scopes_data = credential_data.pop("scopes", None)

for attr, value in credential_data.items():
if attr == "content":
# 使用update_credential方法来更新content,会做验证
credential.update_credential(value)
else:
setattr(credential, attr, value)

credential.updated_by = request.user.username
credential.save()

# 更新凭证作用域
if scopes_data is not None:
# 删除旧的作用域
CredentialScope.objects.filter(credential_id=credential.id).delete()
# 创建新的作用域
if scopes_data:
scope_objects = [
CredentialScope(
credential_id=credential.id,
scope_type=scope.get("scope_type"),
scope_value=scope.get("scope_value"),
)
for scope in scopes_data
]
CredentialScope.objects.bulk_create(scope_objects)

return credential.display_json()
10 changes: 6 additions & 4 deletions bkflow/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making
蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available.
Expand All @@ -17,14 +16,17 @@

to the current version of the project delivered to anyone in the future.
"""
from blueapps.core.exceptions.base import BlueException


class BKFLOWException(Exception):
CODE = None
class BKFLOWException(BlueException):
ERROR_CODE = None
MESSAGE = None
STATUS_CODE = 500

def __init__(self, message=""):
def __init__(self, message="", code=None, errors=None):
self.code = code or "0000000"
self.data = errors or {}
self.message = f"{self.MESSAGE}: {message}" if self.MESSAGE else f"{message}"

def __str__(self):
Expand Down
Loading
Loading