From d23fcf851d636eb73848cfff66230fa1db80e457 Mon Sep 17 00:00:00 2001 From: Mianhuatang8 <2542880657@qq.com> Date: Mon, 9 Feb 2026 16:37:08 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E5=87=AD=E8=AF=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=A2=9E=E5=BC=BA=20=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/docs/apigw-docs.zip | Bin 91018 -> 89064 bytes bkflow/apigw/serializers/credential.py | 89 +++++- bkflow/apigw/serializers/task.py | 1 + bkflow/apigw/urls.py | 2 + bkflow/apigw/views/create_credential.py | 35 ++- bkflow/apigw/views/update_credential.py | 89 ++++++ bkflow/exceptions.py | 10 +- .../static/variables/credential.js | 125 +++++++++ .../variables/collections/credential.py | 44 +++ bkflow/space/admin.py | 9 +- bkflow/space/credential/__init__.py | 15 +- bkflow/space/credential/basic_auth.py | 70 +++++ bkflow/space/credential/bk_access_token.py | 69 +++++ .../space/credential/{bkapp.py => bk_app.py} | 26 +- bkflow/space/credential/custom.py | 70 +++++ bkflow/space/credential/dispatcher.py | 13 +- bkflow/space/credential/resolver.py | 79 ++++++ bkflow/space/credential/scope_validator.py | 82 ++++++ bkflow/space/exceptions.py | 12 +- .../migrations/0008_auto_20251014_1511.py | 83 ++++++ .../0009_encrypt_credential_content.py | 200 ++++++++++++++ bkflow/space/models.py | 100 ++++++- bkflow/space/serializers.py | 23 +- bkflow/space/views.py | 130 +++++++-- bkflow/task/serializers.py | 8 + bkflow/utils/crypt.py | 63 +++++ bkflow/utils/models.py | 131 ++++++++- bkflow/utils/serializer.py | 169 ++++++++++++ config/default.py | 3 + env.py | 4 + frontend/src/config/i18n/cn.js | 4 + frontend/src/config/i18n/en.js | 4 + tests/interface.env | 1 + tests/interface/credential/__init__.py | 18 ++ tests/interface/credential/test_credential.py | 218 +++++++++++++++ .../credential/test_credential_model.py | 253 ++++++++++++++++++ .../credential/test_credential_resolver.py | 176 ++++++++++++ .../test_credential_scope_validator.py | 180 +++++++++++++ .../credential/test_secret_json_field.py | 221 +++++++++++++++ 39 files changed, 2757 insertions(+), 72 deletions(-) create mode 100644 bkflow/apigw/views/update_credential.py create mode 100644 bkflow/pipeline_plugins/static/variables/credential.js create mode 100644 bkflow/pipeline_plugins/variables/collections/credential.py create mode 100644 bkflow/space/credential/basic_auth.py create mode 100644 bkflow/space/credential/bk_access_token.py rename bkflow/space/credential/{bkapp.py => bk_app.py} (76%) create mode 100644 bkflow/space/credential/custom.py create mode 100644 bkflow/space/credential/resolver.py create mode 100644 bkflow/space/credential/scope_validator.py create mode 100644 bkflow/space/migrations/0008_auto_20251014_1511.py create mode 100644 bkflow/space/migrations/0009_encrypt_credential_content.py create mode 100644 bkflow/utils/crypt.py create mode 100644 bkflow/utils/serializer.py create mode 100644 tests/interface/credential/__init__.py create mode 100644 tests/interface/credential/test_credential.py create mode 100644 tests/interface/credential/test_credential_model.py create mode 100644 tests/interface/credential/test_credential_resolver.py create mode 100644 tests/interface/credential/test_credential_scope_validator.py create mode 100644 tests/interface/credential/test_secret_json_field.py diff --git a/bkflow/apigw/docs/apigw-docs.zip b/bkflow/apigw/docs/apigw-docs.zip index 4f25531216f42600caad2a2777073b354791d4d6..369a6ff3c30048dfc43402cabf869b4bdffdcc84 100644 GIT binary patch literal 89064 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

q2@%75(0$MZy1&8ueV5*FZ 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

LFd7QOp$gmHX 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 literal 91018 zcma&ObC4(PvNhVqG=6Q{wr$()Y1_7K_q1)>obH~sZQJ&pcb^k+_dfTYFTSdX%8Dl{ zGU|^fR<5-&s}!U`!BBwyIbfAEbpCbo-xp9I0w7OwdKY&XAYib6*8X)-Re=M7e1Xok zSpAW0VcxiIuJEq}P(V;XYKs4v0P(*P7}(oed+0mcS((_<*%<%-r7MQYwpjR^?w{DL zp)Zf6iQ>n8%e}FJ?rc@Cu(HW#ZK#DW(`wPk+8O81&|-}W`?Dpp8QRi9uzp5rk*Zp{ zrO8@Zu+m0T;FTfU%gp_b`hxR$3U=_%PHtE>#vrfR6ld@KaSrFRUWao;SuTE~R7SCq zrB7>TWoOiIt9kRlp4C}nElV@mie>$l27RK$O#n(?nnFD9`251QB^_lNwdJY54Ue!K z|LM6h)4Syy=mv#e*^SfadzgYv33jWgcz9w8-87MusE`av9KgtvHf^EPB9 zCMC-yAndar=oo()$- z$rAS4BQjJgD47DVKKrd;HNVNKjj-%|yh3s69qn7LZ=N)GaR(@q(0wQP6ItblvPBg> ziy`%xfg;`Yj8<}qwBsSPgOg^O85@#wNRj_Tr`Z@BBVx(KSf2yKxD@3RBYC2sD~KW? z37x<`hAY-PQ64Ca_4yS$B~DH}#I*>+?EFLyW_7HBC&xa(gD4)7JzxHbPz4A%i#X`H zSM)er_&K5h6bjkXpiI1yCDl%ZJw+y<1Z`z})6`=EQ851$_3)F95ih&uLiIG$M6hfG ztG%Khk?YS;4rkrQ_S;1BN1fyJW1cJ1ZQtRqUk)BYd=Y~HaiOn1-j{whsf}zYh&@Kv zL0~(?-dc1eDP_|3$bi5N1eZsbePzsX;zymB;X0MGsG@Ii*?3!Zji*8_o`K&4DYb{5 zv6E@`n-Vy6%Ta4uDiOQwv9vTE!b(NBErx-^KgQl;hNE~!$hSWD863UwYSn*)=@t-8 zzL>w^)(El(jm6eNbJJdfiZwsP(hldzESwNw?shrUSRPEJ%d*}`EqbL>FjVunEot61 z9{Lz27we_Zf{DekPr|703|YlPRyTXJP;<`Cg1KH0{6dnRii_LoxS#*?QWk{jc;V8W z)*}%cx6yqXU#Z%WJb1Wy-g+I)budUHf9gta!3x>^%bBV5)%{`^6V%Y=)+Sk~pGB`& z_Lg6_`b0==vA|DfKeugARr+BES&ZR_Rup;yQEzn2mlTdQcdc<6BLmpp9dj_ec_lSM zu3r0xnny2DpHCEB_z$FTR+cp>S-QDu&`!s6t4w{{?DP0a#+IxRw(jgu`OH5kY__oz zbtA>j;XhaPz`POh%o}*jTO4M8ln7fxL6|)Zue1r5SC@-VgYPa~p0negT@`3~uEJy> zAy7H7$wN%AX}LZcA@i-%4hrUmlfU!SqNK_X&reZ3FChJlg16TfDF|Y!`C;Ttr@E3j zFDVg%5P&JtMQ@6qEF}`KCBvbgalgSg?mcfs$tRARTb*Nb)=@0<=t`WHHrd+`z6-NN z^V~!#J?ZVdVE(&FSB>iuo0*EZ=&0I>=+|*S@P5(?KADLa=pdq}ogjA9mfAs~$wAxZ z)%JiO2Iu$16;aU$i_3!!F)v{EwB7nBoW4C|Innpv)N5nx`8}SyEP<95RWp4e=c+~= zCp{o5ZbZrjbwr>0;HQ!6j{s4j#>AQHdl4Ri9dC_LGajyB>NM6h%0D*sphj#oA@O^+ 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%0D{$SvYAuDVa7_$Qg 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 z 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

F#*QMZB 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 diff --git a/bkflow/apigw/serializers/credential.py b/bkflow/apigw/serializers/credential.py index 4795f2fff1..daaf6f2b3e 100644 --- a/bkflow/apigw/serializers/credential.py +++ b/bkflow/apigw/serializers/credential.py @@ -19,7 +19,53 @@ 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, CredentialScope + + +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 CredentialScopeSerializer(serializers.ModelSerializer): + """凭证作用域序列化器""" + + class Meta: + model = CredentialScope + fields = ["scope_type", "scope_value"] + + +class CredentialScopesChangeSerializer(serializers.Serializer): + """凭证作用域变更序列化器""" + + scopes = serializers.ListField( + child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False, default=list + ) + unlimited = serializers.BooleanField(help_text=_("是否无限制"), required=False, default=False) + + def validate(self, attrs): + if attrs.get("unlimited"): + if attrs.get("scopes"): + raise serializers.ValidationError(_("无限制时不能设置作用域")) + + if not attrs.get("unlimited") and not attrs.get("scopes"): + raise serializers.ValidationError(_("作用域不能为空")) + return attrs class CreateCredentialSerializer(serializers.Serializer): @@ -27,11 +73,22 @@ class CreateCredentialSerializer(serializers.Serializer): 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) + scopes = serializers.ListField( + child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False, default=list + ) - def validate_content(self, value): - content_ser = BkAppCredential.BkAppSerializer(data=value) - content_ser.is_valid(raise_exception=True) - return value + def validate(self, attrs): + # 动态验证content根据type + credential_type = attrs.get("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)}) + + return attrs class UpdateCredentialSerializer(serializers.Serializer): @@ -39,8 +96,22 @@ class UpdateCredentialSerializer(serializers.Serializer): 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) + scopes = serializers.ListField(child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False) + + def validate(self, attrs): + # 如果提供了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)}) - def validate_content(self, value): - content_ser = BkAppCredential.BkAppSerializer(data=value) - content_ser.is_valid(raise_exception=True) - return value + return attrs diff --git a/bkflow/apigw/serializers/task.py b/bkflow/apigw/serializers/task.py index 7d7b499dbb..2d3de2ebcd 100644 --- a/bkflow/apigw/serializers/task.py +++ b/bkflow/apigw/serializers/task.py @@ -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( diff --git a/bkflow/apigw/urls.py b/bkflow/apigw/urls.py index ea3acd51f4..5bed1b7dfb 100644 --- a/bkflow/apigw/urls.py +++ b/bkflow/apigw/urls.py @@ -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 @@ -79,6 +80,7 @@ url(r"^space/(?P\d+)/create_task_without_template/$", create_task_without_template), url(r"^space/(?P\d+)/validate_pipeline_tree/$", validate_pipeline_tree), url(r"^space/(?P\d+)/create_credential/$", create_credential), + url(r"^space/(?P\d+)/credential/(?P\d+)/$", update_credential), url(r"^space/(?P\d+)/get_task_list/$", get_task_list), url(r"^space/(?P\d+)/task/(?P\d+)/get_task_detail/$", get_task_detail), url(r"^space/(?P\d+)/task/(?P\d+)/get_task_states/$", get_task_states), diff --git a/bkflow/apigw/views/create_credential.py b/bkflow/apigw/views/create_credential.py index cb27a8c98c..13ca044279 100644 --- a/bkflow/apigw/views/create_credential.py +++ b/bkflow/apigw/views/create_credential.py @@ -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 @login_exempt @@ -36,9 +36,36 @@ @check_jwt_and_space @return_json_response def create_credential(request, space_id): + """ + 创建凭证 + + :param request: HTTP 请求对象 + :param space_id: 空间ID + :return: 创建的凭证信息 + """ 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", []) + + # 创建凭证和作用域 + with transaction.atomic(): + # 序列化器已经检查过是否存在了 + credential = Credential.create_credential(**credential_data, space_id=space_id, creator=request.user.username) + + # 创建凭证作用域 + if 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() diff --git a/bkflow/apigw/views/update_credential.py b/bkflow/apigw/views/update_credential.py new file mode 100644 index 0000000000..da90a2fa7c --- /dev/null +++ b/bkflow/apigw/views/update_credential.py @@ -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: 更新后的凭证信息 + """ + 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() diff --git a/bkflow/exceptions.py b/bkflow/exceptions.py index 5ebb38754c..0ecf616aa7 100644 --- a/bkflow/exceptions.py +++ b/bkflow/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -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): diff --git a/bkflow/pipeline_plugins/static/variables/credential.js b/bkflow/pipeline_plugins/static/variables/credential.js new file mode 100644 index 0000000000..f499e2c175 --- /dev/null +++ b/bkflow/pipeline_plugins/static/variables/credential.js @@ -0,0 +1,125 @@ + +(function () { + $.atoms.credential = [ + { + tag_code: "credential_meta", + type: "combine", + attrs: { + name: gettext("表格"), + hookable: true, + children: [ + { + tag_code: "credential_type", + type: "select", + attrs: { + name: gettext("凭证类型"), + hookable: true, + items: [ + { + text: gettext("蓝鲸应用凭证"), + value: "BK_APP" + }, + { + text: gettext("蓝鲸 Access Token 凭证"), + value: "BK_ACCESS_TOKEN" + }, + { + text: gettext("Basic Auth"), + value: "BASIC_AUTH" + }, + { + text: gettext("自定义"), + value: "CUSTOM" + } + ], + value: "BK_APP", + validation: [ + { + type: "required" + } + ] + } + }, + { + tag_code: "type", + type: "checkbox", + attrs: { + name: gettext("引用凭证"), + hookable: true, + items: [{value: "0"}], + value: ["0"], + validation: [ + { + type: "required" + } + ] + } + } + ] + } + + }, + { + tag_code: "credential", + meta_transform: function (variable) { + let metaConfig = variable.value; + let remote = false; + let remote_url = ""; + let items = []; + let placeholder = ''; + // if (metaConfig.datasource === "1") { + // remote_url = $.context.get('site_url') + 'api/plugin_query/variable_select_source_data_proxy/?url=' + metaConfig.items_text; + // remote = true; + // } + if (metaConfig.datasource === "0") { + try { + items = JSON.parse(metaConfig.items_text); + } catch (err) { + items = []; + placeholder = gettext('非法下拉框数据源,请检查您的配置'); + } + if (!(items instanceof Array)) { + items = []; + placeholder = gettext('非法下拉框数据源,请检查您的配置'); + } + } + + let multiple = false; + let default_val = metaConfig.default || ''; + + // if (metaConfig.type === "1") { + // multiple = true; + // default_val = []; + // if (metaConfig.default) { + // let vals = metaConfig.default.split(','); + // for (let i in vals) { + // default_val.push(vals[i].trim()); + // } + // } + // } + return { + tag_code: this.tag_code, + type: "select", + attrs: { + name: gettext("下拉框"), + hookable: true, + items: items, + multiple: multiple, + value: default_val, + remote: remote, + remote_url: remote_url, + placeholder: placeholder, + remote_data_init: function (data) { + return data; + }, + validation: [ + { + type: "required" + } + ] + } + } + } + } + ] +})(); diff --git a/bkflow/pipeline_plugins/variables/collections/credential.py b/bkflow/pipeline_plugins/variables/collections/credential.py new file mode 100644 index 0000000000..44d86306fa --- /dev/null +++ b/bkflow/pipeline_plugins/variables/collections/credential.py @@ -0,0 +1,44 @@ +""" +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. +""" +from typing import List + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from pipeline.core.flow.io import StringItemSchema + +from bkflow.pipeline_plugins.variables.base import ( + CommonPlainVariable, + FieldExplain, + SelfExplainVariable, + Type, +) + + +class Credential(CommonPlainVariable, SelfExplainVariable): + code = "credential" + name = _("凭证") + type = "meta" + tag = "credential.credential" + meta_tag = "credential.credential_meta" + form = "{}variables/{}.js".format(settings.STATIC_URL, code) + schema = StringItemSchema(description=_("输入凭证")) + + @classmethod + def _self_explain(cls, **kwargs) -> List[FieldExplain]: + return [FieldExplain(key="${KEY}", type=Type.STRING, description="用户选择的凭证值")] diff --git a/bkflow/space/admin.py b/bkflow/space/admin.py index 5a8d84d628..c251a025dc 100644 --- a/bkflow/space/admin.py +++ b/bkflow/space/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -44,3 +43,11 @@ class CredentialAdmin(admin.ModelAdmin): search_fields = ("space_id", "name", "type") list_filter = ("space_id",) ordering = ["-id"] + + +@admin.register(models.CredentialScope) +class CredentialScopeAdmin(admin.ModelAdmin): + list_display = ("id", "credential_id", "scope_type", "scope_value") + search_fields = ("credential_id", "scope_type", "scope_value") + list_filter = ("credential_id", "scope_type", "scope_value") + ordering = ["-id"] diff --git a/bkflow/space/credential/__init__.py b/bkflow/space/credential/__init__.py index 496f7d0932..d6fa97141f 100644 --- a/bkflow/space/credential/__init__.py +++ b/bkflow/space/credential/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -17,8 +16,16 @@ to the current version of the project delivered to anyone in the future. """ -from .bkapp import BkAppCredential # noqa +from .basic_auth import BasicAuthCredential # noqa +from .bk_access_token import BkAccessTokenCredential # noqa +from .bk_app import BkAppCredential # noqa +from .custom import CustomCredential # noqa from .dispatcher import CredentialDispatcher # noqa - -__ALL__ = ["BkAppCredential", "CredentialDispatcher"] +__ALL__ = [ + "BkAppCredential", + "BkAccessTokenCredential", + "BasicAuthCredential", + "CustomCredential", + "CredentialDispatcher", +] diff --git a/bkflow/space/credential/basic_auth.py b/bkflow/space/credential/basic_auth.py new file mode 100644 index 0000000000..0018a05592 --- /dev/null +++ b/bkflow/space/credential/basic_auth.py @@ -0,0 +1,70 @@ +""" +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. +""" +from rest_framework import serializers + +from bkflow.space.credential.base import BaseCredential + + +class BasicAuthCredential(BaseCredential): + """ + 用户名+密码凭证 + """ + + class BasicAuthSerializer(serializers.Serializer): + username = serializers.CharField(required=True) + password = serializers.CharField(required=True) + + def validate_password(self, value): + """ + 验证字段 password 的值,确保它不是全为 '*' + + :param value: password 值 + :return: 验证后的值 + """ + if all(char == "*" for char in value): + raise serializers.ValidationError("password 格式有误 不应全为 * 字符") + return value + + def value(self): + """ + 获取凭证真实值 + + :return: 凭证的实际内容 + """ + # todo 这里会涉及到加解密的操作 + return self.data + + def display_value(self): + """ + 获取凭证脱敏后的值 + + :return: 脱敏后的凭证内容(password 替换为星号) + """ + self.data["password"] = "*********" + return self.data + + def validate_data(self): + """ + 校验凭证数据格式 + + :return: 验证后的数据 + """ + ser = self.BasicAuthSerializer(data=self.data) + ser.is_valid(raise_exception=True) + return ser.validated_data diff --git a/bkflow/space/credential/bk_access_token.py b/bkflow/space/credential/bk_access_token.py new file mode 100644 index 0000000000..bf9563a28c --- /dev/null +++ b/bkflow/space/credential/bk_access_token.py @@ -0,0 +1,69 @@ +""" +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. +""" +from rest_framework import serializers + +from bkflow.space.credential.base import BaseCredential + + +class BkAccessTokenCredential(BaseCredential): + """ + 蓝鲸登录态凭证 + """ + + class BkAccessTokenSerializer(serializers.Serializer): + access_token = serializers.CharField(required=True) + + def validate_access_token(self, value): + """ + 验证字段 access_token 的值,确保它不是全为 '*' + + :param value: access_token 值 + :return: 验证后的值 + """ + if all(char == "*" for char in value): + raise serializers.ValidationError("access_token 格式有误 不应全为 * 字符") + return value + + def value(self): + """ + 获取凭证真实值 + + :return: 凭证的实际内容 + """ + # todo 这里会涉及到加解密的操作 + return self.data + + def display_value(self): + """ + 获取凭证脱敏后的值 + + :return: 脱敏后的凭证内容(access_token 替换为星号) + """ + self.data["access_token"] = "*********" + return self.data + + def validate_data(self): + """ + 校验凭证数据格式 + + :return: 验证后的数据 + """ + ser = self.BkAccessTokenSerializer(data=self.data) + ser.is_valid(raise_exception=True) + return ser.validated_data diff --git a/bkflow/space/credential/bkapp.py b/bkflow/space/credential/bk_app.py similarity index 76% rename from bkflow/space/credential/bkapp.py rename to bkflow/space/credential/bk_app.py index 83f44bc21d..940a4fc4ec 100644 --- a/bkflow/space/credential/bkapp.py +++ b/bkflow/space/credential/bk_app.py @@ -22,25 +22,49 @@ class BkAppCredential(BaseCredential): + """ + 蓝鲸应用凭证 + """ + class BkAppSerializer(serializers.Serializer): bk_app_code = serializers.CharField(required=True) bk_app_secret = serializers.CharField(required=True) def validate_bk_app_secret(self, value): - # 验证字段 bk_app_secret 的值,确保它不是全为 '*' + """ + 验证字段 bk_app_secret 的值,确保它不是全为 '*' + + :param value: bk_app_secret 值 + :return: 验证后的值 + """ if all(char == "*" for char in value): raise serializers.ValidationError("bk_app_secret 格式有误 不应全为 * 字符") return value def value(self): + """ + 获取凭证真实值 + + :return: 凭证的实际内容 + """ # todo 这里会涉及到加解密的操作 return self.data def display_value(self): + """ + 获取凭证脱敏后的值 + + :return: 脱敏后的凭证内容(bk_app_secret 替换为星号) + """ self.data["bk_app_secret"] = "*********" return self.data def validate_data(self): + """ + 校验凭证数据格式 + + :return: 验证后的数据 + """ ser = self.BkAppSerializer(data=self.data) ser.is_valid(raise_exception=True) return ser.validated_data diff --git a/bkflow/space/credential/custom.py b/bkflow/space/credential/custom.py new file mode 100644 index 0000000000..c2358e3f2d --- /dev/null +++ b/bkflow/space/credential/custom.py @@ -0,0 +1,70 @@ +""" +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. +""" +from rest_framework import serializers + +from bkflow.space.credential.base import BaseCredential + + +class CustomCredential(BaseCredential): + """ + 自定义凭证,支持任意key-value对 + """ + + def value(self): + """ + 获取凭证真实值 + + :return: 凭证的实际内容 + """ + # todo 这里会涉及到加解密的操作 + return self.data + + def display_value(self): + """ + 获取凭证脱敏后的值 + + :return: 脱敏后的凭证内容(所有value替换为星号) + """ + display_data = {} + for key in self.data.keys(): + display_data[key] = "*********" + return display_data + + def validate_data(self): + """ + 校验凭证数据格式 + + :return: 验证后的数据 + """ + if not isinstance(self.data, dict): + raise serializers.ValidationError("自定义凭证内容必须是字典类型") + + if not self.data: + raise serializers.ValidationError("自定义凭证内容不能为空") + + for key, value in self.data.items(): + if not isinstance(key, str): + raise serializers.ValidationError(f"凭证key必须是字符串类型: {key}") + if not isinstance(value, str): + raise serializers.ValidationError(f"凭证value必须是字符串类型: {key}={value}") + # 验证值不是全为 '*' + if all(char == "*" for char in value): + raise serializers.ValidationError(f"凭证值格式有误,不应全为 * 字符: {key}") + + return self.data diff --git a/bkflow/space/credential/dispatcher.py b/bkflow/space/credential/dispatcher.py index 470e88623a..a22195407e 100644 --- a/bkflow/space/credential/dispatcher.py +++ b/bkflow/space/credential/dispatcher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -19,12 +18,20 @@ """ from django.utils.translation import ugettext_lazy as _ -from bkflow.space.credential.bkapp import BkAppCredential +from bkflow.space.credential.basic_auth import BasicAuthCredential +from bkflow.space.credential.bk_access_token import BkAccessTokenCredential +from bkflow.space.credential.bk_app import BkAppCredential +from bkflow.space.credential.custom import CustomCredential from bkflow.space.exceptions import CredentialTypeNotSupport class CredentialDispatcher: - CREDENTIAL_MAP = {"BK_APP": BkAppCredential} + CREDENTIAL_MAP = { + "BK_APP": BkAppCredential, + "BK_ACCESS_TOKEN": BkAccessTokenCredential, + "BASIC_AUTH": BasicAuthCredential, + "CUSTOM": CustomCredential, + } def __init__(self, credential_type, data): credential_cls = self.CREDENTIAL_MAP.get(credential_type) diff --git a/bkflow/space/credential/resolver.py b/bkflow/space/credential/resolver.py new file mode 100644 index 0000000000..0f254cf031 --- /dev/null +++ b/bkflow/space/credential/resolver.py @@ -0,0 +1,79 @@ +""" +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. +""" +from django.utils.translation import ugettext_lazy as _ + +from bkflow.space.credential.scope_validator import validate_credential_scope +from bkflow.space.exceptions import CredentialNotFoundError +from bkflow.space.models import Credential + + +def resolve_credentials(credentials_dict, space_id, scope_type=None, scope_value=None): + """ + 解析凭证字典,将凭证引用转换为实际的凭证值 + + :param credentials_dict: 凭证字典,格式为 {"${token1}": {"value": "credential_id_or_direct_value", ...}} + :param space_id: 空间ID + :param scope_type: 模板的作用域类型(可选) + :param scope_value: 模板的作用域值(可选) + + :return: 解析后的凭证字典,保留原有结构但填充实际值 + + :raises CredentialNotFoundError: 当引用的凭证不存在时 + :raises CredentialScopeValidationError: 当凭证不能在指定作用域使用时 + """ + if not credentials_dict: + return {} + + resolved_credentials = {} + + for key, cred_info in credentials_dict.items(): + # 创建凭证信息的副本 + resolved_info = dict(cred_info) + + # 获取凭证值 + value = cred_info.get("value", "") + + # 如果value是数字,尝试作为credential_id解析 + if isinstance(value, (int, str)) and str(value).isdigit(): + credential_id = int(value) + try: + credential = Credential.objects.get(id=credential_id, space_id=space_id, is_deleted=False) + + # 验证凭证作用域 + validate_credential_scope(credential, scope_type, scope_value) + + # 获取凭证的实际值 + resolved_info["value"] = credential.value + resolved_info["credential_id"] = credential_id + resolved_info["credential_name"] = credential.name + resolved_info["credential_type"] = credential.type + + except Credential.DoesNotExist: + raise CredentialNotFoundError( + _("凭证不存在: space_id={space_id}, credential_id={credential_id}").format( + space_id=space_id, credential_id=credential_id + ) + ) + else: + # 如果不是数字,直接使用提供的值 + resolved_info["value"] = value + + resolved_credentials[key] = resolved_info + + return resolved_credentials diff --git a/bkflow/space/credential/scope_validator.py b/bkflow/space/credential/scope_validator.py new file mode 100644 index 0000000000..68cf39fc45 --- /dev/null +++ b/bkflow/space/credential/scope_validator.py @@ -0,0 +1,82 @@ +""" +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. +""" +from django.utils.translation import ugettext_lazy as _ + +from bkflow.space.exceptions import CredentialScopeValidationError + + +def validate_credential_scope(credential, template_scope_type, template_scope_value): + """ + 验证凭证是否可以在指定的模板作用域中使用 + + :param credential: Credential 模型实例 + :param template_scope_type: 模板的作用域类型 + :param template_scope_value: 模板的作用域值 + :return: 验证通过返回 True + :raises CredentialScopeValidationError: 当凭证不能在指定作用域中使用时 + """ + if not credential.can_use_in_scope(template_scope_type, template_scope_value): + raise CredentialScopeValidationError( + _("凭证 {name}(ID:{id}) 不能在作用域 {scope_type}:{scope_value} 中使用").format( + name=credential.name, + id=credential.id, + scope_type=template_scope_type or "None", + scope_value=template_scope_value or "None", + ) + ) + return True + + +def filter_credentials_by_scope(credentials_queryset, scope_type, scope_value): + """ + 根据作用域过滤凭证列表,返回可以在指定作用域中使用的凭证 + + :param credentials_queryset: Credential 查询集 + :param scope_type: 作用域类型 + :param scope_value: 作用域值 + :return: 过滤后的凭证查询集 + """ + from bkflow.space.models import CredentialScope + + # 获取所有凭证ID + all_credential_ids = set(credentials_queryset.values_list("id", flat=True)) + + # 获取有作用域限制的凭证ID + credentials_with_scope = set( + CredentialScope.objects.filter(credential_id__in=all_credential_ids).values_list("credential_id", flat=True) + ) + + # 没有作用域限制的凭证ID(可以在任何地方使用) + credentials_without_scope = all_credential_ids - credentials_with_scope + + # 如果模板没有作用域,返回所有凭证 + if not scope_type and not scope_value: + return credentials_queryset + + # 查找匹配当前作用域的凭证ID + matching_credential_ids = set( + CredentialScope.objects.filter( + credential_id__in=credentials_with_scope, scope_type=scope_type, scope_value=scope_value + ).values_list("credential_id", flat=True) + ) + + # 返回:没有作用域限制的凭证 + 匹配当前作用域的凭证 + available_credential_ids = credentials_without_scope | matching_credential_ids + + return credentials_queryset.filter(id__in=available_credential_ids) diff --git a/bkflow/space/exceptions.py b/bkflow/space/exceptions.py index 859fd25c34..64813807ab 100644 --- a/bkflow/space/exceptions.py +++ b/bkflow/space/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -46,3 +45,14 @@ class CredentialTypeNotSupport(BKFLOWException): class SpaceNotExists(BKFLOWException): CODE = None MESSAGE = _("空间不存在") + + +class CredentialScopeValidationError(BKFLOWException): + CODE = None + MESSAGE = _("凭证作用域验证失败") + + +class CredentialNotFoundError(BKFLOWException): + CODE = None + MESSAGE = _("凭证不存在") + STATUS_CODE = 404 diff --git a/bkflow/space/migrations/0008_auto_20251014_1511.py b/bkflow/space/migrations/0008_auto_20251014_1511.py new file mode 100644 index 0000000000..beefb6dd41 --- /dev/null +++ b/bkflow/space/migrations/0008_auto_20251014_1511.py @@ -0,0 +1,83 @@ +# Generated by Django 3.2.25 on 2025-10-14 07:11 + +from django.db import migrations, models + +import bkflow.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0007_alter_space_app_code"), + ] + + operations = [ + migrations.CreateModel( + name="CredentialScope", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("credential_id", models.IntegerField(db_index=True, verbose_name="凭证ID")), + ("scope_type", models.CharField(blank=True, max_length=128, null=True, verbose_name="作用域类型")), + ("scope_value", models.CharField(blank=True, max_length=128, null=True, verbose_name="作用域值")), + ], + options={ + "verbose_name": "凭证作用域", + "verbose_name_plural": "凭证作用域表", + }, + ), + migrations.AlterField( + model_name="credential", + name="content", + field=bkflow.utils.models.SecretSingleJsonField(blank=True, default=dict, null=True, verbose_name="凭证内容"), + ), + migrations.AlterField( + model_name="credential", + name="type", + field=models.CharField( + choices=[ + ("BK_APP", "蓝鲸应用凭证"), + ("BK_ACCESS_TOKEN", "蓝鲸登录态凭证"), + ("BASIC_AUTH", "用户名+密码"), + ("CUSTOM", "自定义"), + ], + max_length=32, + verbose_name="凭证类型", + ), + ), + migrations.AlterField( + model_name="spaceconfig", + name="name", + field=models.CharField( + choices=[ + ("token_expiration", "Token过期时间"), + ("token_auto_renewal", "是否开启Token自动续期"), + ("engine_space_config", "引擎模块配置"), + ("callback_hooks", "回调配置"), + ("uniform_api", "API 插件配置 (如更改配置,可能对已存在数据产生不兼容影响,请谨慎操作)"), + ("superusers", "空间管理员"), + ("canvas_mode", "画布模式"), + ("gateway_expression", "网关表达式"), + ("api_gateway_credential_name", "API_GATEWAY使用的凭证配置"), + ("space_plugin_config", "空间插件配置"), + ], + max_length=32, + verbose_name="配置项", + ), + ), + migrations.AlterField( + model_name="spaceconfig", + name="value_type", + field=models.CharField( + choices=[("JSON", "JSON"), ("TEXT", "文本"), ("REF", "引用")], + default="TEXT", + max_length=32, + verbose_name="配置类型", + ), + ), + migrations.AddIndex( + model_name="credentialscope", + index=models.Index( + fields=["credential_id", "scope_type", "scope_value"], name="space_crede_credent_6fe099_idx" + ), + ), + ] diff --git a/bkflow/space/migrations/0009_encrypt_credential_content.py b/bkflow/space/migrations/0009_encrypt_credential_content.py new file mode 100644 index 0000000000..6a2e7fb735 --- /dev/null +++ b/bkflow/space/migrations/0009_encrypt_credential_content.py @@ -0,0 +1,200 @@ +""" +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 django.conf import settings +from django.db import connection, migrations + +from bkflow.utils.crypt import BaseCrypt + + +def encrypt_existing_credentials(apps, schema_editor): + """ + 加密已存在的未加密凭证数据 + + :param apps: Django apps registry + :param schema_editor: 数据库 schema 编辑器 + """ + Credential = apps.get_model("space", "Credential") + table_name = Credential._meta.db_table + crypt = BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + # 统计信息 + total_count = 0 + encrypted_count = 0 + skipped_count = 0 + error_count = 0 + + print("\n开始检查并加密未加密的凭证数据...") + + def is_value_encrypted(raw_value: str) -> bool: + """ + 通过解密并重新加密后与原始密文比对,判断是否为本系统的加密值。 + 说明:BaseCrypt 的加解密在相同 KEY/IV 下是确定性的,因此 encrypt(decrypt(x)) == x 成立。 + """ + if not isinstance(raw_value, str) or not raw_value: + return False + try: + plain = crypt.decrypt(raw_value) + re_encrypted = crypt.encrypt(plain) + return re_encrypted == raw_value + except Exception: + return False + + with connection.cursor() as cursor: + cursor.execute(f"SELECT id, name, type, content FROM {connection.ops.quote_name(table_name)}") + rows = cursor.fetchall() + + for row in rows: + total_count += 1 + cred_id, cred_name, cred_type, raw_content = row + + # 解析原始 content(以数据库中的原样字符串/JSON存储为准) + try: + content_obj = ( + raw_content if isinstance(raw_content, dict) else json.loads(raw_content) if raw_content else None + ) + except Exception: + content_obj = None + + if not content_obj or not isinstance(content_obj, dict): + skipped_count += 1 + continue + + try: + updated = False + encrypted_content = {} + for key, value in content_obj.items(): + if value is not None and isinstance(value, str): + # 已是密文则保持原样;否则进行加密 + if is_value_encrypted(value): + encrypted_content[key] = value + else: + encrypted_content[key] = crypt.encrypt(value) + updated = True + else: + encrypted_content[key] = value + + if not updated: + skipped_count += 1 + print(f" 跳过已加密的凭证: ID={cred_id}, Name={cred_name}") + continue + + # 直接以原生 SQL 更新,避免字段 from_db_value/to_python 的再次处理 + with connection.cursor() as cursor: + cursor.execute( + f"UPDATE {connection.ops.quote_name(table_name)} SET content=%s WHERE id=%s", + [json.dumps(encrypted_content), cred_id], + ) + + encrypted_count += 1 + print(f" ✓ 成功加密凭证: ID={cred_id}, Name={cred_name}, Type={cred_type}") + + except Exception as e: + error_count += 1 + print(f" ✗ 加密失败: ID={cred_id}, Name={cred_name}, Error={str(e)}") + + # 打印统计信息 + print("\n" + "=" * 70) + print("凭证数据加密完成!") + print(f" 总计: {total_count} 条") + print(f" 已加密: {encrypted_count} 条") + print(f" 已跳过: {skipped_count} 条(已加密或空数据)") + print(f" 失败: {error_count} 条") + print("=" * 70 + "\n") + + +def reverse_encrypt(apps, schema_editor): + """ + 回滚操作:解密已加密的凭证数据 + + :param apps: Django apps registry + :param schema_editor: 数据库 schema 编辑器 + """ + Credential = apps.get_model("space", "Credential") + table_name = Credential._meta.db_table + crypt = BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + print("\n开始回滚:解密凭证数据...") + + decrypted_count = 0 + error_count = 0 + + def is_value_encrypted(raw_value: str) -> bool: + if not isinstance(raw_value, str) or not raw_value: + return False + try: + plain = crypt.decrypt(raw_value) + # 与加密迁移中相同的判定策略 + return crypt.encrypt(plain) == raw_value + except Exception: + return False + + with connection.cursor() as cursor: + cursor.execute(f"SELECT id, name, content FROM {connection.ops.quote_name(table_name)}") + rows = cursor.fetchall() + + for row in rows: + cred_id, cred_name, raw_content = row + try: + content_obj = ( + raw_content if isinstance(raw_content, dict) else json.loads(raw_content) if raw_content else None + ) + except Exception: + content_obj = None + if not content_obj or not isinstance(content_obj, dict): + continue + + try: + updated = False + decrypted_content = {} + for key, value in content_obj.items(): + if value is not None and isinstance(value, str) and is_value_encrypted(value): + decrypted_content[key] = crypt.decrypt(value) + updated = True + else: + decrypted_content[key] = value + + if not updated: + continue + + with connection.cursor() as cursor: + cursor.execute( + f"UPDATE {connection.ops.quote_name(table_name)} SET content=%s WHERE id=%s", + [json.dumps(decrypted_content), cred_id], + ) + decrypted_count += 1 + print(f" ✓ 成功解密凭证: ID={cred_id}, Name={cred_name}") + + except Exception as e: + error_count += 1 + print(f" ✗ 解密失败: ID={cred_id}, Name={cred_name}, Error={str(e)}") + + print(f"\n回滚完成!解密 {decrypted_count} 条,失败 {error_count} 条\n") + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0008_auto_20251014_1511"), + ] + + operations = [ + migrations.RunPython(encrypt_existing_credentials, reverse_encrypt), + ] diff --git a/bkflow/space/models.py b/bkflow/space/models.py index b35b626f09..7cd8184282 100644 --- a/bkflow/space/models.py +++ b/bkflow/space/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -33,7 +32,7 @@ ) from bkflow.space.credential import CredentialDispatcher from bkflow.space.exceptions import SpaceNotExists -from bkflow.utils.models import CommonModel +from bkflow.utils.models import CommonModel, SecretSingleJsonField class SpaceCreateType(Enum): @@ -253,16 +252,27 @@ def get_config(cls, space_id, config_name, *args, **kwargs): class CredentialType(Enum): # 蓝鲸应用凭证 BK_APP = "BK_APP" + # 蓝鲸登录态凭证 + BK_ACCESS_TOKEN = "BK_ACCESS_TOKEN" + # 用户名+密码 + BASIC_AUTH = "BASIC_AUTH" + # 自定义凭证 + CUSTOM = "CUSTOM" class Credential(CommonModel): - CREDENTIAL_CHOICES = [(CredentialType.BK_APP.value, _("蓝鲸应用凭证"))] + CREDENTIAL_CHOICES = [ + (CredentialType.BK_APP.value, _("蓝鲸应用凭证")), + (CredentialType.BK_ACCESS_TOKEN.value, _("蓝鲸登录态凭证")), + (CredentialType.BASIC_AUTH.value, _("用户名+密码")), + (CredentialType.CUSTOM.value, _("自定义")), + ] space_id = models.IntegerField(_("空间ID")) name = models.CharField(_("凭证名"), max_length=32) desc = models.CharField(_("凭证描述"), max_length=128, null=True, blank=True) type = models.CharField(_("凭证类型"), max_length=32, choices=CREDENTIAL_CHOICES) - content = models.JSONField(_("凭证内容"), null=True, blank=True, default=dict) + content = SecretSingleJsonField(_("凭证内容"), null=True, blank=True, default=dict) def display_json(self): credential = CredentialDispatcher(self.type, data=self.content) @@ -284,6 +294,15 @@ def value(self): def create_credential(cls, space_id, name, type, content, creator, desc=None): """ 创建一个凭证 + + :param space_id: 空间ID + :param name: 凭证名称 + :param type: 凭证类型 + :param content: 凭证内容 + :param creator: 创建者 + :param desc: 凭证描述(可选) + + :return: 创建的凭证实例 """ if not Space.exists(space_id): raise SpaceNotExists("space_id: {}".format(space_id)) @@ -302,12 +321,83 @@ def create_credential(cls, space_id, name, type, content, creator, desc=None): return credential def update_credential(self, content): + """ + 更新凭证内容 + + :param content: 新的凭证内容 + """ credential = CredentialDispatcher(self.type, data=content) validate_data = credential.validate_data() - self.data = validate_data + self.content = validate_data self.save() + def get_scopes(self): + """ + 获取凭证的作用域列表 + + :return: 凭证作用域查询集 + """ + return CredentialScope.objects.filter(credential_id=self.id) + + def has_scope(self): + """ + 检查凭证是否设置了作用域 + + :return: 如果设置了作用域返回 True,否则返回 False + """ + return self.get_scopes().exists() + + def can_use_in_scope(self, template_scope_type, template_scope_value): + """ + 检查凭证是否可以在指定作用域中使用 + 如果凭证没有设置作用域,则可以在任何作用域使用 + 如果模板没有作用域(scope_type和scope_value都为空),则可以使用任何凭证 + 否则,凭证的作用域必须匹配模板的作用域 + + :param self: 凭证实例 + :param template_scope_type: 作用域类型 + :param template_scope_value: 作用域值 + :return: 如果可以使用返回 True,否则返回 False + """ + if not self.has_scope(): + # 凭证没有设置作用域,不允许被使用 + return False + + if not template_scope_type and not template_scope_value: + # 模板没有作用域,可以使用任何凭证 + return True + + # 检查是否有匹配的作用域 + return ( + self.get_scopes() + .filter( + scope_type=template_scope_type, + scope_value=template_scope_value, + ) + .exists() + ) + class Meta: verbose_name = _("空间凭证") verbose_name_plural = _("空间凭证表") unique_together = ("space_id", "name") + + +class CredentialScope(models.Model): + """ + 凭证作用域 + 用于控制凭证的使用范围 + 未关联任何作用域的凭证不受作用域限制,可以在任何地方使用 + """ + + id = models.AutoField(primary_key=True) + credential_id = models.IntegerField(_("凭证ID"), db_index=True) + scope_type = models.CharField(_("作用域类型"), max_length=128, null=True, blank=True) + scope_value = models.CharField(_("作用域值"), max_length=128, null=True, blank=True) + + class Meta: + verbose_name = _("凭证作用域") + verbose_name_plural = _("凭证作用域表") + indexes = [ + models.Index(fields=["credential_id", "scope_type", "scope_value"]), + ] diff --git a/bkflow/space/serializers.py b/bkflow/space/serializers.py index d0f3bba214..ef714da65c 100644 --- a/bkflow/space/serializers.py +++ b/bkflow/space/serializers.py @@ -23,8 +23,7 @@ from bkflow.exceptions import ValidationError from bkflow.space.configs import SpaceConfigHandler, SpaceConfigValueType -from bkflow.space.credential import CredentialDispatcher -from bkflow.space.models import Credential, Space, SpaceConfig +from bkflow.space.models import Space, SpaceConfig logger = logging.getLogger(__name__) @@ -74,23 +73,3 @@ def validate_configs(self, configs): logger.exception(f"[validate_configs] error: {e}") raise serializers.ValidationError(e.message) return configs - - -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() - data["data"] = credential.display_value() - else: - data["data"] = {} - - return data - - class Meta: - model = Credential - fields = "__all__" diff --git a/bkflow/space/views.py b/bkflow/space/views.py index f6262f3fe8..d44fe53dd5 100644 --- a/bkflow/space/views.py +++ b/bkflow/space/views.py @@ -21,7 +21,7 @@ import django_filters from blueapps.account.decorators import login_exempt from django.conf import settings -from django.db import DatabaseError +from django.db import DatabaseError, transaction from django.db.models import Q from django.utils.decorators import method_decorator from django_filters.rest_framework import DjangoFilterBackend, FilterSet @@ -36,6 +36,9 @@ from bkflow.apigw.serializers.credential import ( CreateCredentialSerializer, + CredentialScopesChangeSerializer, + CredentialScopeSerializer, + CredentialSerializer, UpdateCredentialSerializer, ) from bkflow.apigw.serializers.space import CreateSpaceSerializer @@ -46,9 +49,11 @@ SpaceConfigHandler, SuperusersConfig, ) +from bkflow.space.credential.scope_validator import filter_credentials_by_scope from bkflow.space.exceptions import SpaceConfigDefaultValueNotExists from bkflow.space.models import ( Credential, + CredentialScope, CredentialType, Space, SpaceConfig, @@ -61,7 +66,6 @@ ) from bkflow.space.serializers import ( CredentialBaseQuerySerializer, - CredentialSerializer, SpaceConfigBaseQuerySerializer, SpaceConfigBatchApplySerializer, SpaceConfigSerializer, @@ -70,6 +74,7 @@ from bkflow.utils.api_client import ApiGwClient, HttpRequestResult from bkflow.utils.mixins import BKFLOWDefaultPagination from bkflow.utils.permissions import AdminPermission, AppInternalPermission +from bkflow.utils.serializer import params_valid from bkflow.utils.views import AdminModelViewSet, SimpleGenericViewSet logger = logging.getLogger("root") @@ -89,6 +94,20 @@ class CredentialViewSet(AdminModelViewSet): filter_backends = [DjangoFilterBackend] filter_class = CredentialFilterSet + def get_queryset(self): + """根据作用域过滤凭证""" + queryset = super().get_queryset() + + # 从查询参数获取作用域信息 + scope_type = self.request.query_params.get("scope_type") + scope_value = self.request.query_params.get("scope_value") + + # 如果提供了作用域信息,过滤凭证 + if scope_type or scope_value: + queryset = filter_credentials_by_scope(queryset, scope_type, scope_value) + + return queryset + @action(detail=False, methods=["GET"]) def get_api_gateway_credential(self, request, *args, **kwargs): space_id = request.query_params.get("space_id") @@ -352,15 +371,31 @@ def create(self, request, *args, **kwargs): serializer = CredentialBaseQuerySerializer(data=self.request.query_params) serializer.is_valid(raise_exception=True) space_id = serializer.validated_data.get("space_id") + try: - credential = Credential.create_credential( - space_id=space_id, - name=credential_data["name"], - type=credential_data["type"], - content=credential_data["content"], - creator=request.user.username, - desc=credential_data.get("desc"), - ) + with transaction.atomic(): + credential = Credential.create_credential( + space_id=space_id, + name=credential_data["name"], + type=credential_data["type"], + content=credential_data["content"], + creator=request.user.username, + desc=credential_data.get("desc"), + ) + + # 创建凭证作用域 + scopes = credential_data.get("scopes", []) + if 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) + except DatabaseError as e: err_msg = f"创建凭证失败 {str(e)}" logger.error(err_msg) @@ -379,13 +414,33 @@ def partial_update(self, request, *args, **kwargs): serializer = UpdateCredentialSerializer(data=request.data) serializer.is_valid(raise_exception=True) - for attr, value in serializer.validated_data.items(): - setattr(instance, attr, value) - - instance.updated_by = request.user.username - updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] try: - instance.save(update_fields=updated_keys) + with transaction.atomic(): + # 更新凭证基本信息 + scopes_data = serializer.validated_data.pop("scopes", None) + for attr, value in serializer.validated_data.items(): + setattr(instance, attr, value) + + instance.updated_by = request.user.username + updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] + instance.save(update_fields=updated_keys) + + # 更新凭证作用域 + if scopes_data is not None: + # 删除旧的作用域 + CredentialScope.objects.filter(credential_id=instance.id).delete() + # 创建新的作用域 + if scopes_data: + scope_objects = [ + CredentialScope( + credential_id=instance.id, + scope_type=scope.get("scope_type"), + scope_value=scope.get("scope_value"), + ) + for scope in scopes_data + ] + CredentialScope.objects.bulk_create(scope_objects) + except DatabaseError as e: err_msg = f"更新凭证失败 {str(e)}" logger.error(err_msg) @@ -394,6 +449,49 @@ def partial_update(self, request, *args, **kwargs): response_serializer = CredentialSerializer(instance) return Response(response_serializer.data, status=status.HTTP_200_OK) + @action(detail=True, methods=["put", "patch"]) + @params_valid(CredentialScopesChangeSerializer) + def update_scopes(self, request, pk=None, params=None): + """更新凭证作用域""" + try: + instance = self.get_object() + except Credential.DoesNotExist as e: + err_msg = f"更新凭证不存在 {str(e)}" + logger.error(err_msg) + return Response(err_msg, status=404) + + # 验证scopes数据 + params = params or {} + if params.get("unlimited"): + scopes_data = [{"scope_type": None, "scope_value": None}] + else: + scopes_data = params.get("scopes", []) + + try: + with transaction.atomic(): + # 删除旧的作用域 + CredentialScope.objects.filter(credential_id=instance.id).delete() + # 创建新的作用域 + scope_objects = [ + CredentialScope( + credential_id=instance.id, + scope_type=scope.get("scope_type"), + scope_value=scope.get("scope_value"), + ) + for scope in scopes_data + ] + CredentialScope.objects.bulk_create(scope_objects) + except DatabaseError as e: + err_msg = f"更新凭证作用域失败 {str(e)}" + logger.error(err_msg) + return Response(exception=True, data={"detail": err_msg}) + + # 返回更新后的凭证信息 + credential_scopes = [] + for scope_object in scope_objects: + credential_scopes.append(CredentialScopeSerializer(scope_object).data) + return Response(credential_scopes, status=status.HTTP_200_OK) + def destroy(self, request, *args, **kwargs): try: instance = self.get_object() diff --git a/bkflow/task/serializers.py b/bkflow/task/serializers.py index ea72778e5d..e5e7f9c05b 100644 --- a/bkflow/task/serializers.py +++ b/bkflow/task/serializers.py @@ -78,8 +78,10 @@ def validate(self, value): raise serializers.ValidationError(str(e)) constants = value.pop("constants", {}) + credentials = value.pop("credentials", {}) pipeline_tree = value.get("pipeline_tree") try: + # 处理constants for key, c_value in constants.items(): if key not in pipeline_tree.get("constants", {}): continue @@ -87,6 +89,12 @@ def validate(self, value): meta = copy.deepcopy(pipeline_tree["constants"][key]) pipeline_tree["constants"][key]["meta"] = meta pipeline_tree["constants"][key]["value"] = c_value + + # 处理credentials - 如果有credentials参数但pipeline_tree中还没有credentials字段,添加它 + # 注意:在apigw视图中已经解析了credentials,这里只是确保它们被保留 + if credentials and "credentials" not in pipeline_tree: + pipeline_tree["credentials"] = credentials + standardize_pipeline_node_name(pipeline_tree) validate_web_pipeline_tree(pipeline_tree) except PipelineException as e: diff --git a/bkflow/utils/crypt.py b/bkflow/utils/crypt.py new file mode 100644 index 0000000000..c27a2a558a --- /dev/null +++ b/bkflow/utils/crypt.py @@ -0,0 +1,63 @@ +""" +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 base64 + +from Crypto.Cipher import AES +from django.conf import settings + + +class BaseCrypt: + _bk_crypt = False + + # KEY 和 IV 的长度需等于16 + ROOT_KEY = b"TencentBkApp-Key" + ROOT_IV = b"TencentBkApp--Iv" + encoding = None + + def __init__(self, instance_key=settings.SECRET_KEY, encoding="utf-8"): + self.INSTANCE_KEY = instance_key + self.encoding = encoding + + def encrypt(self, plaintext): + """ + 加密 + :param plaintext: 需要加密的内容 + :return: + """ + decrypt_key = self.__parse_key() + if isinstance(plaintext, str): + plaintext = plaintext.encode(encoding=self.encoding) + secret_txt = AES.new(decrypt_key, AES.MODE_CFB, self.ROOT_IV).encrypt(plaintext) + return base64.b64encode(secret_txt).decode("utf-8") + + def decrypt(self, ciphertext): + """ + 解密 + :param ciphertext: 需要解密的内容 + :return: + """ + decrypt_key = self.__parse_key() + # 先解base64 + secret_txt = base64.b64decode(ciphertext) + # 再解对称加密 + plain = AES.new(decrypt_key, AES.MODE_CFB, self.ROOT_IV).decrypt(secret_txt) + return plain.decode(encoding=self.encoding) + + def __parse_key(self): + return self.INSTANCE_KEY[:24].encode() diff --git a/bkflow/utils/models.py b/bkflow/utils/models.py index 1c0873c68a..50e02691a4 100644 --- a/bkflow/utils/models.py +++ b/bkflow/utils/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -20,10 +19,12 @@ import hashlib import json +from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from pipeline.models import CompressJSONField, SnapshotManager +from bkflow.utils.crypt import BaseCrypt from bkflow.utils.md5 import compute_pipeline_md5 @@ -44,7 +45,7 @@ def delete(self, using=None, keep_parents=False): self.save() def hard_delete(self): - super(CommonModel, self).delete() + super().delete() class CommonSnapshotManager(SnapshotManager): @@ -82,3 +83,129 @@ def has_change(self, data): """ md5 = compute_pipeline_md5(data) return md5, self.md5sum != md5 + + +class BaseSecretField: + """ + Secret字段:入库加密, 出库解密 + """ + + _crypt = BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + def from_db_value(self, value, expression, connection): + if not value: + return None + return self._crypt.decrypt(value) + + def to_python(self, value): + if value is None: + return value + return self._crypt.encrypt(value) + + def get_prep_value(self, value): + if value is None: + return value + return self._crypt.encrypt(value) + + +class SecretField(BaseSecretField, models.CharField): + """ + Secret字段:入库加密, 出库解密 + """ + + def from_db_value(self, value, expression, connection): + return super().from_db_value(value, expression, connection) + + def to_python(self, value): + return super().to_python(value) + + def get_prep_value(self, value): + return super().get_prep_value(value) + + +class SecretTextField(BaseSecretField, models.TextField): + """ + Secret字段:入库加密, 出库解密 + """ + + def from_db_value(self, value, expression, connection): + return super().from_db_value(value, expression, connection) + + def to_python(self, value): + return super().to_python(value) + + def get_prep_value(self, value): + return super().get_prep_value(value) + + +class SecretSingleJsonField(models.JSONField): + """ + Secret JSON 字段:只支持单层 JSON 结构,对每个 key 的 value 进行加密/解密 + + 示例: + {"username": "admin", "password": "secret123"} + 存储时:{"username": "encrypted_admin", "password": "encrypted_secret123"} + """ + + _crypt = BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + def from_db_value(self, value, expression, connection): + """ + 从数据库读取时,解密 JSON 中所有 value + + :param value: 数据库中的加密 JSON 数据 + :param expression: 查询表达式 + :param connection: 数据库连接 + :return: 解密后的 JSON 数据 + """ + if not value: + return value + + # 先调用父类方法获取 JSON 对象 + value = super().from_db_value(value, expression, connection) + + if not isinstance(value, dict): + return value + + # 解密每个 value + decrypted_data = {} + for key, val in value.items(): + if val is not None: + try: + decrypted_data[key] = self._crypt.decrypt(val) + except Exception: + # 如果解密失败,返回原值(可能是未加密的旧数据) + decrypted_data[key] = val + else: + decrypted_data[key] = val + + return decrypted_data + + def get_prep_value(self, value): + """ + 写入数据库时,加密 JSON 中所有 value + + :param value: 原始 JSON 数据 + :return: 加密后的 JSON 数据 + """ + if value is None: + return value + + if not isinstance(value, dict): + raise ValueError("SecretSingleJsonField 只支持字典类型的数据") + + # 检查是否为单层 JSON + for key, val in value.items(): + if isinstance(val, (dict, list)): + raise ValueError("SecretSingleJsonField 只支持单层 JSON 结构,不支持嵌套对象或数组") + + # 加密每个 value + encrypted_data = {} + for key, val in value.items(): + if val is not None and isinstance(val, str): + encrypted_data[key] = self._crypt.encrypt(val) + else: + encrypted_data[key] = val + + # 调用父类方法处理 JSON 序列化 + return super().get_prep_value(encrypted_data) diff --git a/bkflow/utils/serializer.py b/bkflow/utils/serializer.py new file mode 100644 index 0000000000..3d405cbf3f --- /dev/null +++ b/bkflow/utils/serializer.py @@ -0,0 +1,169 @@ +""" +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 functools import wraps + +from django.core.handlers.wsgi import WSGIRequest +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from rest_framework.request import Request + +from bkflow.exceptions import ValidationError + + +def params_valid(serializer: serializers.Serializer, add_params: bool = True): + """参数校验装饰器 + + :param serializer: serializer类 + :param add_params: 是否将校验后的参数添加到request.cleaned_params中 + :return: 参数校验装饰器 + """ + + def decorator(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + # 获得Django的request对象 + _request = kwargs.get("request") + + if not _request: + for arg in args: + if isinstance(arg, (Request, WSGIRequest)): + _request = arg + break + + if not _request: + raise ValidationError(_("该装饰器只允许用于Django的View函数(包括普通View函数和Class-base的View函数)")) + + # 校验request中的参数 + params = {} + if _request.method in ["GET"]: + if isinstance(_request, Request): + params = _request.query_params + else: + params = _request.GET + elif _request.is_ajax(): + if isinstance(_request, Request): + params = _request.data + else: + params = _request.json() + else: + if isinstance(_request, Request): + params = _request.data + else: + params = _request.POST + + cleaned_params = custom_params_valid(serializer=serializer, params=params) + _request.cleaned_params = cleaned_params + + # 执行实际的View逻辑 + params_add = False + try: + # 语法糖,使用这个decorator的话可直接从view中获得参数的字典 + if "params" not in kwargs and add_params: + kwargs["params"] = cleaned_params + params_add = True + except TypeError: + if params_add: + del kwargs["params"] + resp = view_func(*args, **kwargs) + return resp + + return wrapper + + return decorator + + +def format_serializer_errors(errors: dict, fields: dict, params: dict, prefix: str = " "): + """格式化序列化器错误信息 + + :param errors: 错误信息 + :param fields: 序列化器字段 + :param params: 参数 + :return: 格式化后的错误信息 + """ + message = _("参数校验失败:{wrap}").format(wrap="\n") if prefix == " " else "\n" + for key, field_errors in list(errors.items()): + sub_message = "" + label = key + if key not in fields: + sub_message = json.dumps(field_errors, ensure_ascii=False) + else: + field = fields[key] + label = field.label or field.field_name + if ( + hasattr(field, "child") + and isinstance(field_errors, list) + and len(field_errors) > 0 + and not isinstance(field_errors[0], str) + ): + for index, sub_errors in enumerate(field_errors): + if sub_errors: + sub_format = format_serializer_errors( + sub_errors, field.child.fields, params, prefix=prefix + " " + ) + # return sub_format + sub_message += _("{wrap}{prefix}第{index}项:").format( + wrap="\n", + prefix=prefix + " ", + index=index + 1, + ) + sub_message += sub_format + else: + if isinstance(field_errors, dict): + if hasattr(field, "child"): + sub_foramt = format_serializer_errors( + field_errors, field.child.fields, params, prefix=prefix + " " + ) + else: + sub_foramt = format_serializer_errors(field_errors, field.fields, params, prefix=prefix + " ") + sub_message += sub_foramt + elif isinstance(field_errors, list): + for index in range(len(field_errors)): + field_errors[index] = field_errors[index].format(**{key: params.get(key, "")}) + sub_message += "{index}.{error}".format(index=index + 1, error=field_errors[index]) + sub_message += "\n" + # 对使用 Validate() 时 label == 'non_field_errors' 的特殊情况做处理 + if label == "non_field_errors": + message += "{prefix} {message}".format(prefix=prefix, message=sub_message) + else: + message += "{prefix}{label}: {message}".format(prefix=prefix, label=label, message=sub_message) + return message + + +def custom_params_valid(serializer: serializers.Serializer, params: dict, many: bool = False): + """校验参数 + + :param serializer: 校验器 + :param params: 原始参数 + :param many: 是否为多个参数 + :return: 校验通过的参数 + """ + _serializer = serializer(data=params, many=many) + try: + _serializer.is_valid(raise_exception=True) + except serializers.ValidationError as e: # pylint: disable=broad-except # noqa + try: + message = format_serializer_errors(_serializer.errors, _serializer.fields, params) + except Exception as e: # pylint: disable=broad-except + if isinstance(e.message, str): + message = e.message + else: + message = _("参数校验失败,详情请查看返回的errors") + raise ValidationError(message=message, errors=_serializer.errors) + return list(_serializer.validated_data) if many else dict(_serializer.validated_data) diff --git a/config/default.py b/config/default.py index 4373b3e1d4..d4e6f2e9aa 100644 --- a/config/default.py +++ b/config/default.py @@ -417,6 +417,9 @@ def handler_filter_injection(filters: list): # 允许 static、openapi 路径跨域访问 CORS_URLS_REGEX = r"^/(static\/components|openapi)/.*$" +# 加密字段配置 +PRIVATE_SECRET = env.PRIVATE_SECRET or SECRET_KEY + """ 以下为框架代码 请勿修改 """ diff --git a/env.py b/env.py index fde9f5dea1..b2fab36ee7 100644 --- a/env.py +++ b/env.py @@ -179,5 +179,9 @@ PIPELINE_RERUN_MAX_TIMES = int(os.getenv("PIPELINE_RERUN_MAX_TIMES", 100)) BAMBOO_DJANGO_ERI_NODE_RERUN_LIMIT = int(os.getenv("BAMBOO_DJANGO_ERI_NODE_RERUN_LIMIT", 100)) +# 模板最大递归次数 TEMPLATE_MAX_RECURSIVE_NUMBER = int(os.getenv("TEMPLATE_MAX_RECURSIVE_NUMBER", 10)) REQUEST_RETRY_NUMBER = int(os.getenv("REQUEST_RETRY_NUMBER", 3)) + +# 加密字段配置 +PRIVATE_SECRET = os.getenv("PRIVATE_SECRET", "") diff --git a/frontend/src/config/i18n/cn.js b/frontend/src/config/i18n/cn.js index 1578a507a9..0eb70a6e99 100644 --- a/frontend/src/config/i18n/cn.js +++ b/frontend/src/config/i18n/cn.js @@ -926,6 +926,10 @@ const cn = { 凭证管理: '凭证管理', 内容: '内容', '凭证删除后不可恢复,确认删除?': '凭证删除后不可恢复,确认删除?', + 蓝鲸应用凭证: '蓝鲸应用凭证', + '蓝鲸 Access Token 凭证': '蓝鲸 Access Token 凭证', + 'Basic Auth': 'Basic Auth', + 自定义: '自定义', 值类型: '值类型', 表单模式: '表单模式', json模式: 'json模式', diff --git a/frontend/src/config/i18n/en.js b/frontend/src/config/i18n/en.js index 9ba229c05f..5a4a7b9d4d 100644 --- a/frontend/src/config/i18n/en.js +++ b/frontend/src/config/i18n/en.js @@ -926,6 +926,10 @@ const en = { 凭证管理: 'Credential Management', 内容: 'Content', '凭证删除后不可恢复,确认删除?': 'The voucher cannot be recovered once deleted. Are you sure you want to delete it?', + 蓝鲸应用凭证: 'BK App Credential', + '蓝鲸 Access Token 凭证': 'BK Access Token', + 'Basic Auth': 'Basic Auth', + 自定义: 'Custom', 值类型: 'Value Type', 表单模式: 'Form Mode', json模式: 'JSON Mode', diff --git a/tests/interface.env b/tests/interface.env index f9d03d3394..341cfe6a41 100644 --- a/tests/interface.env +++ b/tests/interface.env @@ -17,4 +17,5 @@ SKIP_APIGW_CHECK=1 APP_INTERNAL_TOKEN=123456 ENABLE_BK_PLUGIN_AUTHORIZATION=1 +PRIVATE_SECRET=test_secret_key_32_bytes_long! diff --git a/tests/interface/credential/__init__.py b/tests/interface/credential/__init__.py new file mode 100644 index 0000000000..732d2b8d52 --- /dev/null +++ b/tests/interface/credential/__init__.py @@ -0,0 +1,18 @@ +""" +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. +""" diff --git a/tests/interface/credential/test_credential.py b/tests/interface/credential/test_credential.py new file mode 100644 index 0000000000..c217056e50 --- /dev/null +++ b/tests/interface/credential/test_credential.py @@ -0,0 +1,218 @@ +""" +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 pytest +from rest_framework import serializers + +from bkflow.space.credential.basic_auth import BasicAuthCredential +from bkflow.space.credential.bk_access_token import BkAccessTokenCredential +from bkflow.space.credential.custom import CustomCredential +from bkflow.space.credential.dispatcher import CredentialDispatcher +from bkflow.space.exceptions import CredentialTypeNotSupport + + +class TestBkAccessTokenCredential: + """测试蓝鲸登录态凭证""" + + def test_validate_valid_data(self): + """测试验证有效数据""" + data = {"access_token": "valid_token_123"} + credential = BkAccessTokenCredential(data=data) + validated = credential.validate_data() + assert validated == data + + def test_validate_invalid_data_missing_field(self): + """测试验证缺少必填字段""" + data = {} + credential = BkAccessTokenCredential(data=data) + with pytest.raises(serializers.ValidationError): + credential.validate_data() + + def test_validate_invalid_data_all_asterisks(self): + """测试验证全为星号的 token""" + data = {"access_token": "*********"} + credential = BkAccessTokenCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "不应全为 * 字符" in str(exc_info.value) + + def test_value_returns_original_data(self): + """测试返回原始值""" + data = {"access_token": "test_token"} + credential = BkAccessTokenCredential(data=data) + assert credential.value() == data + + def test_display_value_masks_token(self): + """测试脱敏显示""" + data = {"access_token": "secret_token"} + credential = BkAccessTokenCredential(data=data) + display = credential.display_value() + assert display["access_token"] == "*********" + + +class TestBasicAuthCredential: + """测试用户名+密码凭证""" + + def test_validate_valid_data(self): + """测试验证有效数据""" + data = {"username": "admin", "password": "secret123"} + credential = BasicAuthCredential(data=data) + validated = credential.validate_data() + assert validated == data + + def test_validate_invalid_data_missing_username(self): + """测试验证缺少用户名""" + data = {"password": "secret123"} + credential = BasicAuthCredential(data=data) + with pytest.raises(serializers.ValidationError): + credential.validate_data() + + def test_validate_invalid_data_missing_password(self): + """测试验证缺少密码""" + data = {"username": "admin"} + credential = BasicAuthCredential(data=data) + with pytest.raises(serializers.ValidationError): + credential.validate_data() + + def test_validate_invalid_password_all_asterisks(self): + """测试验证全为星号的密码""" + data = {"username": "admin", "password": "***"} + credential = BasicAuthCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "不应全为 * 字符" in str(exc_info.value) + + def test_display_value_masks_password(self): + """测试脱敏显示""" + data = {"username": "admin", "password": "secret123"} + credential = BasicAuthCredential(data=data) + display = credential.display_value() + assert display["username"] == "admin" + assert display["password"] == "*********" + + +class TestCustomCredential: + """测试自定义凭证""" + + def test_validate_valid_data(self): + """测试验证有效数据""" + data = {"api_key": "key123", "api_secret": "secret456"} + credential = CustomCredential(data=data) + validated = credential.validate_data() + assert validated == data + + def test_validate_empty_data(self): + """测试验证空数据""" + data = {} + credential = CustomCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "不能为空" in str(exc_info.value) + + def test_validate_non_dict_data(self): + """测试验证非字典类型数据""" + data = "not_a_dict" + credential = CustomCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "必须是字典类型" in str(exc_info.value) + + def test_validate_nested_dict(self): + """测试验证嵌套字典(不支持)""" + data = {"key1": {"nested": "value"}} + credential = CustomCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "必须是字符串类型" in str(exc_info.value) + + def test_validate_array_value(self): + """测试验证数组值(不支持)""" + data = {"key1": ["item1", "item2"]} + credential = CustomCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "必须是字符串类型" in str(exc_info.value) + + def test_validate_non_string_key(self): + """测试验证非字符串 key""" + # 注意:在 Python 中,dict key 通常会被转换为字符串 + # 这个测试主要验证类型检查逻辑 + data = {"key1": "value1"} + credential = CustomCredential(data=data) + validated = credential.validate_data() + assert validated == data + + def test_validate_value_all_asterisks(self): + """测试验证全为星号的值""" + data = {"key1": "***"} + credential = CustomCredential(data=data) + with pytest.raises(serializers.ValidationError) as exc_info: + credential.validate_data() + assert "不应全为 * 字符" in str(exc_info.value) + + def test_display_value_masks_all_values(self): + """测试脱敏显示(所有值都脱敏)""" + data = {"key1": "value1", "key2": "value2"} + credential = CustomCredential(data=data) + display = credential.display_value() + assert display["key1"] == "*********" + assert display["key2"] == "*********" + + +class TestCredentialDispatcher: + """测试凭证分发器""" + + def test_get_bk_app_credential(self): + """测试获取蓝鲸应用凭证""" + data = {"bk_app_code": "app", "bk_app_secret": "secret"} + credential = CredentialDispatcher("BK_APP", data=data) + assert credential is not None + assert credential.validate_data() == data + + def test_get_bk_access_token_credential(self): + """测试获取蓝鲸登录态凭证""" + data = {"access_token": "token123"} + credential = CredentialDispatcher("BK_ACCESS_TOKEN", data=data) + assert credential is not None + assert credential.validate_data() == data + + def test_get_basic_auth_credential(self): + """测试获取用户名+密码凭证""" + data = {"username": "admin", "password": "secret"} + credential = CredentialDispatcher("BASIC_AUTH", data=data) + assert credential is not None + assert credential.validate_data() == data + + def test_get_custom_credential(self): + """测试获取自定义凭证""" + data = {"key1": "value1"} + credential = CredentialDispatcher("CUSTOM", data=data) + assert credential is not None + assert credential.validate_data() == data + + def test_unsupported_credential_type(self): + """测试不支持的凭证类型""" + with pytest.raises(CredentialTypeNotSupport): + CredentialDispatcher("UNKNOWN_TYPE", data={}) + + def test_lowercase_type_not_supported(self): + """测试小写凭证类型不支持(必须大写)""" + data = {"access_token": "token"} + # 小写类型应该抛出异常 + with pytest.raises(CredentialTypeNotSupport): + CredentialDispatcher("bk_access_token", data=data) diff --git a/tests/interface/credential/test_credential_model.py b/tests/interface/credential/test_credential_model.py new file mode 100644 index 0000000000..735070d7dc --- /dev/null +++ b/tests/interface/credential/test_credential_model.py @@ -0,0 +1,253 @@ +""" +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 pytest +from django.db import IntegrityError + +from bkflow.space.models import Credential, CredentialScope, CredentialType, Space + + +@pytest.mark.django_db +class TestCredentialModel: + """测试凭证模型""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def test_credential(self, test_space): + """创建测试凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="test_credential", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app", "bk_app_secret": "secret"}, + creator="test_user", + desc="Test credential", + ) + yield credential + credential.hard_delete() + + def test_create_credential(self, test_space): + """测试创建凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="new_credential", + type=CredentialType.BK_ACCESS_TOKEN.value, + content={"access_token": "token123"}, + creator="test_user", + ) + + assert credential.id is not None + assert credential.space_id == test_space.id + assert credential.name == "new_credential" + assert credential.type == CredentialType.BK_ACCESS_TOKEN.value + assert credential.creator == "test_user" + + # 清理 + credential.hard_delete() + + def test_create_duplicate_credential_name(self, test_space, test_credential): + """测试创建重复名称的凭证""" + from django.db import transaction + + with pytest.raises(IntegrityError): + with transaction.atomic(): + Credential.create_credential( + space_id=test_space.id, + name="test_credential", # 与 fixture 中的名称重复 + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app2", "bk_app_secret": "secret2"}, + creator="test_user", + ) + + def test_update_credential(self, test_credential): + """测试更新凭证内容""" + new_content = {"bk_app_code": "new_app", "bk_app_secret": "new_secret"} + test_credential.update_credential(new_content) + + # 重新从数据库读取 + credential = Credential.objects.get(id=test_credential.id) + assert credential.content == new_content + + def test_credential_value_property(self, test_credential): + """测试凭证 value 属性""" + value = test_credential.value + assert isinstance(value, dict) + assert "bk_app_code" in value + assert "bk_app_secret" in value + + def test_credential_display_json(self, test_credential): + """测试凭证 display_json 方法""" + display = test_credential.display_json() + assert display["id"] == test_credential.id + assert display["space_id"] == test_credential.space_id + assert display["type"] == test_credential.type + assert "content" in display + # bk_app_secret 应该被脱敏 + assert display["content"]["bk_app_secret"] == "*********" + + def test_soft_delete(self, test_credential): + """测试软删除""" + credential_id = test_credential.id + test_credential.delete() + + # 验证软删除 + assert test_credential.is_deleted is True + + # 验证在查询中被过滤 + with pytest.raises(Credential.DoesNotExist): + Credential.objects.get(id=credential_id, is_deleted=False) + + # 但仍然存在于数据库 + credential = Credential.objects.get(id=credential_id) + assert credential.is_deleted is True + + def test_hard_delete(self, test_space): + """测试硬删除""" + credential = Credential.create_credential( + space_id=test_space.id, + name="to_be_deleted", + type=CredentialType.CUSTOM.value, + content={"key": "value"}, + creator="test_user", + ) + credential_id = credential.id + + credential.hard_delete() + + # 验证已从数据库删除 + with pytest.raises(Credential.DoesNotExist): + Credential.objects.get(id=credential_id) + + +@pytest.mark.django_db +class TestCredentialScope: + """测试凭证作用域""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def test_credential(self, test_space): + """创建测试凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="scoped_credential", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app", "bk_app_secret": "secret"}, + creator="test_user", + ) + yield credential + credential.hard_delete() + + def test_create_credential_scope(self, test_credential): + """测试创建凭证作用域""" + scope = CredentialScope.objects.create( + credential_id=test_credential.id, scope_type="project", scope_value="project_1" + ) + + assert scope.id is not None + assert scope.credential_id == test_credential.id + assert scope.scope_type == "project" + assert scope.scope_value == "project_1" + + # 清理 + scope.delete() + + def test_get_scopes(self, test_credential): + """测试获取凭证的作用域列表""" + # 创建多个作用域 + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_2") + + scopes = test_credential.get_scopes() + assert scopes.count() == 2 + + # 清理 + scopes.delete() + + def test_has_scope(self, test_credential): + """测试检查凭证是否设置了作用域""" + # 初始状态:没有作用域 + assert test_credential.has_scope() is False + + # 添加作用域 + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + + # 现在应该有作用域 + assert test_credential.has_scope() is True + + # 清理 + test_credential.get_scopes().delete() + + def test_can_use_in_scope_without_scope(self, test_credential): + """测试没有作用域限制的凭证不能被使用""" + # 凭证没有设置作用域,不允许被使用 + assert test_credential.can_use_in_scope("project", "project_1") is False + assert test_credential.can_use_in_scope(None, None) is False + + def test_can_use_in_scope_with_matching_scope(self, test_credential): + """测试匹配作用域的凭证可以使用""" + # 添加作用域 + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + + # 匹配的作用域应该可以使用 + assert test_credential.can_use_in_scope("project", "project_1") is True + + # 不匹配的作用域不能使用 + assert test_credential.can_use_in_scope("project", "project_2") is False + assert test_credential.can_use_in_scope("template", "template_1") is False + + # 清理 + test_credential.get_scopes().delete() + + def test_can_use_in_scope_with_template_no_scope(self, test_credential): + """测试模板没有作用域时,有作用域的凭证也可以使用""" + # 添加作用域 + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + + # 模板没有作用域(都为 None),有作用域的凭证也可以使用 + assert test_credential.can_use_in_scope(None, None) is True + + # 清理 + test_credential.get_scopes().delete() + + def test_multiple_scopes(self, test_credential): + """测试多个作用域""" + # 添加多个作用域 + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_2") + + # 两个作用域都应该可以使用 + assert test_credential.can_use_in_scope("project", "project_1") is True + assert test_credential.can_use_in_scope("project", "project_2") is True + + # 其他作用域不能使用 + assert test_credential.can_use_in_scope("project", "project_3") is False + + # 清理 + test_credential.get_scopes().delete() diff --git a/tests/interface/credential/test_credential_resolver.py b/tests/interface/credential/test_credential_resolver.py new file mode 100644 index 0000000000..50be60bbeb --- /dev/null +++ b/tests/interface/credential/test_credential_resolver.py @@ -0,0 +1,176 @@ +""" +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 pytest + +from bkflow.space.credential.resolver import resolve_credentials +from bkflow.space.exceptions import ( + CredentialNotFoundError, + CredentialScopeValidationError, +) +from bkflow.space.models import Credential, CredentialScope, CredentialType, Space + + +@pytest.mark.django_db +class TestResolveCredentials: + """测试凭证解析器""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def test_credential(self, test_space): + """创建测试凭证(有作用域)""" + credential = Credential.create_credential( + space_id=test_space.id, + name="test_credential", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app", "bk_app_secret": "secret"}, + creator="test_user", + ) + # 添加默认作用域 + CredentialScope.objects.create(credential_id=credential.id, scope_type="test", scope_value="test_1") + yield credential + credential.hard_delete() + + @pytest.fixture + def scoped_credential(self, test_space): + """创建有作用域的凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="scoped_credential", + type=CredentialType.BASIC_AUTH.value, + content={"username": "admin", "password": "secret"}, + creator="test_user", + ) + CredentialScope.objects.create(credential_id=credential.id, scope_type="project", scope_value="project_1") + yield credential + credential.hard_delete() + + def test_resolve_empty_credentials(self, test_space): + """测试解析空凭证字典""" + result = resolve_credentials({}, test_space.id, None, None) + assert result == {} + + def test_resolve_none_credentials(self, test_space): + """测试解析 None""" + result = resolve_credentials(None, test_space.id, None, None) + assert result == {} + + def test_resolve_credential_by_id(self, test_space, test_credential): + """测试通过 ID 解析凭证""" + credentials_dict = { + "${token1}": { + "desc": "测试凭证", + "index": 1, + "key": "${token1}", + "name": "凭证1", + "show_type": "show", + "value": str(test_credential.id), + "version": "legacy", + } + } + + result = resolve_credentials(credentials_dict, test_space.id, "test", "test_1") # 匹配fixture中的作用域 + + assert "${token1}" in result + assert result["${token1}"]["credential_id"] == test_credential.id + assert result["${token1}"]["credential_name"] == "test_credential" + assert result["${token1}"]["credential_type"] == CredentialType.BK_APP.value + # value 应该是解密后的实际内容 + assert isinstance(result["${token1}"]["value"], dict) + + def test_resolve_credential_by_int_id(self, test_space, test_credential): + """测试通过整数 ID 解析凭证""" + credentials_dict = {"${token1}": {"value": test_credential.id, "name": "凭证1"}} # 整数类型 + + result = resolve_credentials(credentials_dict, test_space.id, "test", "test_1") # 匹配fixture中的作用域 + + assert "${token1}" in result + assert result["${token1}"]["credential_id"] == test_credential.id + + def test_resolve_non_existent_credential(self, test_space): + """测试解析不存在的凭证""" + credentials_dict = {"${token1}": {"value": "99999", "name": "不存在的凭证"}} # 不存在的 ID + + with pytest.raises(CredentialNotFoundError) as exc_info: + resolve_credentials(credentials_dict, test_space.id, None, None) + assert "不存在" in str(exc_info.value) + + def test_resolve_credential_with_scope_validation_pass(self, test_space, scoped_credential): + """测试解析凭证时作用域验证通过""" + credentials_dict = {"${token1}": {"value": str(scoped_credential.id), "name": "作用域凭证"}} + + # 匹配的作用域应该通过 + result = resolve_credentials(credentials_dict, test_space.id, "project", "project_1") + + assert "${token1}" in result + assert result["${token1}"]["credential_id"] == scoped_credential.id + + def test_resolve_credential_with_scope_validation_fail(self, test_space, scoped_credential): + """测试解析凭证时作用域验证失败""" + credentials_dict = {"${token1}": {"value": str(scoped_credential.id), "name": "作用域凭证"}} + + # 不匹配的作用域应该失败 + with pytest.raises(CredentialScopeValidationError): + resolve_credentials(credentials_dict, test_space.id, "project", "project_2") # 不匹配 + + def test_resolve_direct_value(self, test_space): + """测试解析直接提供的值(非 ID)""" + credentials_dict = {"${token1}": {"value": "direct_token_value", "name": "直接值"}} # 不是数字,直接使用 + + result = resolve_credentials(credentials_dict, test_space.id, None, None) + + assert "${token1}" in result + assert result["${token1}"]["value"] == "direct_token_value" + assert "credential_id" not in result["${token1}"] + + def test_resolve_multiple_credentials(self, test_space, test_credential, scoped_credential): + """测试解析多个凭证""" + # 添加匹配的作用域到 scoped_credential + CredentialScope.objects.create(credential_id=scoped_credential.id, scope_type="test", scope_value="test_1") + + credentials_dict = { + "${token1}": {"value": str(test_credential.id), "name": "凭证1"}, + "${token2}": {"value": str(scoped_credential.id), "name": "凭证2"}, + "${token3}": {"value": "direct_value", "name": "直接值"}, + } + + result = resolve_credentials(credentials_dict, test_space.id, "test", "test_1") # 匹配作用域 + + assert len(result) == 3 + assert "${token1}" in result + assert "${token2}" in result + assert "${token3}" in result + assert result["${token1}"]["credential_id"] == test_credential.id + assert result["${token2}"]["credential_id"] == scoped_credential.id + assert result["${token3}"]["value"] == "direct_value" + + def test_resolve_credential_with_empty_value(self, test_space): + """测试解析空值的凭证""" + credentials_dict = {"${token1}": {"value": "", "name": "空值凭证"}} # 空值 + + result = resolve_credentials(credentials_dict, test_space.id, None, None) + + # 空值应该被跳过或保持原样 + assert "${token1}" in result + assert result["${token1}"]["value"] == "" diff --git a/tests/interface/credential/test_credential_scope_validator.py b/tests/interface/credential/test_credential_scope_validator.py new file mode 100644 index 0000000000..4bc1951246 --- /dev/null +++ b/tests/interface/credential/test_credential_scope_validator.py @@ -0,0 +1,180 @@ +""" +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 pytest + +from bkflow.space.credential.scope_validator import ( + filter_credentials_by_scope, + validate_credential_scope, +) +from bkflow.space.exceptions import CredentialScopeValidationError +from bkflow.space.models import Credential, CredentialScope, CredentialType, Space + + +@pytest.mark.django_db +class TestValidateCredentialScope: + """测试凭证作用域验证""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def credential_with_scope(self, test_space): + """创建有作用域限制的凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="scoped_credential", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app", "bk_app_secret": "secret"}, + creator="test_user", + ) + CredentialScope.objects.create(credential_id=credential.id, scope_type="project", scope_value="project_1") + yield credential + credential.hard_delete() + + @pytest.fixture + def credential_without_scope(self, test_space): + """创建没有作用域限制的凭证""" + credential = Credential.create_credential( + space_id=test_space.id, + name="no_scope_credential", + type=CredentialType.BASIC_AUTH.value, + content={"username": "admin", "password": "secret"}, + creator="test_user", + ) + yield credential + credential.hard_delete() + + def test_validate_credential_with_matching_scope(self, credential_with_scope): + """测试验证匹配作用域的凭证""" + # 应该通过验证 + result = validate_credential_scope(credential_with_scope, "project", "project_1") + assert result is True + + def test_validate_credential_with_non_matching_scope(self, credential_with_scope): + """测试验证不匹配作用域的凭证""" + # 应该抛出异常 + with pytest.raises(CredentialScopeValidationError) as exc_info: + validate_credential_scope(credential_with_scope, "project", "project_2") + assert "不能在作用域" in str(exc_info.value) + + def test_validate_credential_without_scope_fails(self, credential_without_scope): + """测试验证没有作用域的凭证(应该失败)""" + # 凭证没有作用域,不允许使用 + with pytest.raises(CredentialScopeValidationError): + validate_credential_scope(credential_without_scope, "project", "project_1") + + def test_validate_credential_with_scope_on_template_without_scope(self, credential_with_scope): + """测试在没有作用域的模板中使用有作用域的凭证""" + # 模板没有作用域,有作用域的凭证可以使用 + result = validate_credential_scope(credential_with_scope, None, None) + assert result is True + + +@pytest.mark.django_db +class TestFilterCredentialsByScope: + """测试根据作用域过滤凭证""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def setup_credentials(self, test_space): + """创建测试凭证""" + # 1. 没有作用域的凭证 + cred1 = Credential.create_credential( + space_id=test_space.id, + name="no_scope", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app1", "bk_app_secret": "secret1"}, + creator="test_user", + ) + + # 2. 有匹配作用域的凭证 + cred2 = Credential.create_credential( + space_id=test_space.id, + name="matching_scope", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app2", "bk_app_secret": "secret2"}, + creator="test_user", + ) + CredentialScope.objects.create(credential_id=cred2.id, scope_type="project", scope_value="project_1") + + # 3. 有不匹配作用域的凭证 + cred3 = Credential.create_credential( + space_id=test_space.id, + name="non_matching_scope", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app3", "bk_app_secret": "secret3"}, + creator="test_user", + ) + CredentialScope.objects.create(credential_id=cred3.id, scope_type="project", scope_value="project_2") + + yield {"no_scope": cred1, "matching": cred2, "non_matching": cred3} + + # 清理 + cred1.hard_delete() + cred2.hard_delete() + cred3.hard_delete() + + def test_filter_with_no_template_scope(self, test_space, setup_credentials): + """测试模板没有作用域时的过滤""" + queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) + + # 模板没有作用域,应该返回所有凭证 + filtered = filter_credentials_by_scope(queryset, None, None) + assert filtered.count() == 3 + + def test_filter_with_matching_scope(self, test_space, setup_credentials): + """测试匹配作用域的过滤""" + queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) + + # 应该返回:没有作用域的 + 匹配作用域的 + filtered = filter_credentials_by_scope(queryset, "project", "project_1") + + # 应该只有 2 个凭证:no_scope 和 matching + assert filtered.count() == 2 + names = [c.name for c in filtered] + assert "no_scope" in names + assert "matching_scope" in names + assert "non_matching_scope" not in names + + def test_filter_with_non_existing_scope(self, test_space, setup_credentials): + """测试不存在的作用域过滤""" + queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) + + # 只应该返回没有作用域的凭证 + filtered = filter_credentials_by_scope(queryset, "project", "project_999") + + assert filtered.count() == 1 + assert filtered.first().name == "no_scope" + + def test_filter_empty_queryset(self, test_space): + """测试空查询集的过滤""" + queryset = Credential.objects.filter(space_id=99999) # 不存在的 space_id + + filtered = filter_credentials_by_scope(queryset, "project", "project_1") + assert filtered.count() == 0 diff --git a/tests/interface/credential/test_secret_json_field.py b/tests/interface/credential/test_secret_json_field.py new file mode 100644 index 0000000000..a58f53e9c8 --- /dev/null +++ b/tests/interface/credential/test_secret_json_field.py @@ -0,0 +1,221 @@ +""" +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 pytest +from django.conf import settings +from django.db import connection + +from bkflow.space.models import Credential, CredentialType, Space +from bkflow.utils.crypt import BaseCrypt + + +@pytest.mark.django_db +class TestSecretSingleJsonField: + """测试 SecretSingleJsonField 加密字段""" + + @pytest.fixture + def test_space(self): + """创建测试空间""" + space = Space.objects.create(name="test_space", app_code="test_app", creator="test_user") + yield space + space.hard_delete() + + @pytest.fixture + def crypt(self): + """创建加密器""" + return BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + def test_encrypt_on_save(self, test_space, crypt): + """测试保存时自动加密""" + # 创建凭证 + credential = Credential.create_credential( + space_id=test_space.id, + name="test_encrypt", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app", "bk_app_secret": "secret123"}, + creator="test_user", + ) + + # 从数据库直接读取原始值 + with connection.cursor() as cursor: + cursor.execute("SELECT content FROM space_credential WHERE id=%s", [credential.id]) + raw_content = cursor.fetchone()[0] + + # 数据库返回的是 JSON 字符串,需要解析 + import json + + if isinstance(raw_content, str): + raw_content = json.loads(raw_content) + + # 验证数据库中的值是加密的 + assert isinstance(raw_content, dict) + assert "bk_app_code" in raw_content + assert "bk_app_secret" in raw_content + + # 验证值已加密(不等于原始值) + assert raw_content["bk_app_code"] != "app" + assert raw_content["bk_app_secret"] != "secret123" + + # 验证可以解密 + decrypted_code = crypt.decrypt(raw_content["bk_app_code"]) + decrypted_secret = crypt.decrypt(raw_content["bk_app_secret"]) + assert decrypted_code == "app" + assert decrypted_secret == "secret123" + + # 清理 + credential.hard_delete() + + def test_decrypt_on_read(self, test_space): + """测试读取时自动解密""" + # 创建凭证 + original_content = {"username": "admin", "password": "secret456"} + credential = Credential.create_credential( + space_id=test_space.id, + name="test_decrypt", + type=CredentialType.BASIC_AUTH.value, + content=original_content, + creator="test_user", + ) + + # 通过 ORM 读取(应该自动解密) + credential = Credential.objects.get(id=credential.id) + assert credential.content == original_content + assert credential.content["username"] == "admin" + assert credential.content["password"] == "secret456" + + # 清理 + credential.hard_delete() + + def test_update_encrypted_field(self, test_space): + """测试更新加密字段""" + # 创建凭证 + credential = Credential.create_credential( + space_id=test_space.id, + name="test_update", + type=CredentialType.CUSTOM.value, + content={"key1": "value1"}, + creator="test_user", + ) + + # 更新内容 + new_content = {"key1": "new_value1", "key2": "value2"} + credential.update_credential(new_content) + + # 重新读取验证 + credential = Credential.objects.get(id=credential.id) + assert credential.content == new_content + assert credential.content["key1"] == "new_value1" + assert credential.content["key2"] == "value2" + + # 清理 + credential.hard_delete() + + def test_none_value_not_encrypted(self, test_space): + """测试 None 值不加密""" + credential = Credential.create_credential( + space_id=test_space.id, + name="test_none", + type=CredentialType.CUSTOM.value, + content={"key1": "value1"}, + creator="test_user", + ) + + # 验证可以正常保存和读取 + credential = Credential.objects.get(id=credential.id) + assert credential.content["key1"] == "value1" + + # 清理 + credential.hard_delete() + + def test_empty_dict(self, test_space): + """测试空字典""" + # 创建时传入空字典应该会报错(CustomCredential 不允许空字典) + with pytest.raises(Exception): # ValidationError + Credential.create_credential( + space_id=test_space.id, + name="test_empty", + type=CredentialType.CUSTOM.value, + content={}, + creator="test_user", + ) + + def test_single_level_json_only(self, test_space): + """测试只支持单层 JSON""" + # 嵌套字典应该失败(在 CustomCredential 验证时就会失败) + with pytest.raises(Exception): # ValidationError + Credential.create_credential( + space_id=test_space.id, + name="test_nested", + type=CredentialType.CUSTOM.value, + content={"key1": {"nested": "value"}}, + creator="test_user", + ) + + def test_multiple_credentials_encryption(self, test_space): + """测试多个凭证的加密独立性""" + # 创建多个凭证 + cred1 = Credential.create_credential( + space_id=test_space.id, + name="cred1", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app1", "bk_app_secret": "secret1"}, + creator="test_user", + ) + + cred2 = Credential.create_credential( + space_id=test_space.id, + name="cred2", + type=CredentialType.BK_APP.value, + content={"bk_app_code": "app2", "bk_app_secret": "secret2"}, + creator="test_user", + ) + + # 验证两个凭证的内容独立 + cred1_reloaded = Credential.objects.get(id=cred1.id) + cred2_reloaded = Credential.objects.get(id=cred2.id) + + assert cred1_reloaded.content["bk_app_code"] == "app1" + assert cred1_reloaded.content["bk_app_secret"] == "secret1" + assert cred2_reloaded.content["bk_app_code"] == "app2" + assert cred2_reloaded.content["bk_app_secret"] == "secret2" + + # 清理 + cred1.hard_delete() + cred2.hard_delete() + + def test_special_characters_encryption(self, test_space): + """测试特殊字符的加密""" + special_content = {"key1": "value!@#$%^&*()", "key2": "中文测试", "key3": "emoji🎉"} + + credential = Credential.create_credential( + space_id=test_space.id, + name="test_special", + type=CredentialType.CUSTOM.value, + content=special_content, + creator="test_user", + ) + + # 验证特殊字符正确加密和解密 + credential = Credential.objects.get(id=credential.id) + assert credential.content == special_content + assert credential.content["key1"] == "value!@#$%^&*()" + assert credential.content["key2"] == "中文测试" + assert credential.content["key3"] == "emoji🎉" + + # 清理 + credential.hard_delete() From a982afa22ae4821918c3ba177eb36c66247f9ff2 Mon Sep 17 00:00:00 2001 From: dengyh Date: Fri, 24 Oct 2025 11:54:21 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E4=BD=9C=E7=94=A8=E5=9F=9F=E5=88=97=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/space/credential/scope_validator.py | 7 ++-- bkflow/space/views.py | 38 ++++++++++++++++++++++ bkflow/template/views/template.py | 29 ++++++++++++++++- urls.py | 5 ++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/bkflow/space/credential/scope_validator.py b/bkflow/space/credential/scope_validator.py index 68cf39fc45..7f869a4cef 100644 --- a/bkflow/space/credential/scope_validator.py +++ b/bkflow/space/credential/scope_validator.py @@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from bkflow.space.exceptions import CredentialScopeValidationError +from bkflow.space.models import CredentialScope def validate_credential_scope(credential, template_scope_type, template_scope_value): @@ -52,8 +53,6 @@ def filter_credentials_by_scope(credentials_queryset, scope_type, scope_value): :param scope_value: 作用域值 :return: 过滤后的凭证查询集 """ - from bkflow.space.models import CredentialScope - # 获取所有凭证ID all_credential_ids = set(credentials_queryset.values_list("id", flat=True)) @@ -65,9 +64,9 @@ def filter_credentials_by_scope(credentials_queryset, scope_type, scope_value): # 没有作用域限制的凭证ID(可以在任何地方使用) credentials_without_scope = all_credential_ids - credentials_with_scope - # 如果模板没有作用域,返回所有凭证 + # 如果模板没有作用域,只返回没有设置作用域的凭证 if not scope_type and not scope_value: - return credentials_queryset + return credentials_queryset.filter(id__in=credentials_without_scope) # 查找匹配当前作用域的凭证ID matching_credential_ids = set( diff --git a/bkflow/space/views.py b/bkflow/space/views.py index d44fe53dd5..a81f5a9e74 100644 --- a/bkflow/space/views.py +++ b/bkflow/space/views.py @@ -449,6 +449,44 @@ def partial_update(self, request, *args, **kwargs): response_serializer = CredentialSerializer(instance) return Response(response_serializer.data, status=status.HTTP_200_OK) + @swagger_auto_schema( + method="get", + operation_summary="获取凭证作用域", + ) + @action(detail=True, methods=["get"]) + def list_scopes(self, request, pk=None, params=None): + """获取凭证的作用域列表""" + try: + credential = self.get_object() + except Credential.DoesNotExist as e: + err_msg = f"凭证不存在 {str(e)}" + logger.error(err_msg) + return Response({"error": err_msg}, status=status.HTTP_404_NOT_FOUND) + + # 获取凭证的所有作用域 + scopes = CredentialScope.objects.filter(credential_id=credential.id) + serializer = CredentialScopeSerializer(scopes, many=True) + + # 判断是否为无限制凭证(没有设置任何作用域) + if scopes.count() == 1 and scopes.first().scope_type is None and scopes.first().scope_value is None: + is_unlimited = True + else: + is_unlimited = False + + return Response( + { + "credential_id": credential.id, + "credential_name": credential.name, + "unlimited": is_unlimited, + "scopes": serializer.data, + } + ) + + @swagger_auto_schema( + methods=["put", "patch"], + operation_summary="更新凭证作用域", + request_body=CredentialScopesChangeSerializer, + ) @action(detail=True, methods=["put", "patch"]) @params_valid(CredentialScopesChangeSerializer) def update_scopes(self, request, pk=None, params=None): diff --git a/bkflow/template/views/template.py b/bkflow/template/views/template.py index d2ac0c70ac..8de7259d70 100644 --- a/bkflow/template/views/template.py +++ b/bkflow/template/views/template.py @@ -32,6 +32,7 @@ from rest_framework.viewsets import GenericViewSet from webhook.signals import event_broadcast_signal +from bkflow.apigw.serializers.credential import CredentialSerializer from bkflow.apigw.serializers.task import ( CreateMockTaskWithPipelineTreeSerializer, CreateTaskSerializer, @@ -59,8 +60,9 @@ UniformApiConfig, UniformAPIConfigHandler, ) +from bkflow.space.credential.scope_validator import filter_credentials_by_scope from bkflow.space.exceptions import SpaceConfigDefaultValueNotExists -from bkflow.space.models import SpaceConfig +from bkflow.space.models import Credential, SpaceConfig from bkflow.space.permissions import SpaceSuperuserPermission from bkflow.space.utils import build_default_pipeline_tree_with_space_id from bkflow.template.exceptions import AnalysisConstantsRefException @@ -696,6 +698,31 @@ def rollback_template(self, request, *args, **kwargs): ) return Response(data=draft_template.data) + @swagger_auto_schema( + method="get", + operation_description="获取流程有权限的凭证列表", + ) + @action(methods=["GET"], detail=True, url_path="credentials") + def credentials(self, request, *args, **kwargs): + """ + 获取当前流程有权限的凭证列表 + 根据 Template 的 scope_type 和 scope_value 来过滤凭证 + """ + template = self.get_object() + + # 获取当前空间下的所有凭证 + credentials_queryset = Credential.objects.filter(space_id=template.space_id, is_deleted=False) + + # 根据模板的作用域过滤凭证 + filtered_credentials = filter_credentials_by_scope( + credentials_queryset, template.scope_type, template.scope_value + ) + + # 序列化凭证数据 + serializer = CredentialSerializer(filtered_credentials, many=True) + + return Response({"results": serializer.data, "count": len(serializer.data)}) + @method_decorator(login_exempt, name="dispatch") class TemplateInternalViewSet(BKFLOWCommonMixin, mixins.RetrieveModelMixin, SimpleGenericViewSet): diff --git a/urls.py b/urls.py index 65b48c762b..66dd276771 100644 --- a/urls.py +++ b/urls.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ TencentBlueKing is pleased to support the open source community by making 蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. @@ -51,9 +50,9 @@ schema_view = get_schema_view( openapi.Info( - title="BK-SOPS API", + title="BK-FLOW API", default_version="v1", - description="标准运维API文档,接口返回中默认带有result、data、message等字段,如果响应体中没有体现,则说明响应体只展示了其中data字段的内容。", + description="BKFlow API文档,接口返回中默认带有result、data、message等字段,如果响应体中没有体现,则说明响应体只展示了其中data字段的内容。", ), public=True, permission_classes=(permissions.IsAdminUser,), From d2e7dbba30e2c31c9e04b3857311d6f6e2ad79d6 Mon Sep 17 00:00:00 2001 From: dengyh Date: Mon, 27 Oct 2025 14:48:56 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E5=87=AD=E8=AF=81=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=8E=A5=E5=8F=A3=E5=A2=9E=E5=8A=A0=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E5=8F=82=E6=95=B0=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/serializers/credential.py | 47 +- bkflow/apigw/views/create_credential.py | 9 +- bkflow/space/credential/scope_validator.py | 41 +- .../migrations/0010_credential_scope_level.py | 23 + bkflow/space/models.py | 59 +- bkflow/space/serializers.py | 10 +- bkflow/space/views.py | 166 ++--- frontend/src/api/ajax.js | 1 - frontend/src/assets/images/apigw.png | Bin 0 -> 72224 bytes .../src/assets/images/apigw_access_token.png | Bin 0 -> 77836 bytes .../src/components/common/FullCodeEditor.vue | 8 +- frontend/src/config/i18n/cn.js | 37 +- frontend/src/config/i18n/en.js | 34 + frontend/src/constants/index.js | 54 +- frontend/src/scss/mixins/credentialScope.scss | 70 ++ .../src/store/modules/credentialConfig.js | 8 + frontend/src/store/modules/template.js | 5 +- .../Space/Credential/CredentialDialog.vue | 163 ----- .../components/CredentialContentDialog.vue | 66 ++ .../components/CredentialContentTable.vue | 169 +++++ .../components/CredentialSlider.vue | 628 ++++++++++++++++++ .../Credential/components/ImageViewer.vue | 126 ++++ .../views/admin/Space/Credential/index.vue | 461 ++++++++----- .../Space/Template/CreateTaskSideslider.vue | 20 + .../NodeConfig/AccessCredential.vue | 125 ++++ .../TemplateEdit/NodeConfig/NodeConfig.vue | 49 +- .../TabGlobalVariables/VariableEdit.vue | 5 +- .../TemplateMock/MockExecute/index.vue | 47 ++ .../credential/test_credential_model.py | 29 +- .../credential/test_credential_resolver.py | 10 +- .../test_credential_scope_validator.py | 48 +- 31 files changed, 1967 insertions(+), 551 deletions(-) create mode 100644 bkflow/space/migrations/0010_credential_scope_level.py create mode 100644 frontend/src/assets/images/apigw.png create mode 100644 frontend/src/assets/images/apigw_access_token.png create mode 100644 frontend/src/scss/mixins/credentialScope.scss delete mode 100644 frontend/src/views/admin/Space/Credential/CredentialDialog.vue create mode 100644 frontend/src/views/admin/Space/Credential/components/CredentialContentDialog.vue create mode 100644 frontend/src/views/admin/Space/Credential/components/CredentialContentTable.vue create mode 100644 frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue create mode 100644 frontend/src/views/admin/Space/Credential/components/ImageViewer.vue create mode 100644 frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue diff --git a/bkflow/apigw/serializers/credential.py b/bkflow/apigw/serializers/credential.py index daaf6f2b3e..4cec7225f3 100644 --- a/bkflow/apigw/serializers/credential.py +++ b/bkflow/apigw/serializers/credential.py @@ -20,7 +20,8 @@ from rest_framework import serializers from bkflow.space.credential import CredentialDispatcher -from bkflow.space.models import Credential, CredentialScope +from bkflow.space.models import Credential, CredentialScopeLevel +from bkflow.space.serializers import CredentialScopeSerializer class CredentialSerializer(serializers.ModelSerializer): @@ -42,37 +43,17 @@ class Meta: fields = "__all__" -class CredentialScopeSerializer(serializers.ModelSerializer): - """凭证作用域序列化器""" - - class Meta: - model = CredentialScope - fields = ["scope_type", "scope_value"] - - -class CredentialScopesChangeSerializer(serializers.Serializer): - """凭证作用域变更序列化器""" - - scopes = serializers.ListField( - child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False, default=list - ) - unlimited = serializers.BooleanField(help_text=_("是否无限制"), required=False, default=False) - - def validate(self, attrs): - if attrs.get("unlimited"): - if attrs.get("scopes"): - raise serializers.ValidationError(_("无限制时不能设置作用域")) - - if not attrs.get("unlimited") and not attrs.get("scopes"): - raise serializers.ValidationError(_("作用域不能为空")) - return attrs - - 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( child=CredentialScopeSerializer(), help_text=_("凭证作用域列表"), required=False, default=list ) @@ -82,6 +63,9 @@ def validate(self, attrs): credential_type = attrs.get("type") content = attrs.get("content") + 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() @@ -96,9 +80,18 @@ class UpdateCredentialSerializer(serializers.Serializer): 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,否则需要从实例获取 diff --git a/bkflow/apigw/views/create_credential.py b/bkflow/apigw/views/create_credential.py index 13ca044279..913bfb62a5 100644 --- a/bkflow/apigw/views/create_credential.py +++ b/bkflow/apigw/views/create_credential.py @@ -26,7 +26,7 @@ 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, CredentialScope +from bkflow.space.models import Credential, CredentialScope, CredentialScopeLevel @login_exempt @@ -50,14 +50,17 @@ def create_credential(request, space_id): # 提取作用域数据 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) + credential = Credential.create_credential( + **credential_data, space_id=space_id, creator=request.user.username, scope_level=scope_level + ) # 创建凭证作用域 - if scopes: + if scope_level == CredentialScopeLevel.PART.value and scopes: scope_objects = [ CredentialScope( credential_id=credential.id, diff --git a/bkflow/space/credential/scope_validator.py b/bkflow/space/credential/scope_validator.py index 7f869a4cef..8e3cad15ff 100644 --- a/bkflow/space/credential/scope_validator.py +++ b/bkflow/space/credential/scope_validator.py @@ -19,7 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from bkflow.space.exceptions import CredentialScopeValidationError -from bkflow.space.models import CredentialScope +from bkflow.space.models import CredentialScope, CredentialScopeLevel def validate_credential_scope(credential, template_scope_type, template_scope_value): @@ -54,28 +54,39 @@ def filter_credentials_by_scope(credentials_queryset, scope_type, scope_value): :return: 过滤后的凭证查询集 """ # 获取所有凭证ID - all_credential_ids = set(credentials_queryset.values_list("id", flat=True)) + all_credential_ids = list(credentials_queryset.values_list("id", flat=True)) - # 获取有作用域限制的凭证ID - credentials_with_scope = set( - CredentialScope.objects.filter(credential_id__in=all_credential_ids).values_list("credential_id", flat=True) - ) + # 如果模板没有作用域(scope_type 和 scope_value 都为 None) + if not scope_type and not scope_value: + # 只返回 scope_level == ALL 的凭证(空间内开放) + return credentials_queryset.filter(id__in=all_credential_ids, scope_level=CredentialScopeLevel.ALL.value) - # 没有作用域限制的凭证ID(可以在任何地方使用) - credentials_without_scope = all_credential_ids - credentials_with_scope + # 模板有作用域时,需要过滤: + # 1. scope_level == ALL 的凭证(空间内开放,可以在任何地方使用) + # 2. scope_level == PART 且作用域匹配的凭证 - # 如果模板没有作用域,只返回没有设置作用域的凭证 - if not scope_type and not scope_value: - return credentials_queryset.filter(id__in=credentials_without_scope) + # 获取 scope_level == ALL 的凭证ID + all_level_credential_ids = set( + credentials_queryset.filter(id__in=all_credential_ids, scope_level=CredentialScopeLevel.ALL.value).values_list( + "id", flat=True + ) + ) + + # 获取 scope_level == PART 的凭证ID + part_level_credential_ids = set( + credentials_queryset.filter(id__in=all_credential_ids, scope_level=CredentialScopeLevel.PART.value).values_list( + "id", flat=True + ) + ) - # 查找匹配当前作用域的凭证ID + # 查找 scope_level == PART 且作用域匹配的凭证ID matching_credential_ids = set( CredentialScope.objects.filter( - credential_id__in=credentials_with_scope, scope_type=scope_type, scope_value=scope_value + credential_id__in=part_level_credential_ids, scope_type=scope_type, scope_value=scope_value ).values_list("credential_id", flat=True) ) - # 返回:没有作用域限制的凭证 + 匹配当前作用域的凭证 - available_credential_ids = credentials_without_scope | matching_credential_ids + # 返回:scope_level == ALL 的凭证 + scope_level == PART 且作用域匹配的凭证 + available_credential_ids = all_level_credential_ids | matching_credential_ids return credentials_queryset.filter(id__in=available_credential_ids) diff --git a/bkflow/space/migrations/0010_credential_scope_level.py b/bkflow/space/migrations/0010_credential_scope_level.py new file mode 100644 index 0000000000..375465d160 --- /dev/null +++ b/bkflow/space/migrations/0010_credential_scope_level.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2025-11-07 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0009_encrypt_credential_content"), + ] + + operations = [ + migrations.AddField( + model_name="credential", + name="scope_level", + field=models.CharField( + choices=[("all", "空间内开放"), ("part", "设置作用域"), ("none", "不开放")], + default="none", + max_length=32, + verbose_name="作用域级别", + ), + ), + ] diff --git a/bkflow/space/models.py b/bkflow/space/models.py index 7cd8184282..9ccfc393e5 100644 --- a/bkflow/space/models.py +++ b/bkflow/space/models.py @@ -260,6 +260,14 @@ class CredentialType(Enum): CUSTOM = "CUSTOM" +class CredentialScopeLevel(Enum): + """凭证作用域级别""" + + ALL = "all" + PART = "part" + NONE = "none" + + class Credential(CommonModel): CREDENTIAL_CHOICES = [ (CredentialType.BK_APP.value, _("蓝鲸应用凭证")), @@ -268,10 +276,19 @@ class Credential(CommonModel): (CredentialType.CUSTOM.value, _("自定义")), ] + CREDENTIAL_SCOPE_LEVEL_CHOICES = [ + (CredentialScopeLevel.ALL.value, _("空间内开放")), + (CredentialScopeLevel.PART.value, _("设置作用域")), + (CredentialScopeLevel.NONE.value, _("不开放")), + ] + space_id = models.IntegerField(_("空间ID")) name = models.CharField(_("凭证名"), max_length=32) desc = models.CharField(_("凭证描述"), max_length=128, null=True, blank=True) type = models.CharField(_("凭证类型"), max_length=32, choices=CREDENTIAL_CHOICES) + scope_level = models.CharField( + _("作用域级别"), max_length=32, choices=CREDENTIAL_SCOPE_LEVEL_CHOICES, default=CredentialScopeLevel.NONE.value + ) content = SecretSingleJsonField(_("凭证内容"), null=True, blank=True, default=dict) def display_json(self): @@ -291,7 +308,7 @@ def value(self): return credential.value() @classmethod - def create_credential(cls, space_id, name, type, content, creator, desc=None): + def create_credential(cls, space_id, name, type, content, creator, desc=None, scope_level=None): """ 创建一个凭证 @@ -301,6 +318,7 @@ def create_credential(cls, space_id, name, type, content, creator, desc=None): :param content: 凭证内容 :param creator: 创建者 :param desc: 凭证描述(可选) + :param scope_level: 作用域级别(可选,默认为 NONE) :return: 创建的凭证实例 """ @@ -308,6 +326,8 @@ def create_credential(cls, space_id, name, type, content, creator, desc=None): raise SpaceNotExists("space_id: {}".format(space_id)) credential = CredentialDispatcher(type, data=content) validate_data = credential.validate_data() + if scope_level is None: + scope_level = CredentialScopeLevel.NONE.value credential = cls( space_id=space_id, name=name, @@ -316,6 +336,7 @@ def create_credential(cls, space_id, name, type, content, creator, desc=None): content=validate_data, creator=creator, updated_by=creator, + scope_level=scope_level, ) credential.save() return credential @@ -350,32 +371,38 @@ def has_scope(self): def can_use_in_scope(self, template_scope_type, template_scope_value): """ 检查凭证是否可以在指定作用域中使用 - 如果凭证没有设置作用域,则可以在任何作用域使用 - 如果模板没有作用域(scope_type和scope_value都为空),则可以使用任何凭证 - 否则,凭证的作用域必须匹配模板的作用域 :param self: 凭证实例 :param template_scope_type: 作用域类型 :param template_scope_value: 作用域值 :return: 如果可以使用返回 True,否则返回 False """ - if not self.has_scope(): - # 凭证没有设置作用域,不允许被使用 + # scope_level == NONE 的凭证不能使用 + if self.scope_level == CredentialScopeLevel.NONE.value: return False - if not template_scope_type and not template_scope_value: - # 模板没有作用域,可以使用任何凭证 + # scope_level == ALL 的凭证可以在任何地方使用 + if self.scope_level == CredentialScopeLevel.ALL.value: return True - # 检查是否有匹配的作用域 - return ( - self.get_scopes() - .filter( - scope_type=template_scope_type, - scope_value=template_scope_value, + # scope_level == PART 的凭证需要检查作用域匹配 + if self.scope_level == CredentialScopeLevel.PART.value: + # 如果模板没有作用域,PART 类型的凭证不能使用 + if not template_scope_type and not template_scope_value: + return False + + # 检查是否有匹配的作用域 + return ( + self.get_scopes() + .filter( + scope_type=template_scope_type, + scope_value=template_scope_value, + ) + .exists() ) - .exists() - ) + + # 默认不允许使用(向后兼容旧逻辑) + return False class Meta: verbose_name = _("空间凭证") diff --git a/bkflow/space/serializers.py b/bkflow/space/serializers.py index ef714da65c..4d3007eff7 100644 --- a/bkflow/space/serializers.py +++ b/bkflow/space/serializers.py @@ -23,7 +23,7 @@ from bkflow.exceptions import ValidationError from bkflow.space.configs import SpaceConfigHandler, SpaceConfigValueType -from bkflow.space.models import Space, SpaceConfig +from bkflow.space.models import CredentialScope, Space, SpaceConfig logger = logging.getLogger(__name__) @@ -58,6 +58,14 @@ class SpaceConfigBaseQuerySerializer(serializers.Serializer): space_id = serializers.IntegerField(help_text=_("空间ID")) +class CredentialScopeSerializer(serializers.ModelSerializer): + """凭证作用域序列化器""" + + class Meta: + model = CredentialScope + fields = ["scope_type", "scope_value"] + + class CredentialBaseQuerySerializer(serializers.Serializer): space_id = serializers.IntegerField(help_text=_("空间ID")) diff --git a/bkflow/space/views.py b/bkflow/space/views.py index a81f5a9e74..257d72500a 100644 --- a/bkflow/space/views.py +++ b/bkflow/space/views.py @@ -36,8 +36,6 @@ from bkflow.apigw.serializers.credential import ( CreateCredentialSerializer, - CredentialScopesChangeSerializer, - CredentialScopeSerializer, CredentialSerializer, UpdateCredentialSerializer, ) @@ -54,6 +52,7 @@ from bkflow.space.models import ( Credential, CredentialScope, + CredentialScopeLevel, CredentialType, Space, SpaceConfig, @@ -66,15 +65,15 @@ ) from bkflow.space.serializers import ( CredentialBaseQuerySerializer, + CredentialScopeSerializer, SpaceConfigBaseQuerySerializer, SpaceConfigBatchApplySerializer, SpaceConfigSerializer, SpaceSerializer, ) from bkflow.utils.api_client import ApiGwClient, HttpRequestResult -from bkflow.utils.mixins import BKFLOWDefaultPagination +from bkflow.utils.mixins import BKFLOWDefaultPagination, BKFlowOrderingFilter from bkflow.utils.permissions import AdminPermission, AppInternalPermission -from bkflow.utils.serializer import params_valid from bkflow.utils.views import AdminModelViewSet, SimpleGenericViewSet logger = logging.getLogger("root") @@ -345,6 +344,9 @@ class CredentialConfigAdminViewSet(ModelViewSet, SimpleGenericViewSet): serializer_class = CredentialSerializer permission_classes = [AdminPermission | SpaceSuperuserPermission] pagination_class = BKFLOWDefaultPagination + filter_backends = [DjangoFilterBackend, BKFlowOrderingFilter] + ordering_fields = ["id", "name", "type", "create_at", "update_at"] + ordering = ["-create_at"] # 默认按创建时间倒序 def get_object(self): serializer = CredentialBaseQuerySerializer(data=self.request.query_params) @@ -363,6 +365,39 @@ def get_queryset(self): queryset = queryset.filter(space_id=space_id, is_deleted=False) return queryset + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response(self.fill_credential_scopes(serializer.data)) + + def fill_credential_scopes(self, credential_data): + """ + 填充凭证作用域 + """ + scopes = CredentialScope.objects.filter(credential_id=credential_data["id"]) + credential_data["scopes"] = CredentialScopeSerializer(scopes, many=True).data + return credential_data + + def update_scopes(self, credential, scope_level, scopes, update=False): + """ + 更新凭证作用域 + """ + # 创建凭证作用域 + if scope_level == CredentialScopeLevel.PART.value: + if update: + CredentialScope.objects.filter(credential_id=credential.id).delete() + + if 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) + def create(self, request, *args, **kwargs): credential_serializer = CreateCredentialSerializer(data=request.data) credential_serializer.is_valid(raise_exception=True) @@ -381,27 +416,18 @@ def create(self, request, *args, **kwargs): content=credential_data["content"], creator=request.user.username, desc=credential_data.get("desc"), + scope_level=credential_data.get("scope_level"), + ) + self.update_scopes( + credential, credential_data.get("scope_level"), credential_data.get("scopes"), update=False ) - - # 创建凭证作用域 - scopes = credential_data.get("scopes", []) - if 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) - except DatabaseError as e: err_msg = f"创建凭证失败 {str(e)}" logger.error(err_msg) return Response(exception=True, data={"detail": err_msg}) + response_serializer = CredentialSerializer(credential) - return Response(response_serializer.data, status=status.HTTP_201_CREATED) + return Response(self.fill_credential_scopes(response_serializer.data), status=status.HTTP_201_CREATED) def partial_update(self, request, *args, **kwargs): try: @@ -425,22 +451,7 @@ def partial_update(self, request, *args, **kwargs): updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] instance.save(update_fields=updated_keys) - # 更新凭证作用域 - if scopes_data is not None: - # 删除旧的作用域 - CredentialScope.objects.filter(credential_id=instance.id).delete() - # 创建新的作用域 - if scopes_data: - scope_objects = [ - CredentialScope( - credential_id=instance.id, - scope_type=scope.get("scope_type"), - scope_value=scope.get("scope_value"), - ) - for scope in scopes_data - ] - CredentialScope.objects.bulk_create(scope_objects) - + self.update_scopes(instance, serializer.validated_data.get("scope_level"), scopes_data, update=True) except DatabaseError as e: err_msg = f"更新凭证失败 {str(e)}" logger.error(err_msg) @@ -449,91 +460,12 @@ def partial_update(self, request, *args, **kwargs): response_serializer = CredentialSerializer(instance) return Response(response_serializer.data, status=status.HTTP_200_OK) - @swagger_auto_schema( - method="get", - operation_summary="获取凭证作用域", - ) - @action(detail=True, methods=["get"]) - def list_scopes(self, request, pk=None, params=None): - """获取凭证的作用域列表""" - try: - credential = self.get_object() - except Credential.DoesNotExist as e: - err_msg = f"凭证不存在 {str(e)}" - logger.error(err_msg) - return Response({"error": err_msg}, status=status.HTTP_404_NOT_FOUND) - - # 获取凭证的所有作用域 - scopes = CredentialScope.objects.filter(credential_id=credential.id) - serializer = CredentialScopeSerializer(scopes, many=True) - - # 判断是否为无限制凭证(没有设置任何作用域) - if scopes.count() == 1 and scopes.first().scope_type is None and scopes.first().scope_value is None: - is_unlimited = True - else: - is_unlimited = False - - return Response( - { - "credential_id": credential.id, - "credential_name": credential.name, - "unlimited": is_unlimited, - "scopes": serializer.data, - } - ) - - @swagger_auto_schema( - methods=["put", "patch"], - operation_summary="更新凭证作用域", - request_body=CredentialScopesChangeSerializer, - ) - @action(detail=True, methods=["put", "patch"]) - @params_valid(CredentialScopesChangeSerializer) - def update_scopes(self, request, pk=None, params=None): - """更新凭证作用域""" - try: - instance = self.get_object() - except Credential.DoesNotExist as e: - err_msg = f"更新凭证不存在 {str(e)}" - logger.error(err_msg) - return Response(err_msg, status=404) - - # 验证scopes数据 - params = params or {} - if params.get("unlimited"): - scopes_data = [{"scope_type": None, "scope_value": None}] - else: - scopes_data = params.get("scopes", []) - + def destroy(self, request, *args, **kwargs): try: with transaction.atomic(): - # 删除旧的作用域 + instance = self.get_object() CredentialScope.objects.filter(credential_id=instance.id).delete() - # 创建新的作用域 - scope_objects = [ - CredentialScope( - credential_id=instance.id, - scope_type=scope.get("scope_type"), - scope_value=scope.get("scope_value"), - ) - for scope in scopes_data - ] - CredentialScope.objects.bulk_create(scope_objects) - except DatabaseError as e: - err_msg = f"更新凭证作用域失败 {str(e)}" - logger.error(err_msg) - return Response(exception=True, data={"detail": err_msg}) - - # 返回更新后的凭证信息 - credential_scopes = [] - for scope_object in scope_objects: - credential_scopes.append(CredentialScopeSerializer(scope_object).data) - return Response(credential_scopes, status=status.HTTP_200_OK) - - def destroy(self, request, *args, **kwargs): - try: - instance = self.get_object() - instance.hard_delete() + instance.hard_delete() except Credential.DoesNotExist as e: err_msg = f"删除凭证不存在 {str(e)}" logger.error(err_msg) diff --git a/frontend/src/api/ajax.js b/frontend/src/api/ajax.js index 14ab28e961..af122c0c84 100644 --- a/frontend/src/api/ajax.js +++ b/frontend/src/api/ajax.js @@ -48,7 +48,6 @@ axios.interceptors.response.use( } const { response } = error; - console.log(response); if (response.data.message) { response.data.msg = response.data.message; } diff --git a/frontend/src/assets/images/apigw.png b/frontend/src/assets/images/apigw.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc6b29c54fd4ed4c64b391f4952e95e65e96cad GIT binary patch literal 72224 zcmce-XFQxw)IThZ)h(g~(R*7&kBHuh2o_OR@1hG)BCGe_iQZYg2dnq!okWe^iMr2{ z-+ll8ub$8I>UqO8*EMs_cTSr#XJ&)mDa&AEyud&~LBW=jm3)u<2Sq_aqXaxbzM%wc zq@tiazI~_oK?;FDoLyXzk}_Xi-yWY{93G#Yo?q_nAOGG(&~r-cAD%4#S{H&Mj7)5s znp=1H_!3fbxwr*xZ*LFf->j@`uB~q_EH3^2z4B-C&;G&TpRMh=`Gwux-HV*`QV-h= zBz45-?(V*kNA=O+Ap)^}_4jJ`7_onXFfvV?Kp=V$h=i1ev#hkO&Qiow!_3TFgo#Z; zL03-a`qur}t&W1+$bQ6lb?(yg?}X}!)6)}I6Iw5^-@~!Rd?aC>{&!3~Sv$KfpZX=VgOMkDYrlvZ!5f^{g%GMC=f3JOf{Y*_w zPc9MhSq&hbFXQ7ATw2-96PuY@0$p8Q=NISGbIYJt`Uz>JZXmvPc-Qq~RlsihpAT~osYcX!?U+gnD_y*t_N@RYVLdn?+^`gCDjf2kqF^TM0Xy-#l_LZ#OKiu0K6vKu@WDY_Q2uN4 z)?yR;fW`;cLS7Y@*ib}xT#A{w)=soB)%ecL%;%i&1VV~@SPioS~6<RVJ0DKaA@&oCzn?Z&;!#mh+Fs!!|3=1uP*8mO>99~x zs2CwgcZ!oEK|y(|3`ah)JbX)qK0-nHz;ORf5FP1o1&a6I9PS6dOS=C?+un3PJQDWM z^9j!RXBGU~z1u)!hnsgGQHWJ*8WAeW$K7c$Y;i8UXKi_P>H+jn0~As^a66j%&zp`s zx@J=1Cv+^yVXSCXNI0_UugHjTOpzS%iUm?(&LGSuC_W#%W}&6uw2eaB zeEc1GPshpkuNbNfZl1ryfSkVGZQs&wJW{1oe{Db2cB64y^`-HiirpL( zNXDpc3xFjt$@JX1+dgK2e?O(jFg51obvt#Ewb)+%g5q(a6ZH9V&_fF^(eVp&uWVlT za)n06tF@EF;i?VCtv@X+@b@i)c}{K}oLd$+P0G zhCN`E8=`L}qG1z~B0 z4Z~&n3G=6CQ**|BX0C22q~kz`g}sOyuHEr_JL{_PJ3<$G;E&$!7O2TqR@2t$k?F5E z``_}ihY#S)O(qPuXHzsjJG?n?iH7=#6>b_dJr4-$PhQNVg=BoD!pmEe`O#3iccjGt zzS&P46C0j(@Odn{zqQM{%tiZQaI9fX*{h(AkO{Kg?sq{=7n&yjOTJV(lhsD+Eywp( z`>W@oRpM~dJ-{?Jv$@9jR>JmrcN^8G%4eh@c<7Gb^lN8G3=>YI;Gg6_N0L2}7>_Kq zE`+p?jp0cczKbQr4EUFM`v_Zb#UnE)Mu);s1*q3N?A9G|!JP_%QyHWb`!& zi|Uiy@NbKPnI9Z(!zYU?*GcB>qJAYv*&F}KTHOD}ijKA#F``_9?mx#Cyc;aA&~Bfu zt0h3AT<5EA*L8*;!JhlRDF8<#F1m~L^V|@%CN&*aqh>0E{ac}CjMZ4mXVK<<88>u^ z9)(#7-uhVfOL{ElBz>?Z{AX0mVwBGho9Q$Kcx#&r;npX7|Is~y_W-$nSi!jkVzTHR zoHP&@iMLU(kT(9^{7s~#lZ9)&-1C%Hcq1vmXa46Zm ziz@Y>AWI9{>$d{0H>89N9X_0Vy6}~kH7JVY6x)2A9)jQB-X zMQ@BDJT>BxXdJ8`(j)vgy&lOJ$04D{Dy?){n681KA#k{QffFLI5e4F*7E<=KipuFk zSw3!!F?$gY`^v7sWOvbf3V*Hu7DeUNfv?GEFq|^RLW5M+v#37|L~+I{=ex9Lga-~y zWJG+&<&6g2`#V+O^E_H{_*lFRNs*r%zDB$3xws&m=|V|7Y}ny&z=VHJGgA-ONG{c0 z<&j5++{}WEOh9+SAdanCCzqBV-7|&60cLO8OwziBnwWz?JC#E}T@9W93$C%4@cO@t zjdwFiDm=B&!WfH+0u4F&*n<`Paau@Vgj5X3I$R$c%;N7KA%u^r-toNvKfWisw>COxEZtM~575!|G@S_U3H83lXwpqcdvoA1V8%%bm1kswppijSqW7N{w@*=^;}OG!0&#;`8q}6v=TxM;qCxc zG(&IKy51Kcet^~Bpe%D4INjnG87nCKt_Z5N1)Z|BosZ9rlZO9%*Z;QXMKtWQIZk!4 zr{pq!!Ti;vpOfT*ITN}Bt)+I}RX~d)dJ;3%YJK=K#uJNPxI6_P8=*W~FMUIf^@0O4 zUERkJN$Oo=qfFExbJJ)nZq-oLGHOf*CFq(4k<0t|yOGp`q>I$gBjnAtYImYPcs9As zOuuLimLpDHmz$@eTCzYcB?N5aK%c8(j#I7N4Dh7^)KI)*PxKUTSgg8%(f6k(;!}>+ zaVpw=&&|hE*WXS=|4D^t$eak464z$xDOO(2w_%LxbZUFgFHc)F7&xtAs!L~|sr zpfNo;)F82LaBC&fIHm{RN4=oIpAu7@p^O71@lf*l1qIba!E{Be*NSq?Fzt(RN?c=L zzVVcBE11Www8hpf+PjK*{!nP28k^Hm^(Q^lPsu6;?BLP=#y+$>C{P>; z_=`RqURM4`2yzA^ElJF1nP&tJ6B?og$t8-suy1_jXMxXm-V*~#3izA*<;tQ6YLjjd zQGm8n{emkH@2m>HpDaKod|p1$It+@gCQOG>(_;1

CtgnyF=6DfqS z$b@Y&}ai9|Sl()X4&`R5)C=Eov zKdjBdQ8`qe&N_x|F_i$FtSYmeN%nn>LOoe(50N!T#!KoNf6dp_h!vCNVI-(Cny=m$buBs8@?!f%=!wwGfVAcmCt$kiecmY+1x*_n<#Z#&2) z2d|yO_n(J9x|n&pm?|3!bCCo0#e8?E|G*({l5s`F`;mG;qf2(Vfaz?~5EgU0fYb6@AV2vlcPHPA8H4zGSxL#61k zCLHPy4=nV<0SBK{;2$y~edM%$r@Um*vB{&|hsPM<=V68fb?^Cx-v&eb+y@gjbmL@M zl;8&D8w}&a^sP8S!z{xg&@E+ZU3iWr_KVkq2RXFcN=j?r9{(?Te1EVE-!_T+%L&Xk zs`0Nmcf%En1E8}LIgbWSG0+T>e-HSO*%FesboZ%G04%cX-{?$EG@ zEmTPP z1UVh&Y+r{fet8lHk(6MWdvhAg%#kNCy=`v~b}t(f;IuKMlNZsk80KOA8-yR~P)TgxVTU#pG>8|cJCFS*4es;$njIT01}DVCny zshvadQ2KMI*fHPAq7nZ2?%Vb5rZlO8Wg|JTqWt!-{!q;T>pnQzY8F4QIIs{(iWZ~5 z&30rbOG4hYK&Y9cwjaYP?~om=LaziDU&Ypp>UKFC%X;2(TLg86dkfshlE`ItNI{v^ zxpyX62~1~LGr+WJ0!ETh8P;$+X;m;?LMI1)!wlH@Ohe!>fB>R9!h z&x)S4y>blAk;%Pe+f;A6PA<{Z@wem+PM|S5Qu5*-J>Wkd^C23*i8F~rO#SiMf-4^~ z0+;a(h7uD3AAi#)a6pa$R5hWTHhQ}TSd)6?3TXV5< zN~-`rrlf!Ttam(apI;EHH5UKgD8Pa)j+s<#c^e?O@m zv&Vrv|^texED$CMELW33Pmt2a0;`6gVKDS- zJ=9a&VT!bR3q9n^g)8P)7aJ8;RVY-fq|GHYHjEwe4))hC;{dBT#3|`38M9m>>IW7G z@L7oNU&o>>eADJaLrc=dS8R)eA4HXyrhz+C56chT@0eu;UNJb4F@pj^lfA2bfo`yM z{On7|5xOU8YDTaR>uJFkBXfVvez}8$D3CJ@CRq_|5y;8-Yg2tx3;yB4Y0R_7P?IWs zh1lud1o4h}-ob@Zo}x?O@rgKE1=V#?f^n-#Zes)2LFh=_j?aG>MLe6Q7Xtmvelc- z&b@m!NM5lpZ?OnyO|l1N-vnGFL`QTYl8~;lvDR&*fpj5w#UQfrpuEqw~aV({OQ=D z_g@iDX%FWB0n_J z?!Lm%2Esyt37g9lyq313kJM9}VB*76A&OGaHKV^!v z|5K(A`afj~CuEu8%RMd>DwL;OE;rXw?GacB)f^Zo)(`cHhbl(OebFNJetg?Q+a#cM zANj^0>b?@feP803x##o!q0Yf~-)4H?pmY!Jp;BV-(Eh)=?pE0Klqf;%$$K&)PcVoc z6Fa=2W|w_SY!-m`$sY8JX62o`10kA@H8V}U)6STSbCX!Uqoe)1U;#oI;sBcZz=OA4 zxlJREpTiaQ<>0Dg=Vf0+RrW7MMXpf2ZuY6~@uIXjL6P{7S}g&g%KL|Cn0v|J%Dd`NFAVjkKQ1He@W623Hdl!E|x#9D%z5?bV&&&`9#iYRA zB^U^WHo&PKC@?)?{JD}RG_}H1mS#l|?+kH2upw?Ch_^{YR|1cegS zty4kfm62y4c_wd)*)r5*Yk<7+0BE*^KCp<9G6hY->P~x*jtTxV2~DdF!pT$OVAU_V zUK>ha)>8N&H@fvy8m#M;M^a{OpDR52Cf8Ie8TL(BCM7E{;RaC?6t?E(&+V<`6ky_^MEa=TL{@ic> zbx9-mrc=`~6JOxHB$VWxM{O39d`RA>z9SI7!h8Ks`a&ydjXMW8s#Hh9@ z2?}c2@#djsOWG-(J`<*B)qDNVuGBw-_!e67B!j%2G>vPAGSPX&c4MhaGIBU;b~>K% zLxgpfa{5^nG(DDhAdO|7Co4k%QFzk#rg2y0%9*M3)Sje`F~{P2N~jx z#or46Su`{SnTDElfH_NA!lOsxZRD}!Pmic1e}?D)et?+XxJSAjeV4+ODsX%MN3MWM z_F#BdnY&M!ubayN|A&6^qEgEK?P|&;CyG;|feJ0Pq78gaOwM4)0{-{2oe{M)>^Q#& z{rX@4$eHitxA0h}rMW0|jM| z(fU$*lK^gv$-tpx7s|~mo~Z4&b%4HO+0L;0**^VKend0z*0vcZzeL8V&;}T=b6_M5 z_?*(eKoy=4n-uD;1gLxOC)rp2=s$Y&<~na2Tzn=rEZPkf1%=0zX=IPqx=D*3O)yp1~jHfT>={&P}rsM90L4EINDBpRWI>u9; zF+=W@60?|uD$UKYg9@cG}yIq*Sq>AC@)z$^^Sn=j|*g_xBhprNir->&1p#sEb!a<+}&hi zmVlv~CjI<%ht_Z-=h$Gq70-QuLh<1G9>XHKE9esNC&r8>z0&}1<0qAUk#zw&?^ zKi7y$Kg}S}xNJ3a)LQj`uT`8S!+3kRQx;kAGnAHq0~kAgpvn*qJT%--E7H3jV{-p# zo{x~oo|Uk6uPl<^zx1}lsdH?etXc6^1j0RNFXRE&u0W@y+#+RkS)?9O%a)*lKDT8V z2busvMk+XYv_?}4RXn910xXKTgjgK{WBpiBrA~c_?=A~nEf};Iz>68i3)yz))R-r5 ziB>+=+nL0Jc+Oxmh6mwEw-^H)@9dTg^6Q*V-!enl&(#yf|Dz?-Za`%`FLL=R=E4?O zwZjWZtv__qzxn1xkXOMtn5MvqvpW&XP|}(KkVdpZVU+9mp`oKZ`+<_9k+a)QlB96| ze4gEA?|9K-(ubuJ1w}~Ob-41Iw7}jUbRG3CDFPf4-H4=%5K?1LbBIa8^{t6*5k=STr+})Ob*%*CsF%o~*D{9uwN(j7P=ulAR z-t`u?m5>W_ExJgkV6YM!OsaziOFqUIEJL?;(N}fBY8Oxnef*%0`g^7@S-R1D3b^E% zbLpWN1IOoe0=SD%YjRc+jV?JvlVtG0Kz>q(er)trZy~p@J8*yjXkT30O3^6P9%G%e9hdMIP{6@wyi}rHmxtIrAhN_i#Be6|h0RNwH~lXtqBvm*XT;a$oW$#DN13DDplPt_ z@7v@nc5!z(NPT$j*cUjR56-1hVpL0vX~(*4Cl`L-C&5@~NF&@0Pr`!SwmT@e*z$c2 zGHxAtmpscCEi$0I!yj%SNOO<(tqjPTHKQzg*GFhuTLo7l9U>_62oWv774lyBmqc7GLr0jo(8N5(R z#IHPiD!JWny4XTl#6o3^yb4f7tEiPrifIs9Z8jN1Au z!{CsY;_&+S`(zOX_C94NC7Cl>Q8%!<{PF_o%;P1L6~qUD)HfV%1^x?9b=4Fyumn{A zL0^r6e9cnG4Kn3;9pQH3@gIix>WVk+&lWkQC$zM5V;#?-mniWLLMyNQF6$`%QO(nLutj z=@&bh1Uy?U+>Yhmbn28!hDCO{5G4N#MJ1`Rj5~?#oN*VsKbo&c@GaotKq6!my5jW` zGR#3AfA$afg@|8*p2eS8&}&}9N5^P%*IJkC|xNj*ssyWvEz z+KF;$BARYcKZ4!NUE;sA_{4g{($485?u6UpItH}#kV6|_&RPV5>?k1Pdd-Z3db0-in-LTPVXV}| zk9y&MXFkiINSXMQ^rlZOWbzzF6vjh@x6FTaM84 zjn(D=t7k|Y3!Jz*c~fZF@n(oHSjgyLpxuFH-qwe78uUWH<|JbaSYjwD~}4XnDVL zefHAOnOJ615jVL`5{IAQdx1i#2wW`SM#pXj+{yB2r>h1a98Vbr`aI)RiX?=R69PL? zH71e*&se|waSS24weM+ByZjjye&{OHIx3{IyoX5K)E3A!4f7A!jry&UsWSG6IbNV% zc}<#AWa1=Rw?dwl?V|`283j{Mg*dGuky)KyNTbnw+4Bd%Tf5;i2pir-hNvWCA2}(e z$wTS;(^$emEvN@0nzOohH))h_D2Rh-+4!Rb;91Lsc#vpihv78z5mtn4BJ^^)e7ok* z>}B~qG4I(XBMSN~CLH9seY20BRSDk*B7bR6OlW51Q5+MW?BhQ86YJ~bXex}7>BpLBW`@5l14JI6F7-<6 zCB$8q*VDa;lGl?0feAdnNx~=yt%na3vvwQt9lTH)YdAECx@@4iSG!NUmLlA%UJr|S zer5rCdW*W`L=M~obon#Knk(rzpe89Vow&3}e4T~|d9i;mWXbLp#!p zY>d=b#kFT2f%lQJZ8;n^xq}%2OX)IQ)=X(HnqCidFL^JL!@ftT%T8pGXTEcl3=$>s z_Ta1-r&ZJ;GQ0}IKNYi=n#oqN;}E_2A&@!Su#$Wq;=UsQIym%A%l_*hf6}HAF$DZ+`tCcaCY>yI$r7DNQzy+GYQ@~*(Iu{41r<;Hr znb+Yn+CgF&FAwgpQaK^^x{QcWn1Cjd5K3bhh&R#QDj%asD`$U9c~1k%9zfk9?Xn{# zw@t9vOA{x-t1iw&sU1)2LQE95yldzkmAnKFa5RR7S=EWW@G4?-+ zAo3^zsQn`6La$g7F`s+mLxeqGfN@;Sl)B{1=MC$Px#9fbXdyIgA~hmf zP?LsJ+gF^g7e^V3^kTJMn(W2Eq{TJt(Za-mc@iga(dTKoBJg9!u7{A1G-D^hM1Q@x zDvHDz(qllAmNi;1sAZu}J~Y)bqYqp?XhWD+&2`7$!GehDV^nQKd`~CD!pzO=yuLc_ zJO_jj_hfvYVpTZgP>+=MUk6UPl!3zmNaUx45H$Cl8NmE(>>y=mdhBS~;*+S^Yzf z^2c6q#WO`;5?p^(A?j5V+HXXq-JL*ZX-Po9m8(QV^xjM@Lo_?fGWj~gIvfz zdjpN1*`e&Fn77AJEiK-X(7NjOqrK|=y7bc?x~`l7Ju3_+yW-7Lt$OoV+#<1f^^QoM zwS^@F_Ht7xfDzJ3gzOn9xC#(f8QQ{uv^P9Pww3Mm9-<}+75p$QO;}cm1n>kTM4ZT) zBh?5i^L?6YF8}t^WY{~qQKch1z3(l4^Bhfk&)vgbW~x46kNzGa%lg@l3ko9s`_~M) z>ix~Qx1_f`_ar~{{l7hb3O@UrJJqwuO@)kXKJm{Zl=2?kkE-4g(q)%OJ5xjOJ$%s+ z$uE9a{s;H*eYE&H4ro%A$yAO%EfeouWpawU-?S-*!Oq-BSnN7hfi9v!T<3i2+AOZ? zxFNdGy@WD+WKd`;1iGkoshv5$g6k1eFYgaTTU_oi!c8m2y1pjfQy3f0joSx$!f4yL)%l*{4{Df1k{)>-j!fQ0w5E!RSh~`LyZ5S*o6z0UZ>&6S` z;8@jM4e>X9fW1!$DVFhsc6Bxe!R&5V^;%y*7D3Y!6hZe;XTCtsuGSr{b^zBrduL?z z&s6V|jtgSQDkY=ng_n8;0bkPaL4=gjk_zX&21+Go{*+v$!tA$8;}%al2<~+OK(laUVHanV<0j#K7kfDwN+qp5- zFxYJwfB-4`w+;$cX1UrueVvD?pz)!jM~unD-U@ z1JdGPczje+;Hb@S#{&E8G;F2oObp{29I}IRd%8%BeoE3aXXQ9(M?*uLxILE!4V6Ed zzuQ;VVjy)N57$QH6y*^4T=NE=RoT8G*niStwjOZl#V?tSDq1FJRWpYKfC(urza7uL zx%H+kxhgNu!s0T7);K*do+&FCXK~g>c2YHHs#LBfU>zhe3gzl1G18_0*4k*HA@fLP znC>hN3Sy>Qg3Jekf@-jcP{HMR^7UENE+-bs7aY<1Y?BUM=f24919krYr}feKrLFsH zl@YDK7!!uCAsYgEckLVqAc<*~+;y>cq`x-`F=$s1(jN@)AX>`E$%23o$o>CAI(TL3 z{d*cX{QmzhIjD)?y`xD&yY{5ONIMHaj+ne4*hS83Nd@Y6R*HI!tdh1-zmS{cMG~Nb z`&a)MHMl_Lr{vjDAIN|%AxJU;vb04AyL)d!VYHxo@NLN%xcCj!lM~5LF(%a7X7s@?EMH93b<}v5JfM> zL$X6@`$luokctz~vBT3&NsSZ=5~p$Yp@CcCrF$dmzkR~5WvQ8ylc=I{)76q=Q+H># zKVhPpYX?$#G;>t4IX8jGxuKxoFmtCoWofpJa53I@<5LxLvG;iYPGczDc;!RMkNosT zuTpyHbK_-Zk%uD>l#fzsFwLTd?rW9!h^4TJ*^vB7>C^;uH>0usmp#hXQt9^F9!Z_# z&W3iGv?gK?8;Vc8?wsWuFG}MK0JvqzlqTxa2Y-H4j&<2Dry3{m0W1@|EFZHg(-CM` zv3q9e$!+MfC_%aYZHS$rX?+K#;|4a+GbIx#q4bqbz4wFPSca=gmeRu}>|dwnK-TC@ zB0&Fx%?58PERfwyZACV%bR%kr!}H|l-8O3pgQ+w_RD%J@(!TCuq`5w+lo{)dx)iVLiaTf%f5C4+W1f8r8M7mx^IUGmhn@f0zY{`0y5O{}7@;yx(j{g%TU~YufZy*y*(i@IiN$Zv zE*Z@BRBPE@kW@FdHYW=${5lVueX`wRgjbWk63YJ1JMOsQ%{_jLRRKA!naDk(;qfOv%LHpe5Tt&C- z@jh-0`{5gz1OCY?8Ms;G8!k=Ltm;_7F4`l7v^PXEUk5mhf@=R}hQ zcDQ;sbc8)gAYo#KJ8GiH4<^XRtcL&m9s>$c1Mip!(pc9#adoFrgZSA}W0Zt$uUEKK zHVCj45N%89K)yYX>Xn`0mUEPLG)xd-(cHvNr}#iP&rm+0%vy+pVEWgCBf~zj?mB6 z(4cSY-Pn#0TZS|pPT^B^PoJ#K`rgliSt(!o8WZhg63J(0B3+A1ChJ4r=tZ|Wd6rg^ z-^Up$IL4r;Acg%GDbft7{&>0Sw#X9AwPx~B-b8QSmFkziUAZbB6#IUxF;+w>2E{$p zcO_@+&i6Oe^FTPi{;CnNrBCn(=YrkPnb&g(X*kKWsO0+5za_>cOc>ketL+aq3^BP@ zekxGq|BYWqxGq0St&!YB#D_uFWxRgzI%2gqR49Zt;77)OTGI8xCE5>>kpn@$Lo-)+ zGuP@wIJ4P)?A9{+fEziN4eWzNNO~;TP?d3_!j`v#Qv1F{iELTCofBmxJ-`uMb66fx#SOQ-1tLv-`JlvPy6N=Ja#t>5VmPRfU8=(Aud^)DbEL~duwdTFbnv7>n8)LMaMuOb%%vPpJaAGL z{@*R46@6)ZhX0<6qo5Dh+5ps@t)X;^|J_{Ds2alO(~%T@nL734cc^m4pTFi3irqzDG}C_wT-~pfrMYs5zyXzc5gG zLZgQKVM=xNJ`hjH%Gp%^ZM?}&BA4oh=p;xrpD*ZhdgQQu6oXz|@w<6bXO@YNRi&>^ zR3shj(^Gc1q&srtLM@}&oL|n2Gjr}xL-uWJ_vvK_|M4psh?k#f-wNUkG0!@7!8nl} zehjIgB>#E^x5V!=ZYTdH1^V-}=P~=%9-*21ySM#uu#Anb*SV}S8#K}q`H+pDaf|_C z)OnyXgNaFzoyk``oIak?=E4Xa@{nllSQrW#!>B3E+$_^SQMp@ZL)#0#6Jc*dYP7~t z=}W|4AfCwf_Yz!Q(ukbAYyS9DjSP)M=gH11;i^_I9R7oe%i-2&sMhC*?!<29PuLR# zi+P@eV0iP18#gIkSX$p+j4u;9!NXLt_;pwzI3~zTp9{Z;>Df%EoO>)4{jQ>>BjrE$ z$r<)NsQX&*KTLEw9+-TD zbGyrtoK*agsO-lvIuugpN7a(qu4+Sx(mGUr&?h9&`dSxl61#!JhR@DSo%;#_62(D+L^Svy?sV3tL2DU9O`ZlKndnN zkU8W7Gp@-e4u}?gs5=R0_4VbiEgpBqf39Mks4%TBI_T!$xmib$vCMPS?dKTNju?@q zFUbTFiVi}s+nB`XwP_k{U&Es^DtiY+Owb)Mi?*NRcfW3-!y2e_uJf&N{jE=9v13S5 zC(!g5{Uf|2z#;xtLmq6vHB@Zkr5gBGqI`XvNrA+-)NR8RDUyhJp!$FMCyRom(TAY5j{cyFcl#pgZF)4`@8=;bN&IHMW_IVDQEi zFEEtwxFjCL!PFMHtM^;~?9Ebq6W)eA{)RfN`*;msdUfqIvMk?dqPn@n&ut|kw$9Ly zsOlp^E0zWFV0*sS%5d9^#!7!^+Ja*jy;Vd-jCNi|yexg+r zN1F7Zl^qgA-s;QNv`SK|I_!`wHYcWHXdDs%3}hDQO#wtzzkbnl;p%S9#o$f5)?Hx} zPsfJkdZ5a~?po7S5D|J%vqChQw22@8F#_$^527rk0qa~e4{NG_KE|mqv)Dmu*N^Y= zdm+6uQxn8r?m!`l`K+?mqcZK`dB}PHqW^_Xfb8bZof=Fj`K=5w$xZnrRVhCP>`u^#1C=zRbQw_Ng!N- z)kejJAjfI%bGMeMX5uL5XJZ;6Fue1I!|)CwZdsPY&Gk3lj+xi3pYt3y|2WSHEeUgG zg)~5aU$|H=Z44B!O{|3{7|^h-l3n!Fu12w51nkTno?wdPUla$*QD!s#sC4ZqQk7IC z@6Lz8v;c)Ago;m9u^`zvV%V%t27JF#DZV&*{#2|QUlNaP3Dwi^#t48KJ7L9hyDeB z_Pen{`Q#fRF<@RPsu7Ah$LV;N#;&j%ej2zcWVYcA$-K7KJ4pOi*(=YGTUu-59udLY zjKDG-jk;J#DMW2OJ3E)oB}B1I6>}UKSC`gI5&1VlT<85b6m)Qo97ek9 zR^4qaL$9APy+4L}mvu5;4%q$*_P>^Zb{*9>-offlfk79^5yrp`9>Lboj7?1Zz4GX< z_qmSHUzj$(+jT*rC`MI(@M)#k%);FEGeOT_xtT`^w9-Oyis3|8%+bf2Zb@L>e1L+8 zA$s0+UjlGjXzpsUNp6l^2CLEVL-IZg>vPDRaPBRKA@_zgNxTSpD;syu=|pLhfF3QhuVmsyE4?0(LrQ;P#a(9)pm z8Z^A8@9lp(y#sE@dk*u{4>Eu+oo!EV9V?d{@be6gI>%0x4NDEKq#P}owMGr!UKb?1 zJE2p={p51xTxn=2iq7i5Ci4`T&s_kl5UDURm6v5W{AFt~AYr~6uOdNskwrcb@;8HP z8mWP{)=a?8aVjXt1KtcLgv9M1A73WcZvRUE@Az7B<{N#TF0?Dxs8-UtSP6Qmd>YJN zF%gW7Z>Hl6lM;DI#HnaXa4s+|hV@ijHq*YCXlPY18s=(gtnbmE&u1YQ=gngSs z3CJ{(n9+>i1N%G)x&{4eXFekGP`L@OEJ}aFxZB|d?&X!5xmg6pSmRHTLozGXaDFR6 z{$Yy;o*VR8&s%RVxkSAttUhhP1{;`~U#LLn< z3FA-3vW46vp35O7Xsb_d++L%jOrhV)Nr&M+#V#SZ-)hz>-ly4M#``zcNq8EO98mcc zKjep3QUKOK3$4uPYW>*z+a+N@IK~)N-mB%Et|m_*eKlkm`8%^XSS;8RTfwVdj*)E} zN#d;s6y{6q>dcI-NuH3*198L%5khkO`I5TCU=~fm*$kdX$H&39ffUXZ0K%FE)6+AY zOoBBSwjw>^ft+Fk`qbn;RLR5$2L;7E4hbl~-QE5#!OjRY3|EyAdB~zo3w^$30Xx#D zBH%-V6%O9BwW=kArynRIZti{`MQlR5e!CWTB=6myB4W?I2Y7hj-e73DHT7y=s5C8R zBJP}S1tGfWn8l0e1MBy-bn~C_;^2%d9BScxBE92~j02GHhAU;rr`m1umd00mgQ|`U zY<8-mdP0!a-Nn}8@pmNMM?Pg{_U|O$siN7l066$w0G@VnbkPD_9Dx&kVo&+e&@`pm z)jPGwpQk>RYFCn!M6*A|{d6pHP+@1|kaaXgNq6hDZU5>e!iI&RY#nMNPDr4(i{ zpOr1cxzjpUvx==lmVBLRTfYFSflR?#sF49(d141oskrQ3#1Q>7N$Iu$xt4z5Q zh>mc7Dx&QHXeyvCG0mnfIeNHp|8_)I)OAKwI3DtsT9I+Gmr+egQ@s&=T&Xf^9#ado zb|=b+tJ?w(6GKotOd1xVKy6(+if5`mb3y-b{mGx|Vr2iaZxQPoE~H_oEAuzApm3J$ z^bIdZ+O9Fi?NZ~bl-eKt@o6vy#lh1WZ32c-R<_-+d$aV>yB4_Sj#k(nYfVe4(u!8< z$Gav({?N3ylu1j|6CENHsX*K$<9-*dl<3EyIhl@Mi|}au}by;=c@E@Nx#3chqe`@M-SafZm1eBWZC@_-}lHri<4S0|5f#8-ZARI zbC=b*k;TbTW9>$HoI}Z}%4$!}8~~$%TA&${ymi#c;@pV;TK z_t|?N(0Sc{?+ZUpBY9?fe8gar;f9wpu~N7=ixknvrm^sp6bFn|iShvC$`-qMVfQAR zzOq<~2_ul`z);vm1&S0X6`HBMvohs>w0c=ysqz@fNN-%KJtb$0!L$~X{~00i^a(M? z*x-tDu|J-#_kB2FmMKhuv1s}_muCT5_Uyc3z^qi;hTT!-~K`lQ`!QG9=ox{k|wJOj36 zY=gOERqCG!?)8S1aJ%V_l44ji`#;CDn$SL2?ehWV!1}a*8)s10Cg!)2JAd`P=VK*H zsluAtrqPE7=S}x8-4$TYAOeILx{BkN2^pGXI;e|~EJV56V)hm5lA{p8KJf8tR)Zu~ zDpM#?@ulmyX`C3T$?TTOvYOY!h?%&uazz_yc;|Aan2^&# zgwA_!cQ5~rxq_P?#DfvKaS#*>M zxc>i!yjUc$T348!3$Ga3q{ooHsbm}Qe~U*!TUrqzDkjCwb&&?cS+)?!ZY(YY?w-&N zjC?q&f8e@lnGdeo^2A5TIjOY1LHCY5T8_#a>i_nJgw5JMCs#!<$imI7Er+c88N5`^ zJ`Y&F=-N?_eXP((BRC9-{b9R?Iu+udewlC^K*WxG@B?_7`kBNRQW0B z{9~kRKX)VrMv>jUe1c8+Tf}Rr0D!KF3<9r*_+Af} zPcUfaX_iuoF^g92_j-9(GTKO`Z%a!oB8P&sS^W5}n`7;FNDLePP~`YINlboMaM*b3 z7bIl^X0o_CnBVPxN81u`@RB;wH~QWJi=iuj9c$!#cbv@52QxMMh-cXQ^+PFIZ4C#dD~t;=%Gq&B~Z=DnxQNy3eCC zs&N~MJwaiNa#gEGaaDhQwL*l5A7F$S%(s}g*FV^vAY-lA5$5eh(r{mECsK-Tvjedu zTTAwB43^U6Be8C8dd#HucJo~iR!m=?df;7GJ`;vsB?HqSHiUfuQjiVt%J!pQ`sH;z zyy)Sc{^6eW;g0j=lK!RsSdjhulF1Lp4~2)_GRN5vKDh%dT)^m6cZAFQ^{mnl1d-P$ zz@3Uq64hy>gp)H+q=-{=jshIWVndhHRo_HZ7mdz(5l8PCN1yVX_CV8oaBVq!{wVgX zSyXT9$RuG1_ALHodbY9mwB8X}aMQpr_Wy4x*;CW(G@y?ZT$2dDS}rs4oLY@IH@f%Pvy zKD{u=Y{Of8cMDG)cb$@pE%eV9j2&CAB^HwW{%`c+kNy7tkd@c-mr0EyBcPgZ4!+&| z#PHdjF<$o!OXPJ|&Hdn)%c1mB%cA%fms>G3*7zu?&o+0=+zc%`d+Lay!*w2h24j27`D6u zC}!!@}N|GZE#A*_ComKDf1hJ#2kgK3Zj(!iIKR=%W z1eoa*Og4)_9dgQqbTd|EW?jbU5SA0taP2a!AV+7XG9DRVF}oZ>gcxEyt57l(+gZrf z+e%gF`j!gH%U%avSGi6Ux^Pzr{v z7Y5lo!r+pj-Q^lZ>|zOsMAuIvGOP7+vwwAl9Ky^EbHuh!cacU8W%P1z zZLFcs_b*TfkCZi?Jt&2qHBwNnze&<8kcBt%eo}y^oP*3%OaNfJ_w(akT)En3d>;Nx zWY5~i5G#Y0HHLFrk%xE45K(67a%S_8l(?lYD7UDypcwSIujVlTbAG2bH(Gm8-@PG1 z@A7r^$<7^{g1afNzboyS6@P>3p-g$T{i$fBT{QP@Nz+NM5S4kkxRT#tKN_0Qa3RSG zWhOei<$jdaqm?szczY9nuJGcjEkhvPDdo~9k&FwP=_qgZpxnMNlx%XbjqRi{U7!*n zb=0~mh*{BI zjr!%%7IFI;9qr)BpYig~4hq8dq1q7HyXzSNTOYlwA!HtqzxH7+ww`K^b>x~Z{F+9- zgB@vM9CG8l6Pb18=By9cznh(cmH{>oAby=wkilo8s}j(bvUD1{x#c|22+7)E-C-T$ z>$MbEdrKn>l=RJlksQ_Uv;tLbo!mnC>9l@3L9QZ48k#OPUm(FLSTVM(vQK78F>#*u zuO8AvONuK!8tv5d6j@`>U>_ss7UbNAq0M|!Hst=)i#6{hq|g~M$}MRWW_~LP#*M0y zij`YP-dW$IMu=U`c>HkS1b8l9ids0#5xurmB!WSKmpmX7TjxP!kXlcUoLF?no1BK_ z+#ErJf=IMhBl|;-^>-+U)kR{smFB~U0tlORZ;j*Fh4zT6C5ON$m`2}9l-iiu;s+JF zq>{gz^axA0Dk&n-WTU=4l9{VD%SYEM(aN3~W5N5zA341std|Jo9w$9||K8eZ!}$EE zWsn|}D+1qHgCO;rqRDv)ba9RKrFv?gGXDLw-!%mmm#ffd>o%a)V8EW)WbL^PS}GbX zzpMxyxT+tPgOB|E5t}PA|4m}cIQ=J!=O5??_6oox%119%`hG#%yDOIq%$(_8Gbzk-l=Xq`NaXPS#@bC_v zFe}TNdMzr3e41uf7MhzN0#{Hm4`fT~#41FVp!*J<+MttGjnc4azD*(?BwjJOJ~==7 z%ly#8!{cbamI|soIgAfUiG>abZMN>pb6v1-MHVj>U{q;K=cM$O#v#DQ#v8a1fwpny z+HMhD*;O1cZVtCHqSK%!+B{-yLy40xqd0G^)!t{s&JYpVkSUUmWj(lvdkedgaUL60}S>><#wV#QWWPPl`+jWrTmi*nR2t zhn4q-~%m`2){59Jpa>NU}#(4Wzt4G1hs$hTjy3mAc!O%0ZiObu5LgiQ$Lz@To z?-Shd-v7V)@8jbgMnME4L0i%NFXYs&+6{@Cb2ic-i)0ld-G+V2dB0%eyszSi*Htdz zDw#y}4`mk&(qQRSlMG4N6D(K&d&IzEWS1e&8!XpN-OCH>mTFJ*&!zc=q8 z_Qb={-a3a};|wlcfUkz&`TJ7O>4$fDCKxy!-rWP3V%w_IW6aEQsduIe2?Aoe49esw z@tk%e>n~2z?P_t|*A~7QE2_kuce+3Nm>s;P7>=Ww{`jcWWPP*0p?Z z@HSZaC``F)5^E9)Q2#xuS$x8gI(%7Vp}FDJefjW@MWmWx5Ah$n|778@h}gfYEO8_5 zn&$n5;Ltqt#dkL}*~5Nq%QN!2UQX8xdyX_bR53g`Pk0s{uoq0VKQ847*an7?BR(eW zCaui*8(PT7MCdq0!BWqDd01H|`De#kZ(gG`!7^t&7{Pf2%vo`x6hc97%~V#_GN4ur z8jUQ491Aw(b!4oVAzmIxQAg~=!sMc{o4tOPECyX%w*<1+3wwN~XS_u&mp1I>@hdjX z&^dy`ex$p^h0!{?cLq|p{ALiykLU7+R|alPy0+H(9N9dZ^#W3OE7Oj2?NAPaxF7X< zdns)63#xssy|4)X-07d~&DX^usKZvA;OvSa=Q4xigqSnmEjbWe_Ol?S+Hzwp zC`iTmT+A%a9`n`@TLICBS%dkQ$J})Vqwgz34UyTKy2sDKc?gK-zHG+mJ!`%l{M=JHYhuWRmGhc@Z*>rI z^?Y=xn^s6-)5FqY+`aZNOZI$cmE|#QE6zbg;-QLB{>Y{*ylz_P8*?g;YoSmMt+*jl zPf&dqH2P8|&DhT9Po}_7%H{jADPg6+f%CG0!ZOa#5kyjT_u zz>=X3gt#dr9Djef)>AUM0f8K=w>TM<-{XWT&yH@Ey>Xlc04oh*!adYTDE(nqw{Q=3 zsogFKLfSriAG3ESz~eqdh;b0Kyz~f2Hw9GTX~*N`0+JD}V^%Eqa{uo0CCsaqm#42H zVAg#0`h3>Pu3jw#n$iC_ZPQ?E$n*uX@~pC!hE8n--*^>(e7-aIdAX1=-Mgj!_F`p{ z(NzW=+8!T|erbMV!jk6r#}IuX(GStv17DC#m3YGnRtI6Xofh5@xajs01qV32ESEs5guG|2ABuLk2JEq#Wy z@T}r<*W{MXO%Z-fLj|^v^}K)^;5sUHug8Ako@LTZ|8h7p4rB*G5U$j`%v{n59<3#r z=zB9pMnqg#0Rrublz|gMbiCgGJf5YNKX`EUdKTg$6|5J6^-M?rWrUP zj}OP4aeUbU$CuM^9QpD8A@TihiFQEG{wIMEcKmCxCIS|_{D-hi*#F>88VRk#QwW#d ze#jF>khUVB%7>Gr7j~%$U`GS^1P?kypvOKfkH%_#u{wM6 za!8$DZdAeNX*ZS$029qVMT(G_loY>8g?R4g`T2`^^hv&T!~KSL`Spql;Zv6-*2T#c z$Z1kw7j32`G)^k{T*1-aKOIJ4>a{_k#i+J<*(BO^uH@1BzHo8^M z<%ZGSDH#td&zM6a*{`42RxKx|vUtqiJ2##07yrF`YT#C3VDJB;wKvq3e}w5(?g-$f z27KCDhhssUpEp>d*mZQpF!lMF-#8K5hI;GTk0}r`p7_FI80FP?T?DyyY7e47<%f^w z97|B$$h%EZIr$?F3dB>bY+H!qMq@OEcG6~d{NQ4)(yY;`-&ZCzV(QW_5t15)YtGFW zN0k!2e@3o&I%sChUA+a3d<-*0=ViY?)4-Pv(0M{kRu&_M$R^;`Np$PLrW~!XZEkY_ zG>a_Z(5U=r7c{)4*jU;IZVxkkJoi2bt* zWNZn=c$^FrwGre%7Fy!_Ulh`Dg~LAo?Ru+>+(VkZ+)IXy+l4xaiJmH^P0N}q-b*zm zPqJzZl#NdDcuBPxtw$ZTl1;=__LY^nJj;dI`*8%7dy$dwfkgJw`~7)gRk{09uT}EZ zotT+%`XoF&E`%0x6$&ZbRBDx9bL~vbOh4pDSW!i-4wxbXHZ+X7zNUdPedM@j zuy}sL5~Oy%zy#lHG1|vk8kz2aJ{lc*f{cI!DIiBf*_WA>>38}yYA)HJTvloE#5!gq;At zs9XzZ-pK+i%%4qJzQXa@;m_lOm{2ANn>hr_ECKdGX26W%I9dG)VH=6Opn)L%{=Q&= zSmm(zi+N3{SS|md@Ts%djQ0$HVcbhuv+^JmMn+L`Xo2ZuN7Ft0cY5MEm}w))>Gvo!Y1~#$ z`{3@Z4zGFa=_|y|_$%LczY=j8X_?Xsbb@^j%TC|%?H~FAQabv}y4RL3EX9IRd?DH6 z7w7mF!jKG%pj8=#q+}fK;Kf5n7(r47(^3<+td<~!bw$D|6nbDS*&wy4W7hq0P^!+O#y7MVTa8X%Ck{#-aGIQvz4Oz11R&gYDs zZOHhu>seZ(ZWY&U-xqgYJpL3lZ_&Gyd+B8j=rtdRlOt9YGLT(|4|PIHKS+X=JZdA+ zDMv}@>Vsm{cN+)9Ays=lVtKb?*a7*0(kZ5A9#j%Q)xGO~C8?x;dbmHm<$Zvt1$-TQ za&L!J)tO*bq-{aPpsJw*Z^QZ8vwj`xozm!`lPW(;c4n?_U47%J$EPm5#N4OJi{fGu zo6_(Da{vj{IS7bKG@MBohkbY$WWw_-@N+RYx1+(pbA#NRA70FuN=uN+3vv}l@``q$ zcxKeN)v_PO41k;O_o)}G?;ZQIDvl=$lTV!H)^-_Uk9Sn^L(T~9l(JPly;R+4E@S*; zoJ&_*tvE)EbF|M&d!~|FC9p*-o9N(I8JZ1K_`Dx0rVG`kt!M8%9sQ9>qCJYxUen_Z z^b$R&q{TU)8HfhD&qSd4c=o-SWTzZF*jLj3ReGD~_!v-wsT+$u;3<>I8b^5SbC0R$ zn4I!s`o1PxOJZGbf@uV%7CBJ_M%vM6e-HbnMF%R*XA4cz#Wi=hR4|W>N#}&IE55$0 zIhos-DP@7i12{hINhg}#K9qrf%M`_VoH>yq)XEtU_7qfwAbUSFL6v)p9Aqf2vITn> zwH^0E8dLJPV_L*Cksc<;O=**E6&MJht!cYRAzGXIDIL3kjzBY4YiEP(4LW(?1LiG9 zCGA+|;({**soh}V+PP7}bD7qfcDSWIi0zHMlO#YvN_hmDtszpqi;+f!=)8&nhtPWI zR56jmfae>m(Wc^0IwfMSBTsjI4C@Q^r)z;O6Gm|3+P&;Haf_R}pUmWwAF9;$*C#NO zUvo_cudSTirDE_7jzf16`CWqTJNnOzou$MPvj^I5JOG)>$hr^bNcFefTg;3Yl{c^n z4qq)0G-#FHZjtbSv(H$h2x400Qne>w}P=84xxTE z!3Jv+wz>Adq09$X-T#F6@Sdb2V*sC4TS`y|>~CZEGbS2hQyG%Z(E4}>WhKV=WSZSI zmF8e`hc_eo4d7RvIqUCBhHe>SqqDTRV#)cQh3oh-mcTYq992Do42Vyd;#n+{t_Ar4 zneeQl(3{_kuvwHLDGwbg>VfV=lF(*IVTwZufWY{CH~rRP(1wU{%mDVI2v;_{XpW8< z@7V90K4FucC%Gj;2h^De*dB8{Q#RAC+gsl$b23}LtciF&-?P59=wIpVMOqpYl?dH_ z5gPN5ANy(i-SvW8Xl1`o7r|q4H0owo>Oje9|E0M`a(euEJ46U;sI4BJ*7@reeDcu= zz2tz8&|i9g-`JLg@xMB7Ef=Jb5xXML`#usfy*fWz!On-P6FRZz7N=vUfm%Xc7i0m@ zb-cTfq`qNlwkz4GG-IQ)=J^1B*~3{9gF4nlEQ4506UKBOZf?CbE@_w!NJgy?1Hkon z#}M6eE#dQPlx$z~quTgIj%SFd?(*mmT)Qq|*?Y@(w5(a3NBt~ah~T-50F3IxE)9Xg zTjJl#8c0D~cP_c13kzg&K-m4&GYK%w=24p3q$AGt7M>u`Q@d5WOE;haTKkBkJKXiMt2}E-s4xu29dO5mCflH=#tb@juJJ)F92PM1L@ zfAqs{1(a+Rs4&LqH;!g@bZc1=&K4eBU z$ii)F`a8bVc<*=@C81NBa6;fn0o&$S34K2VAxMRIFg6x>7#R?;x;e~rQVZN(;zgy- zQ0+emjC!mh4Oec(DE+dJ8u!Z0=|>I-WBJ>DfmPt8NI~WsP-B4}rJ33Bly%ADv}GD# z2uLI1#s}5V<=9-Tk9CAg+FMwF)lL0V0K2r;H>?EGe1L?ub(u7kyf&%hov*h|DM7DI zpG40EwU&Li2w`U*bgwYIhL=`ZPPjGzj1vyt#~3p^U`2VHrcMaEt4!PA$P4U0I2EM; z)~gc_$0lt4aS!ZZeZ_GW3v8};xZl3um<)~${kw1L2&DT86l5gWT%5Ui`_FCee=#6& z0V;7ZAiw{249GuN&%S@*A|GS@ul`@M23L5o=cz~o5hNMHy??X*etkc$ev&o3zB4!@ z?1_MPkrLsOCMT$R$vH#Pw@*XC`zXEAo5fBal`ztx^F)#y#GcqwoC6XNk8>; z)}i@xOT07@@~cv|;4#6a(u-7FBniAW5WnrOw-IL29BxPz`xhyh_&+!03KqgsrY^LP zLH3%xa)QXm+rGU#KXdCL){37j3>zh$1p)&Ie6;(7nHc453e5*dJx{xa%VG#DV_|)q z+*Cm%F^~4YUY&C%OT7G<5=n=ap=^~X-}Zc&M*cnX&DM*|AZ{rSrPiO5w%a5a{(0;g z114wWwbJ2P-E`R(^>*@?(>JA)ISr7GamMPscV_J*L-C12z%>X8qn{a;jSbqL*4V6( zrG9$m4Ii8zm3hXYZIeH7>zRGIC-n_abset1Rsef}>tvAt{VP|;Q?}n6DAm@Ngt80? zB&E^>TdD5E%ntRCiMP4I34VVET7Hg)4=(9HBMhfo@iQlkvfq{+m9eWY+OSxfgrp?h zagdo{i~CT}HEXNxO(m<-NAMh{!NYs8@Zod9B?6u%hsZk%TS|h(!iEH-;SrFvYAt2o zA;?7jUFPi}EB*SPeiStW_gT5+P&<#mJ(`@lgTNo*OWx>#u+^ZHxj(kU*|F;!7=-=J zO&X5Q>vM33BoPfWM!LS`8bs$T2Tc>ghCs+bgh~_M81|4plx_A1B3J3T&G6$Jmm)m& zOA^m#|2RjvG>Q2B-q;QOIHqRnMoWfqUE&lhM*x-->zql}i?_N%Rm7qg)VU!ML}Rdz z7V*rxET~0XUA@J?5PnYfcVUjKdz5#b1%+GKc{oL$B` z!U3m>hDRBm)aEsKph7->{U0sPaWwn3F!T2L*&9VxDGjI4!`IK6sZgi97a&ed?Za5~ z^TxT91=1Ag42`mV@`pIZ0fDWzO#J&UGn6iHT#ZktROrP$D>+n8blNg}rt6S;x{l7G z03c(Pomw&)Guhm*Dlp@@&Ac7PJlL|MM(qHZ_@HPU3vX)wqbS6#CCQg8SZb3@mraW} z^Ct3r`%f?n<`bSGA*W~v;xpxbzVguwViox1{wX0d>O+XA2qweBsnlKSW3=73P>!i zf3h``1Qxgcm7QTzj(s+ru04G*^(T3xb{Pti;(s<6jgH8VpvP3y4VB-Vo$<)=ic=}A zK+&zel(r73@SLuQo@6FEg2DQwzD5x1aufTuUKanudYck{%a;4E76Mg%uw_cbB!ZzJb~q4o3goHc$9N7K30&$O9!$z!4Bc0pGi1z~$tLWU z8&i6(MqN29Z)stYDNz)sEWxGvt$^T&^53!!*4{QH;>3tlY!25m-y)=VM-26UV{csk znUG6)r{GGS5>WaejXZ<;cftHnWf;&XKc>ES%C;rcnLuFVNL)b4ue#UmIja}wV~#_) zOVN=_wxxp~hTwUkG0uD$h)pHIiK#~SlXHj&CiIvH+{NG8F$qWWNvGAYJA45x*PR*= zeXA*_MJ%~*&s}!z>}G0Da`AFWekRBbNlRjH5Z7OQmGzlD8alw)NpMMTeeHKhGZ5dE z8H6Vy5#P^)0iCO-=`Ij*Wov}id4||o>PZPKTQm;_&mfG8aWlNeUp5V zv7jZmv<5E5@6}Hl3uX7BH{1_!+5WY?aN}=UHXRQ+4FmLwH+zodGTGTE`#wpR7yhsn8Zwe8X5HJ;}>YGv8_pV_vR^g*>% z#Z*YKX~0=E>4k7>mxi_oFded&un)@y$yi69et$O7*6m&jzIDD=n$TB@mnz6Za* zu4N%WWX2fT>|IlP=8v8UM@y+i2_g(ax*j8D?EyIVGo24(7NqSKQLvv;K2Wgd?rZtT zSX%}z2RRli9R0ZQ*Q2wc1M1-Oz%Q3UFDpxR6ro2?3EG*xsF$RMZl4c1akEWIOJbfq zAH<&3hwxbRpOUw`EIQ8Y6?6!rD0G*wQk+~z2!rkNs)O_DbA9K! z5z6qor6;aiZZjxR6N)&C`wZF2zve+#c8t-v^btLKb?i<(9bX!k@aZ?)1p&6v{1&>c zuczfhjp8+a1nJ*>Krg<7#cWS>xdwDP_Ha-_9|zela9E-mmvZ(GXyCNbhd4Cx6{n^C zi8DodCy2uy!wr;(C^f5Re4a;C)6=S(I;H-Y>-eqPbDLL@iU$K=wWMe^p9N>A=a-%|3t2}i3HkWt!> zeEvZ=c6z(`WkS{yN7eWVN6&7j1=UrL@xl^0abnFGl*5d;o~u(Z(@Qe zz)gJRhe}kWD8}mZy-}qHpVlBkbS2tmH18ME)2N5XbsS?4;<3djiq(;N#v2=LPVw2f zuq+vPJWJ^TsETto%Z)SK_e@lLmXat)pnAXTS3!_ViwjBwx3k-xkw?Prza&@%WirUk z2_)LapHh$7r3|tml9j8M|7`RBbBLeF`|c{8?(N&ODq`lV#L3AclswQvR#^CBoNz-- zQRiQH1T-lt^FURfwy-R`Aeo$&eEQKXr@OTFSYJ?wfu|d7kXDVZ5V?&`Uovti>%xg9;`@kr#l4G z0)zH>xWzcvE4jsA5@W_t*aN|t!Q;Ur8s*^hDyO^{_^iI4_{^97`&%=0)nfD#n1|^K z1~>kNDkyR9Sq@(ZJP2S>uygkC*~^)np&-2rHO0&A!k(#_Vmx4>>sQ{)^-ZFPMQObTP7)#a7YX-Z3$Q(UT_qhVptu5G{hmE8A1J^^PXQoo*OcO#rP08@0pE~V%%%K%T~Q)?q2OoDs~!<2C>j1IXG zuuy6H2j`(1EYo0)l-c@t9a6|s@TC$}!XrCXXWh(D*O_r60$0OkY!5_*)rsIE$Fwfw z*hl5;)ZaKUHY79`&93_LtxW&KvRPLTZDhCKc##yQu)CtQF^lrP)y7LlfuH1@ zkO}`3&iR)ji&Az(Cy{GvzLO)l%B(ClSUeFov^kR^n})oP<#%|dREBCS$gKI3q#y4i zJm-EkANigbA|v`oOdeqb8n26<#Fj0jwJ0G4o2MxMv%B^@a#|mo4XoerQ#t7CO-dbD zC%d3&VH=V1oiUz1@5||vg?}`1wt#X~zcNmxIc!8wBSj-s1A1Eu`OOKK3$Ed}ul6DZd~bV^vt!lZr-y1M%x zC{%ivDStbgzNuK)f^?##2vVpU6*4$jV(-F$eN!H zWO3<-XtQB3XOq<}8F#$;F2hyS?3IA)mW?>UemNg+2gQFF5q)f87yFCa?V_tL?t~?& z#esLW*>{;({2Rl?U`FTHmU?b_f~}rNC~B>T@4D8vKJjKwG6Y*d?}e9g??34hFu=Zj zF*kx?5xUhKFh1z-X5Tp3C~5~h>nJz%!^of;qH|DcJj5lZEY*`hsJ!u?LTfs2n*!?t zGu;Anb8iM{J*ne;ktg(agu&7&TDZXZo^;v>Xw+*fPLkSUcoCGBGGXjmV#FQm zXfu#2vkRg#NUs%(rb{-7RrzFh_R^YPE7^pInINrjZJM|)d-=|B#?oyi z{N9>m!nq^AvYywia`4H%5P^+K2t|$Zi*5~U)P^>eLNRcOsvxF&r{-5rNyDd%%U;_< zA?qX<(8_^)3W*r!us@C3@8~Dprmr}K?jv}9ZQbz5Gsg{_vS}V*u3_v;O-G90yQHQA zRtHUovv}=ipHPMoE6NcLk<4(sh|O&Gu6VICa9_5@!~?_Erl~M>oauS_Nu>)(M0gh_QM3_w02>mmx;i$5_-piNDNAX3x$T!N z_X2ylO_SeCfzoGkeNQ5~qudprlMx{gY;sdqRxTIXkNQdy{kqCgap|VcCJ+Oj0nt2} zRPuP(1lQ?Mk(NWO&D#W+m|moG9kV6>3Rr@Y1E}zz+f$S8wN)X#XmnOGh994Sd%-}W z&V}1pZ;aaz-_lia+r{~h>E2AfQ-$4+wRcVxP8DW*y-NjM{88FbWnB%S*YSc263{70 z-<9S|CaN8@n|Q(agCB&5y+07FhPkO_)229>6%56?_k2-jL0J>1uy7?ghe|2l5p=YK z$~VaNv^w3X07TQ@i}9KX>dl|u`I3z+dTG8wi7^MA_2%)*cKJO4&&t;YaUlkX9LH4N z0#A(T)S2t1oBAnbxzwRMNIi~+OGjbb{Ajfey=nX0%J08=V{ld zc8{9u>IWxzf(e}yB{;uF)h!iFwC`XB^0OFR(XslzJZ3_u+H240tVv3voN`83ZX-ww>FF^rUr^HDeLz=~$VXBzJ9KG-D_z-Qz_2L?xE_{FO%g-iGe_pVa)bQT1 zry*;M%A+W%djCqHA+2ib!2NBqB1VY#j-q@b`kWP|8=2IO{=oC~O#(0>u=|u?{A@_z zT7cRsa|>l-(9xen7^0gcT5wCJj)TDMNY?xyod*ovTN;GJhgh}+;*fODfu(o~R|V4) zn7q`StnI8_Nk^CN#zfv1d(A^6dowmy3DX&MRypJH8d+i0m4ZBi)=Z1!K18INoc7$7 zbXzQ`{ZGSHi+3#tO6I=YOZ@87IgAAJvm%TR6zIg$x2PYCa>pTeC~u8Y5>~xNWBMUl z_@sNvlz88X7!lG>YGN>t5@4@ulmQR!UtM}>_r`2V+3bm-g4^C5X{6{~NQwzz%?k-{DpL|9G>2@hcI7u*U2M;Rv2pbT3lc6j>qHL6_J=3eD39iar2 zv@~!JlLrA;vXF*K_w%UR{Jkz%qgbF7{;d41{MgXJK*b5Ixz(^ke23wrxVVY9ziuS4L5g%KR}#-i{C*CWQ1 zx5$D~Ubfi2=w?!poPK$KBenrgJ|fA7y?D_opwQg7Iwk)j9@dK}Zh#s$W(m{?hC z&7ij_j#_35#q%rCvE8(s6W|-c1l?>1EA9TDYKiD{FFAKj@WJ_>6)hzhxZh@=!uxYe zaiAXAL^t`C0SZjk3WV2d^fbKq5PO1n3{?V@8~0w>VFZGVYK)K4{nDw&&Zn5cqoa@S z#WaYU36y*P4yZ2oaD?lqq}_Dz_UgP-Ygt>w4zx)Rw7J_67tIYWF|4IXss?KV+C*A5 za6fx?tc{F`l}4Ecp#Yt0(A36pwu#bIY3FiU5(rP7C}txZzgI^@O@2ml`oIwk1>@U1 zTp}ipQke2>e2kL0X!{ku&m*;=iuoG{uqR)tOr*B2OlSjD@#wg5N{#;63>)k5N_5RZA^~O;q14!9PchYcAyvCBmfQk)Oo#j}@rb z-fU-}cN(F^0uEZFwfW-Ac!w|@Fn{@(z@E=pA{w<39+&{Usrqn|jgwGI1zzvP4xe~q z?-iy5pUH12@Z*{wz*E1Qi2i+G)R8yjAJcj>XIyf(g>yX-Iqf1%N+C}$z}ap_mzDzV zchYdq+yS(J9x?)FiLeIf^(VK@lhG{T{#00RYV2lg8WVy|hY8G|%?I|QQ?@H%8Aw2G zOsD`9zXKj?>AgxHAVrCxuD3Cu<}7vHwuc8vNog=SH1+y-`A2DPl0 zG#*FTl?G3MU$S7*ung}qyY+P_LP8c*&)9yII|%8(;18MA7tQg)AsqG zAVSR(R1;UH_-Ig3z@x$AJXLMNDml=@fRt@HAMmSvXM-sst3(|Udp=(H!#Ci`KM@=n zN0pv7JGGLpliPW)np*-EuRmds#~icKv+XsMX=ff#yWPwP*z{UsMf72O?J!y7K+_JU z4y0?#dP{0iwBQ5V2&Ie5BH_B#!kT=BKOW48Iy*fma6)rr@PBNv5&2dP&RaZ5tb632 zlku@2&N4>4QGeS><3i@RrhY2_tZI9V-PqZ5e95BkM-n?HUhuO&cYqfY7 z(#0NYhajyg%_UdU5SG3KWtCOAH)v(A(7S588*(efhkUq^$*Y3E?bY#wbqwK{mXIgH$86>vu@4gcDcggCn9&W z7bHC;Jy3S)^$UA{>N&CT;{!%4Bvnq2t(j7QolwVHU+xke^eE}6hXLe?Ys7+x5;ww#EqjWpw;K%QD~9iE}nng8ql2@xg73*A=yign zw&$f3J4|VNe!PxSqPU+eeQ)z!NQnC<`Gi`c2GGL0)Ze%7I_1d7=?+^S2niNS2{`6e zKNuz>+0MP;|EEQP=>>e+R1l)$|DjG9xvGhD#JwoNrH}AC7q4hHL(*jzOi(LjPDu1X z{)Ml87P`97I1cpMb6)aN`IdGJ+QBTAb-Tg9$LS&kD>S#|lj|&+KB(RAq4kWsDprD% zLb=ide?V#Ufr-P@3~lymy>vwTcWp-BD5gYf`i9O#_CTU~OXNj*u(nh{h6zDIDQq>@&8saOE{}ugv}9Wvcx&P8b107d zD@TjHd>%({!8Hr@e0{{tm9Tf0nV%Yv5xAMgSy)mlU=#o1l;Q&EAcOR8KOcw?p0#s` z5PF6@{YrehUF9ng(OqspxsG(KOqQK8J&rtkKW$GkS8a{+Fy)tqYoP=%#kXn?NhfmN zV~Cs%u0lFiK%r*+z888 zpV<+z$|6BQd zdA4T+{+6cs1aDsvNb~U~NVWUsHkVn>d&D`q1)z3L=iP=D{7OB_%zmHkoEYu0Ez7=k zIp|4i2sF|L9xgo{2Ej3jQ@$^A(rEC{T8nO-RR-gs0BllZDxlB+ukUK~FIH32Iu; z-e2M!ySEuoT37HFYY!?$ADY$=cTrA!{lqGZc|_OwTO;|^H5$~L?fYuUFtS?nZ^?o{ z@xIs;*p51+NZaw90_&H~GUNtlfRxK+KS!-j;T=A32h$@CM0{zC-1wMZa%~NtWV`|; zk)sTViN)o5JeocoeUopErHh1t@RtWCpUEUXzD1kr65a+MX`!6ziQwUsjhO7ROJ|={ zJh*ZjZ;GJsoVgKY%647Nm%2DV3@w4Py*PgN7A1?{5)Ojq|8s<}g?zkP|0Q>2$5z zarI1jXR!pt^r4ys zB&v~>4BZ@^a^R&8FGapexiVBt{@y9DMv10htN66UB;?siWoykGMmCye&i`M{92cG* zDIU#`g(<@+gfi<>)HfmPifIK=`p-EL-Gg7>nJa@i=iM;MYy@5b8Yvwr03#XUW8`+% zxB#?pr^*Il*T4s!@jotewGw{{?*2>%9c0_NIGTTu%;#2c&-6k+CuKq+RY*wJmQtaL z@BU_h?Kip)ZFSn>wsv=}0kD0VaLo$y^#&wY_FG$oT~Ru$r>!Lw?B{!q2fU!J`I16N zdr2XUUT*Xw@2@olf|n2Hq5#|Prl!lan9}#%-{IbDlm=sD&n_v*zBsdjz@wKdBtcMw z>K#t$-WSX^oQZVfIv9&z6wfSjke_d}O{U>FCcwNYQpD0>p!91oq7*#FOT2^Td&IDS zn3HARosX2u!~1CtQ^w9|K{0Ubs@q^fsImct({Gj9Bt7%+MQQg0Y}>V8yBR2H0EL= zcP$hAeD^t}F8fDX*S{>v29wk$JN$zsoV3p1%x1qB(&OCbci0(~r+hlGWf-*M^x-rI ziI^Kk=20*A(2sHLUd*CG^C0=jusWBDQXr@BTXZl;7b9K7LN-w>ER8Xqpy506d3q)& znDXA&3oo`hrBZ(6dAv-DUHz=>ieQ^TK3!VN)!>Ry(2IU+vEL{Y}y|Izl9aZ$Zdm{>40gLFwtcS=e(0s=!b#2_t5NvaH8 zA|Z&Rlu|>N)R2flfUfPW0j90=04GI8&uLs|?$PwB$SYX3bBt(3skW zBO)`r*@L$)lEnJGPtWF)jo7F3#(yO1`?I-JP6?-g6P?S=xG5;1bF9;NmaQ)SO@^zW zf+ltcqH?IW)O`HuJLL)3wQ^IG=tP`4(mQ8meyu=E;@u-X>L*S!@QG-|zTHyjKRL@H z(q+oW&Qs3Ek}GeZM@v^RWtBrz+ZTsNl=Z4dc^*A{50ak4-4!wDw}qS|EX0TH=7J$A zf#u%94ptM?)VMxzV2`9{1%e6@yq$9_{WgSE2Ua%7Ip53Uy`!`G6`E$Gim=_ta*5S3 zqXTD%{+kG8BkilnOU-6-SNMv{L`8{@>REU+->_QiyR9$;p20hiPchA+8pOm3CHQPK zglH|mR5S|hW94Iqy52FJIKT!r5NpZpaj3TAdq)Buqoa3l+7-jJmx!vVJ>X!50#FpD zq0NloG3uC8>%M_iz{Q8g{oMQ$Xe-)BVFG3uJ909#HVsBGXD2ZtpWW7`(mMzNJ0CQG zP((DUI{h6e<<>F;Ql~6Y#`o(^(6orC#Oyi2t%2J*J7*pU*}TC;1$xYj!5%Jk#7HQW zsfkHP0)a;N=i9>?U=MOjsx+J>|76S3CRe=%oZG`(V4?7Ofd!BUA zh3oHmJj!qB`gD^u#>#80Rjum%a<^$3yTnjZjLyszj%%!6g-u}>sIA=J^ah_!Evir? zi})h$t~cz_!?;Y-2jngosdV7U#2-1O{Gq>o8QjCp$3=wWg`8V|x617v9Uh4#W)SO2DhH1LFZo~^pWYz(qe+*hoL~0E1C-j(%)i3`nkzz%bDfF4Vg;%ZqsIxL^7V!d z8t`4>75htdL|o&%bxJ{Xp4Xv&-k6x z0@2}@Zd8&U*`Ih9H2hlx+RA(S#f7Pq1S&fh!Em1WQ*4RM}bj!M$^!nx)P%EVc+mr7?782|)|Q3tlt zNl-+Z5`vM-9TkxS&TWoQunk*CQ7~@|5YM{s_*q~5p#|R%o*&b4QVvdBvt9mnW_b6g z=B0(OH~Sx(N=F-4U_ay_%XBeqs|;!b(WGifR5w90S1+_1t~8?N&Ku@_Jj*)x_lN!7 zkAmU>YrQZYoqSs-z@2_uG&I)sgJN%|-T9`1(2rOR>MnMyJ{NE;AB{LP_NFC^yF-nH zBqTpia*k*%{lpa+S0dh}9iQ+N+Glz%x7(8N?gbTC2v%DvO&&qN#h5h2P)No5TNE~s zGH`hRE*N^Re5a=!*G zTfDNOX26>7u5;xRgU0ihzcq!{K*KsJ{OWym@o)6U83u1YUDxBF#unN;%z}D%TbuKi z0Je;do=+1499l2og4ZKkKg5i_b!o5VFd7Na<1{}x-X#-uWWihDVd$y9uaK7I1`YlB zP*I6?qy>9Z!uvy7#+#==3ZDKtE}z@_(084I7nb#Jg*YGOMaldmlMtVaxF#nU3wW+Oro@=9^%yk@*LPRw*${k>xn3=Ucj3aF{^qJU0r;&8M$ zcSw^iLXjp{c~+Mf`R|4NM4drF^p1${vn4l@&#O&|Uk6Z+vYm8KZX%^%%S6@h%rLcH zYej#bPCcN+w8+(?}2-!KI>V#{s{C0o(A(*i{N8F9SU12=usub=ePNV$L|H5#g;bhPz8a~ z@SS(F;fQ!SFSkF(;+sW05`@=N`q zgMETx-EoPdB|S>xuxB4ZDH51ISXBk;KDmWyG@3}RE!Vz^{%OD^^6mz<$FRL*;zWrP zquCR*4BD!ut-_7CO-t#0iVOx<^`HIe`>|`tp_fz80UjILK6%lLa$Zh!AQ=kT-^Qn` z(H9zzUy<9d`1<=A_5;Krbo%}kQ*7*#D4&Xh^)GN$@u2&p@OT@e!%*FrUKGNetKTSa zv|`vCnAECBsGv9TCLzGjT?2oV+hqGA@VKE$8@x;Ib?YGoqZF&OB656|Esr={% zLk}Dxyb&^!b~=~kIUKkj-h+%V_J6mh$Wcpp{9CBZh`^7Pc;|75`D`T9o)pvjy?c+Q~`WeEgHRn>(XjyDe)eP^J zYp=%mpvclP{g#lnM*K)P#K&@80c&2^@JYHP_U)wbNx*RMZ)&vQtx-TOWaBC2xVLj{ z$WFCeaa?_3xRqJ|v^*{|0+x6)n;2p_IzL{xj5_sqx&lXW4NHQ4i_po^QY0v4AX?lE zGw7_;$0fQZ5!SaVldoUKf*VzDv-6KfsRU#=^MMf zgbb+#C9a}!Et-#X=e3|fp1%U*f$iDmb)unutKLtdX4Zr z&KM=qBGa(8fjn$c3C#T)V$y48tPtf5wq9WaU$SxlLkWI zLHR42#tbUusbE8v;#{E>#C>Ly50kbfb{~K46I*JsY`s5I8KWPysL;sqiJQg_VvyPr z9LkJZar9xrQq<7)?&NJ4ciVyYG+lRAwQE6=HgG37$yj^2lbBHHHz5RWETt6~CQ^l2TU$G(Jw$rwt->2hkA}sZ<>|J++Xom0Ap$!C zYDBPJ_eniVcf*uUp^HW)L^G0ibZAgNiyj}L`)xS#kbmV${X#UXqseWl^x?rW} zcz@*lco3q@S(i*pEwBv^{rC7^`Jlk;nG8OMXe{mu8Ge*d%=EVJ;_KfM@v?z(Ke?;; z?JenWM)Q+?%uXe6k|4v!&TsTr+*YxtJVq=;9{JWj$F_P@nf#&`(a??Tn&hHma|{ql z6yB+1ewpKW^N>Gj2x%gA{!^wY<7=}#2U{!~IZ~$2qCO^qoO{uiJ77BFEyOU%e#rij z&r;dP5}ON7ZtSsgw#b?DY9k+u92y<`e^(AUR@hLUXPRCns;e&`dU|8bDzw{wI`Q+D z{=Vl2ur?a~qEof8^e$^ohu(_QS7V8s03!S~x#O(dm<(nY4?uxVK}xzNDTD z)k+o8%cT?XlVW&8fLw6<)oRx_OLV094k0j}~z9wK`ND4c=HG)AZB zv>7i78L~8?$)MXDmTOZ68|@zrZ6M=#?h||^TmDF?bxNNEdEevJ@6AHCe{slAMgig~ zW(W?KZ%JS?``A~~^w?ty#>m$%JaKHbFgISEL40f}uBVeW6L8yHf0vHZL4B zXf+Y@>U(w1;*->|$tk_U)4_?#m*48CPWtuh9%|AEf&^6Gyzday;4}XjmpeNvMJ0yn zs*8TMMr9bTWffo$<#%HG!JN1cqnoc>4#mgfq+1!(YiF~%hEfP*=s)S|l2wW%T@GKx zaV8sZu(R8L6G(c@jB~s=91}pL;(c&tE|TRy6T-6zZ4r8puto;{l)LJ%^Sqh3Np4Ch z4hlm2ZWi3*N_f4|OfP+pQz1Raf?smG)kzXk9m|8~L|@y*zijZl_9LlX!#76IGV9Vu z66DXOqElKyE<$Gs{Z@(S#G<5w=K)5q6t z@w~W-faF!lVyL>vK**J}F#+<`*=wK0^Vr!_*SdxVzd9B>t5Q>!)RC*lz3<|;F&B#+ zA4$H+7hdQxZF{AL0YitoWH9_Qef?G*L@9v%_%ZT+N6Hf^o#^zv!)bKdUY;P$7<8!j zw?V^94es$AdyRY08WW_RKul%N;WVd4l3!BpnkOZuY$_%cK>J|_6wAOw03+YNuHO53 z+z>ko#uxWuk|9&AIr3nY&y8{<0j7ejt!z<$*d*^<(dYSDcQELiqWP0^EVt0k?0_Z8 z(;WT%?HcWqVpIQ32%jvJiqDcZ5+}H6LyeCR>-WGfg5Va*{P7=~3hc!;2H$W*ULCt# z^7~TPth~t|Tp!_gGpC1sxr#RPGtSeLEW@-7WhV7!?Ke^FC_PvBfwMQ5^wNS6r&D&r zZFcaQ<5%fD83H}SvKP&Sc5#_n+F!s27_H_rv9MA`Q29{8Z_@!zLhEr)YIKP^%4`lGj}R*IJ%bBlGb>W41z<+=#O4K zL0$?ia>$Uq@#sC?^6T_;8Y}q14^OToPam$HeS5}(4h_~Y^bS*>u2p(HNF=-PxjO*! zOPAEM%|rWWFGRwJU*;@9?n-~}%|K=b5NO!4BDp1H%_J}PqNU6l@3Z$2aJ1ly&`?EB z1Zj^z;mUfn4qP-7LK;|B^npKLB(y9%o01tAmP58J=RGhY4its@0gM+s-`jd7l_1aH z%7KO|3#=gwP$e9rTDaa4a`W}>an#=2@=h`aat)>ZS#KOruc4*<4?-CTR|5cNgHdKHyEh_{lFk88K zK?j|D9SrcOw7oym0O{V6`aN{na+X-(fwU~~0=S`z$WqCbPg>E_?hJ#xIj6yTujrbA z1gg(=xt_JVdW*x&R`!tFKFrTVP<7BZ{JSWfn|wP$EZx74FG>D;2m;;0l`+!!Q5+~K zYo;iL{|Sd|Qmp5{-G*RO1zjuNWoFH7uCj~J#`NCzbeqEzITn?|4t|Ul zLahOJcIZhx&98M5gfX)=ll;W^*E3%@q{*rND(dr`IJzoXKS!iHUv zapPx#bU5kx4-#csWN>V%bKGx;_z92%Fptb)^}#=Vgss5^o`2~jrFQZ$34#-07}(Ne zUzq3!VNn=4U-wp=DXn z*d7;iVc{sYRaJ5(e^%-RI(guau$Q5ro$CaL$4Sk0MYjHwGq?39Ec_?P)f2AVQ^ zj#93@uwCjo4s>MpnbYo{qv3A@rL+h9&p{6Y8^qrs3i8boN0LCf>;aSqH82PyKxp&;*jEDse4MsyqLFg=us zF4E{P?iq$9xzSX3hX#V|f-+^KYx0;Lp0JQZ2A>r)d)20Z*F+h}Gi`cKyl9FKeX1fb z_4#*7O}DD{i6;L88@>G50+@JI}% zTMe^hudtJeQ>s2iC=c7W2Z?&Xm>Wk5aZ=r7G`gPcd$22Oh*Aw2ohR^u;u=cq2bkeS z&cAS=1_lNxxsveIpDZ}RrJ=xlqM7_S*m~_7!SP_~2|r=(h1Iu9E7lY^lpOiwF4I^remML+ySjiO zOQ82mD)DtMACwCDrF95L@hcH#p#BftcT`0b`cy~<9*n1z_o0#P!L%GL7MYnbLWHoX z2m)r@*=}zJFCMf5a*$5sPyrn}w5s6`Mp=afjqmZ=#Ac|j3`(u)A@mcK3^Fvv`diXu zC6t#$CTJ(42Dl(>0c-{+XIU^xqt5PU)95nFKUB`Z!==98@Z+b*RXam(5;GOJ$%g3q zMy4H^7}W6#K1})O4;z6j?~l*2fuKhMcgAl#jsN*7d$shyPw%y&fcGs+1k8*~y*b!# z%+;VEB;tP7cI-p8zQd2{7#<57s=1#XdsC2naB@)VCn|Jgc#AgVU{ooIYqisD6Z0!) zDo@DdP302;ek>>>n5ehz2rY~E8}z;;8i5TJa@}PjAM}$-QlyH&1Drh=1dieP{)m+v z)9A~~#_9I;XWZX&5A12xZUQkV71Jv>?i&jcqH8|spGsH>OC?0gu=6HvDIhCiqo_zp zV*e$L!vg(Qu(!XEU@}l;%TDCC59upb>_Nk! zW1w2Qewf*;nGCL-1y|mPVeGR}LY@bn4Zlj$W*g*B1G;xeNXWQ)p|lQG&}dmsAKd@e zwig>esD2j(__0a_;&g6@vDxX6OeaGQLm*TuQAxnCkk< z128GL^4f7}Wcqg!bA9QDS~=M$4A4D5ce45VTYbUw0)`1Ii^ zjhNHowAd#vg?X);M zBo5pcEFTDRXS>ETo*51~vu3>iV$icUar|lXqdI|hZM3u1ft`8w$Zk3eT{+Bs%;k-H*m&gq+NGxIj^{7YkU#rmJ9L;*_pF^LBh|E|>7iP&T-ph5* zUBo_^tO??2?5t`{7;>Nc$Esf@q=kF#%%Exs0ew4JIGIvoOhJ5x6QLu&tTfYMLqU*7 z4A3MhIh*|89S~H-jODk2UVO{C))+Hkht1U{Mf%ykn$qKN_`Q?_bi4=L_Q9AN!jvRG zDktj)-CjAdlXU)-X^;hd)9s|?@Ds_};Xkl+b$SJj=8Y(oMJP|2ItWjjvOEIj;Q1?Ko-u2zd_~>cnZr6-7u`f0Iw1ROGZSf?wxBg!F**jLl3o zvZF(ZrHKiGbyseT50`8_pm;((1jx(1C~Pu(3aR;?c_B6d!60vb>Po#LG-!QH*Hx9< zESGUx&hjRL@u_VoYxT4Ud~rLD9%`grF36EQPnwv3`p|K>h%e<~L8mKyf8K1Xd+@bs zLQ3iDBGzk#VYsuS$zuwGZ{uhCEU-hOW?MBe9p@f9gqVbP%8K>2{9hI^q6K{a1XN2sm9!k0K0jub}{r1rJ2Dw8V=Xsa)yq*F=LOwjOu4*Au2G&K}WNQbb5C1eA#P}`#Xz0hGCZ? zMa2-TY^2>y1c4q2Cyq5;B(M05m<)AgB^f7m2+#JJK3(CTXMSXVLd=rzR6=;f)Ua}4 z2S-KsAJM6`uju|ZmUnjPwfvK}!a$(+?_d4{g4~qX=G&Be*I_SY&O=Bgwx`OH6`@_< zjvhuD116~r92W;pOfQqsdcBbsIS~fSw_DAH}OOxSNRfq!4bD~L7^&sY1 zr)2_k7jA23Vv$}V)vR9!h_*u12M5aA&?GYaJ?QDmQtkD_H`RoM5wc`9x`P*=)>a{c zC1hN}{lZB@v+q5lzID(=7Kp#bls`vvO?q!Hw-$Xw8Ho7gq-zP}@>us(U||z@<+KJ; z9H=*olhU9`QWrGf!3S4KkZy6#)j`-YNmJENwo~2|DfS?NUUUWcM8fw7e=K|z9@{yB z{MdLA8N^ea0OK9EEV$kZuk~-VE#ZN`H>@Ic!~jX!8yonW9?J;uQoHUlW!XMGjNboLX~H%@k8-O$a2oc$hFVCdL$xgx*oOOa zPs&dEOfIUo58DNe2Pk*B*&MUCeo|k2J(kUMA#vrA0)k%R)n}CNLnbh0zQB}sRL^$$ z0Mo?4fE-C*x0^TV`nJkoe|y~MPjS}Hg`Drip66raNCMYN$O+a*(&VHkn+=8WD+M>r z<6R1CaNVv4!V_m9#fSGs0@v|KQvNKO_?2|Mmir^egh9~pS@6QQH{Ws?E8E?bIdZKH z*m>p4V(IaQ5}8;Omzk5S(*FMESni>b^!80*Y0^=J0j`N(ifevL*Jh}L&be)xr$HYH zT$(PknEbsi$M1*Y8eiJkcD_l}9#G5`HtRgH#F@4i(I{y9E`UaeX9GtJ8cL3s-sA`O zZYXnQUgRac-{l>ueK8&_4GD7d{yJscwmq^PDINYJ!%eDFaJ&V!e^p>CAilF*uz_`m!*9En`hrt$5oG;Gg_ zH~A|rGa80o=6c-7@n^d`;?b2ea|@ch_{zKaj9=|dqro)R1U0Ye`C0C~P=2gxU~&y- zgWg0>+DsA}8EYiv(>bTN?Ty6M=O=hK!kmc-N9ivc zjmv<~HLNMx76@t5g3dfm9yOg1zP@*xM6LP%K(&U}d>c}{#mW7DCU9#mHNy}9;tim{ z^J<~@_ZpvLqrLn829gBei7`5CPnP1|=OvIE#Qmw@s)TAOP_@@H?Lz@ZF>7AA7=&#{QuY^f@S@00b49`6Q_4GdJMAHy;Jri*^8KPRP7~ zJVUxB0p}w^GI*@vqwE}=O3I%Yl~1@?01)T~YQo!-lK7FE#2zZBe+ZX63Ic$UOo2<9 znLi-mV7(0tRp6e-1Vqe{2t%NZT?pLk77h6L-^DD1pkg4t{K(=CR6%=RBlR~Pf>tyF z0md4awHqlRbr2iuO?#i?7VE=E4l=r?eHWt<3Pmt;o7obmZ4l@@s%;MsvP&!W7o;khEz@1)93G5cIWa*63<2t3(vmVw42X&U-l{@U-!>Q&b@=DKo76ntRiR z-T|9oyL~bHKrlx@^w^rHm^#-*qm0sLW5nwzzLfH?vwr!!Xsv%g=@}SfHd&lE)lG62 z1)Xx)9#w$A*Md7*!X-4eXrhp7d9zE-QpkVXr+$V<4m2=7OF)5_mV5p(*5ONN(dkH) z)bq*M7m$oU&h*Nxltj78EaMSMbz2uNZ38Jebv&&Ro-wOHUDInb42@05eSLFT(MJL8 zkrA~ok~s0Tg73te&wdI!>Zoa7H~q2cri6O?XsOTuu`@0y@vnPLmMMmOx(p7_z+;0x zNT){_SfpiFDLHGMALU|bHYK3em@^G#8@3eCr`N^{b+Lnz!v@rX2RNvpINCcnLa;px zK+M97;(3f8K7+^9w7u0#9y?QfUBR2)-V;UEn^^#mp1h3XcR&R@(Fl=a49G}j2RQXu zGsSOEgw#CYuZ=uUTOCk}T#|;`<%J5_e>bK@QZNc4vA(F|ALh6{XEYS;w$|@gn{;D} zcgCFtS4Tu3yh@7R_IStvccoZ}fcF;4AFbJ+A&oVQSk z1?`~d38Y=YP~1-xP<#BwMp_Mo$PVoH-tH*7rS-}7DvOUxn_F9J0>~$+XtL#hzb}GV z8{AuY-1G5a-co}tF1z#c0p0hZolprs3Mg!|?Tf&j$k}%yyUi$6cyFaobYDC!WgmU2 zt&;jP`MZ6A^_r*9bhL40$;SsjUwDdyEltB(F2`EMe^SRop;& zZeu@Bf)8@3RG4pRcvIKn#W;>ujP66(oP^AFLc!L`goWL3aWkPb* zgX%>X2f>eH#x#$>B-Roq4mPCrdv2^ER!ds@&{)ktC*uEva~c{0QMM>Tj7c!@d`UbF z>{|r2-VOsSOwYscHoSEay9zL=<=dWwhu}wZm)96VywE617G^IAjaLs!qbGUZ<6K-kCxgwUoG{02YNEZv zkcI1_rnlEh={K@Kg$uBTNxNaA|r$`!rB@OkHj1M{IbT{QvX=@-j?^dSP?-T$8I@p5)#y!xZ zmBO_OP}1(^IRWer(Za|??zB8P+a8U6dTVVi@mjB=weyq$zN8+{(uv&x9iyt0wI2nm zYZo;m?7J$$#<^#2#S{A;xJ=;@L3?(oxuyde!wpW#SCWr(%TuR1Hi%*ZaNF zs1H*wk6u=PjC2REz~E7x8DJ9m!RuMl{p6Xc>^|2|eLa)eLkbV%!GA`7)LXdS0EwJs zpm?vw^C2N*Zp6=7W}vDSA+qINbZwWZ`s=0e?2sd7Iax8xv3i=7l_AaBa{yQ5%Dpr> zHO@{*6P&1eK7HoqqU$(reI%*3bt+ITDao~BA6K*mst=3P6>bPe>^&_3V4oN-A%N=N zy?4hQ14El8#eCApvv_d!@ChdK-H7BqCbyj?MQT)+f$1_|gc{OOu!R+V_)IH_6{aAc z^O2=}fS~B~o{Co8no#FVbfs11wp+y1w!PvQ~Jet$p9B6rWqg1c|r zTplK~5GrumwV7d!yk>O&_+~)kD2SijmTeUr;w@6$*5nmLKl(T{Q-w@kC3!{ln}2YJ z=Po$J9K-*Il>20$16Iqk*gRj4QX}f`^4xwzpGo?%M|C4DM9mgb3gZb))KjijUX0PI z;A6af7JwDxmi=^mH}ZKguF6Cp(bfGET_o9tMUen0re$keoDqJp0Wy-egUpiovFgM* zf&z0`I!Rj#dD?XS0-1&Dr8{1o&dvDlSk^H70r&1=dP_q)5DHqJsZ@b*DdZQtNx`Cr z{v1_90wq?2LU9Y=HRq5jj;|+6R6Y23peSO?=xJ4!ry!GZ+8FFzZFSu~g)eyq=trGeGl029#^UM=uKn za}yXNvDsB8>?_)i_Rk(&K2KGFEGVxiC50P`5(;%xgm*4a@h2x*m#fBnIb!mdt2{Vtd^RylnS#4?pzCZe_unKt2^0@G*CZS#-PAR+yw;-P9RhSCdOx7=YD2t8)W*pTANB;Sf z;{_aQYsAL~7$>`F+|Lpa07^!3+ec$$)=nrH??;5cv>hHXr?H{1gJm08$wj{{Xi{F} zDpiS$BRF3R$0ivR^7#mJv_^SJbR#vBV2Z9}%Sq*`&`3 zNUShXrx>PJzSU%y-va;ZTxbZCWN|#`FWORua(^x!4$a}`C_&^?!}llTf99|zmVPbw-S;6?{~Bn%ad=E z;;4vYvsR{y{75Fpkl4DNuxrppZ!n_#LlsV%Zuu_fXH9KAeh8hQVt2HNSdc{mVp^bl zD-)i%#T-BX1VEY|_qh+K@OG?{0jw8WZNP52(aYlx2H{vwchD1MNUA81tR`oFOju56 z7aKzI7Q21cOpAWIJzmmHq&mIK-rZCHpU}RpT8K4;W{2(#5)mVSnL++FE1WgT4 z!;A0SIMhaUnmQ56v|UQwi_#&ijj&3gPz^Z+^Pz{Z%w!%F{@j?{qDG>@N(n>uk8z(}ypk6q{FDy)4t@zTmPAe|pV-Nv@!3 z06rt<C8uLU??>a7ez^YokA{jahKk09#TJFX16n$gpO$ zQ~tB7%8glHE7d^B@rfsUd7?s*X?rYHM84q_xiW7#1o<_o6{SveG}Q@;eGI0^i~R+{ z>nk%HN8e7NfClSHcKWBYy zBjKf_-1)gNzfBhzB6UXi85HYk;#A27e0CU|+dg1Wd>bu2*qaAqXm&%ya zjc)_Y6!^_~E3Lo72g8K8U#U{XuV$3w6GoBAKve0W@pVeb4(NN9@#sL2#bCtr%i_6f+b)Z)T>y~~P?0tolf zok%7#CL%}!$#?}7xu66&j<0j9G(YmdBso-*@IIr%h8rmE%WYGB|U*+8Zr25 zAg?Lqhzd+gZcUO%-b92oaL@Enml@5S>Akg94p z`|W826)}j&yW2kh@3z)(N7kus5M`(Bx%>prt6OQlMk7x2r!)cM{}f?UU4!`$iWdCz z`}c`RY2gibyN?YjoOO6q_q0aMmG$TgyeULvY)*f1;U?$a=Tl7sn8BSrxcYWJs!VZv;Kp+CBACOiSUl&dS74Ra_hs^uxu>e|!EH9qfG&Olh0(cBZ0oY++E&Xyz z!*Xbh(E6{oUjH#b!FKx~(&->dB9!AN6hU~qVOWU#za7X}&;uX4MIvH>M@H>CyI-xvSVO=qO@vxa6_tG6?=Q zShsTo;O!}|PrBLLBy6$=xWaQ{D*jX~!TO<62$b&}IB^3wvErLRW``cqUvi7N{e`&1`Xo;uPpYSOdr9%k6caT{g}_!x zU78+Dl06>fsOYd5o;>un`5r|G`Xa>G?Qo+cd0du2%w4(wY5c~=Yal_OFB4DZwiNT zAP-$2Cs=%G`XQj@Tj$9F3HUe+l;Q;svY2}Xf6G|}g-a-T5<$5Q18AT*BZapj7Q&f0 z5wo8F-e>=D+HI5zA8k8)6qXCQ&rMU+69foBMkR`FO(Xo>=MO zv)ZIEsdBC^`fa@J?gQyh{6Bq_0l3tCk;9E~&!8w-USNUK+4c&ZRJS$i9*lPY(7(XP9xgO#^T2WQ81JUX0IWfsco^bw1mewi9$et) z#pwAV15!`4PW>PA_8-)ZPDwZIXB8mI`_M`|_8jF~pUOgwT@n5~IC0p~S1E->H0wWa zh`!_Q8Ym?z#ns66 z9R67bz)APM-7N@>U=sD?$xcRojHbJajlws+lwWGzB;9tN_I8=!cI2Jx8E^>nj!aX- zB8cf$K<-4u57cDZOjvchSn>ZK{$2$OGFP)+IwOxMpyT|2i_14Qx%I0VSO`Z) zZbp9MntuD-^*$O}G&f&f7bkKxX^L(aU)En_$_M{HTrSO z%0lNZ$WRw*?0b{M5Pqvn>0y3k2TQX1tB@`;1~n9CFxmn>1Bbnw`t8c~j*C8k_Abgm z0KFqn1s}svosT9I@$GIM_qk;Um1tLVamdRFW%5j-(E}bSa5)T`Q}_FTat8}{Ct9|(U0FO|HbJ5TqYn1<}?0fBs>EmNGo>0QT z1I6oTPe6e8{?S2)qm`1d8MgyI@o2`p=svh!$z$O5U65iEs?U3)GU2z?pcCWDZv8Vh z-2E!99P8aSIRl@ggi6MCZu4qbIze1|Z?Zu(sq%eVnz^k?ihH$?**{<6BPU*ZTy`@z z~%BErcGu_^17P@0tv) zr5PhPek;LNJ-BVs^7p<%vTYwWJajjGW8YOjx*Pa*?WmTr8~b9A#}@Mt@BBQhQ=n6i zC8YIjjQZMA-s0<{QU1e8ujwpFsTS;U7PK1M-Mz?U3INri!UEY)KD3QFDU<@Zm}}Q!W}1$O^`%X4)jcsc=@kWyDBXJzZ&|9g5jT_ z&IDGL2P+$&qkyi{LgQ9Fj2;5GKQWC5DD-BzIE7O+ed-+}G!(Nn4&vNO&aOzM+CNFt z`gDXwZk(t!bQpbaI*o(pq@~yXZDw*((qq&XpW86Ulm&!qPv8-*BQ#IOWS{xMx#TAI z5&V;K(P)S(qQt)JnMHZ{6PYAKLgk6XGd*NHQf)#`B}pRNtovZwgAemS;KcRpl^Hzb z&=t1#rZ`{0!28dvU(ps{Vf@XYcBD9hyL?#EAD|iOE|oDDd(in%{M3^%CbC47As1`e z=xUhoXk%5;(XZIu+UvztS2y;C4dxq}w)7{cSF42?SI=F>XF^M;5O5EMlvY5z*%#i- zF|n@3-MX&^bjGS=sz@drtmCML15aB9oMo?tV5j$#KeA-7m_!osJ_Lfeh=UA`z=i@! z?WU9r54WsXlbEQPWU;xZ!y^)gI2?(Ty*8P5DG z`dm&JpUsT>H11ve9H5kZtPrI8HGGiWlcMG;rdV9ly_G|)xGu~eap!5zg!*Iu{9PG{ z(HDl!<0W#-(Yrqsj;OZ!lbk&2@*vAXJt^Q^OCu&&f5PiUuM z_T(CLB=AFzlb`ZHX=w}P>pHzscC}Sz1I;R#HNxLj-B0TR`Bxcl~^S_ufCAdw;Lz^*qn} zAK3SM=FFLyGc#vSwug!$Kz>E~6QYRpjS5bc*yrvejsOgSh}2ZVe#HK1_`3R7 z5)#qJJ%5}c+Om7m>uTN*jaJ#zV3OmF+KSi{RBjn42ReW~iphK4Pxvx57pAAB#q9@Xaz z1YSNOai%)KnT%_(zo(t4EURHbn}1+=f8V%`ZOB`(sW3VDmA7TVlq44fBv`%~JBVBn z*OKgi=YU=pt^R&Pi)xSHeXm!QApb>#9zFgJoTPl|WAwWFgKlcGCVNDD!+qHEtt7|T_V4JO)SIE#>n0G+ zk0Y6iFNJ=5q2M!>BO=8QRAmtOoB7L}r@AGFXyUk8B+0@Mf5X~MKuY+B3WL^5=+e3_ zqXwzbuR8llb;sq1+x)*h0V6_zC~Ng){U_ZZCilNdamCNj8w53Mzo@Q{aM^K4!0C2P zbVCA($ufGR92%;*R8HXbjIn(|#H*kJC45iAj;=86?;<7yU&CV{04GSLZL;*YOvt#j zC}JlnQ97oIVQuJ&BXU=Mlq58`fDK^r3#Mb@;$XVR0}jXz)t#oMEc!2hmZ^&%QMH?;MEXe!k3 zTL>=M!X8n!x)&!+(Hi_s6sO?*d4cU+?>B;x4g|CQ>|2^Z(0+i@$M{b+$Cr(%GVo)m z`6j6cfx8|6)v@@hB9=7rmj!)cXEwOu-+hdF!{XS((9VO4rE|g&+D?KkNmN|-eZp9x zn{ZVen}>Ev?L1(;G5c9y@;}Y^0v^;yyY?Obvv@e83K<~K_{5>lHo1)~|6-^PsK78B zeREQ&k-*#c_e)AFdrF56Stok?k!rvJ@atrs7|ybqA1S4)vR;6zEf? z3LOJC$_rZBX?9@-+)VL3Oo#_AeFIz<&;JN6UO}6iV@Kkt>+-Q2_W<*W_dI zyP_kwCrFI24JULf%xV~H9;+G+wo9ZI;~@lD$$cY5P{+VkoZzggI18dTWuWOvU^}f1 z$->WZbaC~C=DAZZxlj*^KAOB{67X@&>nMS5 z9es|;Fkv20XplzcYj`4A(G8G0se?7!-h6QmWm8t`o<0u92_sPAXjJx&uPnvU}k+zAgNFf7e%~{a%oP?eOTz*`TQv59nRJuUM`2 zO$3$oewEs{E`z!!O9Xj0C}PS=9+VXrKF}xGXCu2erC0|3+0AgPJDh-Ee_1(^qAO47 z!JDZ+{`&?>zwZ}>E%YqI>q_^oK|C|A&_XRAZ@K^|Ss6cb=+*>d7N+~v3U$>r{e>1yS#=Bd3nG5%zf&yQG zM@~b@M1Vm8Q82ka25~ME45mp)+pE5!pna@uHSYThx9bYdD#Ze#*FQHAUEM*PDj~F0T z?oS3wS4xyjl}fC15-Tfze+#|>GLgJzI}=~A?~AzYC|FgJTr0x(1@~nk+Hu%N*{ZII zM0=8K=~iGl$Vq)8-rxUmH}aO#6kkZ2o5~~WKFFkvt1PEC+iD*)`Juzv!Ar@!Si)!` zs=DSME6$huK>FkKNqLlw)tpuN40Lb4L4xy)99}ZsYL3$&#d!wAh^^;F8_XC*mp+s> zIDjG|xJVF8hF)A~lgQksWaP@XUo>%Mt zsN&b#HRb%3p1a&(__YT8g-?d{SQ~ZrI)ZiY?BNq*MuBguPS}<7+r!kKSB3f;^@cFV z+l<8D;eY_glpGNwiHOj^(Vu;CWbqQ65+ndN@7^B-64@pkuD~mif!sv=TZMy3nK? zJJiZu;eR%_QVu#I<3ksJF^d4>$|ZV0SAQWNvkrcRRBBP&8X^E%r)F@|s1CHCQ!O6! z+C>s=(K)TAv&5|zjfk2{A=0aJ%wNJQ-?IT!>GU;>n&@Xk6)dQ;aa=_onJXbBi#~X{ z)(0JVpnKnxU4=0j*A`cLCDG!wxod$@N85mo}w64j9~D=%WqeUDbdZ)O{6Gk4jeQi zKeHCNL}F8YkNvzV!21>cO+K_=0u8q*1aZ}gKk7u~z6@hau#lf+2QhOhP z7-@2<_U1@-Kh8n+*lKr9!eZiA;t^LoFkduR?Es|6Y8YWUW<}L6m_ci3B~MJ1uUuRh z0iFeM5`x_DYqK}ycPhvbW0aAijS&=xMbGEOjj~ZVFTKrBpeCJ$5Osew3 z!JFi%;D^3wl1yBXe6zAip)Z<)yX_WccxByQwomGr(bDV9K?Xd3g>c5v!Fggmbg0Y` zK~IZVBXtq9X~5{NGrZ0Isb2K*V%=xLFAlsU`F!~W;WRVBGQRD$QQNl zbpYf-PwXu)or+7T1jaD+*J0_@$Jj_37A-%7fy4$o_wzZ39ga##wQUh=J*mS(t6%rv z8hFM_z-r%I@M^t+CmJ_r2eFoR8dSSzW`AOdTT@c`gP##yDvz5)v^+Cpw9waj9kzPm z!C4g*eLta34vF)PZHa|Xp3|j%vHo9^$E6?guLoJQzIl6f$#@YaF{C4ul^M-##-fSz zzOIpQ0~A{zJ$1yus>dEq`D{Kzli7{fwWCal>gzrQ;#qi;v$#1OYX4qLQ+{;9l%|Lh z6=tr?g#^&BkEBLfNK$oiBQ5TGYa zea&D^F)3SlwYw4sCQH9|?yq^YD@f%c)>Uaw^l}i23Q7SPf-WIj!7*%|wl_RKL2y6C zA`WdimfxhQI-Jc02{WJ1M8hScevupln~p}BEE_<5WHB|xj{q;FZCUFR$Lost*5Oh;dMWj8*y%P@iaK|TPwN(-K>Fc@h zgHL6Vzyzr}vS@u@$%Thihf!+MS38WAn>wx+Qj0jSoiV!FvJ(0cNOiCeMvK!1Y35l&`nupBx z*8UMY(lViw0ytm=lUF1vFDkRSTYixXUIRuc2v~4?GQg$$WkXtK&+IY^E0=>nNEBis zHWmKxB{w-JltgUsc@oS)<8BGS#i1ij1@+Nd+@%2bqA&&DRMyDk?oPc0k^|E^w5vgc zOELjiV8rJtN^V3c*+YOcN>VhTozQ&xuy*d!jV6%~C~H$p(hS)w@qSqAy>j}>{tW=C zC<F+e)Q*zt9DqcLJS=TnHv(AyuFGy5P#5rN;ep@+=*7Ol-VmOw1m)BS zpojRXQ~aPu2p&Mgfq(dbq7=iRek_lSAC{Z0L~O)N{^B8+@FEVY7~W#;08rQr48wyq^KLn3W&zK1zMz_c(o_x5DnU=pV?sZWVuwDf8w7~ix1e-(!ske0xORF^DX1~L$q z3;cUZ9!~@@Cy)l1=umOkstpMAV{5h!0=i3hv-8eRogX0|YZg8wE!dRCDofc6`#Zr; z)qs+%;6pN?2AmubV7?0Af>HB@st7`xWr03m=ZzMlBuyMrof5>1DfSgB_7!$+9k5e_ zop)?bW57NfD3y)Pb3z7U&@gZ|Ct<(st|z10+SwYDZ$l;x@v9YkQ;V8epa*{# zbx(E#luF?VOMf2rMMxccmP29=VE&qVVogorE>=Ba@%+6GlUrMz1zgN;8&?(j@l2<0 zJG1+6dNxS2;WzcWb`?+Hu~@AyAByPZD=m3)=LS_j}>-v9L}0``#O*+gI-mN z)NYn+#H~A1X}q!TUurWO_8X z_57y(^YDPY<&4X`dvVdI_HalVK_b_C$@xLL)IHwTxEviN%p*d4Esvb`qA7V42aavu zMP8sLc~wj_iR%u}3H#?+s&H60<^t{o9mGRbu;tSVfn@W%zf$9yk5!7^%DfwSXuTRS z3Y`=f?sXoj#t(7*cy}oV-erXjS_6U&a8`bJz3DAmIL}5(9k3h@M_T~cfOU6XD^el$ zsh`7G@88V8pju`bj^8H{HP;xSGCuyhw81K*?P?i1aGXG;cK(|8ZrW6h7=^eHN#l zoE%`tCmN84d=lDeLgtBLpS?tinCgk-zSbj zV=yqw!cy;}89JXG(rA2!t_J&b>BN`XMV(5Q2<+S|a7-UroU!?uvJsu&>7V6gnE0T& zvuTC@gJ{L?!P4>|hN`W@@K?K6PNDO#;l!Mwc*%cQ>H}C>N$bj3cs5V%>>t)Q+^d<4 zfQo(C|792huYLnNPtQBM2WyonD>BN$FK&Euqweg0{`7A&xb4yWt@Qk-5=Rxc(Uzuf zmz;B>PuF1nS!lobHdfew<9gUbCnYs*S$jRSl20c??@BBhN^UJ#LD@y#rz@zd4->H341I=h% zo=nNQRVASelPE8Ie{;=jkSj)8DhHa9ATkK-UOP}Mgq)CM)PAKyesnd*c`;^SCrWV5 z7}WOSzjcIxY_#2Zzx+D&!v}`o*y6N+#}h-c z9Wc&FI7fyw{;~8B;taPq8Bw@Op8hiSiHdwO&|!yiGZ(>fly#og;+^*j#F zf_Z3KG!jtGK#HXl71lw6rPV_(2NJnJFXRRZ)i93JB>t!euQ!gXfhcvZTc!zR{7%Q{ z1Yuy~UGH{*IFvnS@d3>WRn2KKL96%P?ITVZ>UTevm><34__u0#O5%i;rqeXIZ$z>>5=fnY(+h38+m2fXVcFVz z67a6(G%Ubo3!GxUbu;-tj4Qw^vGB4~2&tOajXr^L%MSeJH(ss_M3QzotCo}sSK;g2 zZ|4m-mEiPxqFF1)bs1&-XR4mv-d5wGA)nM@y~Py;sC|s|Z)h%VHyZzl{?2<9#&EW0 zCVv7}*A#X9Z_|}bzJ@oO342O4rc`(xJ|81wPLzR`W`1N@ zA`9YKKj&{M`Jy@ZMD5tmjq$O^#=2?zme#x;uk^PtF?K!hLIlTYc|`s*{PKpZOzK(EFXoM(=VrBFZDP+B<_Y z-|7Y3qP$c6+3SbieZsSM=jAngTg0)>o1%eVen)n`oOdKRJBVkK40d~^5hcA|+CK2W z_Lqq5{UH&+Ej(16To!FL^Qs8y6C84}?|WT6Z83aa9bYxH{2Ose1dS+3xGR_u<=wm|W$@zXz}(>8B0qJ6E>KTso)FF>AcuDHiomI96S z1NFe3=7ZRi7|TdAV zFQrNWE=&$I_OS_)Q%leY^845B{}Ej1$(BizJQv|-_v>zTw>OIE4t#SO>-0)oKf@Om z9Qwb>GbfFx5oU@^ev@%NRS;Ev*&XXBm+y{Tw_VP3hm-CQ!D|Si{BZKMrS{C^*3jjUU}*9~dLX^Z+8|H;h6d^Em3Fk^x=FS4^pB0&Ji7?+l1X7;pr(r4o| z^+8i0iWl%zFS`{RnR&3;fV1_cxE25$Q<~kn|aYxKm%b>Xg8^UL6%d1(^fO zD!rW)nx*#c0i~6yQ(8<%8Z@Bka*ZIbXk~J=vGpn#Shl%Qs)`a(^IU*p`q{c^gDF+wO6$=7FYYND<^=jHp^QY|$LkMyCiVvNi3)6Flpx>csR zO~G}EQ)=9NGHdtN&hv!lHh)4D=5@0If`Q*Zqlca$7xQK(JC{ zwaka{Y{v$~9fs);&E`2W0p)6Q9>i*7b@j;b&g&OGV|5Q3p7x3|JcXoY@@_lL)#J+{ zA7^&6vuTElfhQ7G_2S;n4L8e3pnfOQN<3fq^x}^35i_)@=U(gJBz1+U8w--i@e1 z5n%CaW9|qS?h?jiV*(OdC4P*b=SGZMs4@6if7djqDRr}WP4zX)BRq>IQ4oR48 z&$P@|uNe&|Y_J^~f4uZ?&Fv{_H&QsdaUqA)ndsgf=I>Z(f&Q+3)6z>5%b4|o^E(A$ zmmZB$1YX9UmwzfsU?)1y6(9|_yDbk#VAPfT2xCE$1bMWL^h8f0t+c=hYo47&6-fqD zIP#6GJ8HpCox?>a637dSqca8-sRC#R@D z$`3B8y}Q5G+o!OWiwqLm0Sp;N9RyEb&t^^CVH?y)2)w7O)N`}(k&Lt8P;h`IF4GN| z1BD9hDe=TNC=jc7?e%7QHV$36o&aL6*`6dJA1wLbab)HWZ8R- zRJcaZkifGH$v?;Dq9n;e2-$h05^iZ40Tbg%L?u1VGa}#-P^DXH-~lxI;aXJR?s6go zE-4fstXLe6&j{H0Ca(XiU8J)lX3NttdK`_O?7}7#6Njkf(S!S&FuKB-bm`c1rfhn{ z;b4p0g@M7EoG%a8Z#KTJ6;z(hvuyhTmRAz6(9G*A45qE?q=6Kx=HE|6H4UtpQHGa2 zhHPdH7dyM={I2AiCv85A`HNs1bXc-4o=+!!O)+|E+Ct~?OhJN5gG%XE6v<13d_hly zG+HNTM%DxrIo!FRw33B-fniC?)ct~`3LocW6EY)M1)W%Y!;tFT#lr{G`e-assA*rx zKuGaoipXFb+BidhW8N4lvYr>!MgEt?Nh^4d=<7!=cgXO z@Ee};I*Cs*bfl_xeZk*jeE^z%PS%Js@lCaEYaZd-!rj?Gce_Xdl45>O!k8a1yfD_h zoI6#F_*z!kbIk5U0U52ufrPW1mA~!BoBzP!6xR8NO$cFCqh(kHu42E%UM|+otwf~V zI^~_8m#n5XMSb(v#(sCPb+^{;Nb!HmF#E43%ps&sL!=l^iVul3w_erfF5w08iiN-- zn`KC*fn{QJV!QD(ieE{R^rNHD(_Ql?J?{qwajTtgK3(a~ZusRN6%C|+`O)TP^$q_Q z4VMU^5aPMiVw9#SfpY9{fC6N*Hl>9!oi*JN-Do>%T0OY&$EUKjCp!yuuZ8@H*5OeY zvrEO@%$)ZP3tF=HklX18?1UGcDCR2nj~P`ZZ>M4_$Q{s3h;ZR&^zpGmh)#TXs&d%f z0Vo8s9MyuZL?N+;;4XAPsX#Fs=sOlqUc5d~AVNlO7q3q*~%Y2Nf#K{v8M<8R`;2n0Y zLmdg7&+A_|JP;g&e_;4tKoCZ!-YoOmkOoEvw7qGohC?M6X=H_JOv+v6KBV41iU9Uz zbJ)byRmj!PX7QCQI4^5W=CIo5OU>DzNcrD-LIM+pxsY(AsF)JHxa1_Kx)P+ekU!Y` zjzJeB3h#RwjG{`8!34MB&IY zf~Q=#Mfa`hALB!$pN7}ceh>R3kIp8o5~I21BIUR*;ZRFD^wQD<&JZCFyE{zk_P_Su zm-8K*d~ffO`iQ-!F45_6VK1y?-lD;Kp`o-!sZO=0W6H;;>UEmZ&4dtYtofzjdbZu> znN8eQ9fbFN9JX4m_eUBbox*pVSJzY&iR{)Jn5iVztYjnQD0soare$TQ4SBuZPdQ5L zvyR+s3x0s-9H~XM6DpE+B_rajm>1UcMHs=ULsVQYQF;c8FY`ef}PJqkwMwC&Y(lFdAsw4xT7fVT4_(cj#PBaN^XYclv;C0u7pI1jpNv`PgGch48ly=gaz?V{%5+3 z!|#A>2rtF^WUVPR21N>G(?!rnD+hM1>xp=T;tweswg2`v@3T{OzCL8R@zL_<_^#aD z?6eA3Hp?|v`ZL<5sO~IOU*cTIPX91|5s_cp2XL!<7Qx=1xT^tvbWB+m`cNQ#GbK-`-BZo2%ff zKq!LKi_($hWXujM+8ggY6pn~tM!ArI6h}q6ZytRE^)bk4myGwbuA2xuhyMt6zB@k= zJ{018PZ%?hj=njK*;3VI@^2FGHpP-gjogU0Jfg00JJ?mRYH##JqP2$A76;`%->1Z z!OpHd;wi1t5W=m)KF%&3AxD;gzqxQ=%8?`olY0Z~Eb`+Ti)IHl=bK2i229ERN7bIkBYlo|IW{1LJcZr z!w3H}IE3tRfp*xyWgrsJvD?!dYnwRy|EKN%ps)Wf9y(}r$7Q$}xv~e{TIjtN^jwa` zO%DTtC^>n$dbK^>D3axU_n00|Fmagt;OCG=h3=5>MDRG%6LMB6=a}q%ULOABH(_Le zZ$M$RrtjBw@mHg~(Ynz#d#BWay&bawZoPt!ae@gjqFliLK}cyTgy}%l zp76Zywr(*qo*-WVLXV(_JLgb^xcrT=TE-BGG8ic6j$GG(x2bXC2%lT zNDxSfx(2M7_bNVKyRs)-1U=)i>34oSP-n(5N_>&b-(}x?JB89NqwV+bb67o^&^4vz zOF+ZkiMy)M=;I=u=hF2e+g3{RswyfSb3PfVh8IIaXCEDgKPj7Buf-3x3|0=Gm6VsM z3=QkAUB;fvG3q|AR<_kXPNI8VNOY(i1NTtZ%^P3iN^7fizKOZs0yj$AcPxXElxux2 zuKx15j>S0YkQxO&ta@$mzP`m8CKT1+qwbm>tJ!xeh0gVmGeGl>(eU!0fOkueUX5rT z+Nv0iP~un%?bg~Jevo%~R^s!+WVy|KasT71u-x6!vB;tDAa^I3`q@KWmR{Zzdl+J| zG_TlQUf1f|3GQ8U#GlU?;|$=j7m9;UXtBB&r1_APKqp@1WUc?<>!nJ`3NJK(N3|^7 zf|xRQD>ih`327>w1*lm~OGqoKH1+1Db|e}quvBgN#$W6xuFC!m>wR>^rd8vWy}4tb zVeO7{KtEIawlxj^qA$%b;!fNX>C>sbdo;#=?{jmCM9?a44M(PxRs1^HjNvuMFE($V zv($O#JPO4a$bmu$BjM=0oyUAL{svirOfe|xV55M^TiV38I2!y#Rr#2H)w*{tZ3A@S4X+86I32PiiPXB-di9Y7y90?oTHQ}{bIS}*Mx@m58w$g{N|9a1R>Rw@T zYD5h~D2SYGm%j_BmJ^PjvqBmM1S&e`?OtRwE(O(H#P>d0EMml?L(r8mS|U9N4%&Ps z8A2XTMq7?J>#TQspSXPv^Y|-U_;1DCpK8m0kcANk>1@oXyIoQf55y=G#Y9Zcay@@YdWuYLm-Jh7 zrTMF%q&Anq)Edw=$WBgT#1zun$Vz-4xhE`)!gD%e)##{(<4 z!bw3+PwB#QrR6nuO{nhtl#Jj&?V~}iBq*Zy#BeNaF`xn3VvPrNc&0|ZspX^drOLH& zYu7$Nb4TTe&3!|LQ0b>YzNnDMTk6QCd}B+r_W{KL9(Fkt_ZuVG2@FNVHMRmR^|o6p zJgN`RXP&bk+w2f+{gx!RFfVD@6_^y``uYm~W{G3E4*u>0EosjkoZN9RxDlQ2HZk$# z!=Y5>=WW@h*QeH1QXF_>JIL1D_K~U%(asS`HYnHDWpqQgrog4c^Z4`_)Kc(9|8{he zWS-s7v_$iU&u$JHjNK?6A5bAUbbfeG%8;+Rs>Y>C)Z{Lf?9C*VCZhKKg01hNt)u?Z z+2uocy&0OfXm>Jir_m$c`0URP2R9kF>i>iRR*^v$X<9SsI|eC>tW`6cuNfMl4tJ`> zp%em_fi4tAQjCHOblz*Mc`tQ&cuFGo24k_@#3jk)h=^OP{|&euvUL`^GLyTp#ya>x)KOM3 z%r?OJ=($72JwcA0*~uJtFCQeHXWUsHyu7%b)!F4kmLsyCz#^5ub*ZVBe1&yzXgqmH zwEw;O0d#_Y-o=F;e^X1Blfd(j8(|wm@2W>Dm;Rz2pV_UV|24tWs!e(8GaHC`FYh9t zer_GJM(#}KcjtbBpvk7s$@;-2`>x>T%Y!-guWrh}%Bom@ zKcY6Pe_J_JVS&877v1%@rKV%kG5aPXVmvSJVesVMOCQy-`}YoiR(*YW>-f(P0`?!l zCV@)KfH@2x*VFD`(E^wx#ePw#Cp5JOda{S=);dO9qFvcBT$Z+dM`$Vv(#rn zXRDq2_oO^T5|%n`m_h_)1JiF^;9-u+P?6S`D6OPyitmj4-O5wDmzU&%@6Bp^RSCo= zUJFY}WUwk>(Hr_mj<`<9vJ6VY9evn7>Ik3=f=^siA{wOhew-5&ac+JRYL{o81gw2Q zAiK*DsF8^j*=6T?Ohw0tZyxvZ%njhZP1Us^I`oVMLsjshXgGF9m6Kv2?8CYb+GIl! zfc*i0l$R1!B48;8Xn{olw_PmZZ$!hH0PB~(HvfXDl7yc5%HSb$>8fLIfCm0YvRRM> z9A4?_R2UU?7E&Igo90V_Cju{pAmJ)NvnKyb>WR@*qy9-X0qH3XMh5!7#HAR$^(P^$ zL==#>I$@-mKm=BdRFebo4Fh{17$^^tLnmBH6=1}hR2cCl;T{oS%nW*fX%&=@8MRc5 zg!U6B<~KHi>mF8On}zul2tWfpz{++1_lpAjSUCv)1s6E)rNk#(`YTIioKr6Sy+a2w zmo`uU;9mk_%EBNvC5&mZACvj@Qy%7p+Q}SpD9sj_oKJ6@IbCFngyMQ504#c4b5Usr zHimXU&^XMOs%LCXz(if!VrEE+V-SOBD+Y{QFER@lMWzf4wDnBj;tT>U3t?KJCv^Y{ zhgxtD`c1c4&o#dQj)3470TEL|Ql{BtBw0r_^YsICVO><(8E<>mGKQ7h=@8AJoYnlHg0tHQO>O&{`9hxnL3i)&99QZyw znWfab_jdL42zwNUe#n(UH%fee`{#Z8{@n6ugrhF?evt5aQN2GmdM8-7Jl*T_x&W!X zB_=EaKn4vz*|Y{$?P1Nc;M%6vCdS*s^kR3>e~HG8#A3n1i^%%c(!Y;*{z9-cC+8zT zji^iUp>1iM3;7-Yx8V%fGLNISYvxPKv#3?B`Q#f>Xwn> z_>YP{OSsIu151yc9&Gr5p+*M1-^_lOfE!3f0;b6jqZ`dO7zzL}c{&5W#F5jJi?11a z{?uB?q8V1g1Iu>pkV8nYCUxm&(#Ah~Drd@4|JbtDHXG!2@Jpp>aKl4XxX=Mp*!f{R z$!987trExgcV`-dX5JHT#+rv@xR?Mokifk4OoY(iWzV{FHxvshONo%2L66==$F++d zXZq-p@K`LVKGMR>HE8*1wYrHJaw!6A&Zk$a{4owLldBf{eHJ{5&f8240Atzegn!BsH|fxcpxN2>DN{4>_a06ftTQ#}9X_w})U(l6>IonO0Huu%=Wik9 z%#h(%+SWH+UnrBGoV$mz{{{#PWWTGRIo9$pgr9XV`V;+S6XQc5Z&11q zfKr5bw@M{+-!H8d`{WWGA@=aGKQaI7Nfxc>pA!uKhD|WQHzgYmWb2fm%&gXg%5zz^ zl0at~aXQmz-PUlPM+gMmqHNbW6c@Qgq)<7Sx@L-ms}t`Xu%{Tzv7?)r=`xs+YXjf_ zf11G-qtT_t>42h~5V6bgS7@WRmeR!Lly_%><4{6lzJ>^644E()`bML8x&(L>okP}d zJ#O9_ebOB0=TcOe6T&xHxiNbMpZdJi);ZICVn#mf1wHxs*Qx6Mu8#%AB}C5Fl@6ip zl-u$k&<*8UjRJJqcf2!1>5Uja*H~{c&hgP z)MAv)M-0mlI#x`Iu*0m+JzWe!p?CjzVxM6bq1``+?E~zh>G|gJudS>q!eIIOvbVQ_HWPv46}1NVb&b5>jN*dF%1HT1ArZN7Y5D{K!t|guz{Wb zf2-)dlg&`f8b&<0p5TCg5_bY1{$IW2|9>1Laadj2%`i|85_E52o`ve&af+rHWyHVk zV2_snJv|}z00I<$KtyG1-GBYX*2R7j!H$ZHEaHN5ix22u56p)Wbnj$qD)I#hxNsf6 z*R=4?>3m@lsXHpG3Dz6%uzM0X;Gh#fJ20bJ0d6zf6$C~cEMF*sHp>8r$~_WerPhEp z+Z_Vj0R{*2J~HW1B2Gm`{*_`wJ46JDVhq@X1OC)7=|O>_03gMiF%@9i9r+j>&?o9J zC3&Q_I3`v$(|!wvJTk)=F*}1v3~CNTV*kbC1_Cm)0ZarC#|0a$+lC$O127Pnz%3XB z>?aN6whA(6s@9G+RGtZJV|(M})_peG;>*=rPJV0ww*jy)fO`NAX7wq+^&4P)v9uDZ zfwp4B07YU?aRb;1hFt)$VX3De88tbEeB}JI2^ddzHS}|Ch~^HXOE3lSGm5P$D;eeT z6Tmx^6*duvA-R7Ipp?kq-6U5S7qPUq z6!b4Hp9q22mk9<7JO5jqKUMBc+453SSh5Y#PtkR$pc%n4H(!#ZeujQ*oW^XTa_I~wu5~VQCN-% z^ZzFWIkZ&Id^#$8D565~zXDDWpmdcHnwTN*TSl8q>%^cA(J)^)#Pe`zCD!lTjO-3}i;fPv*4?+8RRvJCd>7dSWX)*jr zNa*_I{@G^ftXBvU3&n;EwFKudchlvo=aoP}WDxXN5~PX55oCl>U7% zv3u!qule~9#wML~7c!*%#nxq&TZ=@JuQop1+(0z!Ki=`#{884L*PN+-?N>tiL}|Y- zvz^nF?|k|7rESNMDs_k}SrY5uGsBTfErzGG5reu}H_vC@^WO2^F6;NR$71s7%L^ob z*EKW3zkl_v4GKxH}5v6;iV$AfveMT`Imcsqf|9AQyTdtVirx zS}mq%wzKu23a^|=K6nXErL`?U7QpjKADE9WGd7nNaa1IGuc$`mK=^m{DRJ+{V*Az< zA)-f*1g>`0^p71`IGC!Z$d_4%ikH9`GI<-pl4B!si8PKN`m>0@!vu0V1zz;f%J}QQlpxe^*)5-&)Gv#D7(9cws^Wv zV9iBrRg&bZnwe=NcRDJ~xgGb|2&{XFC!ucsRx)a==u|n7uoO7CBk=3(r?`haU9Gwh6onl#I`?x3)v*z zOhkFOz2lYk{KlVgX$vW0T(dNAT-Z+N<6}e6noB2eo?85R^3{okUF#x+&2+tL0Y8|7 z@2`%D20vT!Q^1sw@(Ei^feO)LfZH7S$L$9^!penEZp780I*Zn{mex^YHo+|(MX@}j zasqs`UpVVgI=t1#bDf9B`p_XQve$>}Rbevnw=wc5kVXJ_(r>^fxYN_&dfY>+dK_DF zWP|FNpI{tqqqCn=c^bbW+mG0`PWoX+9E;FqlfF*p1$C3m{%&0Z(rXq6=XEi8uLOX*7WqvbDjlRgSIST4 zRcB4qO|-mp(Z64rbtOE|%=Eg~G5~EtPQN*@{LU9*ByOJ)wBK%S_>~*8hXbd&Od?9B zjsjvgdcfww6iaf(q|&Sbc`Z{bgy2*+$y#n&iI$>F$M;c94O4ua&>q5tXx*tSrK+v8 zNSzG;!1w2*MQ16gWio`nG~#+aXjdpMpgQL69fAaOa%*G{G^e6@XNISU&&YBN6O#1{ z6d|9VDPh_TosdJnSJH&gBCkvjJBSYkG^p773^b~mkWW)J&9}2 z|B07uUI`MKZJ*_sA2#(fdNjw1y_11JmAJ48_8IjlJ|={#HF3wv)ROeW^OxE#JR|H5 z6C9+?O-t`A$pyMy20cceb(ugsFI2}eST(ATj@sJdX$(?$Pe_Hj!@gRu*|KZNj^k>d zpQa=tBZ^ejOx%(Cu#={*9h_!7O#{o?AHjn>x}{3=p0;hzUV&OAhlv`IjOj z7k?(Q$LS(})KR0R!*`QGq!9DxzDjSBYhM|1xI78un6Ht>B~i$N4`muI_;oxnFs$ybvguFhgE?JHgb!w}TUs@mwu0LA`-mknl0LA(9`6u(604QP;Q{*AEKm=dH-|Vnv$@vVpyz`yf;NRs$gBs>)9em`tiS;;u2L2*p z^^PU03^CqjYxd`%m8v?@6=_66xbVgnK|M`A5>Y&VQ?_t-mu>JylaZ(zU7vGeLi*vz z`_@KFvZr4=U8!yq*rlLIJ|GRP+wVgf%qz{9EruJUQc>^FFD693fT_!(x*v{b-8C(0 z2yujA?KPeWaUtw%Gm+Xm2I%J7YMD0bnW&)D@y+)a&*AHoK(>-pPU2(~6C~4-_Es_& z$4NW>JO%Dt({|pf@S<>*`tFL6t|UujkBW%e4W{^C^*~Ul;U_er30n-`67DoA_NU;RSIY(x-d#YIyqda`_2euru3C) zOGQp+ICiM$^dM9_BEim&zKpJPc5Q(bp*{@@UuS z47i1IER7@Mij#FX?A*J=Cw_-a;!xyFTewz2GWkG>YODj<+&@)!MaN zAAw~A=*A3TT|}Za35JKC$wt!Wk0$^y1(Hxhe^XIsYiK$E_RMpSdKSc8yDH({9Dp;o zU_%7>_nxQ#<;0Uv4V)NxTA8|u_&>&68`Gc``u#slpI~0v!zTXuyBJ|`V@g4}{=*G> zwah|#|GP7cV_x>LRnh+I28O{8oUXN_0h+lfcWHicPW1_p8)~C}Vg!p!h}W1)!vC|s ziZ7*#fj|og(CYm0M&Ckb@`N%D;IH%F4G25Z|G$126X^PeYxO_{u=fLXWgVp&1)H${ E7banh-v9sr literal 0 HcmV?d00001 diff --git a/frontend/src/assets/images/apigw_access_token.png b/frontend/src/assets/images/apigw_access_token.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fbba4ae6c5464513e2089953724b6fb1dc16d0 GIT binary patch literal 77836 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 W#fvr*{^]+$/; const PACKAGE_NAME_REG = /^[^\d][\w]*?$/; // celery的crontab时间表达式正则表达式(分钟 小时 星期 日 月)(以空格分割) @@ -176,7 +212,19 @@ const URL_REG = new RegExp('^(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[- /* eslint-enable */ export { - TASK_STATE_DICT, NODE_DICT, SYSTEM_GROUP_ICON, BK_PLUGIN_ICON, NAME_REG, - INVALID_NAME_CHAR, PACKAGE_NAME_REG, URL_REG, PERIODIC_REG, STRING_LENGTH, - LABEL_COLOR_LIST, DARK_COLOR_LIST, TASK_CATEGORIES, COLOR_BLOCK_LIST, + TASK_STATE_DICT, + NODE_DICT, + SYSTEM_GROUP_ICON, + BK_PLUGIN_ICON, NAME_REG, + INVALID_NAME_CHAR, + PACKAGE_NAME_REG, + URL_REG, + PERIODIC_REG, + STRING_LENGTH, + LABEL_COLOR_LIST, + DARK_COLOR_LIST, + TASK_CATEGORIES, + COLOR_BLOCK_LIST, + CREDENTIAL_TYPE_LIST, + CREDENTIAL_OPEN_SCOPE_LIST, }; diff --git a/frontend/src/scss/mixins/credentialScope.scss b/frontend/src/scss/mixins/credentialScope.scss new file mode 100644 index 0000000000..bdc1f3907c --- /dev/null +++ b/frontend/src/scss/mixins/credentialScope.scss @@ -0,0 +1,70 @@ +@mixin required-asterisk { + height: 8px; + line-height: 1; + content: "*"; + color: #ea3636; + font-size: 12px; + display: inline-block; + vertical-align: middle; + position: absolute; + top: 50%; + transform: translate(3px, -50%); +} + +.credential-slider { + ::v-deep .credential-slider-content { + padding: 24px 40px; + + .plus-shape-icon, + .minus-shape-icon { + color: #c4c6cc; + margin-right: 18px; + cursor: pointer; + + &.is-disabled { + color: #eaebf0; + cursor: not-allowed; + } + } + + .credential-content-table { + thead { + th:not(.is-last) { + .bk-table-header-label { + &::after { + @include required-asterisk; + } + } + } + } + + tbody { + td:not(.is-last) { + .cell { + padding: 0; + + .bk-form-input { + height: 42px; + border: none; + } + } + } + } + } + + .error-tip { + font-size: 12px; + color: #ea3636; + line-height: 18px; + margin: 2px 0 0; + word-break: break-all; + } + } + + .credential-slider-footer { + padding-left: 40px; + .bk-button { + min-width: 88px; + } + } +} diff --git a/frontend/src/store/modules/credentialConfig.js b/frontend/src/store/modules/credentialConfig.js index 78165eb2e3..9f7114a447 100644 --- a/frontend/src/store/modules/credentialConfig.js +++ b/frontend/src/store/modules/credentialConfig.js @@ -14,6 +14,14 @@ export default { createCredential({}, params) { return axios.post(`api/space/admin/credential_config/?space_id=${params.space_id}`, params).then(response => response.data); }, + /** + * 凭证详情接口 + * @param {String} params.spaceId 空间id + * @param {String} params.id 详情id + **/ + getCredential({}, params) { + return axios.get(`api/space/admin/credential_config/${params.id}/?space_id=${params.space_id}`, params).then(response => response.data); + }, updateCredential({}, params) { return axios.patch(`api/space/admin/credential_config/${params.id}/?space_id=${params.space_id}`, params).then(response => response.data); }, diff --git a/frontend/src/store/modules/template.js b/frontend/src/store/modules/template.js index 1bf93ec7c2..861ce71e5e 100644 --- a/frontend/src/store/modules/template.js +++ b/frontend/src/store/modules/template.js @@ -362,7 +362,6 @@ const template = { scope_value, }; state.triggers = triggers; - state.canvas_mode = pipelineData.canvas_mode; this.commit('template/setPipelineTree', pipelineData); }, @@ -1257,6 +1256,10 @@ const template = { const { templateId, space_id, version } = data; return axios.post(`/api/template/${templateId}/rollback_template/`, { version, space_id }).then(response => response.data); }, + // 获取凭证列表 + getCredentialList({}, data) { + return axios.get(`/api/space/admin/credential_config/`, {params: data }).then(response => response.data); + }, }, getters: { // 获取所有模板数据 diff --git a/frontend/src/views/admin/Space/Credential/CredentialDialog.vue b/frontend/src/views/admin/Space/Credential/CredentialDialog.vue deleted file mode 100644 index 6a8dd03ff3..0000000000 --- a/frontend/src/views/admin/Space/Credential/CredentialDialog.vue +++ /dev/null @@ -1,163 +0,0 @@ - - diff --git a/frontend/src/views/admin/Space/Credential/components/CredentialContentDialog.vue b/frontend/src/views/admin/Space/Credential/components/CredentialContentDialog.vue new file mode 100644 index 0000000000..8a9bcbb2fb --- /dev/null +++ b/frontend/src/views/admin/Space/Credential/components/CredentialContentDialog.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/views/admin/Space/Credential/components/CredentialContentTable.vue b/frontend/src/views/admin/Space/Credential/components/CredentialContentTable.vue new file mode 100644 index 0000000000..856fd55f99 --- /dev/null +++ b/frontend/src/views/admin/Space/Credential/components/CredentialContentTable.vue @@ -0,0 +1,169 @@ + + + diff --git a/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue b/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue new file mode 100644 index 0000000000..743b20d8fd --- /dev/null +++ b/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue @@ -0,0 +1,628 @@ + + + + + diff --git a/frontend/src/views/admin/Space/Credential/components/ImageViewer.vue b/frontend/src/views/admin/Space/Credential/components/ImageViewer.vue new file mode 100644 index 0000000000..fec998fc9a --- /dev/null +++ b/frontend/src/views/admin/Space/Credential/components/ImageViewer.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/src/views/admin/Space/Credential/index.vue b/frontend/src/views/admin/Space/Credential/index.vue index 2cdcfd372e..e9624580f2 100644 --- a/frontend/src/views/admin/Space/Credential/index.vue +++ b/frontend/src/views/admin/Space/Credential/index.vue @@ -4,219 +4,348 @@ class="mb20" theme="primary" :disabled="listLoading || !spaceId" - @click="isDialogShow = true"> - {{ $t('新建') }} + @click="handleOperate('create', {})"> + {{ $t("新建") }} + @page-limit-change="handlePageLimitChange" + @filter-change="handleFilterChange" + @sort-change="handleSortChange"> + :min-width="item.min_width" + :fixed="item.fixed" + :sortable="item.sortable ?? false" + :filters="item.filters" + :filter-method="item.filterMethod" + :filter-multiple="item.filterMultiple ?? false" + show-overflow-tooltip> - + type="setting" + :tippy-options="{ zIndex: 3000 }"> +

- + + + + diff --git a/frontend/src/views/admin/Space/Template/CreateTaskSideslider.vue b/frontend/src/views/admin/Space/Template/CreateTaskSideslider.vue index ab3ca2f73a..ec094985ef 100644 --- a/frontend/src/views/admin/Space/Template/CreateTaskSideslider.vue +++ b/frontend/src/views/admin/Space/Template/CreateTaskSideslider.vue @@ -20,6 +20,19 @@ :maxlength="stringLength.TASK_NAME_MAX_LENGTH" :show-word-limit="true" /> + +
+ + + + + + + +
+ + + diff --git a/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue b/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue index d3aa0a11fe..9bea126f51 100644 --- a/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue +++ b/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue @@ -113,6 +113,20 @@ @viewAllSubflowVerison="$emit('viewAllSubflowVerison', $event)" @changeSubNodeVersion="onChangeSubNodeVersion" /> +
+

{{ $t('访问凭证') }}

+ +
'); } - this.updateBasicInfo({ desc }); + if (Object.prototype.hasOwnProperty.call(resp.data, 'credentials')) { + this.updateBasicInfo({ desc, isHaveCredentials: true }); + } else { + this.updateBasicInfo({ desc }); + } + this.credentialLoading = false; // 获取host const { origin } = window.location; const hostUrl = `${origin + window.SITE_URL}plugin_service/data_api/${plugin}/`; @@ -818,6 +846,7 @@ let code = ''; let desc = ''; let version = ''; + let credentials = null; // 节点已选择标准插件 if (component.code && !this.isNotExistAtomOrVersion) { // 节点插件存在 if (component.code === 'remote_plugin') { @@ -827,6 +856,7 @@ basicInfoName = resp.data.name; version = atom.version; desc = atom.desc; + credentials = resp.data.credentials || null; } else if (component.code === 'uniform_api') { code = component.code; version = component.version; @@ -862,6 +892,7 @@ autoRetry: Object.assign({}, { enable: false, interval: 0, times: 1 }, auto_retry), timeoutConfig: timeoutConfig || { enable: false, seconds: 10, action: 'forced_fail' }, executor_proxy: executorProxy ? executorProxy.split(',') : [], + credentials, }; if (component.code === 'uniform_api' && component.api_meta) { // 新版api插件中component包含api_meta字段 const { id, name, api_key: apiKey, meta_url, category = {} } = component.api_meta; @@ -1193,6 +1224,9 @@ }); this.$refs.basicInfo && this.$refs.basicInfo.validate(); // 清除节点保存报错时的错误信息 }, + onChangeCredential(val,) { + this.updateBasicInfo({ credentials: val }); + }, /** * 更新基础信息 * 填写基础信息表单,切换插件/子流程,选择插件版本,子流程更新 @@ -1431,7 +1465,6 @@ // 删除全局变量 deleteVariable(key) { const constant = this.localConstants[key]; - Object.keys(this.localConstants).forEach((key) => { const varItem = this.localConstants[key]; if (varItem.index > constant.index) { @@ -1443,7 +1476,13 @@ }, // 节点配置面板表单校验,基础信息和输入参数 validate() { - return this.$refs.basicInfo.validate().then(() => { + return this.$refs.basicInfo.validate().then(async () => { + if (this.$refs.accessCredential) { + const validations = await Promise.all([this.$refs.accessCredential.validate()]); + if (!validations) { + return false; + }; + } if (this.$refs.inputParams) { let result = this.$refs.inputParams.validate(); // api插件额外校验json类型 @@ -1554,6 +1593,7 @@ autoRetry, timeoutConfig, executor_proxy, + credentials, } = this.basicInfo; // 设置标准插件节点在 activity 的 component.data 值 let data = {}; @@ -1568,6 +1608,9 @@ data, version: this.isThirdParty ? '1.0.0' : version, }; + if (credentials) { + component.credentials = credentials; + } if (this.isApiPlugin && this.basicInfo.pluginId) { // 新版api插件中component包含pluginId字段 const { pluginId, name, metaUrl, groupId, groupName, apiKey } = this.basicInfo; component.api_meta = { diff --git a/frontend/src/views/template/TemplateEdit/TemplateSetting/TabGlobalVariables/VariableEdit.vue b/frontend/src/views/template/TemplateEdit/TemplateSetting/TabGlobalVariables/VariableEdit.vue index 130e72d680..abd90a3e87 100644 --- a/frontend/src/views/template/TemplateEdit/TemplateSetting/TabGlobalVariables/VariableEdit.vue +++ b/frontend/src/views/template/TemplateEdit/TemplateSetting/TabGlobalVariables/VariableEdit.vue @@ -565,7 +565,7 @@ const classify = customType ? 'variable' : 'component'; this.atomConfigLoading = true; this.atomTypeKey = atom; - if (atomFilter.isConfigExists(atom, version, this.atomFormConfig)) { + if (atomFilter.isConfigExists(atom, version, this.atomFormConfig)) { // 判断配置文件是否已经获取过 this.getRenderConfig(); this.$nextTick(() => { this.atomConfigLoading = false; @@ -659,7 +659,6 @@ error_message: i18n.t('默认值不符合正则规则'), }); } - this.renderConfig = [config]; if (!this.variableData.key) { // 新建变量 this.theEditingData.value = atomFilter.getFormItemDefaultValue(this.renderConfig); @@ -768,7 +767,7 @@ this.theEditingData.is_meta = data.type === 'meta'; this.metaTag = data.meta_tag; - const validateSet = this.getValidateSet(); + const validateSet = this.getValidateSet(); // 获取校验规则 this.$set(this.renderOption, 'validateSet', validateSet); this.getAtomConfig(); }, diff --git a/frontend/src/views/template/TemplateMock/MockExecute/index.vue b/frontend/src/views/template/TemplateMock/MockExecute/index.vue index 42c1305dd2..d6b109c0d1 100644 --- a/frontend/src/views/template/TemplateMock/MockExecute/index.vue +++ b/frontend/src/views/template/TemplateMock/MockExecute/index.vue @@ -4,6 +4,30 @@ class="mock-execute">
+ +

{{ $t('填写调试入参') }} @@ -148,11 +172,15 @@ rules: {}, unMockNodes: [], unMockExpend: false, + // mockCredentialValue: '', + // credentialList: [], }; }, computed: { ...mapState({ creator: state => state.username, + // spaceId: state => state.template.spaceId, + // scopeInfo: state => state.template.scopeInfo, }), isUnreferencedShow() { if (this.isLoading) return false; @@ -163,14 +191,22 @@ }, created() { this.loadData(); + // this.loadCredentialList(); }, methods: { ...mapActions('template/', [ 'gerTemplatePreviewData', + // 'getCredentialList' ]), ...mapActions('task/', [ 'createMockTask', ]), + // async loadCredentialList() { + // this.listLoading = true; + // const res = await this.getCredentialList({ space_id: this.spaceId, ...this.scopeInfo }); + // this.credentialList = res.data.results || []; + // this.listLoading = false; + // }, async loadData() { try { const resp = await this.gerTemplatePreviewData({ @@ -313,6 +349,8 @@ } return isEqual; }, + // onChangeMockCredential() { + // }, }, }; @@ -344,6 +382,15 @@ font-weight: Bold; margin-bottom: 16px; } + .credential-wrap { + flex: 1; + display: flex; + flex-direction: column; + padding: 12px 24px 25px 24px; + margin-bottom: 16px; + background: #fff; + box-shadow: 0 2px 4px 0 #1919290d; + } .variable-wrap { flex: 1; display: flex; diff --git a/tests/interface/credential/test_credential_model.py b/tests/interface/credential/test_credential_model.py index 735070d7dc..303111f842 100644 --- a/tests/interface/credential/test_credential_model.py +++ b/tests/interface/credential/test_credential_model.py @@ -19,7 +19,13 @@ import pytest from django.db import IntegrityError -from bkflow.space.models import Credential, CredentialScope, CredentialType, Space +from bkflow.space.models import ( + Credential, + CredentialScope, + CredentialScopeLevel, + CredentialType, + Space, +) @pytest.mark.django_db @@ -212,6 +218,10 @@ def test_can_use_in_scope_without_scope(self, test_credential): def test_can_use_in_scope_with_matching_scope(self, test_credential): """测试匹配作用域的凭证可以使用""" + # 设置 scope_level 为 PART + test_credential.scope_level = CredentialScopeLevel.PART.value + test_credential.save() + # 添加作用域 CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") @@ -226,18 +236,23 @@ def test_can_use_in_scope_with_matching_scope(self, test_credential): test_credential.get_scopes().delete() def test_can_use_in_scope_with_template_no_scope(self, test_credential): - """测试模板没有作用域时,有作用域的凭证也可以使用""" - # 添加作用域 - CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") + """测试模板没有作用域时,scope_level == ALL 的凭证可以使用""" + # 设置 scope_level 为 ALL(空间内开放) + test_credential.scope_level = CredentialScopeLevel.ALL.value + test_credential.save() - # 模板没有作用域(都为 None),有作用域的凭证也可以使用 + # 模板没有作用域(都为 None),scope_level == ALL 的凭证可以使用 assert test_credential.can_use_in_scope(None, None) is True - # 清理 - test_credential.get_scopes().delete() + # 有作用域时也可以使用 + assert test_credential.can_use_in_scope("project", "project_1") is True def test_multiple_scopes(self, test_credential): """测试多个作用域""" + # 设置 scope_level 为 PART + test_credential.scope_level = CredentialScopeLevel.PART.value + test_credential.save() + # 添加多个作用域 CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_1") CredentialScope.objects.create(credential_id=test_credential.id, scope_type="project", scope_value="project_2") diff --git a/tests/interface/credential/test_credential_resolver.py b/tests/interface/credential/test_credential_resolver.py index 50be60bbeb..fa46345405 100644 --- a/tests/interface/credential/test_credential_resolver.py +++ b/tests/interface/credential/test_credential_resolver.py @@ -23,7 +23,13 @@ CredentialNotFoundError, CredentialScopeValidationError, ) -from bkflow.space.models import Credential, CredentialScope, CredentialType, Space +from bkflow.space.models import ( + Credential, + CredentialScope, + CredentialScopeLevel, + CredentialType, + Space, +) @pytest.mark.django_db @@ -46,6 +52,7 @@ def test_credential(self, test_space): type=CredentialType.BK_APP.value, content={"bk_app_code": "app", "bk_app_secret": "secret"}, creator="test_user", + scope_level=CredentialScopeLevel.PART.value, ) # 添加默认作用域 CredentialScope.objects.create(credential_id=credential.id, scope_type="test", scope_value="test_1") @@ -61,6 +68,7 @@ def scoped_credential(self, test_space): type=CredentialType.BASIC_AUTH.value, content={"username": "admin", "password": "secret"}, creator="test_user", + scope_level=CredentialScopeLevel.PART.value, ) CredentialScope.objects.create(credential_id=credential.id, scope_type="project", scope_value="project_1") yield credential diff --git a/tests/interface/credential/test_credential_scope_validator.py b/tests/interface/credential/test_credential_scope_validator.py index 4bc1951246..dc895017a5 100644 --- a/tests/interface/credential/test_credential_scope_validator.py +++ b/tests/interface/credential/test_credential_scope_validator.py @@ -23,7 +23,13 @@ validate_credential_scope, ) from bkflow.space.exceptions import CredentialScopeValidationError -from bkflow.space.models import Credential, CredentialScope, CredentialType, Space +from bkflow.space.models import ( + Credential, + CredentialScope, + CredentialScopeLevel, + CredentialType, + Space, +) @pytest.mark.django_db @@ -46,6 +52,7 @@ def credential_with_scope(self, test_space): type=CredentialType.BK_APP.value, content={"bk_app_code": "app", "bk_app_secret": "secret"}, creator="test_user", + scope_level=CredentialScopeLevel.PART.value, ) CredentialScope.objects.create(credential_id=credential.id, scope_type="project", scope_value="project_1") yield credential @@ -53,13 +60,14 @@ def credential_with_scope(self, test_space): @pytest.fixture def credential_without_scope(self, test_space): - """创建没有作用域限制的凭证""" + """创建没有作用域限制的凭证(scope_level == ALL)""" credential = Credential.create_credential( space_id=test_space.id, name="no_scope_credential", type=CredentialType.BASIC_AUTH.value, content={"username": "admin", "password": "secret"}, creator="test_user", + scope_level=CredentialScopeLevel.ALL.value, ) yield credential credential.hard_delete() @@ -78,16 +86,16 @@ def test_validate_credential_with_non_matching_scope(self, credential_with_scope assert "不能在作用域" in str(exc_info.value) def test_validate_credential_without_scope_fails(self, credential_without_scope): - """测试验证没有作用域的凭证(应该失败)""" - # 凭证没有作用域,不允许使用 - with pytest.raises(CredentialScopeValidationError): - validate_credential_scope(credential_without_scope, "project", "project_1") + """测试验证 scope_level == ALL 的凭证(应该可以使用)""" + # scope_level == ALL 的凭证可以在任何地方使用 + result = validate_credential_scope(credential_without_scope, "project", "project_1") + assert result is True def test_validate_credential_with_scope_on_template_without_scope(self, credential_with_scope): - """测试在没有作用域的模板中使用有作用域的凭证""" - # 模板没有作用域,有作用域的凭证可以使用 - result = validate_credential_scope(credential_with_scope, None, None) - assert result is True + """测试在没有作用域的模板中使用 scope_level == PART 的凭证(应该失败)""" + # 模板没有作用域,scope_level == PART 的凭证不能使用 + with pytest.raises(CredentialScopeValidationError): + validate_credential_scope(credential_with_scope, None, None) @pytest.mark.django_db @@ -104,32 +112,35 @@ def test_space(self): @pytest.fixture def setup_credentials(self, test_space): """创建测试凭证""" - # 1. 没有作用域的凭证 + # 1. scope_level == ALL 的凭证(空间内开放,可以在任何地方使用) cred1 = Credential.create_credential( space_id=test_space.id, name="no_scope", type=CredentialType.BK_APP.value, content={"bk_app_code": "app1", "bk_app_secret": "secret1"}, creator="test_user", + scope_level=CredentialScopeLevel.ALL.value, ) - # 2. 有匹配作用域的凭证 + # 2. scope_level == PART 且有匹配作用域的凭证 cred2 = Credential.create_credential( space_id=test_space.id, name="matching_scope", type=CredentialType.BK_APP.value, content={"bk_app_code": "app2", "bk_app_secret": "secret2"}, creator="test_user", + scope_level=CredentialScopeLevel.PART.value, ) CredentialScope.objects.create(credential_id=cred2.id, scope_type="project", scope_value="project_1") - # 3. 有不匹配作用域的凭证 + # 3. scope_level == PART 且有不匹配作用域的凭证 cred3 = Credential.create_credential( space_id=test_space.id, name="non_matching_scope", type=CredentialType.BK_APP.value, content={"bk_app_code": "app3", "bk_app_secret": "secret3"}, creator="test_user", + scope_level=CredentialScopeLevel.PART.value, ) CredentialScope.objects.create(credential_id=cred3.id, scope_type="project", scope_value="project_2") @@ -144,18 +155,19 @@ def test_filter_with_no_template_scope(self, test_space, setup_credentials): """测试模板没有作用域时的过滤""" queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) - # 模板没有作用域,应该返回所有凭证 + # 模板没有作用域,只应该返回 scope_level == ALL 的凭证 filtered = filter_credentials_by_scope(queryset, None, None) - assert filtered.count() == 3 + assert filtered.count() == 1 + assert filtered.first().name == "no_scope" def test_filter_with_matching_scope(self, test_space, setup_credentials): """测试匹配作用域的过滤""" queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) - # 应该返回:没有作用域的 + 匹配作用域的 + # 应该返回:scope_level == ALL 的凭证 + scope_level == PART 且作用域匹配的凭证 filtered = filter_credentials_by_scope(queryset, "project", "project_1") - # 应该只有 2 个凭证:no_scope 和 matching + # 应该只有 2 个凭证:no_scope (ALL) 和 matching_scope (PART 且匹配) assert filtered.count() == 2 names = [c.name for c in filtered] assert "no_scope" in names @@ -166,7 +178,7 @@ def test_filter_with_non_existing_scope(self, test_space, setup_credentials): """测试不存在的作用域过滤""" queryset = Credential.objects.filter(space_id=test_space.id, is_deleted=False) - # 只应该返回没有作用域的凭证 + # 只应该返回 scope_level == ALL 的凭证(空间内开放,可以在任何地方使用) filtered = filter_credentials_by_scope(queryset, "project", "project_999") assert filtered.count() == 1 From 1e17d7a2ae616f617ba180aa8e441baeb1044498 Mon Sep 17 00:00:00 2001 From: Mianhuatang8 <2542880657@qq.com> Date: Fri, 28 Nov 2025 10:42:30 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E8=8A=82=E7=82=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=B7=BB=E5=8A=A0=E5=87=AD=E8=AF=81=E9=80=89=E6=8B=A9?= =?UTF-8?q?=20--story=3D125449007=20(#488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 节点配置添加凭证选择 --story=125449007 # Reviewed, transaction id: 66075 * fix: 关闭label提示下划线 --story=125449007 # Reviewed, transaction id: 66078 * feat: 节点配置添加凭证选择 --story=125449007 # Reviewed, transaction id: 66104 * fix: 凭证选择校验优化 --story=125449007 # Reviewed, transaction id: 66163 --- .../NodeConfig/AccessCredential.vue | 77 ++++++++----------- .../TemplateEdit/NodeConfig/NodeConfig.vue | 23 +++++- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue b/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue index b57d2562fc..b274e00090 100644 --- a/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue +++ b/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue @@ -2,28 +2,30 @@

+ :rules="credentialRules"> + :desc="item.description" + :rules="credentialRules.curCredential" + :property="'curCredential.' + index + '.value'"> @@ -32,6 +34,7 @@ - diff --git a/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue b/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue index 9bea126f51..3c5011692e 100644 --- a/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue +++ b/frontend/src/views/template/TemplateEdit/NodeConfig/NodeConfig.vue @@ -237,7 +237,6 @@ import jsonFormSchema from '@/utils/jsonFormSchema.js'; import copy from '@/mixins/copy.js'; import AccessCredential from './AccessCredential.vue'; -import { async } from '@antv/x6/lib/registry/marker/async'; export default { name: 'NodeConfig', @@ -556,7 +555,13 @@ import { async } from '@antv/x6/lib/registry/marker/async'; this.inputsRenderConfig = renderConfig; await this.getPluginDetail(); if (this.nodeConfig.component.credentials) { - this.updateBasicInfo({ credentials: this.nodeConfig.component.credentials }); + const backfillData = this.basicInfo.processCredentials.map((item) => { + if (this.nodeConfig.component.credentials[item.key]) { + item.value = this.nodeConfig.component.credentials[item.key].value; + } + return item; + }); + this.updateBasicInfo({ credentials: this.nodeConfig.component.credentials, processCredentials: backfillData }); } // api插件json字段展示解析优化 this.handleJsonValueParse(false, paramsVal); @@ -682,7 +687,17 @@ import { async } from '@antv/x6/lib/registry/marker/async'; desc = descList.join('
'); } if (Object.prototype.hasOwnProperty.call(resp.data, 'credentials')) { - this.updateBasicInfo({ desc, isHaveCredentials: true }); + const processCredentials = []; + resp.data.credentials.forEach((item) => { + processCredentials.push({ + key: item.key, + hook: false, + need_render: true, + value: '', + description: item.description || '', + }); + }); + this.updateBasicInfo({ desc, isHaveCredentials: true, processCredentials }); } else { this.updateBasicInfo({ desc }); } @@ -1478,7 +1493,7 @@ import { async } from '@antv/x6/lib/registry/marker/async'; validate() { return this.$refs.basicInfo.validate().then(async () => { if (this.$refs.accessCredential) { - const validations = await Promise.all([this.$refs.accessCredential.validate()]); + const validations = await this.$refs.accessCredential.validate(); if (!validations) { return false; }; From ed48b80a94eed4b17b58ead5c0e63515aadb00e5 Mon Sep 17 00:00:00 2001 From: dengyh Date: Tue, 2 Dec 2025 20:41:40 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E5=87=AD=E8=AF=81=E7=9A=84=E7=B1=BB=E5=9E=8B=E6=8E=A8?= =?UTF-8?q?=E6=96=AD=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0011_set_credential_type.py | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 bkflow/space/migrations/0011_set_credential_type.py diff --git a/bkflow/space/migrations/0011_set_credential_type.py b/bkflow/space/migrations/0011_set_credential_type.py new file mode 100644 index 0000000000..6f6ea61f80 --- /dev/null +++ b/bkflow/space/migrations/0011_set_credential_type.py @@ -0,0 +1,201 @@ +""" +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 django.conf import settings +from django.db import connection, migrations + +from bkflow.utils.crypt import BaseCrypt + + +def detect_credential_type(content_obj): + """ + 根据凭证内容判断凭证类型 + + :param content_obj: 解密后的凭证内容字典 + :return: 凭证类型字符串 + """ + if not content_obj or not isinstance(content_obj, dict): + return "CUSTOM" + + keys = set(content_obj.keys()) + + # BK_APP: 包含 bk_app_code 和 bk_app_secret,且没有其他字段(或只有这两个) + if keys == {"bk_app_code", "bk_app_secret"}: + return "BK_APP" + + # BK_ACCESS_TOKEN: 包含 access_token,且没有其他字段(或只有这一个) + if keys == {"access_token"}: + return "BK_ACCESS_TOKEN" + + # BASIC_AUTH: 包含 username 和 password,且没有其他字段(或只有这两个) + if keys == {"username", "password"}: + return "BASIC_AUTH" + + # 其他情况或无法确定,使用自定义 + return "CUSTOM" + + +def set_credential_type_from_content(apps, schema_editor): + """ + 根据凭证内容设置凭证类型 + + :param apps: Django apps registry + :param schema_editor: 数据库 schema 编辑器 + """ + Credential = apps.get_model("space", "Credential") + table_name = Credential._meta.db_table + crypt = BaseCrypt(instance_key=settings.PRIVATE_SECRET) + + # 统计信息 + total_count = 0 + updated_count = 0 + skipped_count = 0 + error_count = 0 + + print("\n开始根据凭证内容设置凭证类型...") + + def is_value_encrypted(raw_value: str) -> bool: + """ + 判断值是否已加密 + """ + if not isinstance(raw_value, str) or not raw_value: + return False + try: + plain = crypt.decrypt(raw_value) + re_encrypted = crypt.encrypt(plain) + return re_encrypted == raw_value + except Exception: + return False + + def decrypt_content(raw_content): + """ + 解密凭证内容 + + :param raw_content: 原始内容(可能是加密的 JSON 字符串或字典) + :return: 解密后的内容字典 + """ + # 解析 JSON + try: + content_obj = ( + raw_content if isinstance(raw_content, dict) else json.loads(raw_content) if raw_content else None + ) + except Exception: + content_obj = None + + if not content_obj or not isinstance(content_obj, dict): + return None + + # 解密每个值 + decrypted_content = {} + for key, value in content_obj.items(): + if value is not None and isinstance(value, str): + # 尝试解密 + if is_value_encrypted(value): + try: + decrypted_content[key] = crypt.decrypt(value) + except Exception: + # 解密失败,使用原值(可能是未加密的旧数据) + decrypted_content[key] = value + else: + # 未加密,直接使用 + decrypted_content[key] = value + else: + decrypted_content[key] = value + + return decrypted_content + + with connection.cursor() as cursor: + cursor.execute(f"SELECT id, name, type, content FROM {connection.ops.quote_name(table_name)}") + rows = cursor.fetchall() + + for row in rows: + total_count += 1 + cred_id, cred_name, cred_type, raw_content = row + + # 如果已经有类型且不是空字符串,跳过 + if cred_type and cred_type.strip(): + skipped_count += 1 + print(f" 跳过已有类型的凭证: ID={cred_id}, Name={cred_name}, Type={cred_type}") + continue + + try: + # 解密内容 + decrypted_content = decrypt_content(raw_content) + + if not decrypted_content: + # 无法解析内容,设置为自定义 + detected_type = "CUSTOM" + else: + # 根据内容判断类型 + detected_type = detect_credential_type(decrypted_content) + + # 更新类型 + with connection.cursor() as cursor: + cursor.execute( + f"UPDATE {connection.ops.quote_name(table_name)} SET type=%s WHERE id=%s", + [detected_type, cred_id], + ) + + updated_count += 1 + print(f" ✓ 成功设置凭证类型: ID={cred_id}, Name={cred_name}, Type={detected_type}") + + except Exception as e: + error_count += 1 + print(f" ✗ 设置凭证类型失败: ID={cred_id}, Name={cred_name}, Error={str(e)}") + + # 打印统计信息 + print("\n" + "=" * 70) + print("凭证类型设置完成!") + print(f" 总计: {total_count} 条") + print(f" 已更新: {updated_count} 条") + print(f" 已跳过: {skipped_count} 条(已有类型或空数据)") + print(f" 失败: {error_count} 条") + print("=" * 70 + "\n") + + +def reverse_set_credential_type(apps, schema_editor): + """ + 回滚操作:将凭证类型设置为 CUSTOM(因为无法确定原始类型) + + :param apps: Django apps registry + :param schema_editor: 数据库 schema 编辑器 + """ + Credential = apps.get_model("space", "Credential") + table_name = Credential._meta.db_table + + print("\n开始回滚:将凭证类型设置为 CUSTOM...") + print("注意:回滚操作会将所有凭证类型设置为 CUSTOM,因为无法确定原始类型") + + with connection.cursor() as cursor: + cursor.execute(f"UPDATE {connection.ops.quote_name(table_name)} SET type='CUSTOM'") + affected_rows = cursor.rowcount + + print(f"\n回滚完成!已将 {affected_rows} 条凭证的类型设置为 CUSTOM\n") + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0010_credential_scope_level"), + ] + + operations = [ + migrations.RunPython(set_credential_type_from_content, reverse_set_credential_type), + ] From d46aba366331dac2d5862a77ab5d988118473b9a Mon Sep 17 00:00:00 2001 From: Mianhuatang8 <2542880657@qq.com> Date: Fri, 19 Dec 2025 15:46:51 +0800 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E9=80=89=E6=8B=A9=E5=88=97=E8=A1=A8=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/store/modules/template.js | 2 +- .../template/TemplateEdit/NodeConfig/AccessCredential.vue | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/store/modules/template.js b/frontend/src/store/modules/template.js index 861ce71e5e..9d1348d36e 100644 --- a/frontend/src/store/modules/template.js +++ b/frontend/src/store/modules/template.js @@ -1258,7 +1258,7 @@ const template = { }, // 获取凭证列表 getCredentialList({}, data) { - return axios.get(`/api/space/admin/credential_config/`, {params: data }).then(response => response.data); + return axios.get(`/api/template/${data.template_id}/credentials/`, {params: data }).then(response => response.data); }, }, getters: { diff --git a/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue b/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue index b274e00090..2632eb59d6 100644 --- a/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue +++ b/frontend/src/views/template/TemplateEdit/NodeConfig/AccessCredential.vue @@ -76,7 +76,10 @@ export default { ]), async loadCredentialList() { this.listLoading = true; - const res = await this.getCredentialList({ space_id: this.spaceId, ...this.scopeInfo }); + const res = await this.getCredentialList({ + space_id: this.spaceId, + ...this.scopeInfo, + template_id: this.$route.params.templateId }); this.list = res.data.results || []; this.listLoading = false; }, From 28ec4e781c4d94520ad56acbfaa4879ff1c54dff Mon Sep 17 00:00:00 2001 From: dengyh Date: Tue, 23 Dec 2025 11:08:25 +0800 Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=8F=AF=E7=94=A8=E5=87=AD=E8=AF=81=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/contrib/api/collections/interface.py | 5 - bkflow/space/urls.py | 6 +- bkflow/space/views.py | 292 +++++++++----------- tests/interface/space/test_space_config.py | 11 - 4 files changed, 126 insertions(+), 188 deletions(-) diff --git a/bkflow/contrib/api/collections/interface.py b/bkflow/contrib/api/collections/interface.py index 573883fda6..d6b23b555f 100644 --- a/bkflow/contrib/api/collections/interface.py +++ b/bkflow/contrib/api/collections/interface.py @@ -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 diff --git a/bkflow/space/urls.py b/bkflow/space/urls.py index 1c5f44743a..47cba60de6 100644 --- a/bkflow/space/urls.py +++ b/bkflow/space/urls.py @@ -21,8 +21,7 @@ from rest_framework.routers import DefaultRouter from bkflow.space.views import ( - CredentialConfigAdminViewSet, - CredentialViewSet, + CredentialConfigViewSet, SpaceConfigAdminViewSet, SpaceConfigViewSet, SpaceInternalViewSet, @@ -31,13 +30,12 @@ router = DefaultRouter() router.register(r"", SpaceViewSet) -router.register("credential", CredentialViewSet) router.register(r"internal", SpaceInternalViewSet, basename="internal") router.register(r"config", SpaceConfigViewSet, basename="config") admin_router = DefaultRouter() admin_router.register(r"space_config", SpaceConfigAdminViewSet, basename="space_config") -admin_router.register(r"credential_config", CredentialConfigAdminViewSet, basename="credential_config") +admin_router.register(r"credential_config", CredentialConfigViewSet, basename="credential_config") urlpatterns = [ url(r"^", include(router.urls)), diff --git a/bkflow/space/views.py b/bkflow/space/views.py index 257d72500a..1db4ee37a1 100644 --- a/bkflow/space/views.py +++ b/bkflow/space/views.py @@ -47,7 +47,6 @@ SpaceConfigHandler, SuperusersConfig, ) -from bkflow.space.credential.scope_validator import filter_credentials_by_scope from bkflow.space.exceptions import SpaceConfigDefaultValueNotExists from bkflow.space.models import ( Credential, @@ -79,47 +78,142 @@ logger = logging.getLogger("root") -class CredentialFilterSet(FilterSet): - class Meta: - model = Credential - fields = {"space_id": ["exact"], "name": ["exact"], "type": ["exact"]} - +class CredentialConfigViewSet(AdminModelViewSet): + """ + 凭证接口 + """ -@method_decorator(login_exempt, name="dispatch") -class CredentialViewSet(AdminModelViewSet): queryset = Credential.objects.filter(is_deleted=False) serializer_class = CredentialSerializer - permission_classes = [AdminPermission | AppInternalPermission] - filter_backends = [DjangoFilterBackend] - filter_class = CredentialFilterSet + permission_classes = [AdminPermission | SpaceSuperuserPermission] + pagination_class = BKFLOWDefaultPagination + filter_backends = [DjangoFilterBackend, BKFlowOrderingFilter] + ordering_fields = ["id", "name", "type", "create_at", "update_at"] + ordering = ["-create_at"] # 默认按创建时间倒序 + + def get_object(self): + serializer = CredentialBaseQuerySerializer(data=self.request.query_params) + serializer.is_valid(raise_exception=True) + space_id = serializer.validated_data.get("space_id") + + pk = self.kwargs.get(self.lookup_field) + obj = self.queryset.get(pk=pk, space_id=space_id) + return obj def get_queryset(self): - """根据作用域过滤凭证""" queryset = super().get_queryset() + serializer = CredentialBaseQuerySerializer(data=self.request.query_params) + serializer.is_valid(raise_exception=True) + space_id = serializer.validated_data.get("space_id") + queryset = queryset.filter(space_id=space_id, is_deleted=False) + return queryset + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response(self.fill_credential_scopes(serializer.data)) + + def fill_credential_scopes(self, credential_data): + """ + 填充凭证作用域 + """ + scopes = CredentialScope.objects.filter(credential_id=credential_data["id"]) + credential_data["scopes"] = CredentialScopeSerializer(scopes, many=True).data + return credential_data - # 从查询参数获取作用域信息 - scope_type = self.request.query_params.get("scope_type") - scope_value = self.request.query_params.get("scope_value") + def update_scopes(self, credential, scope_level, scopes, update=False): + """ + 更新凭证作用域 + """ + # 创建凭证作用域 + if scope_level == CredentialScopeLevel.PART.value: + if update: + CredentialScope.objects.filter(credential_id=credential.id).delete() + + if 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) - # 如果提供了作用域信息,过滤凭证 - if scope_type or scope_value: - queryset = filter_credentials_by_scope(queryset, scope_type, scope_value) + def create(self, request, *args, **kwargs): + credential_serializer = CreateCredentialSerializer(data=request.data) + credential_serializer.is_valid(raise_exception=True) + credential_data = credential_serializer.validated_data - return queryset + serializer = CredentialBaseQuerySerializer(data=self.request.query_params) + serializer.is_valid(raise_exception=True) + space_id = serializer.validated_data.get("space_id") - @action(detail=False, methods=["GET"]) - def get_api_gateway_credential(self, request, *args, **kwargs): - space_id = request.query_params.get("space_id") try: - api_gateway_credential_name = SpaceConfig.get_config(space_id, ApiGatewayCredentialConfig.name) - credential = self.queryset.get( - space_id=space_id, name=api_gateway_credential_name, type=CredentialType.BK_APP.value - ) - except (Credential.DoesNotExist, SpaceConfigDefaultValueNotExists) as e: - logger.exception("CredentialViewSet 获取空间下的凭证异常, space_id={}, err={}, ".format(space_id, e)) - return Response({}) + with transaction.atomic(): + credential = Credential.create_credential( + space_id=space_id, + name=credential_data["name"], + type=credential_data["type"], + content=credential_data["content"], + creator=request.user.username, + desc=credential_data.get("desc"), + scope_level=credential_data.get("scope_level"), + ) + self.update_scopes( + credential, credential_data.get("scope_level"), credential_data.get("scopes"), update=False + ) + except DatabaseError as e: + err_msg = f"创建凭证失败 {str(e)}" + logger.error(err_msg) + return Response(exception=True, data={"detail": err_msg}) - return Response(credential.value) + response_serializer = CredentialSerializer(credential) + return Response(self.fill_credential_scopes(response_serializer.data), status=status.HTTP_201_CREATED) + + def partial_update(self, request, *args, **kwargs): + try: + instance = self.get_object() + except Credential.DoesNotExist as e: + err_msg = f"更新凭证不存在 {str(e)}" + logger.error(err_msg) + return Response(err_msg, status=404) + + serializer = UpdateCredentialSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + with transaction.atomic(): + # 更新凭证基本信息 + scopes_data = serializer.validated_data.pop("scopes", None) + for attr, value in serializer.validated_data.items(): + setattr(instance, attr, value) + + instance.updated_by = request.user.username + updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] + instance.save(update_fields=updated_keys) + + self.update_scopes(instance, serializer.validated_data.get("scope_level"), scopes_data, update=True) + except DatabaseError as e: + err_msg = f"更新凭证失败 {str(e)}" + logger.error(err_msg) + return Response(exception=True, data={"detail": err_msg}) + # 序列化更新后的对象 + response_serializer = CredentialSerializer(instance) + return Response(response_serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, *args, **kwargs): + try: + with transaction.atomic(): + instance = self.get_object() + CredentialScope.objects.filter(credential_id=instance.id).delete() + instance.hard_delete() + except Credential.DoesNotExist as e: + err_msg = f"删除凭证不存在 {str(e)}" + logger.error(err_msg) + return Response(err_msg, status=404) + return Response() class SpaceFilterSet(FilterSet): @@ -335,144 +429,6 @@ def destroy(self, request, *args, **kwargs): return Response(exception=True, data={"detail": err_msg}) -class CredentialConfigAdminViewSet(ModelViewSet, SimpleGenericViewSet): - """ - 凭证接口 - """ - - queryset = Credential.objects.all() - serializer_class = CredentialSerializer - permission_classes = [AdminPermission | SpaceSuperuserPermission] - pagination_class = BKFLOWDefaultPagination - filter_backends = [DjangoFilterBackend, BKFlowOrderingFilter] - ordering_fields = ["id", "name", "type", "create_at", "update_at"] - ordering = ["-create_at"] # 默认按创建时间倒序 - - def get_object(self): - serializer = CredentialBaseQuerySerializer(data=self.request.query_params) - serializer.is_valid(raise_exception=True) - space_id = serializer.validated_data.get("space_id") - - pk = self.kwargs.get(self.lookup_field) - obj = self.queryset.get(pk=pk, space_id=space_id) - return obj - - def get_queryset(self): - queryset = super().get_queryset() - serializer = CredentialBaseQuerySerializer(data=self.request.query_params) - serializer.is_valid(raise_exception=True) - space_id = serializer.validated_data.get("space_id") - queryset = queryset.filter(space_id=space_id, is_deleted=False) - return queryset - - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(instance) - return Response(self.fill_credential_scopes(serializer.data)) - - def fill_credential_scopes(self, credential_data): - """ - 填充凭证作用域 - """ - scopes = CredentialScope.objects.filter(credential_id=credential_data["id"]) - credential_data["scopes"] = CredentialScopeSerializer(scopes, many=True).data - return credential_data - - def update_scopes(self, credential, scope_level, scopes, update=False): - """ - 更新凭证作用域 - """ - # 创建凭证作用域 - if scope_level == CredentialScopeLevel.PART.value: - if update: - CredentialScope.objects.filter(credential_id=credential.id).delete() - - if 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) - - def create(self, request, *args, **kwargs): - credential_serializer = CreateCredentialSerializer(data=request.data) - credential_serializer.is_valid(raise_exception=True) - credential_data = credential_serializer.validated_data - - serializer = CredentialBaseQuerySerializer(data=self.request.query_params) - serializer.is_valid(raise_exception=True) - space_id = serializer.validated_data.get("space_id") - - try: - with transaction.atomic(): - credential = Credential.create_credential( - space_id=space_id, - name=credential_data["name"], - type=credential_data["type"], - content=credential_data["content"], - creator=request.user.username, - desc=credential_data.get("desc"), - scope_level=credential_data.get("scope_level"), - ) - self.update_scopes( - credential, credential_data.get("scope_level"), credential_data.get("scopes"), update=False - ) - except DatabaseError as e: - err_msg = f"创建凭证失败 {str(e)}" - logger.error(err_msg) - return Response(exception=True, data={"detail": err_msg}) - - response_serializer = CredentialSerializer(credential) - return Response(self.fill_credential_scopes(response_serializer.data), status=status.HTTP_201_CREATED) - - def partial_update(self, request, *args, **kwargs): - try: - instance = self.get_object() - except Credential.DoesNotExist as e: - err_msg = f"更新凭证不存在 {str(e)}" - logger.error(err_msg) - return Response(err_msg, status=404) - - serializer = UpdateCredentialSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - try: - with transaction.atomic(): - # 更新凭证基本信息 - scopes_data = serializer.validated_data.pop("scopes", None) - for attr, value in serializer.validated_data.items(): - setattr(instance, attr, value) - - instance.updated_by = request.user.username - updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] - instance.save(update_fields=updated_keys) - - self.update_scopes(instance, serializer.validated_data.get("scope_level"), scopes_data, update=True) - except DatabaseError as e: - err_msg = f"更新凭证失败 {str(e)}" - logger.error(err_msg) - return Response(exception=True, data={"detail": err_msg}) - # 序列化更新后的对象 - response_serializer = CredentialSerializer(instance) - return Response(response_serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, *args, **kwargs): - try: - with transaction.atomic(): - instance = self.get_object() - CredentialScope.objects.filter(credential_id=instance.id).delete() - instance.hard_delete() - except Credential.DoesNotExist as e: - err_msg = f"删除凭证不存在 {str(e)}" - logger.error(err_msg) - return Response(err_msg, status=404) - return Response() - - class SpaceConfigViewSet(ModelViewSet, SimpleGenericViewSet): queryset = SpaceConfig.objects.all() serializer_class = SpaceConfigSerializer diff --git a/tests/interface/space/test_space_config.py b/tests/interface/space/test_space_config.py index c369ee5ecd..52a8cf8f29 100644 --- a/tests/interface/space/test_space_config.py +++ b/tests/interface/space/test_space_config.py @@ -144,17 +144,6 @@ def test_gateway_expression(self): with pytest.raises(ValidationError): config_cls.validate("a") - def test_api_gateway_credential_name(self): - config_cls = SpaceConfigHandler.get_config("api_gateway_credential_name") - assert config_cls == ApiGatewayCredentialConfig - assert config_cls.default_value is None - assert config_cls.value_type == SpaceConfigValueType.TEXT.value - - credential_in_string = "Test credential name" - credential_in_dict = {"default": "test credential name"} - assert config_cls.validate(credential_in_string) - assert config_cls.validate(credential_in_dict) - def test_space_plugin_config(self): config_cls = SpaceConfigHandler.get_config("space_plugin_config") assert config_cls == SpacePluginConfig From 6a4917d99b3d8326720e175b0ac690686a2ab1d3 Mon Sep 17 00:00:00 2001 From: dengyh Date: Wed, 25 Feb 2026 17:23:25 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=E5=90=8C=E6=AD=A5master?= =?UTF-8?q?=E6=9C=80=E6=96=B0=E4=BB=A3=E7=A0=81=E5=B9=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E7=9A=84=E6=8A=A5=E9=94=99?= =?UTF-8?q?=20--story=3D125449007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/serializers/credential.py | 7 +- bkflow/apigw/views/update_credential.py | 5 +- bkflow/space/views.py | 13 +++- tests/interface/space/test_space_models.py | 11 +-- tests/interface/space/test_space_views.py | 89 ++++------------------ 5 files changed, 34 insertions(+), 91 deletions(-) diff --git a/bkflow/apigw/serializers/credential.py b/bkflow/apigw/serializers/credential.py index 4cec7225f3..fe966ac716 100644 --- a/bkflow/apigw/serializers/credential.py +++ b/bkflow/apigw/serializers/credential.py @@ -20,6 +20,7 @@ from rest_framework import serializers 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 @@ -69,7 +70,7 @@ def validate(self, attrs): try: credential = CredentialDispatcher(credential_type, data=content) credential.validate_data() - except Exception as e: + except (serializers.ValidationError, CredentialTypeNotSupport) as e: raise serializers.ValidationError({"content": str(e)}) return attrs @@ -96,7 +97,7 @@ def validate(self, attrs): if "content" in attrs: # 如果有type字段使用type,否则需要从实例获取 credential_type = attrs.get("type") - if not credential_type and hasattr(self, "instance"): + if not credential_type and self.instance is not None: credential_type = self.instance.type if credential_type: @@ -104,7 +105,7 @@ def validate(self, attrs): try: credential = CredentialDispatcher(credential_type, data=content) credential.validate_data() - except Exception as e: + except (serializers.ValidationError, CredentialTypeNotSupport) as e: raise serializers.ValidationError({"content": str(e)}) return attrs diff --git a/bkflow/apigw/views/update_credential.py b/bkflow/apigw/views/update_credential.py index da90a2fa7c..02b5d6d225 100644 --- a/bkflow/apigw/views/update_credential.py +++ b/bkflow/apigw/views/update_credential.py @@ -47,14 +47,15 @@ def update_credential(request, space_id, credential_id): :return: 更新后的凭证信息 """ 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)) + ser = UpdateCredentialSerializer(instance=credential, data=data, partial=True) + ser.is_valid(raise_exception=True) + with transaction.atomic(): # 更新凭证基本信息 credential_data = dict(ser.validated_data) diff --git a/bkflow/space/views.py b/bkflow/space/views.py index 1db4ee37a1..fad366071c 100644 --- a/bkflow/space/views.py +++ b/bkflow/space/views.py @@ -180,19 +180,26 @@ def partial_update(self, request, *args, **kwargs): logger.error(err_msg) return Response(err_msg, status=404) - serializer = UpdateCredentialSerializer(data=request.data) + serializer = UpdateCredentialSerializer(instance=instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) try: with transaction.atomic(): # 更新凭证基本信息 scopes_data = serializer.validated_data.pop("scopes", None) + content_data = serializer.validated_data.pop("content", None) + for attr, value in serializer.validated_data.items(): setattr(instance, attr, value) instance.updated_by = request.user.username - updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] - instance.save(update_fields=updated_keys) + + if content_data is not None: + # 使用 update_credential 方法更新,确保经过类型校验和数据转换(内部会 save) + instance.update_credential(content_data) + else: + updated_keys = list(serializer.validated_data.keys()) + ["updated_by", "update_at"] + instance.save(update_fields=updated_keys) self.update_scopes(instance, serializer.validated_data.get("scope_level"), scopes_data, update=True) except DatabaseError as e: diff --git a/tests/interface/space/test_space_models.py b/tests/interface/space/test_space_models.py index 1d8734ceb2..68ddc3c63a 100644 --- a/tests/interface/space/test_space_models.py +++ b/tests/interface/space/test_space_models.py @@ -454,12 +454,7 @@ def test_update_credential(self): content={"bk_app_code": "old", "bk_app_secret": "old_secret"}, ) - # Note: update_credential uses self.data instead of self.content - # This appears to be a bug in the code (should be self.content) - # The method will set a dynamic attribute 'data' on the instance - # but it won't be saved to the database since 'data' is not a model field credential.update_credential({"bk_app_code": "new", "bk_app_secret": "new_secret"}) - # Verify that 'data' attribute was set (even though it's not a model field) - assert hasattr(credential, "data") - # The save() call will complete, but content field won't be updated - # because 'data' is not a model field, so save() won't persist it + credential.refresh_from_db() + assert credential.content["bk_app_code"] == "new" + assert credential.content["bk_app_secret"] == "new_secret" diff --git a/tests/interface/space/test_space_views.py b/tests/interface/space/test_space_views.py index db57f4086b..aac4482ba2 100644 --- a/tests/interface/space/test_space_views.py +++ b/tests/interface/space/test_space_views.py @@ -17,8 +17,7 @@ SpaceCreateType, ) from bkflow.space.views import ( - CredentialConfigAdminViewSet, - CredentialViewSet, + CredentialConfigViewSet, SpaceConfigAdminViewSet, SpaceConfigViewSet, SpaceFilterSet, @@ -27,69 +26,6 @@ ) -@pytest.mark.django_db -class TestCredentialViewSet: - def setup_method(self): - self.factory = APIRequestFactory() - self.user = User.objects.create_superuser(username="testuser", password="password") - self.space = Space.objects.create(name="Test Space", app_code="test_app") - - def test_get_api_gateway_credential_success(self): - """Test get_api_gateway_credential with existing credential""" - # Create credential - Credential.objects.create( - space_id=self.space.id, - name="test_credential", - type=CredentialType.BK_APP.value, - content={"app_code": "test", "app_secret": "secret"}, - ) - - # Create space config - SpaceConfig.objects.create( - space_id=self.space.id, name=ApiGatewayCredentialConfig.name, text_value="test_credential" - ) - - view = CredentialViewSet.as_view({"get": "get_api_gateway_credential"}) - request = self.factory.get(f"/credentials/get_api_gateway_credential/?space_id={self.space.id}") - force_authenticate(request, user=self.user) - - response = view(request) - - assert response.status_code == 200 - # Response is wrapped by SimpleGenericViewSet.finalize_response - assert response.data.get("data") == {"app_code": "test", "app_secret": "secret"} - - def test_get_api_gateway_credential_not_found(self): - """Test get_api_gateway_credential when credential does not exist""" - # Don't create credential, only create space config pointing to non-existent credential - SpaceConfig.objects.create( - space_id=self.space.id, name=ApiGatewayCredentialConfig.name, text_value="nonexistent_credential" - ) - - view = CredentialViewSet.as_view({"get": "get_api_gateway_credential"}) - request = self.factory.get(f"/credentials/get_api_gateway_credential/?space_id={self.space.id}") - force_authenticate(request, user=self.user) - - response = view(request) - - assert response.status_code == 200 - # Should return empty dict when credential not found - assert response.data.get("data") == {} - - def test_get_api_gateway_credential_config_not_exists(self): - """Test get_api_gateway_credential when space config does not exist""" - # Don't create space config - view = CredentialViewSet.as_view({"get": "get_api_gateway_credential"}) - request = self.factory.get(f"/credentials/get_api_gateway_credential/?space_id={self.space.id}") - force_authenticate(request, user=self.user) - - response = view(request) - - assert response.status_code == 200 - # Should return empty dict when config not found - assert response.data.get("data") == {} - - @pytest.mark.django_db class TestSpaceFilterSet: def setup_method(self): @@ -598,7 +534,7 @@ def test_destroy_space_config_exception(self): @pytest.mark.django_db -class TestCredentialConfigAdminViewSet: +class TestCredentialConfigViewSet: def setup_method(self): self.factory = APIRequestFactory() self.user, _ = User.objects.get_or_create( @@ -609,7 +545,7 @@ def setup_method(self): def test_crud_operations(self): """Test create, update, and delete credential""" # Create success - view = CredentialConfigAdminViewSet.as_view({"post": "create"}) + view = CredentialConfigViewSet.as_view({"post": "create"}) data = { "name": "new_cred", "type": CredentialType.BK_APP.value, @@ -651,9 +587,12 @@ def test_crud_operations(self): # Update credential = Credential.objects.create( - space_id=self.space.id, name="test_cred", type=CredentialType.BK_APP.value, content={"key": "old_value"} + space_id=self.space.id, + name="test_cred", + type=CredentialType.CUSTOM.value, + content={"key": "old_value"}, ) - view = CredentialConfigAdminViewSet.as_view({"patch": "partial_update"}) + view = CredentialConfigViewSet.as_view({"patch": "partial_update"}) data = {"content": {"key": "new_value"}} request = self.factory.patch(f"/credentials/{credential.id}/?space_id={self.space.id}", data, format="json") force_authenticate(request, user=self.user) @@ -664,7 +603,7 @@ def test_crud_operations(self): credential = Credential.objects.create( space_id=self.space.id, name="test_cred2", type=CredentialType.BK_APP.value, content={"key": "value"} ) - view = CredentialConfigAdminViewSet.as_view({"delete": "destroy"}) + view = CredentialConfigViewSet.as_view({"delete": "destroy"}) request = self.factory.delete(f"/credentials/{credential.id}/?space_id={self.space.id}") force_authenticate(request, user=self.user) response = view(request, pk=credential.id) @@ -681,7 +620,7 @@ def test_get_queryset(self): space_id=other_space.id, name="cred2", type=CredentialType.BK_APP.value, content={"key": "value2"} ) - viewset = CredentialConfigAdminViewSet() + viewset = CredentialConfigViewSet() viewset.request = type("Request", (), {"query_params": {"space_id": self.space.id}})() queryset = viewset.get_queryset() @@ -692,7 +631,7 @@ def test_create_credential_database_error(self): """Test create credential with DatabaseError""" from django.db import DatabaseError - view = CredentialConfigAdminViewSet.as_view({"post": "create"}) + view = CredentialConfigViewSet.as_view({"post": "create"}) data = { "name": "new_cred", "type": CredentialType.BK_APP.value, @@ -708,7 +647,7 @@ def test_create_credential_database_error(self): def test_partial_update_credential_not_found(self): """Test partial update credential when credential does not exist""" - view = CredentialConfigAdminViewSet.as_view({"patch": "partial_update"}) + view = CredentialConfigViewSet.as_view({"patch": "partial_update"}) data = {"content": {"key": "new_value"}} request = self.factory.patch(f"/credentials/99999/?space_id={self.space.id}", data, format="json") force_authenticate(request, user=self.user) @@ -725,7 +664,7 @@ def test_partial_update_credential_database_error(self): type=CredentialType.BK_APP.value, content={"bk_app_code": "test", "bk_app_secret": "secret"}, ) - view = CredentialConfigAdminViewSet.as_view({"patch": "partial_update"}) + view = CredentialConfigViewSet.as_view({"patch": "partial_update"}) data = {"content": {"bk_app_code": "new_test", "bk_app_secret": "new_secret"}} request = self.factory.patch(f"/credentials/{credential.id}/?space_id={self.space.id}", data, format="json") force_authenticate(request, user=self.user) @@ -738,7 +677,7 @@ def test_partial_update_credential_database_error(self): def test_destroy_credential_not_found(self): """Test destroy credential when credential does not exist""" - view = CredentialConfigAdminViewSet.as_view({"delete": "destroy"}) + view = CredentialConfigViewSet.as_view({"delete": "destroy"}) request = self.factory.delete(f"/credentials/99999/?space_id={self.space.id}") force_authenticate(request, user=self.user) response = view(request, pk=99999) From fd0d549e2e877dec8c9ee1a063495f3ee0143e3e Mon Sep 17 00:00:00 2001 From: dengyh Date: Sat, 28 Feb 2026 15:26:03 +0800 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E6=A1=88=E8=B7=9F=E5=AE=9E=E9=99=85=E6=A3=80=E6=B5=8B=E4=B8=8D?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98=20--story=3D12544?= =?UTF-8?q?9007?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../variables/collections/credential.py | 44 ------------------- frontend/src/config/i18n/cn.js | 2 +- frontend/src/config/i18n/en.js | 2 +- .../components/CredentialSlider.vue | 4 +- 4 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 bkflow/pipeline_plugins/variables/collections/credential.py diff --git a/bkflow/pipeline_plugins/variables/collections/credential.py b/bkflow/pipeline_plugins/variables/collections/credential.py deleted file mode 100644 index 44d86306fa..0000000000 --- a/bkflow/pipeline_plugins/variables/collections/credential.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -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. -""" -from typing import List - -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from pipeline.core.flow.io import StringItemSchema - -from bkflow.pipeline_plugins.variables.base import ( - CommonPlainVariable, - FieldExplain, - SelfExplainVariable, - Type, -) - - -class Credential(CommonPlainVariable, SelfExplainVariable): - code = "credential" - name = _("凭证") - type = "meta" - tag = "credential.credential" - meta_tag = "credential.credential_meta" - form = "{}variables/{}.js".format(settings.STATIC_URL, code) - schema = StringItemSchema(description=_("输入凭证")) - - @classmethod - def _self_explain(cls, **kwargs) -> List[FieldExplain]: - return [FieldExplain(key="${KEY}", type=Type.STRING, description="用户选择的凭证值")] diff --git a/frontend/src/config/i18n/cn.js b/frontend/src/config/i18n/cn.js index e6d6fd1ed2..20b194720c 100644 --- a/frontend/src/config/i18n/cn.js +++ b/frontend/src/config/i18n/cn.js @@ -1088,7 +1088,7 @@ const cn = { '第n项凭证作用域格式错误, 仅支持字母、数字、下划线、连字符': '第{value}项凭证作用域格式错误, 仅支持字母、数字、下划线、连字符', '蓝鲸 Access Token 认证': '蓝鲸 Access Token 认证', '仅支持字母、数字、下划线、连字符': '仅支持字母、数字、下划线、连字符', - '中英文字符、数字或以下字符-)().,以中英文字符、数字开头,32个字符内': '中英文字符、数字或以下字符-)().,以中英文字符、数字开头,32个字符内', + '英文字符、数字或以下字符-)().,以英文字符、数字开头,32个字符内': '英文字符、数字或以下字符-)().,以英文字符、数字开头,32个字符内', 登录态: '登录态', 选择凭证: '选择凭证', 请选择凭证: '请选择凭证', diff --git a/frontend/src/config/i18n/en.js b/frontend/src/config/i18n/en.js index 2e6d3e5dbe..277521294a 100644 --- a/frontend/src/config/i18n/en.js +++ b/frontend/src/config/i18n/en.js @@ -1087,7 +1087,7 @@ const en = { '第n项凭证作用域格式错误, 仅支持字母、数字、下划线、连字符': 'The format of the {value} credential scope is incorrect, only supporting letters, numbers, underscores, and hyphens', '蓝鲸 Access Token 认证': 'BlueKing Access Token Authentication', '仅支持字母、数字、下划线、连字符': 'Only supports letters, numbers, underscores, and hyphens', - '中英文字符、数字或以下字符-)().,以中英文字符、数字开头,32个字符内': 'Chinese and English characters, numbers, or the following characters -) () Starting with Chinese and English characters or numbers, within 32 characters', + '英文字符、数字或以下字符-)().,以英文字符、数字开头,32个字符内': 'Chinese and English characters, numbers, or the following characters -) () Starting with Chinese and English characters or numbers, within 32 characters', 登录态: 'Login state', 选择凭证: 'Select credential', 请选择凭证: 'Please select credential', diff --git a/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue b/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue index 743b20d8fd..06ff1a9f45 100644 --- a/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue +++ b/frontend/src/views/admin/Space/Credential/components/CredentialSlider.vue @@ -27,7 +27,7 @@ :maxlength="32" :placeholder=" $t( - '中英文字符、数字或以下字符-)().,以中英文字符、数字开头,32个字符内' + '英文字符、数字或以下字符-)().,以英文字符、数字开头,32个字符内' ) " /> @@ -266,7 +266,7 @@ export default { const reg = /^[a-zA-Z0-9][a-zA-Z0-9\-)().,]*$/; return reg.test(val); }, - message: this.$t('中英文字符、数字或以下字符-)().,以中英文字符、数字开头,32个字符内'), + message: this.$t('英文字符、数字或以下字符-)().,以英文字符、数字开头,32个字符内'), trigger: 'blur', }, ],