Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(recycle_bin): web api and sync task #1

Merged
merged 1 commit into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/bkuser_core/api/web/category/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def get_queryset(self):

queryset = ProfileCategory.objects.filter(enabled=True)
if settings.ENABLE_IAM:
fs = Permission().make_filter_of_category(operator, IAMAction.VIEW_CATEGORY)
fs = Permission().make_category_filter(operator, IAMAction.VIEW_CATEGORY)
queryset = queryset.filter(fs)

return queryset
Expand Down
2 changes: 1 addition & 1 deletion src/api/bkuser_core/api/web/home/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _get_categories(self, operator):
try:
queryset = ProfileCategory.objects.filter(enabled=True)
if settings.ENABLE_IAM:
fs = Permission().make_filter_of_category(operator, IAMAction.VIEW_CATEGORY)
fs = Permission().make_category_filter(operator, IAMAction.VIEW_CATEGORY)
queryset = queryset.filter(fs)
managed_categories = queryset.all()
except IAMPermissionDenied:
Expand Down
23 changes: 0 additions & 23 deletions src/api/bkuser_core/api/web/recycle_bin/constants.py

This file was deleted.

18 changes: 0 additions & 18 deletions src/api/bkuser_core/api/web/recycle_bin/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,3 @@ def get_category_display_name(self, instance):
class Meta:
model = RecycleBin
fields = ["id", "expires", "category_display_name", "profile", "operator"]


class BatchCategoryRevertInputSlZ(serializers.Serializer):
category_ids = serializers.ListField(required=True, help_text="被软删除的目录id列表")


class BatchCategoryHardDeleteInputSlZ(serializers.Serializer):
category_ids = serializers.ListField(required=True, help_text="进行硬删除的对象id列表")


class CategoryRevertCheckResultOutputSlZ(serializers.Serializer):
category_id = serializers.IntegerField(help_text="目录id")
check_result = serializers.BooleanField(help_text="检查结果")
error_message = serializers.CharField(help_text="检查信息")


class CategoryRevertResultOutputSlZ(serializers.Serializer):
successful_count = serializers.IntegerField(help_text="目录还原成功个数")
17 changes: 6 additions & 11 deletions src/api/bkuser_core/api/web/recycle_bin/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,14 @@
name="recycle_bin.category.list",
),
path(
"categories/check/",
views.RecycleBinBatchCategoryRevertCheckApi.as_view(),
name="recycle_bin.category.revert.check",
"categories/<int:id>/",
views.RecycleBinCategoryRevertHardDeleteApi.as_view(),
name="recycle_bin.category.revert_hard_delete",
),
path(
"categories/revert/",
views.RecycleBinBatchCategoryRevertApi.as_view(),
name="recycle_bin.category.revert",
),
path(
"categories/hard_delete/",
views.RecycleBinCategoryBatchHardDeleteApi.as_view(),
name="recycle_bin.category.hard_delete",
"categories/<int:id>/conflicts/",
views.RecycleBinCategoryRevertConflictApi.as_view(),
name="recycle_bin.category.revert.conflict",
),
path(
"departments/",
Expand Down
39 changes: 24 additions & 15 deletions src/api/bkuser_core/api/web/recycle_bin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,37 @@
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.
"""
import logging
from typing import List

from django.utils.translation import ugettext_lazy as _

from bkuser_core.categories.models import ProfileCategory
from bkuser_core.common.error_codes import error_codes
from bkuser_core.recycle_bin.constants import RecycleBinObjectType
from bkuser_core.recycle_bin.models import RecycleBin

logger = logging.getLogger(__name__)

def list_conflict_before_revert_category(category: ProfileCategory) -> List[str]:
"""
在还原目录前,查询display_name和domain冲突
param category: 即将被还原的目录
return: 冲突列表
"""
conflicts: List[str] = []

def check_category_in_recycle_bin(category_id):
if ProfileCategory.objects.filter(enabled=True, display_name=category.display_name).exists():
conflicts.append(_("目录名称重复"))

try:
instance = ProfileCategory.objects.get(id=category_id)
if instance.default:
logger.warning("Category<%s-%s> is default, can not delete", instance.id, instance.display_name)
return
if ProfileCategory.objects.filter(enabled=True, domain=category.domain).exists():
conflicts.append(_("目录登录域重复"))

RecycleBin.objects.get(object_type=RecycleBinObjectType.CATEGORY.value, object_id=category_id)
return conflicts

except ProfileCategory.DoesNotExist:
raise error_codes.CANNOT_FIND_CATEGORY

except RecycleBin.DoesNotExist:
raise error_codes.CANNOT_FIND_CATEGORY_IN_RECYCLE_BIN.f(category_id=category_id)
def check_conflict_before_revert_category(category: ProfileCategory):
"""
在还原目录前,检查是否存在冲突,主要是检查display_name和domain
param category: 即将被还原的目录
return: raise Exception
"""
conflicts = list_conflict_before_revert_category(category)
if conflicts:
raise error_codes.REVERT_CATEGORY_CONFLICT.f(",".join(conflicts), replace=True)
196 changes: 85 additions & 111 deletions src/api/bkuser_core/api/web/recycle_bin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,24 @@
"""
import logging

from django.db import transaction
from django.conf import settings
from django.db.models import Q
from rest_framework import generics
from rest_framework import generics, status
from rest_framework.response import Response

from bkuser_core.api.web.recycle_bin.constants import CategoryCheckMessageEnum
from bkuser_core.api.web.recycle_bin.serializers import (
BatchCategoryHardDeleteInputSlZ,
BatchCategoryRevertInputSlZ,
CategoryRevertCheckResultOutputSlZ,
CategoryRevertResultOutputSlZ,
RecycleBinCategoryOutputSlZ,
RecycleBinDepartmentOutputSlZ,
RecycleBinProfileOutputSlZ,
RecycleBinSearchInputSlZ,
)
from bkuser_core.api.web.recycle_bin.utils import check_category_in_recycle_bin
from bkuser_core.api.web.utils import get_category_display_name_map
from bkuser_core.api.web.recycle_bin.utils import (
check_conflict_before_revert_category,
list_conflict_before_revert_category,
)
from bkuser_core.api.web.utils import get_category_display_name_map, get_operator
from bkuser_core.api.web.viewset import CustomPagination
from bkuser_core.bkiam.permissions import IAMAction, ManageCategoryPermission, Permission
from bkuser_core.categories.constants import CategoryStatus
from bkuser_core.categories.models import ProfileCategory
from bkuser_core.common.error_codes import error_codes
Expand Down Expand Up @@ -64,9 +63,16 @@ def get_queryset(self):
category_ids = ProfileCategory.objects.filter(
Q(domain__icontains=keyword) | Q(display_name__icontains=keyword),
enabled=False,
status=CategoryStatus.INACTIVE.value,
status=CategoryStatus.DELETED.value,
).values_list("id", flat=True)
queryset = queryset.filter(object_id__in=category_ids)

# 只返回有权限的
operator = get_operator(self.request)
if settings.ENABLE_IAM:
fs = Permission().make_filter_of_category(operator, IAMAction.VIEW_CATEGORY, category_id_key="object_id")
queryset = queryset.filter(fs)

return queryset.all()

def get_serializer_context(self):
Expand Down Expand Up @@ -106,6 +112,14 @@ def get_queryset(self):
"id", flat=True
)
queryset = queryset.filter(object_id__in=department_ids)

# 只返回有目录查看权限的对应部门数据
operator = get_operator(self.request)
if settings.ENABLE_IAM:
fs = Permission().make_filter_of_category(operator, IAMAction.VIEW_CATEGORY)
department_ids = Department.objects.filter(fs, enabled=False).values_list("id", flat=True)
queryset = queryset.filter(object_id__in=department_ids)

return queryset.all()

def get_serializer_context(self):
Expand Down Expand Up @@ -159,6 +173,18 @@ def get_queryset(self):
status=ProfileStatus.DELETED.value,
).values_list("id", flat=True)
queryset = queryset.filter(object_id__in=profile_ids)

# 只返回有目录查看权限的对应用户数据
operator = get_operator(self.request)
if settings.ENABLE_IAM:
fs = Permission().make_filter_of_category(operator, IAMAction.VIEW_CATEGORY)
profile_ids = Profile.objects.filter(
fs,
enabled=False,
status=ProfileStatus.DELETED.value,
).values_list("id", flat=True)
queryset = queryset.filter(object_id__in=profile_ids)

return queryset.all()

def get_serializer_context(self):
Expand Down Expand Up @@ -186,118 +212,66 @@ def get_serializer_context(self):
return output_slz_context


class RecycleBinBatchCategoryRevertCheckApi(generics.CreateAPIView):
queryset = RecycleBin.objects.filter(object_type=RecycleBinObjectType.CATEGORY.value)
serializer_class = CategoryRevertCheckResultOutputSlZ

def post(self, request, *args, **kwargs):
input_slz = BatchCategoryRevertInputSlZ(data=request.data)
input_slz.is_valid(raise_exception=True)
data = input_slz.validated_data
class RecycleBinCategoryRevertHardDeleteApi(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [ManageCategoryPermission]

# 检查目录是否在回收站
for category_id in data["category_ids"]:
check_category_in_recycle_bin(category_id=category_id)

# 待还原,需进行检查的软删除目录
check_detail_list: list = []
for category in ProfileCategory.objects.filter(id__in=data["category_ids"]):
check_detail: dict = {
"category_id": category.id,
"check_status": True,
"error_message": "",
}
# 重名检查
if ProfileCategory.objects.filter(enabled=True, display_name=category.display_name).exists():
check_detail["check_status"] = False
check_detail["error_message"] = CategoryCheckMessageEnum.DUPLICATE_DISPLAY_NAME.value
continue
# 重登录域检查
if ProfileCategory.objects.filter(enabled=True, domain=category.domain).exists():
check_detail["check_status"] = False
check_detail["error_message"] = CategoryCheckMessageEnum.DUPLICATE_DOMAIN.value
continue
queryset = ProfileCategory.objects.filter(enabled=False, status=CategoryStatus.DELETED.value)
lookup_url_kwarg = "id"

check_detail_list.append(check_detail)
def _check_if_category_in_recycle_bin(self, category_id: str):
"""
是否在回收站里
防御性检查,上面queryset已经过滤的是软删除的目录了,理论上会在回收站里
"""
if not RecycleBin.objects.filter(
object_type=RecycleBinObjectType.CATEGORY.value, object_id=category_id
).exists():
raise error_codes.CANNOT_FIND_CATEGORY_IN_RECYCLE_BIN.f(category_id=category_id)

return Response(data=self.serializer_class(instance=check_detail_list, many=True).data)
def put(self, request, *args, **kwargs):
"""还原目录"""
category = self.get_object()
self._check_if_category_in_recycle_bin(category.id)

# 检查是否冲突
check_conflict_before_revert_category(category)

class RecycleBinBatchCategoryRevertApi(generics.CreateAPIView):
queryset = RecycleBin.objects.filter(object_type=RecycleBinObjectType.CATEGORY.value)
serializer_class = CategoryRevertResultOutputSlZ
# 还原
category.revert()
# 恢复原本的定时任务,增加审计日志
post_category_revert.send_robust(
sender=self, instance=category, operator=request.operator, extra_values={"request": request}
)

def post(self, request, *args, **kwargs):
input_slz = BatchCategoryRevertInputSlZ(data=request.data)
input_slz.is_valid(raise_exception=True)
data = input_slz.validated_data
# 删除回收站记录
RecycleBin.objects.filter(object_type=RecycleBinObjectType.CATEGORY.value, object_id=category.id).delete()

# 检查目录是否在回收站
for category_id in data["category_ids"]:
check_category_in_recycle_bin(category_id=category_id)
return Response(status=status.HTTP_200_OK)

categories = ProfileCategory.objects.filter(id__in=data["category_ids"])
def delete(self, request, *args, **kwargs):
"""硬删除目录"""
category = self.get_object()
self._check_if_category_in_recycle_bin(category.id)

# 二次检查
for category in categories:
# 重名检查
if ProfileCategory.objects.filter(enabled=True, display_name=category.display_name).exists():
raise error_codes.REVERT_CATEGORIES_FAILED.f(
category_id=category.id,
error_message=CategoryCheckMessageEnum.DUPLICATE_DISPLAY_NAME.value,
)
# 重登录域检查
if ProfileCategory.objects.filter(enabled=True, domain=category.domain).exists():
raise error_codes.REVERT_CATEGORIES_FAILED.f(
category_id=category.id,
error_message=CategoryCheckMessageEnum.DUPLICATE_DOMAIN.value,
)

# 还原结果初始化
revert_results: dict = {
"successful_count": 0,
}
reverted_category_ids: list = []
with transaction.atomic():
for category in categories:
category.revert()
# 恢复原本的定时任务,增加审计日志
post_category_revert.send_robust(
sender=self, instance=category, operator=request.operator, extra_values={"request": request}
)
revert_results["successful_count"] += 1
reverted_category_ids.append(category.id)

# 删除映射记录
self.queryset.filter(object_id__in=reverted_category_ids).delete()

return Response(self.serializer_class(revert_results).data)


class RecycleBinCategoryBatchHardDeleteApi(generics.DestroyAPIView):
queryset = RecycleBin.objects.filter(object_type=RecycleBinObjectType.CATEGORY.value)
# 增加审计日志, 删除同步功能
post_category_hard_delete.send_robust(
sender=self, instance=category, operator=request.operator, extra_values={"request": request}
)
# 异步:删除人员,部门,关系,目录设置
hard_delete_category_related_resource.delay(category_id=category.id)

def destroy(self, request, *args, **kwargs):
slz = BatchCategoryHardDeleteInputSlZ(data=request.data)
slz.is_valid(raise_exception=True)
data = slz.validated_data
return Response(status=status.HTTP_200_OK)

# 检查目录是否在回收站
for category_id in data["category_ids"]:
check_category_in_recycle_bin(category_id=category_id)

# 删除关联资源
categories_ids = data["category_ids"]
categories = ProfileCategory.objects.filter(
id__in=categories_ids, enabled=False, status=CategoryStatus.DELETED.value
)
class RecycleBinCategoryRevertConflictApi(generics.ListAPIView):
permission_classes = [ManageCategoryPermission]

for category in categories:
# 增加审计日志, 删除同步功能
post_category_hard_delete.send_robust(
sender=self, instance=category, operator=request.operator, extra_values={"request": request}
)
# 删除人员,部门,关系,目录设置
hard_delete_category_related_resource.delay(category_id=category.id)
queryset = ProfileCategory.objects.filter(enabled=False, status=CategoryStatus.DELETED.value)
lookup_url_kwarg = "id"

return Response()
def get(self, request, *args, **kwargs):
"""检查还原目录是否会冲突"""
category = self.get_object()
# 查询冲突
conflicts = list_conflict_before_revert_category(category)
return Response(data=conflicts)
Loading