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
Binary file modified bkflow/apigw/docs/apigw-docs.zip
Binary file not shown.
83 changes: 74 additions & 9 deletions bkflow/apigw/serializers/credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,93 @@
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.exceptions import CredentialTypeNotSupport
from bkflow.space.models import Credential, CredentialScopeLevel
from bkflow.space.serializers import CredentialScopeSerializer

Choose a reason for hiding this comment

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

⚠️ if credential 判断永远为真:CredentialDispatcher() 构造失败时抛 CredentialTypeNotSupport 异常而不返回 None,下面的 else 分支永远不会执行。建议改为 try/except CredentialTypeNotSupport 包裹,在 except 块中设置 data['content'] = {}。


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:

Choose a reason for hiding this comment

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

⚠️ if credential 死代码CredentialDispatcher.__init__ 类型不支持时直接抛出异常,不会返回 None,所以 else: data["content"] = {} 永远不执行。应改为 try/except CredentialTypeNotSupport 明确捕获。

Choose a reason for hiding this comment

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

⚠️ 死代码分支CredentialDispatcher.__init__ 在类型不支持时抛出 CredentialTypeNotSupport 异常,不会返回 None,因此 if credential 永远为真,else: data['content'] = {} 永不执行。建议改用 try/except CredentialTypeNotSupport 来处理不支持的类型。

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=128, required=False, allow_blank=True, allow_null=True)
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

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决,现在使用具体类型捕获异常。

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 (serializers.ValidationError, CredentialTypeNotSupport) as e:
raise serializers.ValidationError({"content": str(e)})

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.

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

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决

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=128, required=False, allow_blank=True, allow_null=True)
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 self.instance is not None:
credential_type = self.instance.type

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

def validate_content(self, value):
content_ser = BkAppCredential.BkAppSerializer(data=value)
content_ser.is_valid(raise_exception=True)
return value
return attrs
1 change: 1 addition & 0 deletions bkflow/apigw/serializers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class CreateTaskWithoutTemplateSerializer(CredentialsValidationMixin, serializer
scope_value = serializers.CharField(help_text=_("任务范围值"), max_length=128, required=False)
description = serializers.CharField(help_text=_("任务描述"), required=False, allow_blank=True)
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={})
custom_span_attributes = serializers.DictField(
Expand Down
2 changes: 2 additions & 0 deletions bkflow/apigw/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from bkflow.apigw.views.renew_space_config import renew_space_config
from bkflow.apigw.views.revoke_token import revoke_token
from bkflow.apigw.views.rollback_template import rollback_template
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 @@ -79,6 +80,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 语法错误

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决

Choose a reason for hiding this comment

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

✅ 此问题已解决,当前代码已完整实现作用域条件判断。

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()
90 changes: 90 additions & 0 deletions bkflow/apigw/views/update_credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
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() 包裹整个更新逻辑

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决

Choose a reason for hiding this comment

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

✅ 此问题似乎已被最近的更改解决,update_credential 视图中已添加 transaction.atomic() 包裹整个更新逻辑。

data = json.loads(request.body)

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))

ser = UpdateCredentialSerializer(instance=credential, data=data, partial=True)
ser.is_valid(raise_exception=True)

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,会做验证

Choose a reason for hiding this comment

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

重复 save() 导致多余数据库写入:update_credential(value) 内部已调用 self.save()(见 models.py),循环结束后的 credential.save()(第 69 行)会再次触发一次额外写入。建议在 content 分支中跳过后续 save(),或将 update_credential() 重构为只更新字段不保存,统一由外层 save() 提交。

credential.update_credential(value)
else:
setattr(credential, attr, value)

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

Choose a reason for hiding this comment

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

🚨 双重 save() 写入update_credential(value)models.py:353)内部已调用 self.save(),此处再次 credential.save() 会将其他字段以旧值再写一次,可能覆盖刚保存的 content。应跳过本次 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()
5 changes: 0 additions & 5 deletions bkflow/contrib/api/collections/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ def _pre_process_headers(self, headers):
def _get_interface_url(self, api_name):
return "{}/{}".format(settings.INTERFACE_APP_URL, api_name)

def get_apigw_credential(self, data):
return self._request(
method="get", url=self._get_interface_url("api/space/credential/get_api_gateway_credential/"), data=data
)

def get_decision_table(self, decision_table_id, data):
return self._request(
method="get", url=self._get_interface_url(f"api/decision_table/internal/{decision_table_id}/"), data=data
Expand Down
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