-
Notifications
You must be signed in to change notification settings - Fork 20
feat: 凭证管理增强 --story=125449007 #548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d23fcf8
a982afa
d2e7dbb
1e17d7a
ed48b80
d46aba3
28ec4e7
6a4917d
fd0d549
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
||
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ 此问题似乎已被最近的更改解决 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ 此问题似乎已被最近的更改解决 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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. | ||
|
|
@@ -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 | ||
|
|
@@ -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: 创建的凭证信息 | ||
| """ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 语法错误: 此行代码不完整 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ 此问题似乎已被最近的更改解决 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| 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: 更新后的凭证信息 | ||
| """ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ 此问题似乎已被最近的更改解决 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,会做验证 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 双重 |
||
|
|
||
| # 更新凭证作用域 | ||
| 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() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.