Skip to content

Commit

Permalink
feat(recycle_bin, categories, audit, period_task, login): 回收站目录项:新增删除…
Browse files Browse the repository at this point in the history
…,还原功能

回收站内删除等同对象硬删除(相关资源一并删除),还原为对象恢复正常状态,操作计入审计日志;阻止异常状态目录用户登录;

BREAKING CHANGE: 原category.delete()将对应的资源disable且删除定时任务。新delete不会操作其资源,定期任务disable无效化,进行硬删除才将定时任务删除

feat TencentBlueKing#901
  • Loading branch information
neronkl committed Apr 14, 2023
1 parent 288fd90 commit 79e6387
Show file tree
Hide file tree
Showing 19 changed files with 468 additions and 27 deletions.
4 changes: 4 additions & 0 deletions src/api/bkuser_core/api/login/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ def login(self, request):
except ProfileCategory.DoesNotExist:
raise error_codes.DOMAIN_UNKNOWN

# 限制异常状态目录下人员登录
if category.inactive:
raise error_codes.CATEGORY_NOT_ENABLED
if category.is_deleted:
logger.info("Category<%s-%s> has been deleted", category.id, category.domain)
raise error_codes.DOMAIN_UNKNOWN

logger.debug(
"do login check, will check in category<%s-%s-%s>", category.type, category.display_name, category.id
Expand Down
2 changes: 2 additions & 0 deletions src/api/bkuser_core/api/web/audit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
("create", _("创建")),
("update", _("更新")),
("delete", _("删除")),
("hard_delete", _("硬删除")),
("revert", _("还原")),
("retrieve", _("获取")),
("sync", _("同步")),
("export", _("导出")),
Expand Down
12 changes: 10 additions & 2 deletions src/api/bkuser_core/api/web/category/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
Permission,
ViewCategoryPermission,
)
from bkuser_core.categories.constants import CategoryType, SyncTaskType
from bkuser_core.categories.constants import CategoryStatus, CategoryType, SyncTaskType
from bkuser_core.categories.exceptions import ExistsSyncingTaskError, FetchDataFromRemoteFailed
from bkuser_core.categories.loader import get_plugin_by_category
from bkuser_core.categories.models import ProfileCategory, SyncTask
Expand Down Expand Up @@ -289,12 +289,20 @@ def patch(self, request, *args, **kwargs):
def delete(self, request, *args, **kwargs):
"""删除用户目录"""
instance = self.get_object()

if instance.default:
raise error_codes.CANNOT_DELETE_DEFAULT_CATEGORY

if instance.status != CategoryStatus.INACTIVE.value:
raise error_codes.CANNOT_DELETE_ACTIVE_CATEGORY

# 依赖 model 的 delete 方法, 执行软删除
instance.delete()
post_category_delete.send_robust(sender=self, instance=instance, operator=request.operator)

# 善后:回收站映射,软删除审计日志
post_category_delete.send_robust(
sender=self, instance=instance, operator=request.operator, extra_values={"request": request}
)
return Response(status=status.HTTP_200_OK)


Expand Down
20 changes: 20 additions & 0 deletions src/api/bkuser_core/api/web/recycle_bin/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 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.
"""
from enum import auto

from bkuser_core.common.enum import AutoNameEnum


class CategoryCheckMessageEnum(AutoNameEnum):
REPEATED_CONNECTION_URL = auto()
REPEATED_DISPLAY_NAME = auto()

_choices_labels = ((REPEATED_CONNECTION_URL, "相同ldap连接域"), (REPEATED_DISPLAY_NAME, "相同目录吗"))
20 changes: 20 additions & 0 deletions src/api/bkuser_core/api/web/recycle_bin/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,23 @@ def get_category_display_name(self, instance):
class Meta:
model = RecycleBin
fields = ["id", "expires", "category_display_name", "profile", "operator"]


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


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


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


class CategoryRevertResultOutputSlZ(serializers.Serializer):
successful_count = serializers.IntegerField(help_text="目录还原成功个数")
failed_count = serializers.IntegerField(help_text="目录还原失败个数")
11 changes: 11 additions & 0 deletions src/api/bkuser_core/api/web/recycle_bin/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
views.RecycleBinCategoryListApi.as_view(),
name="recycle_bin.category.list",
),
path(
"categories/check/",
views.RecycleBinBatchCategoryRevertCheckApi.as_view(),
name="recycle_bin.category.revert.check",
),
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",
),
path(
"departments/",
views.RecycleBinDepartmentListApi.as_view(),
Expand Down
208 changes: 206 additions & 2 deletions src/api/bkuser_core/api/web/recycle_bin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@
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 django.db import transaction
from django.db.models import Q
from rest_framework import generics
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,
Expand All @@ -21,11 +30,15 @@
from bkuser_core.api.web.viewset import CustomPagination
from bkuser_core.categories.constants import CategoryStatus
from bkuser_core.categories.models import ProfileCategory
from bkuser_core.departments.models import Department
from bkuser_core.categories.signals import post_category_hard_delete, post_category_revert
from bkuser_core.departments.models import Department, DepartmentThroughModel
from bkuser_core.profiles.constants import ProfileStatus
from bkuser_core.profiles.models import Profile
from bkuser_core.profiles.models import LeaderThroughModel, Profile
from bkuser_core.recycle_bin.constants import RecycleBinObjectType
from bkuser_core.recycle_bin.models import RecycleBin
from bkuser_core.user_settings.models import Setting

logger = logging.getLogger(__name__)


class RecycleBinCategoryListApi(generics.ListAPIView):
Expand Down Expand Up @@ -169,3 +182,194 @@ def get_serializer_context(self):
}
)
return output_slz_context


class RecycleBinBatchCategoryRevertCheckApi(generics.CreateAPIView):
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

# 待还原,需进行检查的软删除目录
deleted_category_ids = data["deleted_category_ids"]
deleted_categories = ProfileCategory.objects.filter(
id__in=deleted_category_ids, enabled=False, status=CategoryStatus.DELETED.value
)
if not deleted_categories:
return Response()

# 当前使能的目录(停用/启用中的目录)
enabled_categories = ProfileCategory.objects.filter(enabled=True)
category_display_name_list = enabled_categories.values_list("display_name", flat=True)

# 获取当前使能的mad/ldap 的连接域
enabled_connection_url_list = Setting.objects.filter(
category_id__in=enabled_categories.values_list("id", flat=True), meta__key="connection_url"
).values_list("value", flat=True)

# 提前暴露 待还原 mad/ldap目录的connection_url
deleted_categories_connection_url = Setting.objects.filter(
category_id__in=deleted_category_ids, meta__key="connection_url"
).values("category_id", "value")
deleted_category_urls_map: dict = {}
for item in deleted_categories_connection_url:
deleted_category_urls_map[item["category_id"]] = item["value"]

# 开始检查
check_result_list: list = []
for del_category in deleted_categories:
# 检查结果初始化
check_detail: dict = {
"category_id": del_category.id,
"category_display_name": del_category.display_name,
"check_status": True,
"error_message": "",
}

# 重名检测
if del_category.display_name in category_display_name_list:
check_detail["check_status"] = False
check_detail["error_message"] = CategoryCheckMessageEnum.REPEATED_DISPLAY_NAME.value
check_result_list.append(check_detail)
continue

# 连接域检查, connection_url=None 不是local就是未配置完全的ldap/mad
connection_url = deleted_category_urls_map.get(del_category.id, None)
if connection_url and connection_url in enabled_connection_url_list:
check_detail["check_status"] = False
check_detail["error_message"] = CategoryCheckMessageEnum.REPEATED_CONNECTION_URL.value
check_result_list.append(check_detail)
continue

check_result_list.append(check_detail)

return Response(data=self.serializer_class(instance=check_result_list, many=True).data)


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

def post(self, request, *args, **kwargs):
input_slz = BatchCategoryRevertInputSlZ(data=request.data)
input_slz.is_valid(raise_exception=True)
data = input_slz.validated_data

# 还原软删除目录状态
reverting_category_ids = data["deleted_category_ids"]
reverting_categories = ProfileCategory.objects.filter(
id__in=reverting_category_ids, enabled=False, status=ProfileStatus.DELETED.value
)

# 当前使能的目录(停用/启用中的目录)
enabled_categories = ProfileCategory.objects.filter(enabled=True)
enabled_category_display_names = enabled_categories.values_list("display_name", flat=True)

# 获取当前使能的mad/ldap 的连接域
enabled_categories_connection_urls = Setting.objects.filter(
category_id__in=enabled_categories.values_list("id", flat=True), meta__key="connection_url"
).values_list("value", flat=True)

# 提前暴露待还原 mad/ldap目录的connection_url
reverting_categories_connection_settings = Setting.objects.filter(
category_id__in=reverting_category_ids, meta__key="connection_url"
).values("category_id", "value")
reverting_category_urls: dict = {}
for item in reverting_categories_connection_settings:
reverting_category_urls[item["category_id"]] = item["value"]

# 还原结果初始化
revert_results: dict = {
"successful_count": 0,
"failed_count": 0,
}
# 成功还原的目录id
reverted_category_ids: list = []

for category in reverting_categories:
# 二次检查
# 避免还原过程中,同名目录出现
if category.display_name in enabled_category_display_names:
logger.error(
"Category<%s-%s> get a repeated display_name<%s>",
category.id,
category.display_name,
category.display_name,
)
revert_results["failed_count"] += 1
continue

# 获取不到则可能是未配置,或者为本地目录
connection_url = reverting_category_urls.get(category.id, None)
if connection_url and connection_url in enabled_categories_connection_urls:
logger.error(
"Category<%s-%s> get a repeated connection url<%s>",
category.id,
category.display_name,
category.display_name,
)
revert_results["failed_count"] += 1
continue

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)

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

# 删除关联资源
categories_ids = data["category_ids"].split(",")
with transaction.atomic():
categories = ProfileCategory.objects.filter(id__in=categories_ids)

# 上下级关系删除
logger.info("Categories %s: clear leaders and profiles' relationship.", categories_ids)
relate_profiles = Profile.objects.filter(category_id__in=categories_ids)
LeaderThroughModel.objects.filter(from_profile_id__in=relate_profiles).delete()

# 人员-部门关系删除
logger.info("Categories %s: clear departments and profiles' relationship", categories_ids)
relate_departments = Department.objects.filter(category_id__in=categories_ids)
DepartmentThroughModel.objects.filter(
department_id__in=relate_departments.values_list("id", flat=True)
).delete()

# 清理资源: 人员,部门,目录设置
logger.info("Categories: clear departments, profiles and settings ", categories_ids)
Setting.objects.filter(category_id__in=categories_ids).delete()
relate_departments.delete()
relate_profiles.delete()

# 删除回收站记录
self.queryset.filter(object_id__in=categories_ids).delete()

# 善后,删除定时任务,增加审计日志
[
post_category_hard_delete.send_robust(
sender=self, instance=instance, operator=request.operator, extra_values={"request": request}
)
for instance in categories
]

# 删除目录
categories.delete()

return Response()
4 changes: 4 additions & 0 deletions src/api/bkuser_core/audit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class OperationType(AutoLowerEnum):
CREATE = auto()
UPDATE = auto()
DELETE = auto()
HARD_DELETE = auto()
REVERT = auto()
RETRIEVE = auto()

SYNC = auto()
Expand All @@ -60,6 +62,8 @@ class OperationType(AutoLowerEnum):
(CREATE, "创建"),
(UPDATE, "更新"),
(DELETE, "删除"),
(REVERT, "还原"),
(HARD_DELETE, "硬删除"),
(RETRIEVE, "获取"),
(SYNC, "同步"),
(EXPORT, "导出"),
Expand Down
Loading

0 comments on commit 79e6387

Please sign in to comment.