diff --git a/src/dashboard/apigateway/apigateway/apis/controller/views.py b/src/dashboard/apigateway/apigateway/apis/controller/views.py index 933e8148e..ff741bdf2 100644 --- a/src/dashboard/apigateway/apigateway/apis/controller/views.py +++ b/src/dashboard/apigateway/apigateway/apis/controller/views.py @@ -189,7 +189,10 @@ def _get_resource_permissions( return resource_permissions name_mappings = self._get_released_resource_name_mappings(release) - for permission in AppResourcePermission.objects.filter_permission(api_gateway, bk_app_codes=app_code_list): + queryset = AppResourcePermission.objects.filter(api=api_gateway) + if app_code_list: + queryset = queryset.filter(bk_app_code__in=app_code_list) + for permission in queryset: # 因为 resource_version 为下发生效的真实版本,因此没有匹配的权限无需下发 if permission.resource_id not in name_mappings: continue @@ -207,7 +210,9 @@ def _get_resource_permissions( return resource_permissions def _get_api_permissions(self, api_gateway: Gateway, app_code_list: Optional[List[str]]): - qs = AppAPIPermission.objects.filter_permission(api_gateway, bk_app_codes=app_code_list) + qs = AppAPIPermission.objects.filter(api=api_gateway) + if app_code_list: + qs = qs.filter(bk_app_code__in=app_code_list) return qs diff --git a/src/dashboard/apigateway/apigateway/apis/open/metrics/views.py b/src/dashboard/apigateway/apigateway/apis/open/metrics/views.py index 9cf15351d..38c95914e 100644 --- a/src/dashboard/apigateway/apigateway/apis/open/metrics/views.py +++ b/src/dashboard/apigateway/apigateway/apis/open/metrics/views.py @@ -44,13 +44,13 @@ def query_api_metrics(self, request, *args, **kwargs): end_time = slz.validated_data["end_time"] # 获取网关请求数据 - api_request_data = StatisticsAPIRequestByDay.objects.filter_and_aggregate_by_api( + api_request_data = StatisticsAPIRequestByDay.objects.filter_and_aggregate_by_gateway( start_time=start_time, end_time=end_time, ) # 获取应用请求数据 - app_request_data = StatisticsAppRequestByDay.objects.filter_app_and_aggregate_by_api( + app_request_data = StatisticsAppRequestByDay.objects.filter_app_and_aggregate_by_gateway( start_time=start_time, end_time=end_time, ) diff --git a/src/dashboard/apigateway/apigateway/apis/open/permission/helpers.py b/src/dashboard/apigateway/apigateway/apis/open/permission/helpers.py index c859d2473..2665ac929 100644 --- a/src/dashboard/apigateway/apigateway/apis/open/permission/helpers.py +++ b/src/dashboard/apigateway/apigateway/apis/open/permission/helpers.py @@ -31,6 +31,15 @@ from apigateway.core.models import Gateway, ReleasedResource, Resource +class AppPermissionHelper: + def get_permission_model(self, dimension: str): + if dimension == GrantDimensionEnum.API.value: + return AppAPIPermission + elif dimension == GrantDimensionEnum.RESOURCE.value: + return AppResourcePermission + raise ValueError(f"unsupported dimension: {dimension}") + + class ResourcePermission(BaseModel): class Config: arbitrary_types_allowed = True @@ -142,16 +151,16 @@ def build(self, resources: list) -> list: return [perm.as_dict() for perm in resource_permissions] def _get_api_permission(self): - return AppAPIPermission.objects.filter_permission( - gateway=self.gateway, + return AppAPIPermission.objects.filter( + api=self.gateway, bk_app_code=self.target_app_code, ).first() def _get_resource_permission_map(self): return { perm.resource_id: perm - for perm in AppResourcePermission.objects.filter_permission( - gateway=self.gateway, + for perm in AppResourcePermission.objects.filter( + api=self.gateway, bk_app_code=self.target_app_code, ) } diff --git a/src/dashboard/apigateway/apigateway/apis/open/permission/serializers.py b/src/dashboard/apigateway/apigateway/apis/open/permission/serializers.py index 437aa58c7..35cf3e95e 100644 --- a/src/dashboard/apigateway/apigateway/apis/open/permission/serializers.py +++ b/src/dashboard/apigateway/apigateway/apis/open/permission/serializers.py @@ -30,8 +30,8 @@ PermissionApplyExpireDaysEnum, PermissionStatusEnum, ) -from apigateway.apps.permission.helpers import PermissionDimensionManager from apigateway.apps.permission.models import AppPermissionRecord +from apigateway.biz.permission import PermissionDimensionManager from apigateway.common.fields import TimestampField from apigateway.core.validators import BKAppCodeValidator, ResourceIDValidator from apigateway.utils import time diff --git a/src/dashboard/apigateway/apigateway/apis/open/permission/views.py b/src/dashboard/apigateway/apigateway/apis/open/permission/views.py index 4e649fcf0..02dd4d84a 100644 --- a/src/dashboard/apigateway/apigateway/apis/open/permission/views.py +++ b/src/dashboard/apigateway/apigateway/apis/open/permission/views.py @@ -26,16 +26,20 @@ from rest_framework import status, viewsets from rest_framework.views import APIView -from apigateway.apis.open.permission.helpers import AppPermissionBuilder, ResourcePermissionBuilder +from apigateway.apis.open.permission.helpers import ( + AppPermissionBuilder, + AppPermissionHelper, + ResourcePermissionBuilder, +) from apigateway.apps.permission.constants import ( ApplyStatusEnum, GrantDimensionEnum, GrantTypeEnum, PermissionApplyExpireDaysEnum, ) -from apigateway.apps.permission.helpers import AppPermissionHelper, PermissionDimensionManager from apigateway.apps.permission.models import AppPermissionApply, AppPermissionRecord, AppResourcePermission from apigateway.apps.permission.tasks import send_mail_for_perm_apply +from apigateway.biz.permission import PermissionDimensionManager from apigateway.biz.resource_version import ResourceVersionHandler from apigateway.common.error_codes import error_codes from apigateway.common.permissions import GatewayRelatedAppPermission @@ -81,7 +85,6 @@ def list(self, request, *args, **kwargs): class AppGatewayPermissionViewSet(viewsets.GenericViewSet): - api_permission_exempt = True def allow_apply_by_gateway(self, request, *args, **kwargs): @@ -235,10 +238,10 @@ def revoke(self, request, *args, **kwargs): data = slz.validated_data permission_model = AppPermissionHelper().get_permission_model(data["grant_dimension"]) - permission_model.objects.delete_permission( - gateway=request.gateway, - bk_app_codes=data["target_app_codes"], - ) + permission_model.objects.filter( + api=request.gateway, + bk_app_code__in=data["target_app_codes"], + ).delete() return OKJsonResponse("OK") @@ -270,7 +273,7 @@ def post(self, request, *args, **kwargs): resource_ids=resource_ids, ) - AppResourcePermission.objects.renew_permission( + AppResourcePermission.objects.renew_by_resource_ids( gateway=gateway, bk_app_code=data["target_app_code"], resource_ids=resource_ids, @@ -311,7 +314,6 @@ def list(self, request, *args, **kwargs): status=data.get("apply_status"), query=data.get("query"), order_by="-id", - fuzzy=False, ) page = self.paginate_queryset(queryset) diff --git a/src/dashboard/apigateway/apigateway/apis/open/support/serializers.py b/src/dashboard/apigateway/apigateway/apis/open/support/serializers.py index acdbc371a..ffe02646e 100644 --- a/src/dashboard/apigateway/apigateway/apis/open/support/serializers.py +++ b/src/dashboard/apigateway/apigateway/apis/open/support/serializers.py @@ -26,7 +26,7 @@ class APISDKQueryV1SLZ(serializers.Serializer): api_name = serializers.CharField(allow_null=True, default=None) api_id = serializers.IntegerField(allow_null=True, default=None) - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) def validate_api_id(self, value): if value: @@ -46,7 +46,7 @@ def validate_api_id(self, value): class SDKGenerateV1SLZ(serializers.Serializer): resource_version = serializers.CharField(max_length=128, help_text="资源版本") languages = serializers.ListField( - child=serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()), + child=serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()), help_text="需要生成SDK的语言列表", default=[ProgrammingLanguageEnum.PYTHON.value], ) diff --git a/src/dashboard/apigateway/apigateway/apis/web/metrics/__init__.py b/src/dashboard/apigateway/apigateway/apis/web/metrics/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apis/web/metrics/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/apps/metrics/serializers.py b/src/dashboard/apigateway/apigateway/apis/web/metrics/serializers.py similarity index 97% rename from src/dashboard/apigateway/apigateway/apps/metrics/serializers.py rename to src/dashboard/apigateway/apigateway/apis/web/metrics/serializers.py index 2599a28e2..772d8694c 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/serializers.py +++ b/src/dashboard/apigateway/apigateway/apis/web/metrics/serializers.py @@ -22,7 +22,7 @@ from apigateway.apps.metrics.constants import DimensionEnum, MetricsEnum -class MetricsQuerySLZ(serializers.Serializer): +class MetricsQueryInputSLZ(serializers.Serializer): stage_id = serializers.IntegerField(required=True) resource_id = serializers.IntegerField(allow_null=True, required=False) dimension = serializers.ChoiceField(choices=DimensionEnum.get_choices()) diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/urls.py b/src/dashboard/apigateway/apigateway/apis/web/metrics/urls.py similarity index 88% rename from src/dashboard/apigateway/apigateway/apps/metrics/urls.py rename to src/dashboard/apigateway/apigateway/apis/web/metrics/urls.py index c13706160..abe3e95d2 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/urls.py +++ b/src/dashboard/apigateway/apigateway/apis/web/metrics/urls.py @@ -18,8 +18,8 @@ # from django.urls import path -from apigateway.apps.metrics import views +from .views import QueryRangeApi urlpatterns = [ - path("query_range/", views.QueryRangeAPIView.as_view(), name="metrics.query_range"), + path("query_range/", QueryRangeApi.as_view(), name="metrics.query_range"), ] diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/views.py b/src/dashboard/apigateway/apigateway/apis/web/metrics/views.py similarity index 61% rename from src/dashboard/apigateway/apigateway/apps/metrics/views.py rename to src/dashboard/apigateway/apigateway/apis/web/metrics/views.py index a2289386e..fd889a6d5 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/views.py +++ b/src/dashboard/apigateway/apigateway/apis/web/metrics/views.py @@ -16,27 +16,72 @@ # 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 math + from django.http import Http404 from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework.views import APIView +from rest_framework import generics, status -from apigateway.apps.metrics import serializers from apigateway.apps.metrics.constants import DimensionEnum, MetricsEnum -from apigateway.apps.metrics.dimension_metrics import DimensionMetricsFactory -from apigateway.apps.metrics.utils import MetricsSmartTimeRange +from apigateway.apps.metrics.prometheus.dimension import DimensionMetricsFactory from apigateway.core.models import Resource, Stage from apigateway.utils.responses import OKJsonResponse +from apigateway.utils.time import SmartTimeRange + +from .serializers import MetricsQueryInputSLZ + + +class MetricsSmartTimeRange(SmartTimeRange): + def get_recommended_step(self) -> str: + """根据 time_start, time_end,获取推荐的步长""" + start, end = self.get_head_and_tail() + + return self._calculate_step(start, end) + + def _calculate_step(self, start: int, end: int) -> str: + """ + :param start: 起始时间戳 + :param end: 结束时间戳 + :returns: 推荐步长 + + step via the gap of query time + 1m <- 1h + 5m <- 6h + 10m <- 12h + 30m <- 24h + 1h <- 72h + 3h <- 7d + 12h <- >7d + """ + step_options = ["1m", "5m", "10m", "30m", "1h", "3h", "12h"] + + gap_minutes = math.ceil((end - start) / 60) + if gap_minutes <= 60: + index = 0 + elif gap_minutes <= 360: + index = 1 + elif gap_minutes <= 720: + index = 2 + elif gap_minutes <= 1440: + index = 3 + elif gap_minutes <= 4320: + index = 4 + elif gap_minutes <= 10080: + index = 5 + else: + index = 6 + + return step_options[index] -class QueryRangeAPIView(APIView): +class QueryRangeApi(generics.ListAPIView): @swagger_auto_schema( - query_serializer=serializers.MetricsQuerySLZ, + query_serializer=MetricsQueryInputSLZ, responses={status.HTTP_200_OK: ""}, tags=["Metrics"], ) def get(self, request, *args, **kwargs): - slz = serializers.MetricsQuerySLZ(data=request.query_params) + slz = MetricsQueryInputSLZ(data=request.query_params) slz.is_valid(raise_exception=True) data = slz.validated_data diff --git a/src/dashboard/apigateway/apigateway/apis/web/permission/__init__.py b/src/dashboard/apigateway/apigateway/apis/web/permission/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apis/web/permission/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/apis/web/permission/filters.py b/src/dashboard/apigateway/apigateway/apis/web/permission/filters.py new file mode 100644 index 000000000..651aa21c4 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apis/web/permission/filters.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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_filters import rest_framework as filters + +from apigateway.apps.permission.constants import GrantDimensionEnum, GrantTypeEnum +from apigateway.apps.permission.models import ( + AppAPIPermission, + AppPermissionApply, + AppPermissionRecord, + AppResourcePermission, +) + + +class AppResourcePermissionFilter(filters.FilterSet): + bk_app_code = filters.CharFilter() + query = filters.CharFilter(method="query_filter") + grant_type = filters.ChoiceFilter(choices=GrantTypeEnum.choices()) + resource_id = filters.NumberFilter() + order_by = filters.OrderingFilter( + choices=[(field, field) for field in ["bk_app_code", "-bk_app_code", "expires", "-expires"]] + ) + + class Meta: + model = AppResourcePermission + fields = [ + "bk_app_code", + "query", + "grant_type", + "resource_id", + "order_by", + ] + + def query_filter(self, queryset, name, value): + return queryset.filter(bk_app_code__icontains=value) + + +class AppPermissionApplyFilter(filters.FilterSet): + bk_app_code = filters.CharFilter(lookup_expr="icontains") + applied_by = filters.CharFilter() + grant_dimension = filters.OrderingFilter(choices=GrantDimensionEnum.get_choices()) + + class Meta: + model = AppPermissionApply + fields = [ + "bk_app_code", + "applied_by", + "grant_dimension", + ] + + +class AppGatewayPermissionFilter(filters.FilterSet): + bk_app_code = filters.CharFilter() + query = filters.CharFilter(method="query_filter") + resource_id = filters.NumberFilter() + order_by = filters.OrderingFilter( + choices=[(field, field) for field in ["bk_app_code", "-bk_app_code", "expires", "-expires"]] + ) + + class Meta: + model = AppAPIPermission + fields = [ + "bk_app_code", + "query", + "resource_id", + "order_by", + ] + + def query_filter(self, queryset, name, value): + return queryset.filter(bk_app_code__icontains=value) + + +class AppPermissionRecordFilter(filters.FilterSet): + time_start = filters.DateTimeFilter(method="time_start_filter") + time_end = filters.DateTimeFilter(method="time_end_filter") + bk_app_code = filters.CharFilter() + grant_dimension = filters.OrderingFilter(choices=GrantDimensionEnum.get_choices()) + + class Meta: + model = AppPermissionRecord + fields = [ + "time_start", + "time_end", + "bk_app_code", + "grant_dimension", + ] + + def time_start_filter(self, queryset, name, value): + return queryset.filter(handled_time__gte=value) + + def time_end_filter(self, queryset, name, value): + return queryset.filter(handled_time__lt=value) diff --git a/src/dashboard/apigateway/apigateway/apps/permission/serializers.py b/src/dashboard/apigateway/apigateway/apis/web/permission/serializers.py similarity index 83% rename from src/dashboard/apigateway/apigateway/apps/permission/serializers.py rename to src/dashboard/apigateway/apigateway/apis/web/permission/serializers.py index fcc4209f5..46966c743 100644 --- a/src/dashboard/apigateway/apigateway/apps/permission/serializers.py +++ b/src/dashboard/apigateway/apigateway/apis/web/permission/serializers.py @@ -16,6 +16,7 @@ # 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.db.models import QuerySet from django.utils.translation import gettext as _ from rest_framework import serializers @@ -27,13 +28,44 @@ PermissionApplyExpireDaysEnum, ) from apigateway.apps.permission.models import AppPermissionApply, AppPermissionRecord -from apigateway.common.fields import TimestampField from apigateway.core.constants import ExportTypeEnum +from apigateway.core.models import Resource from apigateway.core.validators import BKAppCodeValidator, ResourceIDValidator from apigateway.utils.time import NeverExpiresTime, to_datetime_from_now -class AppPermissionCreateSLZ(serializers.Serializer): +class AppGatewayPermissionOutputSLZ(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + bk_app_code = serializers.CharField(max_length=32, required=True) + resource_id = serializers.SerializerMethodField() + resource_name = serializers.SerializerMethodField() + resource_path = serializers.SerializerMethodField() + resource_method = serializers.SerializerMethodField() + expires = serializers.SerializerMethodField() + grant_type = serializers.ChoiceField(choices=GrantTypeEnum.choices(), default=GrantTypeEnum.INITIALIZE.value) + renewable = serializers.SerializerMethodField() + + def get_resource_id(self, obj): + return obj.resource.id if hasattr(obj, "resource") else 0 + + def get_resource_name(self, obj): + return obj.resource.name if hasattr(obj, "resource") else "" + + def get_resource_path(self, obj): + return obj.resource.path_display if hasattr(obj, "resource") else "" + + def get_resource_method(self, obj): + return obj.resource.method if hasattr(obj, "resource") else "" + + def get_expires(self, obj): + expires = None if (not obj.expires or NeverExpiresTime.is_never_expired(obj.expires)) else obj.expires + return serializers.DateTimeField(allow_null=True, required=False).to_representation(expires) + + def get_renewable(self, obj): + return bool(obj.expires and obj.expires < to_datetime_from_now(days=RENEWABLE_EXPIRE_DAYS)) + + +class AppPermissionInputSLZ(serializers.Serializer): bk_app_code = serializers.CharField(label="", max_length=32, required=True, validators=[BKAppCodeValidator()]) expire_days = serializers.IntegerField(allow_null=True, required=True) resource_ids = serializers.ListField( @@ -43,11 +75,9 @@ class AppPermissionCreateSLZ(serializers.Serializer): allow_empty=False, allow_null=True, ) - dimension = serializers.ChoiceField(choices=GrantDimensionEnum.get_choices()) -class AppPermissionQuerySLZ(serializers.Serializer): - dimension = serializers.ChoiceField(choices=GrantDimensionEnum.get_choices()) +class AppPermissionExportInputSLZ(serializers.Serializer): bk_app_code = serializers.CharField(allow_blank=True, required=False) resource_id = serializers.IntegerField(allow_null=True, required=False) query = serializers.CharField(allow_blank=True, required=False) @@ -57,9 +87,6 @@ class AppPermissionQuerySLZ(serializers.Serializer): allow_blank=True, required=False, ) - - -class PermissionExportConditionSLZ(AppPermissionQuerySLZ): export_type = serializers.ChoiceField( choices=ExportTypeEnum.choices(), help_text=( @@ -80,53 +107,11 @@ def validate(self, data): return data -class AppPermissionListSLZ(serializers.Serializer): - id = serializers.IntegerField(read_only=True) - bk_app_code = serializers.CharField(max_length=32, required=True) - resource_id = serializers.SerializerMethodField() - resource_name = serializers.SerializerMethodField() - resource_path = serializers.SerializerMethodField() - resource_method = serializers.SerializerMethodField() - expires = serializers.SerializerMethodField() - grant_type = serializers.ChoiceField(choices=GrantTypeEnum.choices(), default=GrantTypeEnum.INITIALIZE.value) - renewable = serializers.SerializerMethodField() - - def get_resource_id(self, obj): - return obj.resource.id if hasattr(obj, "resource") else 0 - - def get_resource_name(self, obj): - return obj.resource.name if hasattr(obj, "resource") else "" - - def get_resource_path(self, obj): - return obj.resource.path_display if hasattr(obj, "resource") else "" - - def get_resource_method(self, obj): - return obj.resource.method if hasattr(obj, "resource") else "" - - def get_expires(self, obj): - expires = None if (not obj.expires or NeverExpiresTime.is_never_expired(obj.expires)) else obj.expires - return serializers.DateTimeField(allow_null=True, required=False).to_representation(expires) - - def get_renewable(self, obj): - return bool(obj.expires and obj.expires < to_datetime_from_now(days=RENEWABLE_EXPIRE_DAYS)) - - -class AppPermissionBatchSLZ(serializers.Serializer): - dimension = serializers.ChoiceField(choices=GrantDimensionEnum.get_choices()) +class AppPermissionIDsSLZ(serializers.Serializer): ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False, required=True) -class AppPermissionApplyQuerySLZ(serializers.Serializer): - bk_app_code = serializers.CharField(allow_blank=True) - applied_by = serializers.CharField(allow_blank=True) - grant_dimension = serializers.ChoiceField( - choices=GrantDimensionEnum.get_choices(), - allow_blank=True, - required=False, - ) - - -class AppPermissionApplySLZ(serializers.ModelSerializer): +class AppPermissionApplyOutputSLZ(serializers.ModelSerializer): bk_app_code = serializers.CharField(label="", max_length=32, validators=[BKAppCodeValidator()]) resource_ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=True) expire_days_display = serializers.SerializerMethodField() @@ -157,36 +142,21 @@ def get_grant_dimension_display(self, obj): return GrantDimensionEnum.get_choice_label(obj.grant_dimension) -class AppPermissionApplyBatchSLZ(serializers.Serializer): - ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False) - part_resource_ids = serializers.DictField( - label="部分审批资源ID", - child=serializers.ListField(child=serializers.IntegerField(), allow_empty=False), - allow_empty=True, - required=False, - ) - status = serializers.ChoiceField( - choices=[ - ApplyStatusEnum.PARTIAL_APPROVED.value, - ApplyStatusEnum.APPROVED.value, - ApplyStatusEnum.REJECTED.value, - ] - ) - comment = serializers.CharField(allow_blank=True, max_length=512) - +class AppResourcePermissionOutputSLZ(serializers.Serializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) -class AppPermissionRecordQuerySLZ(serializers.Serializer): - time_start = TimestampField(allow_null=True, required=False) - time_end = TimestampField(allow_null=True, required=False) - bk_app_code = serializers.CharField(allow_blank=True) - grant_dimension = serializers.ChoiceField( - choices=GrantDimensionEnum.get_choices(), - allow_blank=True, - required=False, - ) + # 填充resource + if isinstance(self.instance, (QuerySet, list)) and self.instance: + resources = Resource.objects.filter(id__in=[perm.resource_id for perm in self.instance]) + resources_map = {resource.id: resource for resource in resources} + for perm in self.instance: + resource = resources_map.get(perm.resource_id) + if resource: + perm.resource = resource -class AppPermissionRecordSLZ(serializers.ModelSerializer): +class AppPermissionRecordOutputSLZ(serializers.ModelSerializer): handled_resources = serializers.SerializerMethodField() expire_days_display = serializers.SerializerMethodField() grant_dimension_display = serializers.SerializerMethodField() @@ -237,5 +207,19 @@ def get_grant_dimension_display(self, obj): return GrantDimensionEnum.get_choice_label(obj.grant_dimension) -class PermissionAppQuerySLZ(serializers.Serializer): - dimension = serializers.ChoiceField(choices=GrantDimensionEnum.get_choices()) +class AppPermissionApplyApprovalInputSLZ(serializers.Serializer): + ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False) + part_resource_ids = serializers.DictField( + label="部分审批资源ID", + child=serializers.ListField(child=serializers.IntegerField(), allow_empty=False), + allow_empty=True, + required=False, + ) + status = serializers.ChoiceField( + choices=[ + ApplyStatusEnum.PARTIAL_APPROVED.value, + ApplyStatusEnum.APPROVED.value, + ApplyStatusEnum.REJECTED.value, + ] + ) + comment = serializers.CharField(allow_blank=True, max_length=512) diff --git a/src/dashboard/apigateway/apigateway/apis/web/permission/urls.py b/src/dashboard/apigateway/apigateway/apis/web/permission/urls.py new file mode 100644 index 000000000..65b4972d1 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apis/web/permission/urls.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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.urls import include, path + +from . import views + +urlpatterns = [ + # app-resource-permission + path( + "app-resource-permissions/", + include( + [ + path( + "", views.AppResourcePermissionListCreateApi.as_view(), name="permissions.app-resource-permissions" + ), + path( + "renew/", + views.AppResourcePermissionRenewApi.as_view(), + name="permissions.app-resource-permissions.renew", + ), + path( + "export/", + views.AppResourcePermissionExportApi.as_view(), + name="permissions.app-resource-permissions.export", + ), + path( + "delete/", + views.AppResourcePermissionDeleteApi.as_view(), + name="permissions.app-resource-permissions.delete", + ), + path( + "bk-app-codes/", + views.AppResourcePermissionAppCodeListApi.as_view(), + name="permissions.app-resource-permissions.get_bk_app_codes", + ), + ] + ), + ), + # app-gateway-permission + path( + "app-gateway-permissions/", + include( + [ + path( + "", views.AppGatewayPermissionListCreateApi.as_view(), name="permissions.app-gateway-permissions" + ), + path( + "renew/", + views.AppGatewayPermissionRenewApi.as_view(), + name="permissions.app-gateway-permissions.renew", + ), + path( + "export/", + views.AppGatewayPermissionExportApi.as_view(), + name="permissions.app-gateway-permissions.export", + ), + path( + "delete/", + views.AppGatewayPermissionDeleteApi.as_view(), + name="permissions.app-gateway-permissions.delete", + ), + path( + "bk-app-codes/", + views.AppGatewayPermissionAppCodeListApi.as_view(), + name="permissions.app-gateway-permissions.get_bk_app_codes", + ), + ] + ), + ), + # app-permission-apply + path( + "app-permission-apply/", + include( + [ + path("", views.AppPermissionApplyListApi.as_view(), name="permissions.app-permission-apply"), + path( + "/", + views.AppPermissionApplyRetrieveApi.as_view(), + name="permissions.app-permission-apply.detail", + ), + path( + "approval/", + views.AppPermissionApplyApprovalApi.as_view(), + name="permissions.app-permission-apply.approval", + ), + ] + ), + ), + # app-permission-record + path( + "app-permission-records/", + views.AppPermissionRecordListApi.as_view(), + name="permissions.app-permission-records", + ), + path( + "app-permission-records//", + views.AppPermissionRecordRetrieveApi.as_view(), + name="permissions.app-permission-records.detail", + ), +] diff --git a/src/dashboard/apigateway/apigateway/apis/web/permission/views.py b/src/dashboard/apigateway/apigateway/apis/web/permission/views.py new file mode 100644 index 000000000..b94b33339 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apis/web/permission/views.py @@ -0,0 +1,500 @@ +# -*- coding: utf-8 -*- +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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 csv +import logging +from io import StringIO +from typing import Any, List + +from blue_krill.async_utils.django_utils import apply_async_on_commit +from django.db import transaction +from django.utils.translation import gettext as _ +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status + +from apigateway.apps.permission.constants import ApplyStatusEnum, GrantTypeEnum +from apigateway.apps.permission.models import ( + AppAPIPermission, + AppPermissionApply, + AppPermissionRecord, + AppResourcePermission, +) +from apigateway.apps.permission.tasks import send_mail_for_perm_handle +from apigateway.biz.permission import PermissionDimensionManager +from apigateway.core.constants import ExportTypeEnum +from apigateway.core.models import Resource +from apigateway.utils.responses import DownloadableResponse, OKJsonResponse +from apigateway.utils.swagger import PaginatedResponseSwaggerAutoSchema + +from .filters import ( + AppGatewayPermissionFilter, + AppPermissionApplyFilter, + AppPermissionRecordFilter, + AppResourcePermissionFilter, +) +from .serializers import ( + AppGatewayPermissionOutputSLZ, + AppPermissionApplyApprovalInputSLZ, + AppPermissionApplyOutputSLZ, + AppPermissionExportInputSLZ, + AppPermissionIDsSLZ, + AppPermissionInputSLZ, + AppPermissionRecordOutputSLZ, + AppResourcePermissionOutputSLZ, +) + +logger = logging.getLogger(__name__) + + +class AppResourcePermissionQuerySetMixin: + def get_queryset(self): + queryset = super().get_queryset() + + # 仅展示资源存在的权限 + resource_ids = Resource.objects.filter(api=self.request.gateway).values_list("id", flat=True) + return queryset.filter(api=self.request.gateway, resource_id__in=resource_ids) + + +class AppResourcePermissionListCreateApi(AppResourcePermissionQuerySetMixin, generics.ListCreateAPIView): + queryset = AppResourcePermission.objects.order_by("-id") + filterset_class = AppResourcePermissionFilter + + @swagger_auto_schema( + auto_schema=PaginatedResponseSwaggerAutoSchema, + responses={status.HTTP_200_OK: AppResourcePermissionOutputSLZ(many=True)}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + """ + 权限列表 + """ + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + serializer = AppResourcePermissionOutputSLZ(page, many=True) + return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) + + @swagger_auto_schema( + responses={status.HTTP_200_OK: ""}, + request_body=AppPermissionInputSLZ, + tags=["Permission"], + ) + def create(self, request, *args, **kwargs): + """ + 主动授权 + """ + slz = AppPermissionInputSLZ(data=request.data, context={"api": request.gateway}) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + AppResourcePermission.objects.save_permissions( + gateway=request.gateway, + resource_ids=data["resource_ids"], + bk_app_code=data["bk_app_code"], + expire_days=data["expire_days"], + grant_type=GrantTypeEnum.INITIALIZE.value, + ) + + return OKJsonResponse("OK") + + +class AppResourcePermissionExportApi(AppResourcePermissionQuerySetMixin, generics.CreateAPIView): + queryset = AppResourcePermission.objects.order_by("-id") + + @swagger_auto_schema( + request_body=AppPermissionExportInputSLZ, + responses={status.HTTP_200_OK: ""}, + tags=["Permission"], + ) + def create(self, request, *args, **kwargs): + """ + 权限导出 + """ + slz = AppPermissionExportInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + if data["export_type"] == ExportTypeEnum.ALL.value: + queryset = self.get_queryset() + elif data["export_type"] == ExportTypeEnum.FILTERED.value: + queryset = AppResourcePermissionFilter(data=data, queryset=self.get_queryset(), request=request).qs + elif data["export_type"] == ExportTypeEnum.SELECTED.value: + queryset = self.get_queryset().filter(id__in=data["permission_ids"]) + + slz = AppResourcePermissionOutputSLZ(queryset, many=True) + content = self._get_csv_content(data["dimension"], slz.data) + + response = DownloadableResponse(content, filename=f"{self.request.gateway.name}-permissions.csv") + # FIXME: change to export excel directly, while the exported csv file copy from mac to windows is not ok now! + # use utf-8-sig for windows + response.charset = "utf-8-sig" if "windows" in request.headers.get("User-Agent", "").lower() else "utf-8" + + return response + + def _get_csv_content(self, data: List[Any]) -> str: + """ + 将筛选出的权限数据,整理为 csv 格式内容 + """ + data = sorted(data, key=lambda x: (x["bk_app_code"], x["resource_name"])) + headers = ["bk_app_code", "resource_name", "resource_path", "resource_method", "expires", "grant_type"] + header_row = { + "bk_app_code": _("蓝鲸应用ID"), + "resource_name": _("资源名称"), + "resource_path": _("请求路径"), + "resource_method": _("请求方法"), + "expires": _("过期时间"), + "grant_type": _("授权类型"), + } + + content = StringIO() + io_csv = csv.DictWriter(content, fieldnames=headers, extrasaction="ignore") + io_csv.writerow(header_row) + io_csv.writerows(data) + + return content.getvalue() + + +class AppResourcePermissionAppCodeListApi(generics.ListAPIView): + @swagger_auto_schema( + responses={status.HTTP_200_OK: ""}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + """获取有权限的应用列表""" + + app_codes = list( + AppResourcePermission.objects.filter(api=request.gateway) + .order_by("bk_app_code") + .distinct() + .values_list("bk_app_code", flat=True) + ) + return OKJsonResponse("OK", data=app_codes) + + +class AppResourcePermissionRenewApi(generics.CreateAPIView): + @swagger_auto_schema(responses={status.HTTP_200_OK: ""}, request_body=AppPermissionIDsSLZ, tags=["Permission"]) + @transaction.atomic + def create(self, request, *args, **kwargs): + """ + 权限续期 + """ + slz = AppPermissionIDsSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + AppResourcePermission.objects.renew_by_ids( + gateway=request.gateway, + ids=data["ids"], + ) + + return OKJsonResponse("OK") + + +class AppResourcePermissionDeleteApi(AppResourcePermissionQuerySetMixin, generics.CreateAPIView): + queryset = AppResourcePermission.objects.order_by("-id") + + @swagger_auto_schema(responses={status.HTTP_200_OK: ""}, request_body=AppPermissionIDsSLZ, tags=["Permission"]) + @transaction.atomic + def create(self, request, *args, **kwargs): + slz = AppPermissionIDsSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + self.get_queryset().filter(id__in=data["ids"]).delete() + return OKJsonResponse("OK") + + +class AppGatewayPermissionQuerySetMixin: + def get_queryset(self): + return super().get_queryset().filter(api=self.request.gateway) + + +class AppGatewayPermissionListCreateApi(AppGatewayPermissionQuerySetMixin, generics.ListCreateAPIView): + queryset = AppAPIPermission.objects.order_by("-id") + filterset_class = AppGatewayPermissionFilter + + @swagger_auto_schema( + auto_schema=PaginatedResponseSwaggerAutoSchema, + responses={status.HTTP_200_OK: AppGatewayPermissionOutputSLZ(many=True)}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + """ + 权限列表 + """ + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + serializer = AppGatewayPermissionOutputSLZ(page, many=True) + return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) + + @swagger_auto_schema( + responses={status.HTTP_200_OK: ""}, + request_body=AppPermissionInputSLZ, + tags=["Permission"], + ) + def create(self, request, *args, **kwargs): + """ + 主动授权 + """ + slz = AppPermissionInputSLZ(data=request.data, context={"api": request.gateway}) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + AppAPIPermission.objects.save_permissions( + gateway=request.gateway, + resource_ids=data["resource_ids"], + bk_app_code=data["bk_app_code"], + expire_days=data["expire_days"], + grant_type=GrantTypeEnum.INITIALIZE.value, + ) + + return OKJsonResponse("OK") + + +class AppGatewayPermissionExportApi(AppGatewayPermissionQuerySetMixin, generics.CreateAPIView): + queryset = AppAPIPermission.objects.order_by("-id") + + @swagger_auto_schema( + request_body=AppPermissionExportInputSLZ, + responses={status.HTTP_200_OK: ""}, + tags=["Permission"], + ) + def create(self, request, *args, **kwargs): + """ + 权限导出 + """ + slz = AppPermissionExportInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + if data["export_type"] == ExportTypeEnum.ALL.value: + queryset = self.get_queryset() + elif data["export_type"] == ExportTypeEnum.FILTERED.value: + queryset = AppGatewayPermissionFilter(data=data, queryset=self.get_queryset(), request=request).qs + elif data["export_type"] == ExportTypeEnum.SELECTED.value: + queryset = self.get_queryset().filter(id__in=data["permission_ids"]) + + slz = AppGatewayPermissionOutputSLZ(queryset, many=True) + content = self._get_csv_content(data["dimension"], slz.data) + + response = DownloadableResponse(content, filename=f"{self.request.gateway.name}-permissions.csv") + # FIXME: change to export excel directly, while the exported csv file copy from mac to windows is not ok now! + # use utf-8-sig for windows + response.charset = "utf-8-sig" if "windows" in request.headers.get("User-Agent", "").lower() else "utf-8" + + return response + + def _get_csv_content(self, data: List[Any]) -> str: + """ + 将筛选出的权限数据,整理为 csv 格式内容 + """ + data = sorted(data, key=lambda x: x["bk_app_code"]) + headers = ["bk_app_code", "expires", "grant_type"] + header_row = { + "bk_app_code": _("蓝鲸应用ID"), + "expires": _("过期时间"), + "grant_type": _("授权类型"), + } + + content = StringIO() + io_csv = csv.DictWriter(content, fieldnames=headers, extrasaction="ignore") + io_csv.writerow(header_row) + io_csv.writerows(data) + + return content.getvalue() + + +class AppGatewayPermissionAppCodeListApi(generics.ListAPIView): + @swagger_auto_schema( + responses={status.HTTP_200_OK: ""}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + """获取有权限的应用列表""" + + app_codes = list( + AppAPIPermission.objects.filter(api=request.gateway) + .order_by("bk_app_code") + .distinct() + .values_list("bk_app_code", flat=True) + ) + return OKJsonResponse("OK", data=app_codes) + + +class AppGatewayPermissionRenewApi(generics.CreateAPIView): + @swagger_auto_schema(responses={status.HTTP_200_OK: ""}, request_body=AppPermissionIDsSLZ, tags=["Permission"]) + @transaction.atomic + def create(self, request, *args, **kwargs): + """ + 权限续期 + """ + slz = AppPermissionIDsSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + AppAPIPermission.objects.renew_by_ids( + gateway=request.gateway, + ids=data["ids"], + ) + + return OKJsonResponse("OK") + + +class AppGatewayPermissionDeleteApi(AppGatewayPermissionQuerySetMixin, generics.CreateAPIView): + queryset = AppAPIPermission.objects.order_by("-id") + + @swagger_auto_schema(responses={status.HTTP_200_OK: ""}, request_body=AppPermissionIDsSLZ, tags=["Permission"]) + @transaction.atomic + def create(self, request, *args, **kwargs): + slz = AppPermissionIDsSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + + self.get_queryset().filter(id__in=data["ids"]).delete() + return OKJsonResponse("OK") + + +class AppPermissionApplyQuerySetMixin: + def get_queryset(self): + return AppPermissionApply.objects.filter(api=self.request.gateway).order_by("-id") + + +class AppPermissionApplyListApi(AppPermissionApplyQuerySetMixin, generics.ListAPIView): + filterset_class = AppPermissionApplyFilter + + @swagger_auto_schema( + auto_schema=PaginatedResponseSwaggerAutoSchema, + responses={status.HTTP_200_OK: AppPermissionApplyOutputSLZ(many=True)}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + """ + 获取权限申请单列表 + """ + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + page = self.paginate_queryset(queryset) + + serializer = AppPermissionApplyOutputSLZ(page, many=True) + return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) + + +class AppPermissionApplyRetrieveApi(AppPermissionApplyQuerySetMixin, generics.RetrieveAPIView): + lookup_field = "id" + + @swagger_auto_schema(tags=["Permission"]) + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + slz = AppPermissionApplyOutputSLZ(instance) + return OKJsonResponse("OK", data=slz.data) + + +class AppPermissionRecordListApi(generics.ListAPIView): + filterset_class = AppPermissionRecordFilter + + def get_queryset(self): + return ( + AppPermissionRecord.objects.filter(api=self.request.gateway) + .exclude(status=ApplyStatusEnum.PENDING.value) + .order_by("-handled_time") + ) + + @swagger_auto_schema( + auto_schema=PaginatedResponseSwaggerAutoSchema, + responses={status.HTTP_200_OK: AppPermissionRecordOutputSLZ(many=True)}, + tags=["Permission"], + ) + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + serializer = AppPermissionRecordOutputSLZ( + page, + many=True, + context={ + "resource_id_map": Resource.objects.filter_id_object_map(request.gateway.id), + }, + ) + return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) + + +class AppPermissionRecordRetrieveApi(generics.RetrieveAPIView): + lookup_field = "id" + + def get_queryset(self): + return AppPermissionRecord.objects.filter(api=self.request.gateway).order_by("-handled_time") + + @swagger_auto_schema(tags=["Permission"]) + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + slz = AppPermissionRecordOutputSLZ( + instance, + context={ + "resource_id_map": Resource.objects.filter_id_object_map(request.gateway.id), + }, + ) + return OKJsonResponse("OK", data=slz.data) + + +class AppPermissionApplyApprovalApi(AppPermissionApplyQuerySetMixin, generics.CreateAPIView): + @swagger_auto_schema( + responses={status.HTTP_200_OK: ""}, request_body=AppPermissionApplyApprovalInputSLZ, tags=["Permission"] + ) + @transaction.atomic + def create(self, request, *args, **kwargs): + """ + 审批操作 + """ + slz = AppPermissionApplyApprovalInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) + + data = slz.validated_data + part_resource_ids = data.get("part_resource_ids", {}) + + queryset = self.get_queryset().filter(id__in=data["ids"]) + + for apply in queryset: + manager = PermissionDimensionManager.get_manager(apply.grant_dimension) + record = manager.handle_permission_apply( + gateway=request.gateway, + apply=apply, + status=data["status"], + comment=data["comment"], + handled_by=request.user.username, + part_resource_ids=part_resource_ids.get(f"{apply.id}"), + ) + + try: + apply_async_on_commit(send_mail_for_perm_handle, args=[record.id]) + except Exception: + logger.exception("send mail to applicant fail. record_id=%s", record.id) + + # 删除申请单 + queryset.delete() + + return OKJsonResponse("OK") diff --git a/src/dashboard/apigateway/apigateway/apps/docs/esb/sdk/serializers.py b/src/dashboard/apigateway/apigateway/apps/docs/esb/sdk/serializers.py index cf4346d28..c6dfff254 100644 --- a/src/dashboard/apigateway/apigateway/apps/docs/esb/sdk/serializers.py +++ b/src/dashboard/apigateway/apigateway/apps/docs/esb/sdk/serializers.py @@ -16,25 +16,24 @@ # 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 blue_krill.data_types.enum import EnumField, StructuredEnum from rest_framework import serializers -from apigateway.common.constants import ChoiceEnum - -class ProgrammingLanguageEnum(ChoiceEnum): - PYTHON = "python" +class ProgrammingLanguageEnum(StructuredEnum): + PYTHON = EnumField("python") class SDKQuerySLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKDocConditionSLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKUsageExampleConditionSLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKSLZ(serializers.Serializer): diff --git a/src/dashboard/apigateway/apigateway/apps/docs/gateway/sdk/serializers.py b/src/dashboard/apigateway/apigateway/apps/docs/gateway/sdk/serializers.py index b172f9886..ad9a98d6d 100644 --- a/src/dashboard/apigateway/apigateway/apps/docs/gateway/sdk/serializers.py +++ b/src/dashboard/apigateway/apigateway/apps/docs/gateway/sdk/serializers.py @@ -16,26 +16,26 @@ # 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 blue_krill.data_types.enum import EnumField, StructuredEnum from rest_framework import serializers from apigateway.apps.docs.gateway.constants_ext import UserAuthTypeEnum -from apigateway.common.constants import ChoiceEnum -class ProgrammingLanguageEnum(ChoiceEnum): - PYTHON = "python" +class ProgrammingLanguageEnum(StructuredEnum): + PYTHON = EnumField("python") class SDKQuerySLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKDocConditionSLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKUsageExampleConditionSLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) class SDKSLZ(serializers.Serializer): diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/managers.py b/src/dashboard/apigateway/apigateway/apps/metrics/managers.py index 4f31a1740..b38af0718 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/managers.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/managers.py @@ -23,7 +23,7 @@ class StatisticsAPIRequestManager(models.Manager): - def filter_and_aggregate_by_api(self, start_time, end_time): + def filter_and_aggregate_by_gateway(self, start_time, end_time): """ 过滤,并根据网关聚合数据 """ @@ -52,7 +52,7 @@ def filter_and_aggregate_by_api(self, start_time, end_time): class StatisticsAppRequestManager(models.Manager): - def filter_app_and_aggregate_by_api(self, start_time, end_time): + def filter_app_and_aggregate_by_gateway(self, start_time, end_time): """ 过滤出蓝鲸应用,并根据网关聚合数据 """ diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/models.py b/src/dashboard/apigateway/apigateway/apps/metrics/models.py index 77413ed5a..96b4ea459 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/models.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/models.py @@ -19,9 +19,10 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from apigateway.apps.metrics.managers import StatisticsAPIRequestManager, StatisticsAppRequestManager from apigateway.common.mixins.models import TimestampedModelMixin +from .managers import StatisticsAPIRequestManager, StatisticsAppRequestManager + class StatisticsModelMixin(models.Model): total_count = models.BigIntegerField(default=0) @@ -72,8 +73,8 @@ class Meta: # class StatisticsAppRequestByHour(StatisticsAppRequest): # class Meta: -# verbose_name = '应用请求统计(按小时)' -# verbose_name_plural = '应用请求统计(按小时)' +# verbose_name = '应用请求统计 (按小时)' +# verbose_name_plural = '应用请求统计 (按小时)' # db_table = 'metrics_stats_app_request_by_hour' diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/__init__.py b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_utils.py b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/base.py similarity index 56% rename from src/dashboard/apigateway/apigateway/tests/apps/metrics/test_utils.py rename to src/dashboard/apigateway/apigateway/apps/metrics/prometheus/base.py index a828b2932..97ba7c253 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_utils.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/base.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # TencentBlueKing is pleased to support the open source community by making # 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. @@ -15,27 +16,17 @@ # 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 abc import ABC +from typing import List, Optional, Tuple -from apigateway.apps.metrics.utils import MetricsSmartTimeRange +from django.conf import settings -class TestMetricsSmartTimeRange: - @pytest.mark.parametrize( - "time_range_minutes, expected", - [ - (10, "1m"), - (59, "1m"), - (60, "1m"), - (300, "5m"), - (360, "5m"), - (720, "10m"), - (1440, "30m"), - (4320, "1h"), - (10080, "3h"), - (20000, "12h"), - ], - ) - def test_get_recommended_step(self, time_range_minutes, expected): - smart_time_range = MetricsSmartTimeRange(time_range=time_range_minutes * 60) - assert smart_time_range.get_recommended_step() == expected +class BasePrometheusMetrics(ABC): + default_labels = getattr(settings, "PROMETHEUS_DEFAULT_LABELS", []) + metric_name_prefix = getattr(settings, "PROMETHEUS_METRIC_NAME_PREFIX", "") + + def _get_labels_expression(self, labels: List[Tuple[str, str, Optional[str]]]) -> str: + return ", ".join( + [f'{label}{expression}"{value}"' for label, expression, value in labels if value not in [None, ""]] + ) diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/dimension_metrics.py b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/dimension.py similarity index 94% rename from src/dashboard/apigateway/apigateway/apps/metrics/dimension_metrics.py rename to src/dashboard/apigateway/apigateway/apps/metrics/prometheus/dimension.py index 90f0a3525..beac29240 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/dimension_metrics.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/dimension.py @@ -16,8 +16,8 @@ # 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 abc import ABC, abstractmethod -from typing import ClassVar, Dict, List, Optional, Tuple, Type +from abc import abstractmethod +from typing import ClassVar, Dict, Optional, Type from django.conf import settings @@ -25,15 +25,7 @@ from apigateway.common.error_codes import error_codes from apigateway.components.prometheus import prometheus_component - -class BasePrometheusMetrics(ABC): - default_labels = getattr(settings, "PROMETHEUS_DEFAULT_LABELS", []) - metric_name_prefix = getattr(settings, "PROMETHEUS_METRIC_NAME_PREFIX", "") - - def _get_labels_expression(self, labels: List[Tuple[str, str, Optional[str]]]) -> str: - return ", ".join( - [f'{label}{expression}"{value}"' for label, expression, value in labels if value not in [None, ""]] - ) +from .base import BasePrometheusMetrics class BaseDimensionMetrics(BasePrometheusMetrics): diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/stats_metrics.py b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/statistics.py similarity index 97% rename from src/dashboard/apigateway/apigateway/apps/metrics/stats_metrics.py rename to src/dashboard/apigateway/apigateway/apps/metrics/prometheus/statistics.py index 5470bd44a..07c767f0e 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/stats_metrics.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/prometheus/statistics.py @@ -20,9 +20,10 @@ from django.conf import settings -from apigateway.apps.metrics.dimension_metrics import BasePrometheusMetrics from apigateway.components.prometheus import prometheus_component +from .base import BasePrometheusMetrics + class BaseStatisticsMetrics(BasePrometheusMetrics): @abstractmethod diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/statistics.py b/src/dashboard/apigateway/apigateway/apps/metrics/statistics.py index 8bf77d6a0..e25de4ae1 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/statistics.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/statistics.py @@ -20,14 +20,15 @@ from collections import defaultdict from typing import Optional -from apigateway.apps.metrics.models import StatisticsAPIRequestByDay, StatisticsAppRequestByDay -from apigateway.apps.metrics.stats_metrics import ( +from apigateway.core.models import Gateway, Resource +from apigateway.utils.time import utctime + +from .models import StatisticsAPIRequestByDay, StatisticsAppRequestByDay +from .prometheus.statistics import ( StatisticsAPIRequestDurationMetrics, StatisticsAPIRequestMetrics, StatisticsAppRequestMetrics, ) -from apigateway.core.models import Gateway, Resource -from apigateway.utils.time import utctime logger = logging.getLogger(__name__) diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/tasks.py b/src/dashboard/apigateway/apigateway/apps/metrics/tasks.py index d6f698016..ac844fada 100644 --- a/src/dashboard/apigateway/apigateway/apps/metrics/tasks.py +++ b/src/dashboard/apigateway/apigateway/apps/metrics/tasks.py @@ -19,7 +19,7 @@ import arrow from celery import shared_task -from apigateway.apps.metrics.statistics import StatisticsHandler +from .statistics import StatisticsHandler @shared_task(name="apigateway.apps.metrics.tasks.statistics_request_by_day") diff --git a/src/dashboard/apigateway/apigateway/apps/metrics/utils.py b/src/dashboard/apigateway/apigateway/apps/metrics/utils.py deleted file mode 100644 index 28e696572..000000000 --- a/src/dashboard/apigateway/apigateway/apps/metrics/utils.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# TencentBlueKing is pleased to support the open source community by making -# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. -# Copyright (C) 2017 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 math - -from apigateway.utils.time import SmartTimeRange - - -class MetricsSmartTimeRange(SmartTimeRange): - def get_recommended_step(self) -> str: - """根据 time_start, time_end,获取推荐的步长""" - start, end = self.get_head_and_tail() - - return self._calculate_step(start, end) - - def _calculate_step(self, start: int, end: int) -> str: - """ - :param start: 起始时间戳 - :param end: 结束时间戳 - :returns: 推荐步长 - - step via the gap of query time - 1m <- 1h - 5m <- 6h - 10m <- 12h - 30m <- 24h - 1h <- 72h - 3h <- 7d - 12h <- >7d - """ - step_options = ["1m", "5m", "10m", "30m", "1h", "3h", "12h"] - - gap_minutes = math.ceil((end - start) / 60) - if gap_minutes <= 60: - index = 0 - elif gap_minutes <= 360: - index = 1 - elif gap_minutes <= 720: - index = 2 - elif gap_minutes <= 1440: - index = 3 - elif gap_minutes <= 4320: - index = 4 - elif gap_minutes <= 10080: - index = 5 - else: - index = 6 - - return step_options[index] diff --git a/src/dashboard/apigateway/apigateway/apps/permission/constants.py b/src/dashboard/apigateway/apigateway/apps/permission/constants.py index ca8de0fd8..a3003bb48 100644 --- a/src/dashboard/apigateway/apigateway/apps/permission/constants.py +++ b/src/dashboard/apigateway/apigateway/apps/permission/constants.py @@ -16,10 +16,8 @@ # 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 os from blue_krill.data_types.enum import EnumField, StructuredEnum -from django.conf import settings from django.utils.translation import gettext_lazy as _ from apigateway.common.constants import ChoiceEnum @@ -87,7 +85,3 @@ class GrantDimensionEnum(StructuredEnum): DEFAULT_PERMISSION_EXPIRE_DAYS = 180 # 可续期的过期天数,权限有效期小于此值,允许续期,否则,不允许 RENEWABLE_EXPIRE_DAYS = 30 - -PERMISSION_MAIL_NOTIFY_COUNTDOWN_SECONDS = 10 - -APIGW_LOGO_PATH = os.path.join(settings.BASE_DIR, "static/img/api_gateway.png") diff --git a/src/dashboard/apigateway/apigateway/apps/permission/helpers.py b/src/dashboard/apigateway/apigateway/apps/permission/helpers.py deleted file mode 100644 index 2a71af319..000000000 --- a/src/dashboard/apigateway/apigateway/apps/permission/helpers.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -# -# TencentBlueKing is pleased to support the open source community by making -# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. -# Copyright (C) 2017 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 abc import ABCMeta, abstractmethod -from typing import List, Optional, Tuple - -from django.utils.translation import gettext as _ - -from apigateway.apps.permission.constants import ( - RENEWABLE_EXPIRE_DAYS, - ApplyStatusEnum, - GrantDimensionEnum, - GrantTypeEnum, -) -from apigateway.apps.permission.models import ( - AppAPIPermission, - AppPermissionApply, - AppPermissionApplyStatus, - AppPermissionRecord, - AppResourcePermission, -) -from apigateway.common.error_codes import error_codes -from apigateway.core.models import Gateway, Resource - - -class AppPermissionHelper: - def get_permission_model(self, dimension): - if dimension == GrantDimensionEnum.API.value: - return AppAPIPermission - elif dimension == GrantDimensionEnum.RESOURCE.value: - return AppResourcePermission - raise error_codes.INVALID_ARGS.format(f"unsupported grant_dimension: {dimension}") - - -class PermissionDimensionManager(metaclass=ABCMeta): - @classmethod - def get_manager(cls, grant_dimension: str) -> "PermissionDimensionManager": - if grant_dimension == GrantDimensionEnum.API.value: - return APIPermissionDimensionManager() - elif grant_dimension == GrantDimensionEnum.RESOURCE.value: - return ResourcePermissionDimensionManager() - - raise error_codes.INVALID_ARGS.format(f"unsupported grant_dimension: {grant_dimension}") - - @abstractmethod - def handle_permission_apply( - self, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - comment: str, - handled_by: str, - part_resource_ids: Optional[List[int]], - ) -> AppPermissionRecord: - """处理权限申请""" - - @abstractmethod - def save_permission_apply_status( - self, - bk_app_code: str, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - resources: List[Resource], - ): - """保存权限申请状态""" - - @abstractmethod - def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: - """资源权限申请时,获取展示的资源名称列表""" - - @abstractmethod - def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - """资源审批时,获取审批通过的资源名称列表""" - - @abstractmethod - def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - """资源审批时,获取审批拒绝的资源名称列表""" - - @abstractmethod - def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: - """判断是否允许申请权限""" - - -class APIPermissionDimensionManager(PermissionDimensionManager): - def handle_permission_apply( - self, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - comment: str, - handled_by: str, - part_resource_ids=None, - ) -> AppPermissionRecord: - if status == ApplyStatusEnum.APPROVED.value: - AppAPIPermission.objects.save_permissions( - gateway=gateway, - bk_app_code=apply.bk_app_code, - grant_type=GrantTypeEnum.APPLY.value, - expire_days=apply.expire_days, - ) - - self._handle_apply_status(apply, status) - - # 添加到已审批单据记录 - return AppPermissionRecord.objects.save_record( - record_id=apply.apply_record_id, - gateway=gateway, - bk_app_code=apply.bk_app_code, - applied_by=apply.applied_by, - applied_time=apply.created_time, - handled_by=handled_by, - resource_ids=apply.resource_ids, - handled_resource_ids={}, - status=status, - comment=comment, - reason=apply.reason, - expire_days=apply.expire_days, - grant_dimension=apply.grant_dimension, - ) - - def save_permission_apply_status( - self, - bk_app_code: str, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - resources=None, - ): - AppPermissionApplyStatus.objects.update_or_create( - bk_app_code=bk_app_code, - api=gateway, - resource=None, - grant_dimension=GrantDimensionEnum.API.value, - defaults={ - "apply": apply, - "status": status, - }, - ) - - def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: - return ["该网关所有资源,包括新建资源"] - - def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - if status == ApplyStatusEnum.APPROVED.value: - return self.get_resource_names_display(gateway_id, resource_ids) - return [] - - def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - if status == ApplyStatusEnum.APPROVED.value: - return [] - - return self.get_resource_names_display(gateway_id, resource_ids) - - def _handle_apply_status(self, apply, status: str): - # 按网关申请被拒绝后,如果不删除申请状态记录,应用看到网关下所有资源申请状态均为“拒绝”,体验不友好, - # 因此,按网关申请时,同意、拒绝,均删除申请状态记录 - AppPermissionApplyStatus.objects.filter(apply=apply).delete() - - def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: - is_pending = AppPermissionApplyStatus.objects.is_permission_pending_by_gateway(gateway_id, bk_app_code) - if is_pending: - return False, _("权限申请中,请联系网关负责人审批。") - - api_perm = AppAPIPermission.objects.filter( - api_id=gateway_id, - bk_app_code=bk_app_code, - ).first() - - if api_perm and not api_perm.allow_apply_permission: - return False, _("权限有效期小于 {days} 天时,才可申请。").format(days=RENEWABLE_EXPIRE_DAYS) - - return True, "" - - -class ResourcePermissionDimensionManager(PermissionDimensionManager): - def handle_permission_apply( - self, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - comment: str, - handled_by: str, - part_resource_ids: Optional[List[int]], - ): - approved_resource_ids, rejected_resource_ids = self._split_resource_ids( - status, - apply.resource_ids, - part_resource_ids, - ) - - # 如果审批同意,则更新权限信息 - # 如果审批驳回,则不做任何处理 - if approved_resource_ids: - AppResourcePermission.objects.save_permissions( - gateway=gateway, - resource_ids=approved_resource_ids, - bk_app_code=apply.bk_app_code, - grant_type=GrantTypeEnum.APPLY.value, - expire_days=apply.expire_days, - ) - - # 更新应用访问资源权限申请状态 - # 若拒绝,则更新状态为拒绝 - # 若同意,则删除单据对应记录 - self._handle_apply_status(apply, rejected_resource_ids) - - # 添加到已审批单据记录 - return AppPermissionRecord.objects.save_record( - record_id=apply.apply_record_id, - gateway=gateway, - bk_app_code=apply.bk_app_code, - applied_by=apply.applied_by, - applied_time=apply.created_time, - handled_by=handled_by, - resource_ids=apply.resource_ids, - handled_resource_ids={ - ApplyStatusEnum.APPROVED.value: approved_resource_ids, - ApplyStatusEnum.REJECTED.value: rejected_resource_ids, - }, - status=status, - comment=comment, - reason=apply.reason, - expire_days=apply.expire_days, - grant_dimension=apply.grant_dimension, - ) - - def save_permission_apply_status( - self, - bk_app_code: str, - gateway: Gateway, - apply: AppPermissionApply, - status: str, - resources=None, - ): - for resource in resources: - AppPermissionApplyStatus.objects.update_or_create( - bk_app_code=bk_app_code, - api=gateway, - resource=resource, - grant_dimension=GrantDimensionEnum.RESOURCE.value, - defaults={ - "apply": apply, - "status": status, - }, - ) - - def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: - return Resource.objects.filter_resource_names(gateway_id, ids=resource_ids) - - def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - return self.get_resource_names_display(gateway_id, resource_ids) - - def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: - return self.get_resource_names_display(gateway_id, resource_ids) - - def _handle_apply_status(self, apply: AppPermissionApply, rejected_resource_ids: List[int]): - if rejected_resource_ids: - AppPermissionApplyStatus.objects.filter(apply=apply, resource_id__in=rejected_resource_ids).update( - apply=None, - status=ApplyStatusEnum.REJECTED.value, - ) - - AppPermissionApplyStatus.objects.filter(apply=apply).delete() - - def _split_resource_ids(self, status, resource_ids, part_resource_ids=None): - """ - 拆分资源ID 为通过、驳回两组 - :param status: 审批状态 - :param resource_ids: 申请单据中的资源ID - :param part_resource_ids: 部分审批时,部分审批的资源ID - """ - if status == ApplyStatusEnum.APPROVED.value: - return resource_ids, [] - - elif status == ApplyStatusEnum.REJECTED.value: - return [], resource_ids - - elif status == ApplyStatusEnum.PARTIAL_APPROVED.value: - resource_id_set = set(resource_ids) - part_resource_id_set = set(part_resource_ids or []) - return list(resource_id_set & part_resource_id_set), list(resource_id_set - part_resource_id_set) - - raise ValueError("unsupported apply status: {status}") - - def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: - return False, _("授权维度 grant_dimension 暂不支持 {grant_dimension}。").format( - grant_dimension=GrantDimensionEnum.RESOURCE.value, - ) diff --git a/src/dashboard/apigateway/apigateway/apps/permission/managers.py b/src/dashboard/apigateway/apigateway/apps/permission/managers.py index e46e2af38..d65d16c98 100644 --- a/src/dashboard/apigateway/apigateway/apps/permission/managers.py +++ b/src/dashboard/apigateway/apigateway/apps/permission/managers.py @@ -17,7 +17,7 @@ # to the current version of the project delivered to anyone in the future. # import datetime -from typing import List, Optional, Text +from typing import List, Optional from django.db import models @@ -28,7 +28,6 @@ GrantTypeEnum, ) from apigateway.apps.permission.utils import calculate_expires -from apigateway.core.models import Resource from apigateway.utils.time import now_datetime, to_datetime_from_now @@ -36,43 +35,6 @@ class AppAPIPermissionManager(models.Manager): def filter_public_permission_by_app(self, bk_app_code: str): return self.filter(bk_app_code=bk_app_code, api__is_public=True) - def filter_permission( - self, - gateway, - bk_app_code: Optional[Text] = None, - bk_app_codes: Optional[List[str]] = None, - query: Optional[Text] = None, - grant_type: Optional[Text] = None, - resource_ids: Optional[List[int]] = None, - ids: Optional[List[int]] = None, - order_by: Optional[Text] = None, - fuzzy: bool = False, - ): - queryset = self.filter(api=gateway) - - if bk_app_code: - queryset = queryset.filter(bk_app_code=bk_app_code) - - if bk_app_codes is not None: - queryset = queryset.filter(bk_app_code__in=bk_app_codes) - - if query and fuzzy: - # 模糊搜索 - queryset = queryset.filter(bk_app_code__icontains=query) - - if grant_type and grant_type != GrantTypeEnum.INITIALIZE.value: - # AppAPIPermission 全部为主动授权, - # 为保持与 AppResourcePermission 相同的筛选逻辑,保留参数 grant_type - queryset = queryset.none() - - if ids is not None: - queryset = queryset.filter(id__in=ids) - - if order_by: - queryset = queryset.order_by(order_by) - - return queryset - def save_permissions(self, gateway, resource_ids=None, bk_app_code=None, grant_type=None, expire_days=None): self.update_or_create( api=gateway, @@ -82,86 +44,34 @@ def save_permissions(self, gateway, resource_ids=None, bk_app_code=None, grant_t }, ) - def renew_permission(self, gateway, ids=None): - queryset = self.filter_permission(gateway=gateway, ids=ids) - # 仅续期权限期限小于待续期时间的权限 - expires = to_datetime_from_now(days=DEFAULT_PERMISSION_EXPIRE_DAYS) - queryset = queryset.filter(expires__lt=expires) - queryset.update( + def renew_by_ids(self, gateway, ids, expires=DEFAULT_PERMISSION_EXPIRE_DAYS): + expires = to_datetime_from_now(days=expires) + self.filter(api=gateway, id__in=ids, expires__lt=expires).update( expires=expires, updated_time=now_datetime(), ) - def delete_permission( - self, - gateway, - ids: Optional[List[int]] = None, - bk_app_codes: Optional[List[str]] = None, - resource_ids: Optional[List[int]] = None, - ): - queryset = self.filter_permission( - gateway=gateway, ids=ids, bk_app_codes=bk_app_codes, resource_ids=resource_ids - ) - queryset.delete() - - def add_extend_data_for_representation(self, permissions): - return permissions - class AppResourcePermissionManager(models.Manager): def filter_public_permission_by_app(self, bk_app_code: str): return self.filter(bk_app_code=bk_app_code, api__is_public=True) - def filter_permission( - self, - gateway, - bk_app_code: Optional[str] = None, - bk_app_codes: Optional[List[str]] = None, - query: Optional[str] = None, - grant_type: Optional[str] = None, - resource_ids: Optional[List[int]] = None, - ids: Optional[List[int]] = None, - order_by: Optional[str] = None, - fuzzy: bool = False, - ): - # 仅展示资源存在的权限 - api_resource_ids = Resource.objects.filter(api=gateway).values_list("id", flat=True) - queryset = self.filter(api=gateway, resource_id__in=api_resource_ids) - - if bk_app_code: - queryset = queryset.filter(bk_app_code=bk_app_code) - - if bk_app_codes is not None: - queryset = queryset.filter(bk_app_code__in=bk_app_codes) - - if query and fuzzy: - # 模糊搜索 - queryset = queryset.filter(bk_app_code__icontains=query) - - if grant_type: - queryset = queryset.filter(grant_type=grant_type) - - if resource_ids is not None: - queryset = queryset.filter(resource_id__in=resource_ids) - - if ids is not None: - queryset = queryset.filter(id__in=ids) - - if order_by: - queryset = queryset.order_by(order_by) - - return queryset + def renew_by_ids(self, gateway, ids, expires=DEFAULT_PERMISSION_EXPIRE_DAYS, grant_type=GrantTypeEnum.RENEW.value): + expires = to_datetime_from_now(days=expires) + self.filter(api=gateway, id__in=ids, expires__lt=expires).update( + expires=expires, + grant_type=grant_type, + ) - def renew_permission( + def renew_by_resource_ids( self, gateway, - bk_app_code=None, - resource_ids=None, - ids=None, + bk_app_code, + resource_ids, grant_type=GrantTypeEnum.RENEW.value, expire_days=DEFAULT_PERMISSION_EXPIRE_DAYS, ): - queryset = self.filter_permission(gateway=gateway, bk_app_code=bk_app_code, resource_ids=resource_ids, ids=ids) + queryset = self.filter(api=gateway, bk_app_code=bk_app_code, resource_id__in=resource_ids) # 仅续期权限期限小于待续期时间的权限 expires = to_datetime_from_now(days=expire_days) queryset = queryset.filter(expires__lt=expires) @@ -191,18 +101,6 @@ def renew_not_expired_permissions( grant_type=grant_type, ) - def delete_permission( - self, - gateway, - ids: Optional[List[int]] = None, - bk_app_codes: Optional[List[str]] = None, - resource_ids: Optional[List[int]] = None, - ): - queryset = self.filter_permission( - gateway=gateway, ids=ids, bk_app_codes=bk_app_codes, resource_ids=resource_ids - ) - queryset.delete() - def save_permissions(self, gateway, resource_ids, bk_app_code, grant_type, expire_days=None): expires = calculate_expires(expire_days) @@ -253,40 +151,6 @@ def get_permission_or_none(self, gateway, resource_id, bk_app_code): except self.model.DoesNotExist: return None - def add_extend_data_for_representation(self, permissions): - # 添加资源信息 - resources = Resource.objects.filter(id__in=[perm.resource_id for perm in permissions]) - resources_map = {resource.id: resource for resource in resources} - for perm in permissions: - resource = resources_map.get(perm.resource_id) - if resource: - perm.resource = resource - return permissions - - -class AppPermissionApplyManager(models.Manager): - def filter_apply( - self, - queryset, - bk_app_code: Optional[str] = None, - applied_by: Optional[str] = None, - fuzzy: bool = False, - grant_dimension: Optional[str] = None, - ): - if bk_app_code: - if fuzzy: - queryset = queryset.filter(bk_app_code__icontains=bk_app_code) - else: - queryset = queryset.filter(bk_app_code=bk_app_code) - - if applied_by: - queryset = queryset.filter(applied_by=applied_by) - - if grant_dimension: - queryset = queryset.filter(grant_dimension=grant_dimension) - - return queryset - class AppPermissionRecordManager(models.Manager): def save_record( @@ -338,19 +202,12 @@ def filter_record( applied_by: str = "", applied_time_start: Optional[datetime.datetime] = None, applied_time_end: Optional[datetime.datetime] = None, - handled_time_start: Optional[datetime.datetime] = None, - handled_time_end: Optional[datetime.datetime] = None, - grant_dimension: Optional[str] = None, status: str = "", query: str = "", order_by: str = "", - fuzzy: bool = False, ): if bk_app_code: - if fuzzy: - queryset = queryset.filter(bk_app_code__icontains=bk_app_code) - else: - queryset = queryset.filter(bk_app_code=bk_app_code) + queryset = queryset.filter(bk_app_code=bk_app_code) if applied_by: queryset = queryset.filter(applied_by=applied_by) @@ -358,12 +215,6 @@ def filter_record( if applied_time_start and applied_time_end: queryset = queryset.filter(applied_time__range=(applied_time_start, applied_time_end)) - if handled_time_start and handled_time_end: - queryset = queryset.filter(handled_time__range=(handled_time_start, handled_time_end)) - - if grant_dimension: - queryset = queryset.filter(grant_dimension=grant_dimension) - if status: queryset = queryset.filter(status=status) diff --git a/src/dashboard/apigateway/apigateway/apps/permission/models.py b/src/dashboard/apigateway/apigateway/apps/permission/models.py index 1c768bc27..9300d1b01 100644 --- a/src/dashboard/apigateway/apigateway/apps/permission/models.py +++ b/src/dashboard/apigateway/apigateway/apps/permission/models.py @@ -165,8 +165,6 @@ class AppPermissionApply(TimestampedModelMixin): status = models.CharField(max_length=16, choices=ApplyStatusEnum.get_choices(), db_index=True) apply_record_id = models.IntegerField(null=True, blank=True) - objects = managers.AppPermissionApplyManager() - def __str__(self): return f"" diff --git a/src/dashboard/apigateway/apigateway/apps/permission/tasks.py b/src/dashboard/apigateway/apigateway/apps/permission/tasks.py index 7fb2d0462..5b2831dd1 100644 --- a/src/dashboard/apigateway/apigateway/apps/permission/tasks.py +++ b/src/dashboard/apigateway/apigateway/apps/permission/tasks.py @@ -19,6 +19,7 @@ import base64 import datetime import logging +import os from collections import defaultdict from celery import shared_task @@ -28,20 +29,22 @@ from apigateway.apps.metrics.models import StatisticsAppRequestByDay from apigateway.apps.permission.constants import ( - APIGW_LOGO_PATH, ApplyStatusEnum, GrantDimensionEnum, GrantTypeEnum, PermissionApplyExpireDaysEnum, ) -from apigateway.apps.permission.helpers import PermissionDimensionManager from apigateway.apps.permission.models import AppPermissionApply, AppPermissionRecord, AppResourcePermission +from apigateway.biz.permission import PermissionDimensionManager from apigateway.components.cmsi import cmsi_component from apigateway.utils.file import read_file logger = logging.getLogger(__name__) +APIGW_LOGO_PATH = os.path.join(settings.BASE_DIR, "static/img/api_gateway.png") + + @shared_task(name="apigateway.apps.permission.tasks.send_mail_for_perm_apply", ignore_result=True) def send_mail_for_perm_apply(record_id): """ diff --git a/src/dashboard/apigateway/apigateway/apps/permission/urls.py b/src/dashboard/apigateway/apigateway/apps/permission/urls.py deleted file mode 100644 index 553c87742..000000000 --- a/src/dashboard/apigateway/apigateway/apps/permission/urls.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# -# TencentBlueKing is pleased to support the open source community by making -# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. -# Copyright (C) 2017 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.urls import path - -from apigateway.apps.permission import views - -urlpatterns = [ - # app-permission - path( - "app-permissions/", - views.AppPermissionViewSet.as_view({"get": "list", "post": "create"}), - name="permissions.app-permissions", - ), - path( - "app-permissions/batch/", - views.AppPermissionBatchViewSet.as_view({"post": "renew"}), - name="permissions.app-permissions.renew", - ), - # app-permission export - path( - "app-permissions/export/", - views.AppPermissionViewSet.as_view({"post": "export_permissions"}), - name="permissions.app-permissions.export", - ), - path( - "app-permissions/bk-app-codes/", - views.AppPermissionViewSet.as_view({"get": "get_bk_app_codes"}), - name="permissions.app-permissions.get_bk_app_codes", - ), - # delete 不支持传参,改用 post - path( - "app-permissions/delete/", - views.AppPermissionBatchViewSet.as_view({"post": "destroy"}), - name="permissions.app-permissions.delete", - ), - # app-permission-apply - path( - "app-permission-apply/", - views.AppPermissionApplyViewSet.as_view({"get": "list"}), - name="permissions.app-permission-apply", - ), - path( - "app-permission-apply//", - views.AppPermissionApplyViewSet.as_view({"get": "retrieve"}), - name="permissions.app-permission-apply.detail", - ), - path( - "app-permission-apply/batch/", - views.AppPermissionApplyBatchViewSet.as_view({"post": "post"}), - name="permissions.app-permission-apply.batch", - ), - # app-permission-record - path( - "app-permission-records/", - views.AppPermissionRecordViewSet.as_view({"get": "list"}), - name="permissions.app-permission-records", - ), - path( - "app-permission-records//", - views.AppPermissionRecordViewSet.as_view({"get": "retrieve"}), - name="permissions.app-permission-records.detail", - ), -] diff --git a/src/dashboard/apigateway/apigateway/apps/permission/views.py b/src/dashboard/apigateway/apigateway/apps/permission/views.py deleted file mode 100644 index ca69d44e9..000000000 --- a/src/dashboard/apigateway/apigateway/apps/permission/views.py +++ /dev/null @@ -1,377 +0,0 @@ -# -*- coding: utf-8 -*- -# -# TencentBlueKing is pleased to support the open source community by making -# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. -# Copyright (C) 2017 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 csv -import logging -from io import StringIO -from typing import Any, List - -from blue_krill.async_utils.django_utils import apply_async_on_commit -from django.db import transaction -from django.utils.translation import gettext as _ -from drf_yasg.utils import swagger_auto_schema -from rest_framework import status, viewsets - -from apigateway.apps.permission import serializers -from apigateway.apps.permission.constants import ApplyStatusEnum, GrantDimensionEnum, GrantTypeEnum -from apigateway.apps.permission.helpers import AppPermissionHelper, PermissionDimensionManager -from apigateway.apps.permission.models import AppPermissionApply, AppPermissionRecord -from apigateway.apps.permission.tasks import send_mail_for_perm_handle -from apigateway.core.constants import ExportTypeEnum -from apigateway.core.models import Resource -from apigateway.utils.responses import DownloadableResponse, OKJsonResponse -from apigateway.utils.swagger import PaginatedResponseSwaggerAutoSchema - -logger = logging.getLogger(__name__) - - -class AppPermissionViewSet(viewsets.ModelViewSet): - serializer_class = serializers.AppPermissionListSLZ - - @swagger_auto_schema( - auto_schema=PaginatedResponseSwaggerAutoSchema, - query_serializer=serializers.AppPermissionQuerySLZ, - responses={status.HTTP_200_OK: serializers.AppPermissionListSLZ(many=True)}, - tags=["Permission"], - ) - def list(self, request, *args, **kwargs): - """ - 权限列表 - """ - slz = serializers.AppPermissionQuerySLZ(data=request.query_params) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - queryset = permission_model.objects.filter_permission( - gateway=request.gateway, - bk_app_code=data.get("bk_app_code"), - query=data.get("query"), - grant_type=data["grant_type"], - resource_ids=[data["resource_id"]] if data.get("resource_id") else None, - order_by=data.get("order_by") or "-id", - fuzzy=True, - ) - - page = self.paginate_queryset(queryset) - - page = permission_model.objects.add_extend_data_for_representation(page) - - serializer = serializers.AppPermissionListSLZ(page, many=True) - return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) - - @swagger_auto_schema( - responses={status.HTTP_200_OK: ""}, - request_body=serializers.AppPermissionCreateSLZ, - tags=["Permission"], - ) - def create(self, request, *args, **kwargs): - """ - 主动授权 - """ - slz = serializers.AppPermissionCreateSLZ(data=request.data, context={"api": request.gateway}) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - permission_model.objects.save_permissions( - gateway=request.gateway, - resource_ids=data["resource_ids"], - bk_app_code=data["bk_app_code"], - expire_days=data["expire_days"], - grant_type=GrantTypeEnum.INITIALIZE.value, - ) - - return OKJsonResponse("OK") - - @swagger_auto_schema( - request_body=serializers.PermissionExportConditionSLZ, - responses={status.HTTP_200_OK: ""}, - tags=["Permission"], - ) - def export_permissions(self, request, *args, **kwargs): - slz = serializers.PermissionExportConditionSLZ(data=request.data) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - - if data["export_type"] == ExportTypeEnum.ALL.value: - queryset = permission_model.objects.filter_permission(gateway=request.gateway) - elif data["export_type"] == ExportTypeEnum.FILTERED.value: - queryset = permission_model.objects.filter_permission( - gateway=request.gateway, - bk_app_code=data.get("bk_app_code"), - grant_type=data.get("grant_type"), - resource_ids=[data["resource_id"]] if data.get("resource_id") else None, - query=data.get("query"), - fuzzy=True, - ) - elif data["export_type"] == ExportTypeEnum.SELECTED.value: - queryset = permission_model.objects.filter_permission( - gateway=request.gateway, - ids=data["permission_ids"], - ) - - queryset = permission_model.objects.add_extend_data_for_representation(queryset) - slz = serializers.AppPermissionListSLZ(queryset, many=True) - content = self._get_csv_content(data["dimension"], slz.data) - - response = DownloadableResponse(content, filename=f"{self.request.gateway.name}-permissions.csv") - # FIXME: change to export excel directly, while the exported csv file copy from mac to windows is not ok now! - # use utf-8-sig for windows - response.charset = "utf-8-sig" if "windows" in request.headers.get("User-Agent", "").lower() else "utf-8" - - return response - - @swagger_auto_schema( - query_serializer=serializers.PermissionAppQuerySLZ, - responses={status.HTTP_200_OK: ""}, - tags=["Permission"], - ) - def get_bk_app_codes(self, request, *args, **kwargs): - """获取有权限的应用列表""" - slz = serializers.PermissionAppQuerySLZ(data=request.query_params) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - - app_codes = list( - permission_model.objects.filter(api=request.gateway) - .order_by("bk_app_code") - .distinct() - .values_list("bk_app_code", flat=True) - ) - return OKJsonResponse("OK", data=app_codes) - - def _get_csv_content(self, dimension: str, data: List[Any]) -> str: - """ - 将筛选出的权限数据,整理为 csv 格式内容 - """ - if dimension == GrantDimensionEnum.API.value: - data = sorted(data, key=lambda x: x["bk_app_code"]) - headers = ["bk_app_code", "expires", "grant_type"] - header_row = { - "bk_app_code": _("蓝鲸应用ID"), - "expires": _("过期时间"), - "grant_type": _("授权类型"), - } - - else: - data = sorted(data, key=lambda x: (x["bk_app_code"], x["resource_name"])) - headers = ["bk_app_code", "resource_name", "resource_path", "resource_method", "expires", "grant_type"] - header_row = { - "bk_app_code": _("蓝鲸应用ID"), - "resource_name": _("资源名称"), - "resource_path": _("请求路径"), - "resource_method": _("请求方法"), - "expires": _("过期时间"), - "grant_type": _("授权类型"), - } - - content = StringIO() - io_csv = csv.DictWriter(content, fieldnames=headers, extrasaction="ignore") - io_csv.writerow(header_row) - io_csv.writerows(data) - - return content.getvalue() - - -class AppPermissionBatchViewSet(viewsets.ModelViewSet): - serializer_class = serializers.AppPermissionBatchSLZ - - @swagger_auto_schema( - responses={status.HTTP_200_OK: ""}, request_body=serializers.AppPermissionBatchSLZ, tags=["Permission"] - ) - @transaction.atomic - def renew(self, request, *args, **kwargs): - """ - 权限续期 - """ - slz = self.get_serializer(data=request.data) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - permission_model.objects.renew_permission( - gateway=request.gateway, - ids=data["ids"], - ) - - return OKJsonResponse("OK") - - @swagger_auto_schema( - responses={status.HTTP_200_OK: ""}, request_body=serializers.AppPermissionBatchSLZ, tags=["Permission"] - ) - @transaction.atomic - def destroy(self, request, *args, **kwargs): - slz = self.get_serializer(data=request.data) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - permission_model = AppPermissionHelper().get_permission_model(data["dimension"]) - permission_model.objects.delete_permission( - gateway=request.gateway, - ids=data["ids"], - ) - - return OKJsonResponse("OK") - - -class BaseAppPermissionApplyViewSet(viewsets.ModelViewSet): - lookup_field = "id" - - def get_queryset(self): - return AppPermissionApply.objects.filter(api=self.request.gateway).order_by("-id") - - -class AppPermissionApplyViewSet(BaseAppPermissionApplyViewSet): - serializer_class = serializers.AppPermissionApplySLZ - - @swagger_auto_schema( - auto_schema=PaginatedResponseSwaggerAutoSchema, - query_serializer=serializers.AppPermissionApplyQuerySLZ, - responses={status.HTTP_200_OK: serializers.AppPermissionApplySLZ(many=True)}, - tags=["Permission"], - ) - def list(self, request, *args, **kwargs): - """ - 获取权限申请单列表 - """ - slz = serializers.AppPermissionApplyQuerySLZ(data=request.query_params) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - queryset = self.get_queryset() - queryset = AppPermissionApply.objects.filter_apply( - queryset, - bk_app_code=data.get("bk_app_code"), - applied_by=data.get("applied_by"), - grant_dimension=data.get("grant_dimension"), - fuzzy=True, - ) - - page = self.paginate_queryset(queryset) - - serializer = serializers.AppPermissionApplySLZ(page, many=True) - return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) - - @swagger_auto_schema(tags=["Permission"]) - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - slz = self.get_serializer(instance) - return OKJsonResponse("OK", data=slz.data) - - -class AppPermissionApplyBatchViewSet(BaseAppPermissionApplyViewSet): - serializer_class = serializers.AppPermissionApplyBatchSLZ - - @swagger_auto_schema( - responses={status.HTTP_200_OK: ""}, request_body=serializers.AppPermissionApplyBatchSLZ, tags=["Permission"] - ) - @transaction.atomic - def post(self, request, *args, **kwargs): - """ - 审批操作 - """ - slz = self.get_serializer(data=request.data) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - part_resource_ids = data.get("part_resource_ids", {}) - - queryset = self.get_queryset().filter(id__in=data["ids"]) - - for apply in queryset: - manager = PermissionDimensionManager.get_manager(apply.grant_dimension) - record = manager.handle_permission_apply( - gateway=request.gateway, - apply=apply, - status=data["status"], - comment=data["comment"], - handled_by=request.user.username, - part_resource_ids=part_resource_ids.get(f"{apply.id}"), - ) - - try: - apply_async_on_commit(send_mail_for_perm_handle, args=[record.id]) - except Exception: - logger.exception("send mail to applicant fail. record_id=%s", record.id) - - # 删除申请单 - queryset.delete() - - return OKJsonResponse("OK") - - -class AppPermissionRecordViewSet(viewsets.ModelViewSet): - serializer_class = serializers.AppPermissionRecordSLZ - lookup_field = "id" - - def get_queryset(self): - return AppPermissionRecord.objects.filter(api=self.request.gateway).order_by("-handled_time") - - @swagger_auto_schema( - auto_schema=PaginatedResponseSwaggerAutoSchema, - query_serializer=serializers.AppPermissionRecordQuerySLZ, - responses={status.HTTP_200_OK: serializers.AppPermissionRecordSLZ(many=True)}, - tags=["Permission"], - ) - def list(self, request, *args, **kwargs): - slz = serializers.AppPermissionRecordQuerySLZ(data=request.query_params) - slz.is_valid(raise_exception=True) - - data = slz.validated_data - - queryset = self.get_queryset() - queryset = AppPermissionRecord.objects.filter_record( - queryset, - bk_app_code=data.get("bk_app_code"), - handled_time_start=data.get("time_start"), - handled_time_end=data.get("time_end"), - grant_dimension=data.get("grant_dimension"), - fuzzy=True, - ) - queryset = queryset.exclude(status=ApplyStatusEnum.PENDING.value) - page = self.paginate_queryset(queryset) - - serializer = serializers.AppPermissionRecordSLZ( - page, - many=True, - context={ - "resource_id_map": Resource.objects.filter_id_object_map(request.gateway.id), - }, - ) - return OKJsonResponse("OK", data=self.paginator.get_paginated_data(serializer.data)) - - @swagger_auto_schema(tags=["Permission"]) - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - slz = serializers.AppPermissionRecordSLZ( - instance, - context={ - "resource_id_map": Resource.objects.filter_id_object_map(request.gateway.id), - }, - ) - return OKJsonResponse("OK", data=slz.data) diff --git a/src/dashboard/apigateway/apigateway/apps/support/api_sdk/serializers.py b/src/dashboard/apigateway/apigateway/apps/support/api_sdk/serializers.py index 01cd1e0b4..962139b76 100644 --- a/src/dashboard/apigateway/apigateway/apps/support/api_sdk/serializers.py +++ b/src/dashboard/apigateway/apigateway/apps/support/api_sdk/serializers.py @@ -28,7 +28,7 @@ class APISDKGenerateSLZ(serializers.Serializer): api = serializers.HiddenField(default=CurrentGatewayDefault()) resource_version_id = serializers.IntegerField(required=True) - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices()) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices()) include_private_resources = serializers.BooleanField(label="包含非公开资源") is_public = serializers.BooleanField(label="是否为公开", default=None) version = serializers.CharField( @@ -66,7 +66,7 @@ def validate_is_public(self, value): class APISDKQuerySLZ(serializers.Serializer): - language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.choices(), required=False) + language = serializers.ChoiceField(choices=ProgrammingLanguageEnum.get_choices(), required=False) version_number = serializers.CharField(required=False, allow_blank=True) resource_version_id = serializers.IntegerField(allow_null=True, required=False) diff --git a/src/dashboard/apigateway/apigateway/apps/support/constants.py b/src/dashboard/apigateway/apigateway/apps/support/constants.py index e07e84039..9ef1f2b91 100644 --- a/src/dashboard/apigateway/apigateway/apps/support/constants.py +++ b/src/dashboard/apigateway/apigateway/apps/support/constants.py @@ -28,10 +28,10 @@ class DocTypeEnum(ChoiceEnumMixin, Enum): MARKDOWN = "markdown" -class ProgrammingLanguageEnum(ChoiceEnumMixin, Enum): - UNKNOWN = "unknown" - PYTHON = "python" - GOLANG = "golang" +class ProgrammingLanguageEnum(StructuredEnum): + UNKNOWN = EnumField("unknown") + PYTHON = EnumField("python") + GOLANG = EnumField("golang") class DocLanguageEnum(StructuredEnum): diff --git a/src/dashboard/apigateway/apigateway/apps/support/models.py b/src/dashboard/apigateway/apigateway/apps/support/models.py index 4957d3850..4e4e62789 100644 --- a/src/dashboard/apigateway/apigateway/apps/support/models.py +++ b/src/dashboard/apigateway/apigateway/apps/support/models.py @@ -152,7 +152,7 @@ class APISDK(ConfigModelMixin): name = models.CharField(max_length=128, blank=True, default="", help_text=_("SDK 名称")) url = models.TextField(blank=True, default="", help_text=_("下载地址")) filename = models.CharField(max_length=128, help_text=_("SDK 文件名, 废弃")) - language = models.CharField(max_length=32, choices=ProgrammingLanguageEnum.choices()) + language = models.CharField(max_length=32, choices=ProgrammingLanguageEnum.get_choices()) version_number = models.CharField(max_length=64) include_private_resources = models.BooleanField(default=False) is_public_latest = models.BooleanField(default=False, db_index=True, help_text=_("废弃")) diff --git a/src/dashboard/apigateway/apigateway/biz/permission.py b/src/dashboard/apigateway/apigateway/biz/permission.py index dd7201e19..52f64b64d 100644 --- a/src/dashboard/apigateway/apigateway/biz/permission.py +++ b/src/dashboard/apigateway/apigateway/biz/permission.py @@ -16,9 +16,26 @@ # to the current version of the project delivered to anyone in the future. # -from apigateway.apps.permission.constants import GrantTypeEnum -from apigateway.apps.permission.models import AppResourcePermission -from apigateway.core.models import Gateway +from abc import ABCMeta, abstractmethod +from typing import List, Optional, Tuple + +from django.utils.translation import gettext as _ + +from apigateway.apps.permission.constants import ( + RENEWABLE_EXPIRE_DAYS, + ApplyStatusEnum, + GrantDimensionEnum, + GrantTypeEnum, +) +from apigateway.apps.permission.models import ( + AppAPIPermission, + AppPermissionApply, + AppPermissionApplyStatus, + AppPermissionRecord, + AppResourcePermission, +) +from apigateway.common.error_codes import error_codes +from apigateway.core.models import Gateway, Resource class ResourcePermissionHandler: @@ -39,3 +56,260 @@ def grant_or_renewal_expire_soon( grant_type=GrantTypeEnum.INITIALIZE.value, expire_days=expire_days, ) + + +class PermissionDimensionManager(metaclass=ABCMeta): + @classmethod + def get_manager(cls, grant_dimension: str) -> "PermissionDimensionManager": + if grant_dimension == GrantDimensionEnum.API.value: + return APIPermissionDimensionManager() + elif grant_dimension == GrantDimensionEnum.RESOURCE.value: + return ResourcePermissionDimensionManager() + + raise error_codes.INVALID_ARGS.format(f"unsupported grant_dimension: {grant_dimension}") + + @abstractmethod + def handle_permission_apply( + self, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + comment: str, + handled_by: str, + part_resource_ids: Optional[List[int]], + ) -> AppPermissionRecord: + """处理权限申请""" + + @abstractmethod + def save_permission_apply_status( + self, + bk_app_code: str, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + resources: List[Resource], + ): + """保存权限申请状态""" + + @abstractmethod + def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: + """资源权限申请时,获取展示的资源名称列表""" + + @abstractmethod + def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + """资源审批时,获取审批通过的资源名称列表""" + + @abstractmethod + def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + """资源审批时,获取审批拒绝的资源名称列表""" + + @abstractmethod + def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: + """判断是否允许申请权限""" + + +class APIPermissionDimensionManager(PermissionDimensionManager): + def handle_permission_apply( + self, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + comment: str, + handled_by: str, + part_resource_ids=None, + ) -> AppPermissionRecord: + if status == ApplyStatusEnum.APPROVED.value: + AppAPIPermission.objects.save_permissions( + gateway=gateway, + bk_app_code=apply.bk_app_code, + grant_type=GrantTypeEnum.APPLY.value, + expire_days=apply.expire_days, + ) + + self._handle_apply_status(apply, status) + + # 添加到已审批单据记录 + return AppPermissionRecord.objects.save_record( + record_id=apply.apply_record_id, + gateway=gateway, + bk_app_code=apply.bk_app_code, + applied_by=apply.applied_by, + applied_time=apply.created_time, + handled_by=handled_by, + resource_ids=apply.resource_ids, + handled_resource_ids={}, + status=status, + comment=comment, + reason=apply.reason, + expire_days=apply.expire_days, + grant_dimension=apply.grant_dimension, + ) + + def save_permission_apply_status( + self, + bk_app_code: str, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + resources=None, + ): + AppPermissionApplyStatus.objects.update_or_create( + bk_app_code=bk_app_code, + api=gateway, + resource=None, + grant_dimension=GrantDimensionEnum.API.value, + defaults={ + "apply": apply, + "status": status, + }, + ) + + def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: + return ["该网关所有资源,包括新建资源"] + + def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + if status == ApplyStatusEnum.APPROVED.value: + return self.get_resource_names_display(gateway_id, resource_ids) + return [] + + def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + if status == ApplyStatusEnum.APPROVED.value: + return [] + + return self.get_resource_names_display(gateway_id, resource_ids) + + def _handle_apply_status(self, apply, status: str): + # 按网关申请被拒绝后,如果不删除申请状态记录,应用看到网关下所有资源申请状态均为“拒绝”,体验不友好, + # 因此,按网关申请时,同意、拒绝,均删除申请状态记录 + AppPermissionApplyStatus.objects.filter(apply=apply).delete() + + def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: + is_pending = AppPermissionApplyStatus.objects.is_permission_pending_by_gateway(gateway_id, bk_app_code) + if is_pending: + return False, _("权限申请中,请联系网关负责人审批。") + + api_perm = AppAPIPermission.objects.filter( + api_id=gateway_id, + bk_app_code=bk_app_code, + ).first() + + if api_perm and not api_perm.allow_apply_permission: + return False, _("权限有效期小于 {days} 天时,才可申请。").format(days=RENEWABLE_EXPIRE_DAYS) + + return True, "" + + +class ResourcePermissionDimensionManager(PermissionDimensionManager): + def handle_permission_apply( + self, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + comment: str, + handled_by: str, + part_resource_ids: Optional[List[int]], + ): + approved_resource_ids, rejected_resource_ids = self._split_resource_ids( + status, + apply.resource_ids, + part_resource_ids, + ) + + # 如果审批同意,则更新权限信息 + # 如果审批驳回,则不做任何处理 + if approved_resource_ids: + AppResourcePermission.objects.save_permissions( + gateway=gateway, + resource_ids=approved_resource_ids, + bk_app_code=apply.bk_app_code, + grant_type=GrantTypeEnum.APPLY.value, + expire_days=apply.expire_days, + ) + + # 更新应用访问资源权限申请状态 + # 若拒绝,则更新状态为拒绝 + # 若同意,则删除单据对应记录 + self._handle_apply_status(apply, rejected_resource_ids) + + # 添加到已审批单据记录 + return AppPermissionRecord.objects.save_record( + record_id=apply.apply_record_id, + gateway=gateway, + bk_app_code=apply.bk_app_code, + applied_by=apply.applied_by, + applied_time=apply.created_time, + handled_by=handled_by, + resource_ids=apply.resource_ids, + handled_resource_ids={ + ApplyStatusEnum.APPROVED.value: approved_resource_ids, + ApplyStatusEnum.REJECTED.value: rejected_resource_ids, + }, + status=status, + comment=comment, + reason=apply.reason, + expire_days=apply.expire_days, + grant_dimension=apply.grant_dimension, + ) + + def save_permission_apply_status( + self, + bk_app_code: str, + gateway: Gateway, + apply: AppPermissionApply, + status: str, + resources=None, + ): + for resource in resources: + AppPermissionApplyStatus.objects.update_or_create( + bk_app_code=bk_app_code, + api=gateway, + resource=resource, + grant_dimension=GrantDimensionEnum.RESOURCE.value, + defaults={ + "apply": apply, + "status": status, + }, + ) + + def get_resource_names_display(self, gateway_id: int, resource_ids: List[int]) -> List[str]: + return Resource.objects.filter_resource_names(gateway_id, ids=resource_ids) + + def get_approved_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + return self.get_resource_names_display(gateway_id, resource_ids) + + def get_rejected_resource_names_display(self, gateway_id: int, resource_ids: List[int], status: str) -> List[str]: + return self.get_resource_names_display(gateway_id, resource_ids) + + def _handle_apply_status(self, apply: AppPermissionApply, rejected_resource_ids: List[int]): + if rejected_resource_ids: + AppPermissionApplyStatus.objects.filter(apply=apply, resource_id__in=rejected_resource_ids).update( + apply=None, + status=ApplyStatusEnum.REJECTED.value, + ) + + AppPermissionApplyStatus.objects.filter(apply=apply).delete() + + def _split_resource_ids(self, status, resource_ids, part_resource_ids=None): + """ + 拆分资源ID 为通过、驳回两组 + :param status: 审批状态 + :param resource_ids: 申请单据中的资源ID + :param part_resource_ids: 部分审批时,部分审批的资源ID + """ + if status == ApplyStatusEnum.APPROVED.value: + return resource_ids, [] + + elif status == ApplyStatusEnum.REJECTED.value: + return [], resource_ids + + elif status == ApplyStatusEnum.PARTIAL_APPROVED.value: + resource_id_set = set(resource_ids) + part_resource_id_set = set(part_resource_ids or []) + return list(resource_id_set & part_resource_id_set), list(resource_id_set - part_resource_id_set) + + raise ValueError("unsupported apply status: {status}") + + def allow_apply_permission(self, gateway_id: int, bk_app_code: str) -> Tuple[bool, str]: + return False, _("授权维度 grant_dimension 暂不支持 {grant_dimension}。").format( + grant_dimension=GrantDimensionEnum.RESOURCE.value, + ) diff --git a/src/dashboard/apigateway/apigateway/conf/default.py b/src/dashboard/apigateway/apigateway/conf/default.py index f188e1136..191e33e02 100644 --- a/src/dashboard/apigateway/apigateway/conf/default.py +++ b/src/dashboard/apigateway/apigateway/conf/default.py @@ -269,6 +269,7 @@ "TEST_REQUEST_DEFAULT_FORMAT": "json", "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",), "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S %z", + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), } SWAGGER_SETTINGS = { diff --git a/src/dashboard/apigateway/apigateway/tests/apis/open/permission/test_views.py b/src/dashboard/apigateway/apigateway/tests/apis/open/permission/test_views.py index 3b0113c98..0fe7de96b 100644 --- a/src/dashboard/apigateway/apigateway/tests/apis/open/permission/test_views.py +++ b/src/dashboard/apigateway/apigateway/tests/apis/open/permission/test_views.py @@ -22,8 +22,8 @@ from ddf import G from apigateway.apis.open.permission import views +from apigateway.apis.open.permission.helpers import AppPermissionHelper from apigateway.apps.permission import models -from apigateway.apps.permission.helpers import AppPermissionHelper from apigateway.tests.utils.testing import get_response_json pytestmark = pytest.mark.django_db diff --git a/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/__init__.py b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_serializers.py b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_serializers.py similarity index 93% rename from src/dashboard/apigateway/apigateway/tests/apps/metrics/test_serializers.py rename to src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_serializers.py index a2411449f..c9efea608 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_serializers.py +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_serializers.py @@ -18,7 +18,7 @@ # from django.test import TestCase -from apigateway.apps.metrics import serializers +from apigateway.apis.web.metrics import serializers class TestMetricsQuerySLZ(TestCase): @@ -46,6 +46,6 @@ def test_validate(self): }, ] for test in data: - slz = serializers.MetricsQuerySLZ(data=test["data"]) + slz = serializers.MetricsQueryInputSLZ(data=test["data"]) slz.is_valid() self.assertEqual(slz.validated_data, test["expected"]) diff --git a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_views.py b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_views.py similarity index 71% rename from src/dashboard/apigateway/apigateway/tests/apps/metrics/test_views.py rename to src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_views.py index b955d9f35..5cf0fb9aa 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_views.py +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/metrics/test_views.py @@ -17,11 +17,36 @@ # to the current version of the project delivered to anyone in the future. # +import pytest -class TestQueryRangeAPIView: +from apigateway.apis.web.metrics.views import MetricsSmartTimeRange + + +class TestMetricsSmartTimeRange: + @pytest.mark.parametrize( + "time_range_minutes, expected", + [ + (10, "1m"), + (59, "1m"), + (60, "1m"), + (300, "5m"), + (360, "5m"), + (720, "10m"), + (1440, "30m"), + (4320, "1h"), + (10080, "3h"), + (20000, "12h"), + ], + ) + def test_get_recommended_step(self, time_range_minutes, expected): + smart_time_range = MetricsSmartTimeRange(time_range=time_range_minutes * 60) + assert smart_time_range.get_recommended_step() == expected + + +class TestQueryRangeApi: def test_get(self, mocker, fake_stage, request_view): mocker.patch( - "apigateway.apps.metrics.views.DimensionMetricsFactory.create_dimension_metrics", + "apigateway.apis.web.metrics.views.DimensionMetricsFactory.create_dimension_metrics", return_value=mocker.Mock(query_range=mocker.Mock(return_value={"foo": "bar"})), ) diff --git a/src/dashboard/apigateway/apigateway/tests/apis/web/permission/__init__.py b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/tests/apps/permission/test_serializers.py b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_serializers.py similarity index 62% rename from src/dashboard/apigateway/apigateway/tests/apps/permission/test_serializers.py rename to src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_serializers.py index d7a13156c..db7ef097f 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_serializers.py +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_serializers.py @@ -21,17 +21,18 @@ from django_dynamic_fixture import G from rest_framework.exceptions import ValidationError -from apigateway.apps.permission import models, serializers +from apigateway.apis.web.permission import serializers +from apigateway.apps.permission import models from apigateway.core.models import Gateway, Resource from apigateway.tests.utils.testing import create_request, dummy_time pytestmark = pytest.mark.django_db -class TestAppPermissionCreateSLZ: +class TestAppPermissionInputSLZ: @pytest.fixture(autouse=True) def setup_fixtures(self, mocker): - mocker.patch("apigateway.apps.permission.serializers.BKAppCodeValidator.__call__") + mocker.patch("apigateway.apis.web.permission.serializers.BKAppCodeValidator.__call__") def test_to_internal_value(self, fake_resource): data = [ @@ -39,38 +40,32 @@ def test_to_internal_value(self, fake_resource): "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": [fake_resource.id], - "dimension": "resource", }, { "bk_app_code": "apigw-test", "expire_days": None, "resource_ids": [fake_resource.id], - "dimension": "resource", }, { "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": None, - "dimension": "api", }, { "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": None, - "dimension": "", - "will_error": True, }, { "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": [], - "dimension": "api", "will_error": True, }, ] for test in data: - slz = serializers.AppPermissionCreateSLZ(data=test, context={"api": fake_resource.api}) + slz = serializers.AppPermissionInputSLZ(data=test, context={"api": fake_resource.api}) if not test.get("will_error"): slz.is_valid(raise_exception=True) @@ -81,23 +76,7 @@ def test_to_internal_value(self, fake_resource): slz.is_valid(raise_exception=True) -class TestAppPermissionQuerySLZ(TestCase): - def test_to_internal_value(self): - data = [ - {"dimension": "api", "bk_app_code": "", "grant_type": ""}, - { - "dimension": "resource", - "bk_app_code": "test", - "grant_type": "apply", - }, - ] - for test in data: - slz = serializers.AppPermissionQuerySLZ(data=test) - slz.is_valid() - self.assertEqual(slz.validated_data, test) - - -class TestAppPermissionListSLZ(TestCase): +class TestAppGatewayPermissionOutputSLZ(TestCase): def test_to_representation(self): gateway = G(Gateway) resource = G(Resource, api=gateway, path="/echo/", method="GET") @@ -151,11 +130,11 @@ def test_to_representation(self): ] for test in data: - slz = serializers.AppPermissionListSLZ(instance=test["instance"]) + slz = serializers.AppGatewayPermissionOutputSLZ(instance=test["instance"]) self.assertEqual(slz.data, test["expected"]) -class TestAppPermissionBatchSLZ(TestCase): +class TestAppPermissionIDsSLZ(TestCase): @classmethod def setUpTestData(cls): cls.gateway = G(Gateway, created_by="admin") @@ -166,99 +145,17 @@ def setUpTestData(cls): def test_to_internal_value(self): data = [ { - "dimension": "api", "ids": [1, 2], }, - {"dimension": "resource", "ids": [1, 2, 3]}, - ] - for test in data: - slz = serializers.AppPermissionBatchSLZ(data=test) - slz.is_valid() - self.assertEqual(slz.validated_data, test) - - -class TestAppPermissionApplyQuerySLZ(TestCase): - def test_to_internal_value(self): - data = [ - { - "bk_app_code": "test", - "applied_by": "admin", - "grant_dimension": "api", - }, - { - "bk_app_code": "", - "applied_by": "", - "grant_dimension": "resource", - }, + {"ids": [1, 2, 3]}, ] for test in data: - slz = serializers.AppPermissionApplyQuerySLZ(data=test) + slz = serializers.AppPermissionIDsSLZ(data=test) slz.is_valid() self.assertEqual(slz.validated_data, test) -class TestAppPermissionApplySLZ: - def test_to_internal_value(self, mocker): - mocker.patch("apigateway.apps.permission.serializers.BKAppCodeValidator.__call__", return_value=None) - - data = [ - { - "bk_app_code": "test", - "resource_ids": [1, 2, 3], - }, - { - "bk_app_code": "test", - "resource_ids": [], - }, - ] - for test in data: - slz = serializers.AppPermissionApplySLZ(data=test) - slz.is_valid() - if test.get("will_error"): - assert slz.errors - else: - assert slz.validated_data == test - - def test_to_representation(self): - gateway = G(Gateway) - apply = G( - models.AppPermissionApply, - api=gateway, - bk_app_code="test", - applied_by="admin", - _resource_ids="1;2;3", - status="pending", - created_time=dummy_time.time, - reason="test", - expire_days=180, - grant_dimension="resource", - ) - - data = [ - { - "instance": apply, - "expected": { - "id": apply.id, - "bk_app_code": "test", - "applied_by": "admin", - "status": "pending", - "resource_ids": [1, 2, 3], - "created_time": dummy_time.str, - "reason": "test", - "expire_days": 180, - "grant_dimension": "resource", - "expire_days_display": "6个月", - "grant_dimension_display": "按资源", - }, - } - ] - - for test in data: - slz = serializers.AppPermissionApplySLZ(instance=test["instance"]) - assert slz.data == test["expected"] - - -class TestAppPermissionApplyBatchSLZ(TestCase): +class TestAppPermissionApplyApprovalInputSLZ(TestCase): def test_validate(self): data = [ { @@ -317,7 +214,7 @@ def test_validate(self): ] for test in data: - slz = serializers.AppPermissionApplyBatchSLZ(data=test["params"]) + slz = serializers.AppPermissionApplyApprovalInputSLZ(data=test["params"]) slz.is_valid() if test.get("will_error"): self.assertTrue(slz.errors) diff --git a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_views.py b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_views.py similarity index 69% rename from src/dashboard/apigateway/apigateway/tests/apps/permission/test_views.py rename to src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_views.py index 24ba591b0..dab79dd5a 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_views.py +++ b/src/dashboard/apigateway/apigateway/tests/apis/web/permission/test_views.py @@ -23,8 +23,8 @@ from django.test import TestCase from django_dynamic_fixture import G -from apigateway.apps.permission import models, views -from apigateway.apps.permission.helpers import AppPermissionHelper +from apigateway.apis.web.permission import views +from apigateway.apps.permission import models from apigateway.core.models import Resource from apigateway.tests.utils.testing import APIRequestFactory, create_gateway, dummy_time, get_response_json from apigateway.utils.time import now_datetime @@ -32,20 +32,14 @@ pytestmark = pytest.mark.django_db -class TestAppPermissionViewSet: +class TestAppResourcePermissionViewSet: @pytest.fixture(autouse=True) def setup_fixtures(self, mocker): - mocker.patch("apigateway.apps.permission.serializers.BKAppCodeValidator.__call__") + mocker.patch("apigateway.apis.web.permission.serializers.BKAppCodeValidator.__call__") def test_list(self, fake_resource, request_view): fake_gateway = fake_resource.api - G( - models.AppAPIPermission, - api=fake_gateway, - bk_app_code="test", - ) - G( models.AppResourcePermission, api=fake_gateway, @@ -62,16 +56,6 @@ def test_list(self, fake_resource, request_view): ) data = [ - { - "params": { - "dimension": "api", - "bk_app_code": "test", - "grant_type": "initialize", - }, - "expected": { - "count": 1, - }, - }, { "params": { "dimension": "resource", @@ -87,7 +71,7 @@ def test_list(self, fake_resource, request_view): for test in data: response = request_view( "GET", - "permissions.app-permissions", + "permissions.app-resource-permissions", path_params={"gateway_id": fake_gateway.id}, gateway=fake_gateway, data=test["params"], @@ -107,7 +91,6 @@ def test_create(self, mocker, request_view, fake_resource): "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": [fake_resource.id], - "dimension": "resource", }, "expected": { "expires": dummy_time.time, @@ -119,19 +102,75 @@ def test_create(self, mocker, request_view, fake_resource): "bk_app_code": "apigw-test", "expire_days": None, "resource_ids": [fake_resource.id], - "dimension": "resource", }, "expected": { "expires": None, "permission_model": models.AppResourcePermission, }, }, + ] + + for test in data: + response = request_view( + "POST", + "permissions.app-resource-permissions", + path_params={"gateway_id": fake_gateway.id}, + gateway=fake_gateway, + data=test["params"], + ) + result = response.json() + assert result["code"] == 0, result + + +class TestAppGatewayPermissionViewSet: + @pytest.fixture(autouse=True) + def setup_fixtures(self, mocker): + mocker.patch("apigateway.apis.web.permission.serializers.BKAppCodeValidator.__call__") + + def test_list(self, fake_resource, request_view): + fake_gateway = fake_resource.api + + G( + models.AppAPIPermission, + api=fake_gateway, + bk_app_code="test", + ) + + data = [ + { + "params": { + "bk_app_code": "test", + "grant_type": "initialize", + }, + "expected": { + "count": 1, + }, + } + ] + + for test in data: + response = request_view( + "GET", + "permissions.app-gateway-permissions", + path_params={"gateway_id": fake_gateway.id}, + gateway=fake_gateway, + data=test["params"], + ) + + result = response.json() + assert result["code"] == 0, result + assert result["data"]["count"] == test["expected"]["count"] + + def test_create(self, mocker, request_view, fake_resource): + mocker.patch("apigateway.apps.permission.models.generate_expire_time", return_value=dummy_time.time) + fake_gateway = fake_resource.api + + data = [ { "params": { "bk_app_code": "apigw-test", "expire_days": 180, "resource_ids": None, - "dimension": "api", }, "expected": { "expires": dummy_time.time, @@ -143,7 +182,6 @@ def test_create(self, mocker, request_view, fake_resource): "bk_app_code": "apigw-test", "expire_days": None, "resource_ids": None, - "dimension": "api", }, "expected": { "expires": None, @@ -155,7 +193,7 @@ def test_create(self, mocker, request_view, fake_resource): for test in data: response = request_view( "POST", - "permissions.app-permissions", + "permissions.app-gateway-permissions", path_params={"gateway_id": fake_gateway.id}, gateway=fake_gateway, data=test["params"], @@ -164,7 +202,7 @@ def test_create(self, mocker, request_view, fake_resource): assert result["code"] == 0, result -class TestAppPermissionBatchViewSet(TestCase): +class TestAppResourcePermissionBatchViewSet(TestCase): @classmethod def setUpTestData(cls): cls.factory = APIRequestFactory() @@ -173,12 +211,6 @@ def setUpTestData(cls): def test_renew(self): resource = G(Resource, api=self.gateway) - perm_1 = G( - models.AppAPIPermission, - api=self.gateway, - bk_app_code="test", - ) - perm_2 = G( models.AppResourcePermission, api=self.gateway, @@ -188,12 +220,8 @@ def test_renew(self): ) data = [ - { - "params": {"dimension": "api", "ids": [perm_1.id]}, - }, { "params": { - "dimension": "resource", "ids": [perm_2.id], }, }, @@ -201,17 +229,16 @@ def test_renew(self): for test in data: request = self.factory.post( - f"/apis/{self.gateway.id}/permissions/app-permissions/batch/", data=test["params"] + f"/gateways/{self.gateway.id}/permissions/app-resource-permissions/renew/", data=test["params"] ) - view = views.AppPermissionBatchViewSet.as_view({"post": "renew"}) + view = views.AppResourcePermissionRenewApi.as_view() response = view(request, gateway_id=self.gateway.id) result = get_response_json(response) self.assertEqual(result["code"], 0, result) - permission_model = AppPermissionHelper().get_permission_model(test["params"]["dimension"]) - perm_record = permission_model.objects.filter( + perm_record = models.AppResourcePermission.objects.filter( api=self.gateway, id=test["params"]["ids"][0], ).first() @@ -222,12 +249,6 @@ def test_renew(self): def test_destroy(self): resource = G(Resource, api=self.gateway) - perm_1 = G( - models.AppAPIPermission, - api=self.gateway, - bk_app_code="test", - ) - perm_2 = G( models.AppResourcePermission, api=self.gateway, @@ -239,30 +260,101 @@ def test_destroy(self): data = [ { "params": { - "dimension": "api", - "ids": [perm_1.id], + "ids": [perm_2.id], }, }, + ] + + for test in data: + request = self.factory.post( + f"/gateways/{self.gateway.id}/permissions/app-resource-permissions/delete/", data=test["params"] + ) + + view = views.AppResourcePermissionDeleteApi.as_view() + response = view(request, gateway_id=self.gateway.id) + + result = get_response_json(response) + self.assertEqual(result["code"], 0, result) + + permission_model = models.AppResourcePermission + self.assertFalse( + permission_model.objects.filter( + api=self.gateway, + id=test["params"]["ids"][0], + ).exists() + ) + + +class TestAppGatewayPermissionBatchViewSet(TestCase): + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.gateway = create_gateway() + + def test_renew(self): + resource = G(Resource, api=self.gateway) + + perm_1 = G( + models.AppAPIPermission, + api=self.gateway, + bk_app_code="test", + ) + + data = [ + { + "params": {"ids": [perm_1.id]}, + }, + ] + + for test in data: + request = self.factory.post( + f"/gateways/{self.gateway.id}/permissions/app-gateway-permissions/renew/", data=test["params"] + ) + + view = views.AppGatewayPermissionRenewApi.as_view() + response = view(request, gateway_id=self.gateway.id) + + result = get_response_json(response) + self.assertEqual(result["code"], 0, result) + + permission_model = models.AppAPIPermission + perm_record = permission_model.objects.filter( + api=self.gateway, + id=test["params"]["ids"][0], + ).first() + self.assertTrue( + 180 * 24 * 3600 - 10 < (perm_record.expires - now_datetime()).total_seconds() < 180 * 24 * 3600 + ) + + def test_destroy(self): + resource = G(Resource, api=self.gateway) + + perm_1 = G( + models.AppAPIPermission, + api=self.gateway, + bk_app_code="test", + ) + + data = [ { "params": { - "dimension": "resource", - "ids": [perm_2.id], + "ids": [perm_1.id], }, }, ] for test in data: - request = self.factory.delete( - f"/apis/{self.gateway.id}/permissions/app-permissions/batch/", data=test["params"] + request = self.factory.post( + f"/apis/{self.gateway.id}/permissions/app-gateway-permissions/delete/", data=test["params"] ) - view = views.AppPermissionBatchViewSet.as_view({"delete": "destroy"}) + view = views.AppGatewayPermissionDeleteApi.as_view() response = view(request, gateway_id=self.gateway.id) result = get_response_json(response) self.assertEqual(result["code"], 0, result) - permission_model = AppPermissionHelper().get_permission_model(test["params"]["dimension"]) + permission_model = models.AppAPIPermission self.assertFalse( permission_model.objects.filter( api=self.gateway, @@ -291,11 +383,11 @@ def test_list(self, request_factory, fake_gateway): for test in data: request = request_factory.get( - f"/apis/{fake_gateway.id}/permissions/app-permission-apply/", + f"/gateways/{fake_gateway.id}/permissions/app-permission-apply/", data=test["params"], ) - view = views.AppPermissionApplyViewSet.as_view({"get": "list"}) + view = views.AppPermissionApplyListApi.as_view() response = view(request, gateway_id=fake_gateway.id) result = get_response_json(response) @@ -306,15 +398,15 @@ def test_list(self, request_factory, fake_gateway): class TestAppPermissionApplyBatchViewSet: def test_post(self, mocker, fake_gateway, request_factory): mocker.patch( - "apigateway.apps.permission.helpers.APIPermissionDimensionManager.handle_permission_apply", + "apigateway.biz.permission.APIPermissionDimensionManager.handle_permission_apply", return_value=mock.MagicMock(id=1), ) mocker.patch( - "apigateway.apps.permission.helpers.ResourcePermissionDimensionManager.handle_permission_apply", + "apigateway.biz.permission.ResourcePermissionDimensionManager.handle_permission_apply", return_value=mock.MagicMock(id=1), ) mocker.patch( - "apigateway.apps.permission.views.send_mail_for_perm_handle", + "apigateway.apis.web.permission.views.send_mail_for_perm_handle", return_value=None, ) @@ -353,11 +445,11 @@ def test_post(self, mocker, fake_gateway, request_factory): for test in data: request = request_factory.post( - f"/apis/{fake_gateway.id}/permissions/app-permission-apply/batch/", + f"/gateways/{fake_gateway.id}/permissions/app-permission-apply/approval/", data=test["params"], ) - view = views.AppPermissionApplyBatchViewSet.as_view({"post": "post"}) + view = views.AppPermissionApplyApprovalApi.as_view() response = view(request, gateway_id=fake_gateway.id) result = get_response_json(response) @@ -399,10 +491,10 @@ def test_list(self): for test in data: request = self.factory.get( - f"/apis/{self.gateway.id}/permissions/app-permission-records/", data=test["params"] + f"/gateways/{self.gateway.id}/permissions/app-permission-records/", data=test["params"] ) - view = views.AppPermissionRecordViewSet.as_view({"get": "list"}) + view = views.AppPermissionRecordListApi.as_view() response = view(request, gateway_id=self.gateway.id) result = get_response_json(response) @@ -424,9 +516,9 @@ def test_retrieve(self): ), ) - request = self.factory.get(f"/apis/{self.gateway.id}/permissions/app-permission-records/{record.id}/") + request = self.factory.get(f"/gateways/{self.gateway.id}/permissions/app-permission-records/{record.id}/") - view = views.AppPermissionRecordViewSet.as_view({"get": "retrieve"}) + view = views.AppPermissionRecordRetrieveApi.as_view() response = view(request, gateway_id=self.gateway.id, id=record.id) result = get_response_json(response) diff --git a/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/__init__.py b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 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/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_dimension_metrics.py b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_dimension.py similarity index 84% rename from src/dashboard/apigateway/apigateway/tests/apps/metrics/test_dimension_metrics.py rename to src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_dimension.py index b273ba9f8..3eab5a0a2 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_dimension_metrics.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_dimension.py @@ -16,13 +16,15 @@ # 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 apigateway.apps.metrics import dimension_metrics from apigateway.apps.metrics.constants import DimensionEnum, MetricsEnum +from apigateway.apps.metrics.prometheus import dimension class TestRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -51,14 +53,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.RequestsMetrics() + metrics = dimension.RequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"] class TestFailedRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -87,14 +91,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.FailedRequestsMetrics() + metrics = dimension.FailedRequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestResponseTime95thMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -124,14 +130,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.ResponseTime95thMetrics() + metrics = dimension.ResponseTime95thMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestResponseTime50thMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -161,14 +169,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.ResponseTime50thMetrics() + metrics = dimension.ResponseTime50thMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestResourceRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -197,14 +207,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.ResourceRequestsMetrics() + metrics = dimension.ResourceRequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestResourceFailedRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -233,14 +245,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.ResourceFailedRequestsMetrics() + metrics = dimension.ResourceFailedRequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestAppRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -269,14 +283,16 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.AppRequestsMetrics() + metrics = dimension.AppRequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result class TestResourceNon200StatusRequestsMetrics: def test_get_query_promql(self, mocker): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) data = [ { @@ -305,7 +321,7 @@ def test_get_query_promql(self, mocker): }, ] for test in data: - metrics = dimension_metrics.ResourceNon200StatusRequestsMetrics() + metrics = dimension.ResourceNon200StatusRequestsMetrics() result = metrics._get_query_promql(**test["params"]) assert result == test["expected"], result @@ -316,41 +332,41 @@ def test_create_dimension_metrics(self): { "dimension": "all", "metrics": "requests", - "expected": dimension_metrics.RequestsMetrics, + "expected": dimension.RequestsMetrics, }, { "dimension": "all", "metrics": "failed_requests", - "expected": dimension_metrics.FailedRequestsMetrics, + "expected": dimension.FailedRequestsMetrics, }, { "dimension": "all", "metrics": "response_time_95th", - "expected": dimension_metrics.ResponseTime95thMetrics, + "expected": dimension.ResponseTime95thMetrics, }, { "dimension": "all", "metrics": "response_time_50th", - "expected": dimension_metrics.ResponseTime50thMetrics, + "expected": dimension.ResponseTime50thMetrics, }, { "dimension": "resource", "metrics": "requests", - "expected": dimension_metrics.ResourceRequestsMetrics, + "expected": dimension.ResourceRequestsMetrics, }, { "dimension": "app", "metrics": "requests", - "expected": dimension_metrics.AppRequestsMetrics, + "expected": dimension.AppRequestsMetrics, }, { "dimension": "resource_non200_status", "metrics": "requests", - "expected": dimension_metrics.ResourceNon200StatusRequestsMetrics, + "expected": dimension.ResourceNon200StatusRequestsMetrics, }, ] for test in data: - result = dimension_metrics.DimensionMetricsFactory.create_dimension_metrics( + result = dimension.DimensionMetricsFactory.create_dimension_metrics( DimensionEnum(test["dimension"]), MetricsEnum(test["metrics"]), ) diff --git a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_stats_metrics.py b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_statistics.py similarity index 77% rename from src/dashboard/apigateway/apigateway/tests/apps/metrics/test_stats_metrics.py rename to src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_statistics.py index c488cb376..5fa43c524 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_stats_metrics.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/metrics/prometheus/test_statistics.py @@ -18,7 +18,7 @@ # import pytest -from apigateway.apps.metrics import stats_metrics +from apigateway.apps.metrics.prometheus import statistics class TestStatisticsAPIRequestMetrics: @@ -35,9 +35,11 @@ class TestStatisticsAPIRequestMetrics: ], ) def test_get_query_promql(self, mocker, step, expected): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) - metrics = stats_metrics.StatisticsAPIRequestMetrics() + metrics = statistics.StatisticsAPIRequestMetrics() result = metrics._get_query_promql(step) assert result == expected @@ -56,9 +58,11 @@ class TestStatisticsAPIRequestDurationMetrics: ], ) def test_get_query_promql(self, mocker, step, expected): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) - metrics = stats_metrics.StatisticsAPIRequestDurationMetrics() + metrics = statistics.StatisticsAPIRequestDurationMetrics() result = metrics._get_query_promql(step) assert result == expected @@ -77,9 +81,11 @@ class TestStatisticsAppRequestMetrics: ], ) def test_get_query_promql(self, mocker, step, expected): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) - metrics = stats_metrics.StatisticsAppRequestMetrics() + metrics = statistics.StatisticsAppRequestMetrics() result = metrics._get_query_promql(step) assert result == expected @@ -98,8 +104,10 @@ class TestStatisticsAppRequestByResourceMetrics: ], ) def test_get_query_promql(self, mocker, step, expected): - mocker.patch("apigateway.apps.metrics.dimension_metrics.BaseDimensionMetrics.default_labels", return_value=[]) + mocker.patch( + "apigateway.apps.metrics.prometheus.dimension.BaseDimensionMetrics.default_labels", return_value=[] + ) - metrics = stats_metrics.StatisticsAppRequestByResourceMetrics() + metrics = statistics.StatisticsAppRequestByResourceMetrics() result = metrics._get_query_promql(step) assert result == expected diff --git a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_managers.py b/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_managers.py index 9575dff4c..c3d2f7112 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_managers.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/metrics/test_managers.py @@ -51,7 +51,7 @@ def test_filter_and_aggregate_by_api(self): failed_count=100, ) - result = StatisticsAPIRequestByDay.objects.filter_and_aggregate_by_api( + result = StatisticsAPIRequestByDay.objects.filter_and_aggregate_by_gateway( start_time=dummy_time.time, end_time=dummy_time.time ) @@ -102,7 +102,7 @@ def test_filter_app_and_aggregate_by_api(self): failed_count=100, ) - result = StatisticsAppRequestByDay.objects.filter_app_and_aggregate_by_api( + result = StatisticsAppRequestByDay.objects.filter_app_and_aggregate_by_gateway( start_time=dummy_time.time, end_time=dummy_time.time ) result[gateway_1.id]["bk_app_code_list"] = sorted(result[gateway_1.id]["bk_app_code_list"]) diff --git a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_helpers.py b/src/dashboard/apigateway/apigateway/tests/apps/permission/test_helpers.py deleted file mode 100644 index cd4245a9b..000000000 --- a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_helpers.py +++ /dev/null @@ -1,315 +0,0 @@ -# -*- coding: utf-8 -*- -# -# TencentBlueKing is pleased to support the open source community by making -# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. -# Copyright (C) 2017 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 unittest import mock - -import pytest -from ddf import G - -from apigateway.apps.permission.constants import ApplyStatusEnum, GrantDimensionEnum -from apigateway.apps.permission.helpers import ( - APIPermissionDimensionManager, - PermissionDimensionManager, - ResourcePermissionDimensionManager, -) -from apigateway.apps.permission.models import ( - AppAPIPermission, - AppPermissionApply, - AppPermissionApplyStatus, - AppResourcePermission, -) -from apigateway.core.models import Resource - -pytestmark = pytest.mark.django_db - - -class TestPermissionDimensionManager: - @pytest.mark.parametrize( - "grant_dimension, expected", - [ - ("api", APIPermissionDimensionManager), - ("resource", ResourcePermissionDimensionManager), - ], - ) - def test_get_manager(self, grant_dimension, expected): - manager = PermissionDimensionManager.get_manager(grant_dimension) - assert isinstance(manager, expected) - - -class TestAPIPermissionDimensionManager: - def _make_fake_apply(self, fake_gateway): - return G( - AppPermissionApply, - bk_app_code="test", - api=fake_gateway, - _resource_ids="", - grant_dimension="api", - status=ApplyStatusEnum.PENDING.value, - ) - - def _make_fake_apply_status(self, fake_apply): - return G( - AppPermissionApplyStatus, - apply=fake_apply, - bk_app_code=fake_apply.bk_app_code, - api=fake_apply.api, - resource=None, - grant_dimension=GrantDimensionEnum.API.value, - status=ApplyStatusEnum.PENDING.value, - ) - - def test_handle_permission_apply_approved(self, fake_gateway): - # 审批同意 - apply = self._make_fake_apply(fake_gateway) - self._make_fake_apply_status(apply) - record = APIPermissionDimensionManager().handle_permission_apply( - gateway=apply.api, - apply=apply, - status=ApplyStatusEnum.APPROVED.value, - comment="", - handled_by="admin", - part_resource_ids=None, - ) - assert record.id - assert AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 - assert AppPermissionApplyStatus.objects.filter(api=fake_gateway).count() == 0 - - def test_handle_permission_apply_rejected(self, fake_gateway): - # 审批拒绝 - apply = self._make_fake_apply(fake_gateway) - self._make_fake_apply_status(apply) - - record = APIPermissionDimensionManager().handle_permission_apply( - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - comment="", - handled_by="admin", - part_resource_ids=None, - ) - assert record.id - assert AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 - assert AppPermissionApplyStatus.objects.filter(api=fake_gateway).count() == 0 - - def test_save_permission_apply_status(self, fake_gateway): - apply = self._make_fake_apply(fake_gateway) - - # 新建 - manager = APIPermissionDimensionManager() - manager.save_permission_apply_status( - bk_app_code=apply.bk_app_code, - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.PENDING.value, - ) - - assert ( - AppPermissionApplyStatus.objects.filter( - bk_app_code=apply.bk_app_code, - apply=apply, - status=ApplyStatusEnum.PENDING.value, - api=fake_gateway, - resource=None, - ).count() - == 1 - ) - - # 更新 - manager.save_permission_apply_status( - bk_app_code=apply.bk_app_code, - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - ) - - assert ( - AppPermissionApplyStatus.objects.filter( - bk_app_code=apply.bk_app_code, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - api=fake_gateway, - resource=None, - ).count() - == 1 - ) - - def test_allow_apply_permission(self, mocker, fake_gateway): - manager = APIPermissionDimensionManager() - target_app_code = "test" - - # 权限申请中 - record = G( - AppPermissionApplyStatus, - api=fake_gateway, - bk_app_code=target_app_code, - grant_dimension="api", - status="pending", - ) - result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) - assert result is False - - record.delete() - - # 无权限申请 - result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) - assert result is True - - # 已拥有权限,权限永久有效 - G(AppAPIPermission, api=fake_gateway, bk_app_code=target_app_code, expires=None) - result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) - assert result is False - - # 已拥有权限,权限将过期 - mocker.patch( - "apigateway.apps.permission.models.AppAPIPermission.allow_apply_permission", - new_callable=mock.PropertyMock(return_value=True), - ) - result, _ = manager.allow_apply_permission(fake_gateway, target_app_code) - assert result is True - - -class TestResourcePermissionDimensionManager: - def _make_fake_apply(self, fake_gateway): - r1 = G(Resource, api=fake_gateway) - r2 = G(Resource, api=fake_gateway) - - return G( - AppPermissionApply, - bk_app_code="test", - api=fake_gateway, - _resource_ids=f"{r1.id};{r2.id}", - grant_dimension=GrantDimensionEnum.RESOURCE.value, - status=ApplyStatusEnum.PENDING.value, - ) - - def _make_fake_apply_status(self, fake_apply): - for resource in Resource.objects.filter(id__in=fake_apply.resource_ids): - G( - AppPermissionApplyStatus, - apply=fake_apply, - bk_app_code=fake_apply.bk_app_code, - api=fake_apply.api, - resource=resource, - grant_dimension=GrantDimensionEnum.RESOURCE.value, - status=ApplyStatusEnum.PENDING.value, - ) - - def test_handle_permission_apply_approved(self, fake_gateway): - # 审批同意,全部同意 - apply = self._make_fake_apply(fake_gateway) - self._make_fake_apply_status(apply) - - record = ResourcePermissionDimensionManager().handle_permission_apply( - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.APPROVED.value, - comment="", - handled_by="admin", - part_resource_ids=None, - ) - assert record.id - assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 2 - assert AppPermissionApplyStatus.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 - - def test_handle_permission_apply_rejected(self, fake_gateway): - # 审批拒绝,全部拒绝 - apply = self._make_fake_apply(fake_gateway) - self._make_fake_apply_status(apply) - - record = ResourcePermissionDimensionManager().handle_permission_apply( - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - comment="", - handled_by="admin", - part_resource_ids=None, - ) - assert record.id - assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 - assert ( - AppPermissionApplyStatus.objects.filter( - api=fake_gateway, - bk_app_code=apply.bk_app_code, - apply=None, - ).count() - == 2 - ) - - def test_handle_permission_apply_partial_approved(self, fake_gateway): - # 部分审批 - apply = self._make_fake_apply(fake_gateway) - self._make_fake_apply_status(apply) - - record = ResourcePermissionDimensionManager().handle_permission_apply( - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.PARTIAL_APPROVED.value, - comment="", - handled_by="admin", - part_resource_ids=apply.resource_ids[:1], - ) - assert record.id - assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 - assert AppPermissionApplyStatus.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 - - def test_save_permission_apply_status(self, fake_gateway): - apply = self._make_fake_apply(fake_gateway) - - # 新建 - manager = ResourcePermissionDimensionManager() - manager.save_permission_apply_status( - bk_app_code=apply.bk_app_code, - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.PENDING.value, - resources=Resource.objects.filter(id__in=apply.resource_ids), - ) - - assert ( - AppPermissionApplyStatus.objects.filter( - bk_app_code=apply.bk_app_code, - apply=apply, - status=ApplyStatusEnum.PENDING.value, - api=fake_gateway, - ).count() - == 2 - ) - - # 更新 - manager.save_permission_apply_status( - bk_app_code=apply.bk_app_code, - gateway=fake_gateway, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - resources=Resource.objects.filter(id__in=apply.resource_ids), - ) - - assert ( - AppPermissionApplyStatus.objects.filter( - bk_app_code=apply.bk_app_code, - apply=apply, - status=ApplyStatusEnum.REJECTED.value, - api=fake_gateway, - ).count() - == 2 - ) - - def test_allow_apply_permission(self, fake_gateway): - manager = ResourcePermissionDimensionManager() - result, _ = manager.allow_apply_permission(fake_gateway.id, "test") - assert not result diff --git a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_managers.py b/src/dashboard/apigateway/apigateway/tests/apps/permission/test_managers.py index cb3ff204e..c6b90c485 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/permission/test_managers.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/permission/test_managers.py @@ -19,7 +19,6 @@ import datetime import pytest -from django.test import TestCase from django_dynamic_fixture import G from apigateway.apps.permission import models @@ -46,66 +45,7 @@ def test_filter_public_permission_by_app(self, unique_id): assert 1 == models.AppAPIPermission.objects.filter_public_permission_by_app(unique_id).count() - def test_filter_permission(self): - G( - models.AppAPIPermission, - api=self.gateway, - bk_app_code="test", - ) - G( - models.AppAPIPermission, - api=self.gateway, - bk_app_code="test-2", - ) - - data = [ - { - "params": {}, - "expected": { - "count": 2, - }, - }, - { - "params": { - "bk_app_code": "test", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "bk_app_code": "test", - "grant_type": "initialize", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "bk_app_code": "test", - "grant_type": "apply", - }, - "expected": { - "count": 0, - }, - }, - { - "params": { - "bk_app_codes": ["test-2", "test", "not-exist"], - }, - "expected": { - "count": 2, - }, - }, - ] - - for test in data: - queryset = models.AppAPIPermission.objects.filter_permission(self.gateway, **test["params"]) - assert queryset.count() == test["expected"]["count"] - - def test_renew_permission(self): + def test_renew_by_ids(self): perm_1 = G( models.AppAPIPermission, api=self.gateway, @@ -125,7 +65,7 @@ def test_renew_permission(self): expires=to_datetime_from_now(days=720), ) - models.AppAPIPermission.objects.renew_permission( + models.AppAPIPermission.objects.renew_by_ids( self.gateway, ids=[perm_1.id, perm_2.id, perm_3.id], ) @@ -136,20 +76,6 @@ def test_renew_permission(self): assert to_datetime_from_now(days=179) < perm_2.expires < to_datetime_from_now(days=181) assert to_datetime_from_now(days=719) < perm_3.expires < to_datetime_from_now(days=721) - def test_delete_permission(self, fake_gateway): - p1 = G(models.AppAPIPermission, api=fake_gateway, bk_app_code="app1") - p2 = G(models.AppAPIPermission, api=fake_gateway, bk_app_code="app2") - G(models.AppAPIPermission, api=fake_gateway, bk_app_code="app3") - G(models.AppAPIPermission, api=fake_gateway, bk_app_code="app4") - - models.AppAPIPermission.objects.delete_permission(fake_gateway, ids=[p1.id]) - assert not models.AppAPIPermission.objects.filter(api=fake_gateway, id=p1.id).exists() - assert models.AppAPIPermission.objects.filter(api=fake_gateway, id=p2.id).exists() - - models.AppAPIPermission.objects.delete_permission(fake_gateway, bk_app_codes=["app2", "app3"]) - assert not models.AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code__in=["app2", "app3"]).exists() - assert models.AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code="app4").exists() - class TestAppResourcePermissionManager: @pytest.fixture(autouse=True) @@ -166,78 +92,7 @@ def test_filter_public_permission_by_app(self, unique_id): assert 1 == models.AppResourcePermission.objects.filter_public_permission_by_app(unique_id).count() - def test_filter_permission(self): - G( - models.AppResourcePermission, - api=self.gateway, - bk_app_code="test", - grant_type="initialize", - resource_id=self.resource.id, - ) - G( - models.AppResourcePermission, - api=self.gateway, - bk_app_code="test-2", - grant_type="apply", - resource_id=self.resource.id, - ) - - data = [ - { - "params": {}, - "expected": { - "count": 2, - }, - }, - { - "params": { - "bk_app_code": "test", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "bk_app_code": "test", - "grant_type": "initialize", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "bk_app_code": "test-2", - "grant_type": "apply", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "resource_ids": [self.resource.id], - }, - "expected": { - "count": 2, - }, - }, - { - "params": { - "bk_app_codes": ["test", "test-2"], - }, - "expected": { - "count": 2, - }, - }, - ] - - for test in data: - queryset = models.AppResourcePermission.objects.filter_permission(self.gateway, **test["params"]) - assert queryset.count() == test["expected"]["count"] - - def test_renew_permission(self): + def test_renew_by_ids(self): perm_1 = G( models.AppResourcePermission, api=self.gateway, @@ -260,7 +115,7 @@ def test_renew_permission(self): resource_id=self.resource.id, ) - models.AppResourcePermission.objects.renew_permission( + models.AppResourcePermission.objects.renew_by_ids( self.gateway, ids=[perm_1.id, perm_2.id, perm_3.id], ) @@ -364,72 +219,6 @@ def test_sync_from_api_permission(self): models.AppResourcePermission.objects.sync_from_gateway_permission(gateway, bk_app_code, [resource.id]) assert models.AppResourcePermission.objects.filter(api=gateway, bk_app_code=bk_app_code).count() == 1 - def test_delete_permission(self, fake_gateway): - resource = G(Resource, api=fake_gateway) - p1 = G(models.AppResourcePermission, api=fake_gateway, bk_app_code="app1", resource_id=resource.id) - p2 = G(models.AppResourcePermission, api=fake_gateway, bk_app_code="app2", resource_id=resource.id) - G(models.AppResourcePermission, api=fake_gateway, bk_app_code="app3", resource_id=resource.id) - G(models.AppResourcePermission, api=fake_gateway, bk_app_code="app4", resource_id=resource.id) - - models.AppResourcePermission.objects.delete_permission(fake_gateway, ids=[p1.id]) - assert not models.AppResourcePermission.objects.filter(api=fake_gateway, id=p1.id).exists() - assert models.AppResourcePermission.objects.filter(api=fake_gateway, id=p2.id).exists() - - models.AppResourcePermission.objects.delete_permission(fake_gateway, bk_app_codes=["app2", "app3"]) - assert not models.AppResourcePermission.objects.filter( - api=fake_gateway, bk_app_code__in=["app2", "app3"] - ).exists() - assert models.AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code="app4").exists() - - -class TestAppPermissionApplyManager(TestCase): - @classmethod - def setUpTestData(cls): - cls.gateway = G(Gateway) - - def test_filter_apply(self): - G( - models.AppPermissionApply, - api=self.gateway, - bk_app_code="test", - applied_by="admin", - ) - G( - models.AppPermissionApply, - api=self.gateway, - bk_app_code="test-2", - applied_by="admin-2", - ) - - data = [ - { - "params": {}, - "expected": { - "count": 2, - }, - }, - { - "params": { - "bk_app_code": "test", - }, - "expected": { - "count": 1, - }, - }, - { - "params": { - "applied_by": "admin-2", - }, - "expected": { - "count": 1, - }, - }, - ] - for test in data: - queryset = models.AppPermissionApply.objects.filter(api=self.gateway) - queryset = models.AppPermissionApply.objects.filter_apply(queryset, **test["params"]) - self.assertEqual(queryset.count(), test["expected"]["count"]) - class TestAppPermissionRecordManager: def test_filter_record(self): @@ -459,14 +248,6 @@ def test_filter_record(self): queryset = models.AppPermissionRecord.objects.filter(api__id__in=[api1.id, api2.id]) assert 1 == models.AppPermissionRecord.objects.filter_record(queryset, bk_app_code="test").count() - assert ( - 1 - == models.AppPermissionRecord.objects.filter_record( - queryset, - handled_time_start=now_datetime() - datetime.timedelta(seconds=10), - handled_time_end=now_datetime() + datetime.timedelta(seconds=10), - ).count() - ) assert ( 1 == models.AppPermissionRecord.objects.filter_record( @@ -475,11 +256,4 @@ def test_filter_record(self): applied_time_end=now_datetime() + datetime.timedelta(seconds=10), ).count() ) - assert ( - 1 - == models.AppPermissionRecord.objects.filter_record( - queryset, - grant_dimension="api", - ).count() - ) assert 1 == models.AppPermissionRecord.objects.filter_record(queryset, status="approved").count() diff --git a/src/dashboard/apigateway/apigateway/tests/biz/test_permission.py b/src/dashboard/apigateway/apigateway/tests/biz/test_permission.py index a14ccfc7f..28a637959 100644 --- a/src/dashboard/apigateway/apigateway/tests/biz/test_permission.py +++ b/src/dashboard/apigateway/apigateway/tests/biz/test_permission.py @@ -16,8 +16,27 @@ # to the current version of the project delivered to anyone in the future. # -from apigateway.apps.permission.models import AppResourcePermission -from apigateway.biz.permission import ResourcePermissionHandler +from unittest import mock + +import pytest +from ddf import G + +from apigateway.apps.permission.constants import ApplyStatusEnum, GrantDimensionEnum +from apigateway.apps.permission.models import ( + AppAPIPermission, + AppPermissionApply, + AppPermissionApplyStatus, + AppResourcePermission, +) +from apigateway.biz.permission import ( + APIPermissionDimensionManager, + PermissionDimensionManager, + ResourcePermissionDimensionManager, + ResourcePermissionHandler, +) +from apigateway.core.models import Resource + +pytestmark = pytest.mark.django_db class TestResourcePermissionHandler: @@ -38,3 +57,280 @@ def test_grant_or_renewal_expire_soon(self, fake_gateway, fake_resource): bk_app_code=test["bk_app_code"], ) assert not app_resource_permission.has_expired + + +class TestPermissionDimensionManager: + @pytest.mark.parametrize( + "grant_dimension, expected", + [ + ("api", APIPermissionDimensionManager), + ("resource", ResourcePermissionDimensionManager), + ], + ) + def test_get_manager(self, grant_dimension, expected): + manager = PermissionDimensionManager.get_manager(grant_dimension) + assert isinstance(manager, expected) + + +class TestAPIPermissionDimensionManager: + def _make_fake_apply(self, fake_gateway): + return G( + AppPermissionApply, + bk_app_code="test", + api=fake_gateway, + _resource_ids="", + grant_dimension="api", + status=ApplyStatusEnum.PENDING.value, + ) + + def _make_fake_apply_status(self, fake_apply): + return G( + AppPermissionApplyStatus, + apply=fake_apply, + bk_app_code=fake_apply.bk_app_code, + api=fake_apply.api, + resource=None, + grant_dimension=GrantDimensionEnum.API.value, + status=ApplyStatusEnum.PENDING.value, + ) + + def test_handle_permission_apply_approved(self, fake_gateway): + # 审批同意 + apply = self._make_fake_apply(fake_gateway) + self._make_fake_apply_status(apply) + record = APIPermissionDimensionManager().handle_permission_apply( + gateway=apply.api, + apply=apply, + status=ApplyStatusEnum.APPROVED.value, + comment="", + handled_by="admin", + part_resource_ids=None, + ) + assert record.id + assert AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 + assert AppPermissionApplyStatus.objects.filter(api=fake_gateway).count() == 0 + + def test_handle_permission_apply_rejected(self, fake_gateway): + # 审批拒绝 + apply = self._make_fake_apply(fake_gateway) + self._make_fake_apply_status(apply) + + record = APIPermissionDimensionManager().handle_permission_apply( + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + comment="", + handled_by="admin", + part_resource_ids=None, + ) + assert record.id + assert AppAPIPermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 + assert AppPermissionApplyStatus.objects.filter(api=fake_gateway).count() == 0 + + def test_save_permission_apply_status(self, fake_gateway): + apply = self._make_fake_apply(fake_gateway) + + # 新建 + manager = APIPermissionDimensionManager() + manager.save_permission_apply_status( + bk_app_code=apply.bk_app_code, + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.PENDING.value, + ) + + assert ( + AppPermissionApplyStatus.objects.filter( + bk_app_code=apply.bk_app_code, + apply=apply, + status=ApplyStatusEnum.PENDING.value, + api=fake_gateway, + resource=None, + ).count() + == 1 + ) + + # 更新 + manager.save_permission_apply_status( + bk_app_code=apply.bk_app_code, + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + ) + + assert ( + AppPermissionApplyStatus.objects.filter( + bk_app_code=apply.bk_app_code, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + api=fake_gateway, + resource=None, + ).count() + == 1 + ) + + def test_allow_apply_permission(self, mocker, fake_gateway): + manager = APIPermissionDimensionManager() + target_app_code = "test" + + # 权限申请中 + record = G( + AppPermissionApplyStatus, + api=fake_gateway, + bk_app_code=target_app_code, + grant_dimension="api", + status="pending", + ) + result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) + assert result is False + + record.delete() + + # 无权限申请 + result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) + assert result is True + + # 已拥有权限,权限永久有效 + G(AppAPIPermission, api=fake_gateway, bk_app_code=target_app_code, expires=None) + result, _ = manager.allow_apply_permission(fake_gateway.id, target_app_code) + assert result is False + + # 已拥有权限,权限将过期 + mocker.patch( + "apigateway.apps.permission.models.AppAPIPermission.allow_apply_permission", + new_callable=mock.PropertyMock(return_value=True), + ) + result, _ = manager.allow_apply_permission(fake_gateway, target_app_code) + assert result is True + + +class TestResourcePermissionDimensionManager: + def _make_fake_apply(self, fake_gateway): + r1 = G(Resource, api=fake_gateway) + r2 = G(Resource, api=fake_gateway) + + return G( + AppPermissionApply, + bk_app_code="test", + api=fake_gateway, + _resource_ids=f"{r1.id};{r2.id}", + grant_dimension=GrantDimensionEnum.RESOURCE.value, + status=ApplyStatusEnum.PENDING.value, + ) + + def _make_fake_apply_status(self, fake_apply): + for resource in Resource.objects.filter(id__in=fake_apply.resource_ids): + G( + AppPermissionApplyStatus, + apply=fake_apply, + bk_app_code=fake_apply.bk_app_code, + api=fake_apply.api, + resource=resource, + grant_dimension=GrantDimensionEnum.RESOURCE.value, + status=ApplyStatusEnum.PENDING.value, + ) + + def test_handle_permission_apply_approved(self, fake_gateway): + # 审批同意,全部同意 + apply = self._make_fake_apply(fake_gateway) + self._make_fake_apply_status(apply) + + record = ResourcePermissionDimensionManager().handle_permission_apply( + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.APPROVED.value, + comment="", + handled_by="admin", + part_resource_ids=None, + ) + assert record.id + assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 2 + assert AppPermissionApplyStatus.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 + + def test_handle_permission_apply_rejected(self, fake_gateway): + # 审批拒绝,全部拒绝 + apply = self._make_fake_apply(fake_gateway) + self._make_fake_apply_status(apply) + + record = ResourcePermissionDimensionManager().handle_permission_apply( + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + comment="", + handled_by="admin", + part_resource_ids=None, + ) + assert record.id + assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 0 + assert ( + AppPermissionApplyStatus.objects.filter( + api=fake_gateway, + bk_app_code=apply.bk_app_code, + apply=None, + ).count() + == 2 + ) + + def test_handle_permission_apply_partial_approved(self, fake_gateway): + # 部分审批 + apply = self._make_fake_apply(fake_gateway) + self._make_fake_apply_status(apply) + + record = ResourcePermissionDimensionManager().handle_permission_apply( + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.PARTIAL_APPROVED.value, + comment="", + handled_by="admin", + part_resource_ids=apply.resource_ids[:1], + ) + assert record.id + assert AppResourcePermission.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 + assert AppPermissionApplyStatus.objects.filter(api=fake_gateway, bk_app_code=apply.bk_app_code).count() == 1 + + def test_save_permission_apply_status(self, fake_gateway): + apply = self._make_fake_apply(fake_gateway) + + # 新建 + manager = ResourcePermissionDimensionManager() + manager.save_permission_apply_status( + bk_app_code=apply.bk_app_code, + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.PENDING.value, + resources=Resource.objects.filter(id__in=apply.resource_ids), + ) + + assert ( + AppPermissionApplyStatus.objects.filter( + bk_app_code=apply.bk_app_code, + apply=apply, + status=ApplyStatusEnum.PENDING.value, + api=fake_gateway, + ).count() + == 2 + ) + + # 更新 + manager.save_permission_apply_status( + bk_app_code=apply.bk_app_code, + gateway=fake_gateway, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + resources=Resource.objects.filter(id__in=apply.resource_ids), + ) + + assert ( + AppPermissionApplyStatus.objects.filter( + bk_app_code=apply.bk_app_code, + apply=apply, + status=ApplyStatusEnum.REJECTED.value, + api=fake_gateway, + ).count() + == 2 + ) + + def test_allow_apply_permission(self, fake_gateway): + manager = ResourcePermissionDimensionManager() + result, _ = manager.allow_apply_permission(fake_gateway.id, "test") + assert not result diff --git a/src/dashboard/apigateway/apigateway/urls.py b/src/dashboard/apigateway/apigateway/urls.py index ec5225e1f..3d2fb2d04 100644 --- a/src/dashboard/apigateway/apigateway/urls.py +++ b/src/dashboard/apigateway/apigateway/urls.py @@ -59,15 +59,12 @@ path("backend/apis//backend-services/", include("apigateway.apps.backend_service.urls")), path("backend/apis//ssl/", include("apigateway.apps.ssl_certificate.urls")), # apps: normal - path("backend/apis//metrics/", include("apigateway.apps.metrics.urls")), path("backend/apis//audits/", include("apigateway.apps.audit.urls")), - path("backend/apis//permissions/", include("apigateway.apps.permission.urls")), path("backend/apis//support/", include("apigateway.apps.support.urls")), path("backend/apis//access_strategies/", include("apigateway.apps.access_strategy.urls")), path("backend/apis//plugins/", include("apigateway.apps.plugin.urls")), path("backend/apis//micro-gateways/", include("apigateway.apps.micro_gateway.urls")), path("backend/esb/", include("apigateway.apps.esb.urls")), - # FIXME: change this to a new url in future # switch language path("backend/i18n/setlang/", set_language, name="set_language"), @@ -77,6 +74,8 @@ path("backend/docs/feature/", include("apigateway.apps.docs.feature.urls")), path("backend/docs/feedback/", include("apigateway.apps.docs.feedback.urls")), # refactoring begin ------ + # switch language + path("backend/i18n/setlang/", set_language, name="set_language"), path("backend/apis//logs/", include("apigateway.apis.web.access_log.urls")), path("backend/gateways//logs/", include("apigateway.apis.web.access_log.urls")), path("backend/apis//tests/", include("apigateway.apis.web.api_test.urls")), @@ -84,21 +83,19 @@ # delete it later after frontend changed the url path("backend/apis//labels/", include("apigateway.apis.web.label.urls")), path("backend/gateways//labels/", include("apigateway.apis.web.label.urls")), + path("backend/gateways//permissions/", include("apigateway.apis.web.permission.urls")), path("backend/users/", include("apigateway.apis.web.user.urls")), path("backend/feature/", include("apigateway.apis.web.feature.urls")), - # monitors - path("backend/apis//monitors/", include("apigateway.apis.web.monitor.urls")), + path("backend/gateways//logs/", include("apigateway.apis.web.access_log.urls")), + path("backend/gateways//tests/", include("apigateway.apis.web.api_test.urls")), + path("backend/gateways//labels/", include("apigateway.apis.web.label.urls")), + path("backend/gateways//metrics/", include("apigateway.apis.web.metrics.urls")), path("backend/gateways//monitors/", include("apigateway.apis.web.monitor.urls")), - # todo 不应该放在顶层, 后续要想办法挪到下层 - path( - "backend/apis/monitors/alarm/records/summary/", - AlarmRecordSummaryListApi.as_view(), - name="monitors.alamr_records.summary", - ), + # todo 不应该放在顶层,后续要想办法挪到下层 path( "backend/gateways/monitors/alarm/records/summary/", AlarmRecordSummaryListApi.as_view(), - name="monitors.alamr_records.summary", + name="monitors.alarm_records.summary", ), # refactoring end ------ ] diff --git a/src/dashboard/apigateway/requirements.txt b/src/dashboard/apigateway/requirements.txt index 5e0b54f58..bf1f1e258 100644 --- a/src/dashboard/apigateway/requirements.txt +++ b/src/dashboard/apigateway/requirements.txt @@ -4,7 +4,7 @@ amqp==2.6.1 ; python_full_version >= "3.6.6" and python_version < "3.8" apigw-manager==1.1.7 ; python_full_version >= "3.6.6" and python_version < "3.8" arrow==1.2.3 ; python_full_version >= "3.6.6" and python_version < "3.8" asgiref==3.4.1 ; python_full_version >= "3.6.6" and python_version < "3.8" -attrs==22.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" +attrs==22.2.0 ; python_version < "3.8" and python_full_version >= "3.6.6" backoff==1.10.0 ; python_full_version >= "3.6.6" and python_version < "3.8" beautifulsoup4==4.10.0 ; python_full_version >= "3.6.6" and python_version < "3.8" billiard==3.6.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -35,6 +35,7 @@ deprecated==1.2.13 ; python_full_version >= "3.6.6" and python_version < "3.8" django-celery-beat==2.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" django-cors-headers==3.10.1 ; python_full_version >= "3.6.6" and python_version < "3.8" django-environ==0.8.1 ; python_full_version >= "3.6.6" and python_version < "3.8" +django-filter==2.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" django-prometheus==2.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" django-timezone-field==4.2.3 ; python_full_version >= "3.6.6" and python_version < "3.8" django==3.2.18 ; python_full_version >= "3.6.6" and python_version < "3.8" diff --git a/src/dashboard/apigateway/requirements_dev.txt b/src/dashboard/apigateway/requirements_dev.txt index 9f60c5900..4aa99aa34 100644 --- a/src/dashboard/apigateway/requirements_dev.txt +++ b/src/dashboard/apigateway/requirements_dev.txt @@ -5,8 +5,8 @@ apigw-manager==1.1.7 ; python_full_version >= "3.6.6" and python_version < "3.8" appnope==0.1.2 ; python_full_version >= "3.6.6" and python_version < "3.8" and sys_platform == "darwin" arrow==1.2.3 ; python_full_version >= "3.6.6" and python_version < "3.8" asgiref==3.4.1 ; python_full_version >= "3.6.6" and python_version < "3.8" -atomicwrites==1.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" and sys_platform == "win32" -attrs==22.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" +atomicwrites==1.4.0 ; python_version < "3.8" and sys_platform == "win32" and python_full_version >= "3.6.6" +attrs==22.2.0 ; python_version < "3.8" and python_full_version >= "3.6.6" backcall==0.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" backoff==1.10.0 ; python_full_version >= "3.6.6" and python_version < "3.8" beautifulsoup4==4.10.0 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -30,7 +30,7 @@ cffi==1.15.0 ; python_full_version >= "3.6.6" and python_version < "3.8" cfgv==3.3.1 ; python_full_version >= "3.6.6" and python_version < "3.8" charset-normalizer==2.0.12 ; python_full_version >= "3.6.6" and python_version < "3.8" click==7.1.2 ; python_full_version >= "3.6.6" and python_version < "3.8" -colorama==0.4.4 ; python_full_version >= "3.6.6" and python_version < "3.8" and (sys_platform == "win32" or platform_system == "Windows") +colorama==0.4.4 ; python_version < "3.8" and sys_platform == "win32" and python_full_version >= "3.6.6" or python_full_version >= "3.6.6" and python_version < "3.8" and platform_system == "Windows" commonmark==0.9.1 ; python_version >= "3.7" and python_version < "3.8" contextvars==2.4 ; python_full_version >= "3.6.6" and python_version < "3.7" coreapi==2.3.3 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -47,6 +47,7 @@ django-celery-beat==2.2.0 ; python_full_version >= "3.6.6" and python_version < django-cors-headers==3.10.1 ; python_full_version >= "3.6.6" and python_version < "3.8" django-dynamic-fixture==3.1.2 ; python_full_version >= "3.6.6" and python_version < "3.8" django-environ==0.8.1 ; python_full_version >= "3.6.6" and python_version < "3.8" +django-filter==2.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" django-nose==1.4.7 ; python_full_version >= "3.6.6" and python_version < "3.8" django-prometheus==2.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" django-timezone-field==4.2.3 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -71,10 +72,10 @@ gunicorn==20.1.0 ; python_full_version >= "3.6.6" and python_version < "3.8" identify==2.4.4 ; python_full_version >= "3.6.6" and python_version < "3.8" idna==2.8 ; python_full_version >= "3.6.6" and python_version < "3.8" immutables==0.19 ; python_full_version >= "3.6.6" and python_version < "3.7" -importlib-metadata==4.8.3 ; python_full_version >= "3.6.6" and python_version < "3.8" +importlib-metadata==4.8.3 ; python_version < "3.8" and python_full_version >= "3.6.6" importlib-resources==5.2.3 ; python_full_version >= "3.6.6" and python_version < "3.7" inflection==0.5.1 ; python_full_version >= "3.6.6" and python_version < "3.8" -iniconfig==1.1.1 ; python_full_version >= "3.6.6" and python_version < "3.8" +iniconfig==1.1.1 ; python_version < "3.8" and python_full_version >= "3.6.6" ipython-genutils==0.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" ipython==7.16.3 ; python_full_version >= "3.6.6" and python_version < "3.8" itypes==1.2.0 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -108,26 +109,26 @@ opentelemetry-proto==1.7.1 ; python_full_version >= "3.6.6" and python_version < opentelemetry-sdk==1.7.1 ; python_full_version >= "3.6.6" and python_version < "3.8" opentelemetry-semantic-conventions==0.26b1 ; python_full_version >= "3.6.6" and python_version < "3.8" opentelemetry-util-http==0.26b1 ; python_full_version >= "3.6.6" and python_version < "3.8" -packaging==21.3 ; python_full_version >= "3.6.6" and python_version < "3.8" +packaging==21.3 ; python_version < "3.8" and python_full_version >= "3.6.6" parso==0.7.1 ; python_full_version >= "3.6.6" and python_version < "3.8" pathspec==0.9.0 ; python_full_version >= "3.6.6" and python_version < "3.8" pexpect==4.8.0 ; python_full_version >= "3.6.6" and python_version < "3.8" and sys_platform != "win32" pickleshare==0.7.5 ; python_full_version >= "3.6.6" and python_version < "3.8" pillow==8.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" platformdirs==2.4.0 ; python_full_version >= "3.6.6" and python_version < "3.8" -pluggy==0.13.1 ; python_full_version >= "3.6.6" and python_version < "3.8" +pluggy==0.13.1 ; python_version < "3.8" and python_full_version >= "3.6.6" pre-commit==2.17.0 ; python_full_version >= "3.6.6" and python_version < "3.8" prometheus-client==0.12.0 ; python_full_version >= "3.6.6" and python_version < "3.8" prompt-toolkit==3.0.36 ; python_full_version >= "3.6.6" and python_version < "3.8" protobuf==3.19.4 ; python_full_version >= "3.6.6" and python_version < "3.8" ptyprocess==0.7.0 ; python_full_version >= "3.6.6" and python_version < "3.8" and sys_platform != "win32" py-cpuinfo==8.0.0 ; python_full_version >= "3.6.6" and python_version < "3.8" -py==1.11.0 ; python_full_version >= "3.6.6" and python_version < "3.8" +py==1.11.0 ; python_version < "3.8" and python_full_version >= "3.6.6" pycparser==2.21 ; python_full_version >= "3.6.6" and python_version < "3.8" pydantic==1.9.2 ; python_full_version >= "3.6.6" and python_version < "3.8" -pygments==2.14.0 ; python_full_version >= "3.6.6" and python_version < "3.8" +pygments==2.14.0 ; python_version < "3.8" and python_full_version >= "3.6.6" pyjwt==1.7.1 ; python_full_version >= "3.6.6" and python_version < "3.8" -pyparsing==3.0.7 ; python_full_version >= "3.6.6" and python_version < "3.8" +pyparsing==3.0.7 ; python_version < "3.8" and python_full_version >= "3.6.6" pypi-simple==0.8.0 ; python_full_version >= "3.6.6" and python_version < "3.8" pyrsistent==0.18.0 ; python_full_version >= "3.6.6" and python_version < "3.8" pytest-benchmark==3.4.1 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -136,7 +137,7 @@ pytest-django==4.5.2 ; python_full_version >= "3.6.6" and python_version < "3.8" pytest-mock==3.6.1 ; python_full_version >= "3.6.6" and python_version < "3.8" pytest-pretty==1.1.0 ; python_version >= "3.7" and python_version < "3.8" pytest-xdist==3.0.2 ; python_full_version >= "3.6.6" and python_version < "3.8" -pytest==7.0.1 ; python_full_version >= "3.6.6" and python_version < "3.8" +pytest==7.0.1 ; python_version < "3.8" and python_full_version >= "3.6.6" python-crontab==2.6.0 ; python_full_version >= "3.6.6" and python_version < "3.8" python-dateutil==2.8.1 ; python_full_version >= "3.6.6" and python_version < "3.8" python-editor==1.0.4 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -162,7 +163,7 @@ tenacity==8.0.1 ; python_full_version >= "3.6.6" and python_version < "3.8" tencent-apigateway-common==0.1.11 ; python_full_version >= "3.6.6" and python_version < "3.8" thrift==0.16.0 ; python_full_version >= "3.6.6" and python_version < "3.8" toml==0.10.2 ; python_full_version >= "3.6.6" and python_version < "3.8" -tomli==1.2.3 ; python_full_version >= "3.6.6" and python_version < "3.8" +tomli==1.2.3 ; python_version < "3.8" and python_full_version >= "3.6.6" tox==3.25.1 ; python_full_version >= "3.6.6" and python_version < "3.8" traitlets==4.3.3 ; python_full_version >= "3.6.6" and python_version < "3.8" typed-ast==1.5.1 ; python_full_version >= "3.6.6" and python_version < "3.8" @@ -185,6 +186,6 @@ wcwidth==0.2.5 ; python_full_version >= "3.6.6" and python_version < "3.8" werkzeug==2.0.3 ; python_full_version >= "3.6.6" and python_version < "3.8" whitenoise==5.3.0 ; python_full_version >= "3.6.6" and python_version < "3.8" wrapt==1.15.0 ; python_full_version >= "3.6.6" and python_version < "3.8" -zipp==3.6.0 ; python_full_version >= "3.6.6" and python_version < "3.8" +zipp==3.6.0 ; python_version < "3.8" and python_full_version >= "3.6.6" zope-event==4.6 ; python_full_version >= "3.6.6" and python_version < "3.8" zope-interface==5.5.2 ; python_full_version >= "3.6.6" and python_version < "3.8" diff --git a/src/dashboard/poetry.lock b/src/dashboard/poetry.lock index 58fa686c1..4d574da18 100644 --- a/src/dashboard/poetry.lock +++ b/src/dashboard/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "aiocontextvars" version = "0.2.2" description = "Asyncio support for PEP-567 contextvars backport." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -23,6 +24,7 @@ reference = "tencent" name = "amqp" version = "2.6.1" description = "Low-level AMQP client for Python (fork of amqplib)." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -42,6 +44,7 @@ reference = "tencent" name = "apigw-manager" version = "1.1.7" description = "" +category = "main" optional = false python-versions = ">=3.6.1,<4.0.0" files = [ @@ -72,6 +75,7 @@ reference = "tencent" name = "appnope" version = "0.1.2" description = "Disable App Nap on macOS >= 10.9" +category = "dev" optional = false python-versions = "*" files = [ @@ -88,6 +92,7 @@ reference = "tencent" name = "arrow" version = "1.2.3" description = "Better dates & times for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -108,6 +113,7 @@ reference = "tencent" name = "asgiref" version = "3.4.1" description = "ASGI specs, helper code, and adapters" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -130,6 +136,7 @@ reference = "tencent" name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -146,6 +153,7 @@ reference = "tencent" name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -169,6 +177,7 @@ reference = "tencent" name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" +category = "dev" optional = false python-versions = "*" files = [ @@ -185,6 +194,7 @@ reference = "tencent" name = "backoff" version = "1.10.0" description = "Function decoration for backoff and retry" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -201,6 +211,7 @@ reference = "tencent" name = "beautifulsoup4" version = "4.10.0" description = "Screen-scraping library" +category = "main" optional = false python-versions = ">3.0.0" files = [ @@ -224,6 +235,7 @@ reference = "tencent" name = "billiard" version = "3.6.4.0" description = "Python multiprocessing fork with improvements and bugfixes" +category = "main" optional = false python-versions = "*" files = [ @@ -240,6 +252,7 @@ reference = "tencent" name = "bk-iam" version = "1.3.4" description = "bk-iam python sdk" +category = "main" optional = false python-versions = "*" files = [ @@ -261,6 +274,7 @@ reference = "tencent" name = "bkapi-bcs-api-gateway" version = "1.12.1" description = "bk-bcs容器化部署方案api-gateway" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,<4.0,>=2.7" files = [ @@ -282,6 +296,7 @@ reference = "tencent" name = "bkapi-bk-apigateway" version = "1.0.8" description = "蓝鲸API网关" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,<4.0,>=2.7" files = [ @@ -303,6 +318,7 @@ reference = "tencent" name = "bkapi-client-core" version = "1.1.8" description = "" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -327,6 +343,7 @@ reference = "tencent" name = "bkapi-client-generator" version = "0.1.28" description = "" +category = "main" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -342,6 +359,7 @@ reference = "tencent" name = "bkapi-component-open" version = "1.1.0" description = "" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -360,10 +378,11 @@ reference = "tencent" name = "bkapi-paasv3" version = "1.0.1" description = "蓝鲸 PaaS 平台 - 开发者中心" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,<4.0,>=2.7" files = [ - {file = "bkapi-paasv3-1.0.1.tar.gz", hash = "sha256:cb2467f94eec83ac419e18ea42153728480815f56713f0e2b1078b4f1f28d891"}, + {file = "bkapi-paasv3-1.0.1.tar.gz", hash = "sha256:e0f142320654d7d1c49fe16d7347f8fb0786d00d1d48d42d9ff00567f3b0b332"}, ] [package.dependencies] @@ -381,6 +400,7 @@ reference = "tencent" name = "bkpaas-auth" version = "2.0.6" description = "User authentication django app for blueking internal projects" +category = "main" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -402,6 +422,7 @@ reference = "tencent" name = "black" version = "21.12b0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -435,6 +456,7 @@ reference = "tencent" name = "blue-krill" version = "1.2.3" description = "Tools and common packages for blueking paas" +category = "main" optional = false python-versions = ">=3.6.2,<3.11" files = [ @@ -465,6 +487,7 @@ reference = "tencent" name = "cachetools" version = "4.2.4" description = "Extensible memoizing collections and decorators" +category = "main" optional = false python-versions = "~=3.5" files = [ @@ -481,6 +504,7 @@ reference = "tencent" name = "cattrs" version = "1.0.0" description = "Composable complex class support for attrs." +category = "main" optional = false python-versions = "*" files = [ @@ -503,6 +527,7 @@ reference = "tencent" name = "cattrs" version = "22.2.0" description = "Composable complex class support for attrs and dataclasses." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -524,6 +549,7 @@ reference = "tencent" name = "celery" version = "4.4.7" description = "Distributed Task Queue." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -580,6 +606,7 @@ reference = "tencent" name = "certifi" version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = "*" files = [ @@ -596,6 +623,7 @@ reference = "tencent" name = "cffi" version = "1.15.0" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -663,6 +691,7 @@ reference = "tencent" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -679,6 +708,7 @@ reference = "tencent" name = "charset-normalizer" version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.5.0" files = [ @@ -698,6 +728,7 @@ reference = "tencent" name = "click" version = "7.1.2" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -714,6 +745,7 @@ reference = "tencent" name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -730,6 +762,7 @@ reference = "tencent" name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" +category = "dev" optional = false python-versions = "*" files = [ @@ -749,6 +782,7 @@ reference = "tencent" name = "contextvars" version = "2.4" description = "PEP 567 Backport" +category = "main" optional = false python-versions = "*" files = [ @@ -767,6 +801,7 @@ reference = "tencent" name = "coreapi" version = "2.3.3" description = "Python client library for Core API." +category = "main" optional = false python-versions = "*" files = [ @@ -789,6 +824,7 @@ reference = "tencent" name = "coreschema" version = "0.0.4" description = "Core Schema." +category = "main" optional = false python-versions = "*" files = [ @@ -808,6 +844,7 @@ reference = "tencent" name = "coverage" version = "6.2" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -875,6 +912,7 @@ reference = "tencent" name = "cryptography" version = "3.4.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -919,6 +957,7 @@ reference = "tencent" name = "curlify" version = "2.2.1" description = "Library to convert python requests object to curl command." +category = "main" optional = false python-versions = "*" files = [ @@ -937,6 +976,7 @@ reference = "tencent" name = "dataclasses" version = "0.7" description = "A backport of the dataclasses module for Python 3.6" +category = "main" optional = false python-versions = ">=3.6, <3.7" files = [ @@ -953,6 +993,7 @@ reference = "tencent" name = "decorator" version = "5.1.1" description = "Decorators for Humans" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -969,6 +1010,7 @@ reference = "tencent" name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -991,6 +1033,7 @@ reference = "tencent" name = "distlib" version = "0.3.4" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -1007,6 +1050,7 @@ reference = "tencent" name = "django" version = "3.2.18" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1032,6 +1076,7 @@ reference = "tencent" name = "django-celery-beat" version = "2.2.0" description = "Database-backed Periodic Tasks." +category = "main" optional = false python-versions = "*" files = [ @@ -1054,6 +1099,7 @@ reference = "tencent" name = "django-cors-headers" version = "3.10.1" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1073,6 +1119,7 @@ reference = "tencent" name = "django-dynamic-fixture" version = "3.1.2" description = "A full library to create dynamic model instances for testing purposes." +category = "dev" optional = false python-versions = "*" files = [ @@ -1091,6 +1138,7 @@ reference = "tencent" name = "django-environ" version = "0.8.1" description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." +category = "main" optional = false python-versions = ">=3.4,<4" files = [ @@ -1099,8 +1147,8 @@ files = [ ] [package.extras] -develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] -docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] [package.source] @@ -1108,10 +1156,31 @@ type = "legacy" url = "https://mirrors.cloud.tencent.com/pypi/simple" reference = "tencent" +[[package]] +name = "django-filter" +version = "2.4.0" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"}, + {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"}, +] + +[package.dependencies] +Django = ">=2.2" + +[package.source] +type = "legacy" +url = "https://mirrors.cloud.tencent.com/pypi/simple" +reference = "tencent" + [[package]] name = "django-nose" version = "1.4.7" description = "Makes your Django tests simple and snappy" +category = "dev" optional = false python-versions = "*" files = [ @@ -1131,6 +1200,7 @@ reference = "tencent" name = "django-prometheus" version = "2.2.0" description = "Django middlewares to monitor your application with Prometheus.io." +category = "main" optional = false python-versions = "*" files = [ @@ -1150,6 +1220,7 @@ reference = "tencent" name = "django-timezone-field" version = "4.2.3" description = "A Django app providing database and form fields for pytz timezone objects." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1173,6 +1244,7 @@ reference = "tencent" name = "djangorestframework" version = "3.14.0" description = "Web APIs for Django, made easy." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1193,6 +1265,7 @@ reference = "tencent" name = "docutils" version = "0.18.1" description = "Docutils -- Python Documentation Utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1209,6 +1282,7 @@ reference = "tencent" name = "drf-yasg" version = "1.21.5" description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1239,6 +1313,7 @@ reference = "tencent" name = "elasticsearch" version = "7.7.1" description = "Python client for Elasticsearch" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1264,6 +1339,7 @@ reference = "tencent" name = "elasticsearch-dsl" version = "7.4.1" description = "Python client for Elasticsearch" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1288,6 +1364,7 @@ reference = "tencent" name = "etcd3" version = "0.12.0" description = "Python client for the etcd3 API" +category = "main" optional = false python-versions = "*" files = [ @@ -1309,6 +1386,7 @@ reference = "tencent" name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1328,6 +1406,7 @@ reference = "tencent" name = "execnet" version = "1.9.0" description = "execnet: rapid multi-Python deployment" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1347,6 +1426,7 @@ reference = "tencent" name = "faker" version = "14.2.1" description = "Faker is a Python package that generates fake data for you." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1367,6 +1447,7 @@ reference = "tencent" name = "fakeredis" version = "1.7.4" description = "Fake implementation of redis API for testing purposes." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1393,6 +1474,7 @@ reference = "tencent" name = "filelock" version = "3.4.1" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1413,6 +1495,7 @@ reference = "tencent" name = "future" version = "0.18.2" description = "Clean single-source support for Python 3 and 2" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1428,6 +1511,7 @@ reference = "tencent" name = "gevent" version = "22.10.2" description = "Coroutine-based network library" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" files = [ @@ -1508,6 +1592,7 @@ reference = "tencent" name = "googleapis-common-protos" version = "1.56.3" description = "Common protobufs used in Google APIs" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1530,6 +1615,7 @@ reference = "tencent" name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -1608,6 +1694,7 @@ reference = "tencent" name = "grpcio" version = "1.44.0" description = "HTTP/2-based RPC framework" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1672,6 +1759,7 @@ reference = "tencent" name = "gunicorn" version = "20.1.0" description = "WSGI HTTP Server for UNIX" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1697,6 +1785,7 @@ reference = "tencent" name = "identify" version = "2.4.4" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -1716,6 +1805,7 @@ reference = "tencent" name = "idna" version = "2.8" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1732,6 +1822,7 @@ reference = "tencent" name = "immutables" version = "0.19" description = "Immutable Collections" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1793,6 +1884,7 @@ reference = "tencent" name = "importlib-metadata" version = "4.8.3" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1818,6 +1910,7 @@ reference = "tencent" name = "importlib-resources" version = "5.2.3" description = "Read resources from Python packages" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1841,6 +1934,7 @@ reference = "tencent" name = "inflection" version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1857,6 +1951,7 @@ reference = "tencent" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = "*" files = [ @@ -1873,6 +1968,7 @@ reference = "tencent" name = "ipython" version = "7.16.3" description = "IPython: Productive Interactive Computing" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1913,6 +2009,7 @@ reference = "tencent" name = "ipython-genutils" version = "0.2.0" description = "Vestigial utilities from IPython" +category = "dev" optional = false python-versions = "*" files = [ @@ -1929,6 +2026,7 @@ reference = "tencent" name = "itypes" version = "1.2.0" description = "Simple immutable types for python." +category = "main" optional = false python-versions = "*" files = [ @@ -1945,6 +2043,7 @@ reference = "tencent" name = "jedi" version = "0.17.2" description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1968,6 +2067,7 @@ reference = "tencent" name = "jinja2" version = "3.0.3" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1990,6 +2090,7 @@ reference = "tencent" name = "jsonfield" version = "3.1.0" description = "A reusable Django field that allows you to store validated JSON in your model." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2009,6 +2110,7 @@ reference = "tencent" name = "jsonschema" version = "3.2.0" description = "An implementation of JSON Schema validation for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -2036,6 +2138,7 @@ reference = "tencent" name = "kombu" version = "4.6.11" description = "Messaging library for Python." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2072,6 +2175,7 @@ reference = "tencent" name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2155,6 +2259,7 @@ reference = "tencent" name = "mypy" version = "0.971" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2203,6 +2308,7 @@ reference = "tencent" name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" optional = false python-versions = "*" files = [ @@ -2219,6 +2325,7 @@ reference = "tencent" name = "mysqlclient" version = "2.1.1" description = "Python interface to MySQL" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2240,6 +2347,7 @@ reference = "tencent" name = "nodeenv" version = "1.6.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = "*" files = [ @@ -2256,6 +2364,7 @@ reference = "tencent" name = "nose" version = "1.3.7" description = "nose extends unittest to make testing easier" +category = "dev" optional = false python-versions = "*" files = [ @@ -2273,6 +2382,7 @@ reference = "tencent" name = "opentelemetry-api" version = "1.7.1" description = "OpenTelemetry Python API" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2293,6 +2403,7 @@ reference = "tencent" name = "opentelemetry-exporter-jaeger" version = "1.7.1" description = "Jaeger Exporters for OpenTelemetry" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2313,6 +2424,7 @@ reference = "tencent" name = "opentelemetry-exporter-jaeger-proto-grpc" version = "1.7.1" description = "Jaeger Protobuf Exporter for OpenTelemetry" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2335,6 +2447,7 @@ reference = "tencent" name = "opentelemetry-exporter-jaeger-thrift" version = "1.7.1" description = "Jaeger Thrift Exporter for OpenTelemetry" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2356,6 +2469,7 @@ reference = "tencent" name = "opentelemetry-exporter-otlp" version = "1.7.1" description = "OpenTelemetry Collector Exporters" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2376,6 +2490,7 @@ reference = "tencent" name = "opentelemetry-exporter-otlp-proto-grpc" version = "1.7.1" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2403,6 +2518,7 @@ reference = "tencent" name = "opentelemetry-exporter-otlp-proto-http" version = "1.7.1" description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2427,6 +2543,7 @@ reference = "tencent" name = "opentelemetry-instrumentation" version = "0.26b1" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2448,6 +2565,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-celery" version = "0.26b1" description = "OpenTelemetry Celery Instrumentation" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2473,6 +2591,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-dbapi" version = "0.26b1" description = "OpenTelemetry Database API instrumentation" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2498,6 +2617,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-django" version = "0.26b1" description = "OpenTelemetry Instrumentation for Django" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2526,6 +2646,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-logging" version = "0.26b1" description = "OpenTelemetry Logging instrumentation" +category = "main" optional = false python-versions = "*" files = [ @@ -2549,6 +2670,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-redis" version = "0.26b1" description = "OpenTelemetry Redis instrumentation" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2575,6 +2697,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-requests" version = "0.26b1" description = "OpenTelemetry requests instrumentation" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2601,6 +2724,7 @@ reference = "tencent" name = "opentelemetry-instrumentation-wsgi" version = "0.26b1" description = "WSGI Middleware for OpenTelemetry" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2626,6 +2750,7 @@ reference = "tencent" name = "opentelemetry-proto" version = "1.7.1" description = "OpenTelemetry Python Proto" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2645,6 +2770,7 @@ reference = "tencent" name = "opentelemetry-sdk" version = "1.7.1" description = "OpenTelemetry Python SDK" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2665,6 +2791,7 @@ reference = "tencent" name = "opentelemetry-semantic-conventions" version = "0.26b1" description = "OpenTelemetry Semantic Conventions" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2681,6 +2808,7 @@ reference = "tencent" name = "opentelemetry-util-http" version = "0.26b1" description = "Web util for OpenTelemetry" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2697,6 +2825,7 @@ reference = "tencent" name = "packaging" version = "21.3" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2716,6 +2845,7 @@ reference = "tencent" name = "parso" version = "0.7.1" description = "A Python Parser" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2735,6 +2865,7 @@ reference = "tencent" name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -2751,6 +2882,7 @@ reference = "tencent" name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." +category = "dev" optional = false python-versions = "*" files = [ @@ -2770,6 +2902,7 @@ reference = "tencent" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" optional = false python-versions = "*" files = [ @@ -2786,6 +2919,7 @@ reference = "tencent" name = "pillow" version = "8.4.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2841,6 +2975,7 @@ reference = "tencent" name = "platformdirs" version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2861,6 +2996,7 @@ reference = "tencent" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2883,6 +3019,7 @@ reference = "tencent" name = "pre-commit" version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -2909,6 +3046,7 @@ reference = "tencent" name = "prometheus-client" version = "0.12.0" description = "Python client for the Prometheus monitoring system." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2928,6 +3066,7 @@ reference = "tencent" name = "prompt-toolkit" version = "3.0.36" description = "Library for building powerful interactive command lines in Python" +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -2947,6 +3086,7 @@ reference = "tencent" name = "protobuf" version = "3.19.4" description = "Protocol Buffers" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2987,6 +3127,7 @@ reference = "tencent" name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -3003,6 +3144,7 @@ reference = "tencent" name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3019,6 +3161,7 @@ reference = "tencent" name = "py-cpuinfo" version = "8.0.0" description = "Get CPU info with pure Python 2 & 3" +category = "dev" optional = false python-versions = "*" files = [ @@ -3034,6 +3177,7 @@ reference = "tencent" name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3050,6 +3194,7 @@ reference = "tencent" name = "pydantic" version = "1.9.2" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -3107,6 +3252,7 @@ reference = "tencent" name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3126,6 +3272,7 @@ reference = "tencent" name = "pyjwt" version = "1.7.1" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = "*" files = [ @@ -3147,6 +3294,7 @@ reference = "tencent" name = "pyparsing" version = "3.0.7" description = "Python parsing module" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3166,6 +3314,7 @@ reference = "tencent" name = "pypi-simple" version = "0.8.0" description = "PyPI Simple Repository API client library" +category = "main" optional = false python-versions = "~=3.6" files = [ @@ -3187,6 +3336,7 @@ reference = "tencent" name = "pyrsistent" version = "0.18.0" description = "Persistent/Functional/Immutable data structures" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3222,6 +3372,7 @@ reference = "tencent" name = "pytest" version = "7.0.1" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3252,6 +3403,7 @@ reference = "tencent" name = "pytest-benchmark" version = "3.4.1" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3277,6 +3429,7 @@ reference = "tencent" name = "pytest-cov" version = "4.0.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3300,6 +3453,7 @@ reference = "tencent" name = "pytest-django" version = "4.5.2" description = "A Django plugin for pytest." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3323,6 +3477,7 @@ reference = "tencent" name = "pytest-mock" version = "3.6.1" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3345,6 +3500,7 @@ reference = "tencent" name = "pytest-pretty" version = "1.1.0" description = "pytest plugin for printing summary data as I want it" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3365,6 +3521,7 @@ reference = "tencent" name = "pytest-xdist" version = "3.0.2" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3390,6 +3547,7 @@ reference = "tencent" name = "python-crontab" version = "2.6.0" description = "Python Crontab API" +category = "main" optional = false python-versions = "*" files = [ @@ -3413,6 +3571,7 @@ reference = "tencent" name = "python-dateutil" version = "2.8.1" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -3432,6 +3591,7 @@ reference = "tencent" name = "python-editor" version = "1.0.4" description = "Programmatically open an editor, capture the result." +category = "main" optional = false python-versions = "*" files = [ @@ -3449,6 +3609,7 @@ reference = "tencent" name = "python-json-logger" version = "2.0.7" description = "A python library adding a json log formatter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3465,6 +3626,7 @@ reference = "tencent" name = "python-redis-lock" version = "3.7.0" description = "Lock context manager implemented via redis SETNX/BLPOP." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3487,6 +3649,7 @@ reference = "tencent" name = "pytz" version = "2021.3" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -3503,6 +3666,7 @@ reference = "tencent" name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -3546,6 +3710,7 @@ reference = "tencent" name = "raven" version = "6.10.0" description = "Raven is a client for Sentry (https://getsentry.com)" +category = "main" optional = false python-versions = "*" files = [ @@ -3566,6 +3731,7 @@ reference = "tencent" name = "redis" version = "4.1.4" description = "Python client for Redis database and key-value store" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3591,6 +3757,7 @@ reference = "tencent" name = "requests" version = "2.27.1" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -3617,6 +3784,7 @@ reference = "tencent" name = "responses" version = "0.17.0" description = "A utility library for mocking out the `requests` Python library." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3641,6 +3809,7 @@ reference = "tencent" name = "rich" version = "12.0.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ @@ -3665,6 +3834,7 @@ reference = "tencent" name = "rope" version = "1.1.1" description = "a python refactoring library..." +category = "dev" optional = false python-versions = ">=3" files = [ @@ -3684,6 +3854,7 @@ reference = "tencent" name = "ruamel.yaml" version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" optional = false python-versions = ">=3" files = [ @@ -3707,6 +3878,7 @@ reference = "tencent" name = "ruamel.yaml.clib" version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3717,7 +3889,8 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, @@ -3757,6 +3930,7 @@ reference = "tencent" name = "ruff" version = "0.0.277" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3788,6 +3962,7 @@ reference = "tencent" name = "setuptools" version = "59.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3808,6 +3983,7 @@ reference = "tencent" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3824,6 +4000,7 @@ reference = "tencent" name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" optional = false python-versions = "*" files = [ @@ -3840,6 +4017,7 @@ reference = "tencent" name = "soupsieve" version = "2.3.1" description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3856,6 +4034,7 @@ reference = "tencent" name = "sqlparse" version = "0.4.3" description = "A non-validating SQL parser." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3872,6 +4051,7 @@ reference = "tencent" name = "tenacity" version = "8.0.1" description = "Retry code until it succeeds" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3891,6 +4071,7 @@ reference = "tencent" name = "tencent-apigateway-common" version = "0.1.11" description = "" +category = "main" optional = false python-versions = ">=3.6.2,<3.8" files = [ @@ -3914,6 +4095,7 @@ reference = "tencent" name = "thrift" version = "0.16.0" description = "Python bindings for the Apache Thrift RPC system" +category = "main" optional = false python-versions = "*" files = [ @@ -3937,6 +4119,7 @@ reference = "tencent" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3953,6 +4136,7 @@ reference = "tencent" name = "tomli" version = "1.2.3" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3969,6 +4153,7 @@ reference = "tencent" name = "tox" version = "3.25.1" description = "tox is a generic virtualenv management and test command line tool" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -4000,6 +4185,7 @@ reference = "tencent" name = "traitlets" version = "4.3.3" description = "Traitlets Python config system" +category = "dev" optional = false python-versions = "*" files = [ @@ -4024,6 +4210,7 @@ reference = "tencent" name = "typed-ast" version = "1.5.1" description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -4057,6 +4244,7 @@ reference = "tencent" name = "types-cachetools" version = "5.3.0.4" description = "Typing stubs for cachetools" +category = "dev" optional = false python-versions = "*" files = [ @@ -4073,6 +4261,7 @@ reference = "tencent" name = "types-dataclasses" version = "0.6.6" description = "Typing stubs for dataclasses" +category = "dev" optional = false python-versions = "*" files = [ @@ -4089,6 +4278,7 @@ reference = "tencent" name = "types-docutils" version = "0.19.1.6" description = "Typing stubs for docutils" +category = "dev" optional = false python-versions = "*" files = [ @@ -4105,6 +4295,7 @@ reference = "tencent" name = "types-python-dateutil" version = "2.8.19.10" description = "Typing stubs for python-dateutil" +category = "dev" optional = false python-versions = "*" files = [ @@ -4121,6 +4312,7 @@ reference = "tencent" name = "types-pytz" version = "2022.7.0.0" description = "Typing stubs for pytz" +category = "dev" optional = false python-versions = "*" files = [ @@ -4137,6 +4329,7 @@ reference = "tencent" name = "types-pyyaml" version = "6.0.12.9" description = "Typing stubs for PyYAML" +category = "dev" optional = false python-versions = "*" files = [ @@ -4153,6 +4346,7 @@ reference = "tencent" name = "types-redis" version = "4.3.21.7" description = "Typing stubs for redis" +category = "dev" optional = false python-versions = "*" files = [ @@ -4169,6 +4363,7 @@ reference = "tencent" name = "types-requests" version = "2.28.11.16" description = "Typing stubs for requests" +category = "dev" optional = false python-versions = "*" files = [ @@ -4188,6 +4383,7 @@ reference = "tencent" name = "types-urllib3" version = "1.26.25.4" description = "Typing stubs for urllib3" +category = "dev" optional = false python-versions = "*" files = [ @@ -4204,6 +4400,7 @@ reference = "tencent" name = "typing-extensions" version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4220,6 +4417,7 @@ reference = "tencent" name = "uritemplate" version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4236,6 +4434,7 @@ reference = "tencent" name = "urllib3" version = "1.25.11" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" files = [ @@ -4257,6 +4456,7 @@ reference = "tencent" name = "vine" version = "1.3.0" description = "Promises, promises, promises." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -4273,6 +4473,7 @@ reference = "tencent" name = "virtualenv" version = "20.13.0" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -4301,6 +4502,7 @@ reference = "tencent" name = "watchdog" version = "1.0.2" description = "Filesystem events monitoring" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4335,6 +4537,7 @@ reference = "tencent" name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -4351,6 +4554,7 @@ reference = "tencent" name = "werkzeug" version = "2.0.3" description = "The comprehensive WSGI web application library." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4373,6 +4577,7 @@ reference = "tencent" name = "whitenoise" version = "5.3.0" description = "Radically simplified static file serving for WSGI applications" +category = "main" optional = false python-versions = ">=3.5, <4" files = [ @@ -4392,6 +4597,7 @@ reference = "tencent" name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -4481,6 +4687,7 @@ reference = "tencent" name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4501,6 +4708,7 @@ reference = "tencent" name = "zope-event" version = "4.6" description = "Very basic event publishing system" +category = "main" optional = false python-versions = "*" files = [ @@ -4524,6 +4732,7 @@ reference = "tencent" name = "zope-interface" version = "5.5.2" description = "Interfaces for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -4581,4 +4790,4 @@ reference = "tencent" [metadata] lock-version = "2.0" python-versions = "~3.6.6 || ~3.7" -content-hash = "dda86d0f5e99e5087588d41ef2e870046e67c172bd49a7e08c31109f48c4891d" +content-hash = "2a00e5644f160470402b8f03e57d8b443d48804fa21949acace81dae995d1f68" diff --git a/src/dashboard/pyproject.toml b/src/dashboard/pyproject.toml index 64d2630d7..d369d075e 100644 --- a/src/dashboard/pyproject.toml +++ b/src/dashboard/pyproject.toml @@ -73,6 +73,7 @@ opentelemetry-instrumentation-requests = "0.26b1" opentelemetry-instrumentation-celery = "0.26b1" opentelemetry-instrumentation-logging = "0.26b1" opentelemetry-exporter-jaeger = "1.7.1" +django-filter = "2.4.0" [tool.poetry.group.dev.dependencies] nose = "1.3.7"