Skip to content

Commit

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

* feat(recycle_bin, categories, audit, period_task, login): 回收站目录项:新增删除,还原功能

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

feat TencentBlueKing#901 TencentBlueKing#960
  • Loading branch information
neronkl authored and yuri0528 committed Jun 27, 2023
1 parent 5971357 commit c808574
Show file tree
Hide file tree
Showing 24 changed files with 534 additions and 73 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
13 changes: 12 additions & 1 deletion src/api/bkuser_core/api/web/category/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 _
Expand All @@ -19,11 +20,14 @@
from bkuser_core.bkiam.serializers import AuthInfoSLZ
from bkuser_core.categories.constants import CategoryStatus, CreateAbleCategoryType
from bkuser_core.categories.models import ProfileCategory
from bkuser_core.categories.utils import change_periodic_sync_task_status
from bkuser_core.departments.models import Department
from bkuser_core.profiles.models import Profile
from bkuser_core.profiles.validators import validate_domain
from bkuser_core.user_settings.models import Setting

logger = logging.getLogger(__name__)


class ExtraInfoSerializer(serializers.Serializer):
auth_infos = serializers.ListField(read_only=True, child=AuthInfoSLZ())
Expand Down Expand Up @@ -92,7 +96,6 @@ class CategoryCreateInputSLZ(serializers.Serializer):
def validate(self, data):
if ProfileCategory.objects.filter(domain=data["domain"]).exists():
raise ValidationError(_("登陆域为 {} 的用户目录已存在").format(data["domain"]))

return data

def create(self, validated_data):
Expand Down Expand Up @@ -122,6 +125,14 @@ def update(self, instance, validated_data):
if activated is not None:
instance.status = CategoryStatus.NORMAL.value if activated else CategoryStatus.INACTIVE.value
should_updated_fields.append("status")
# 变更同步任务使能状态
logger.info(
"going to change periodic task status<%s> for Category-<%s>, the category type is %s",
activated,
instance.id,
instance.type,
)
change_periodic_sync_task_status(instance.id, activated)

display_name = validated_data.get("display_name")
if display_name:
Expand Down
14 changes: 11 additions & 3 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 @@ -242,7 +242,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 Expand Up @@ -292,12 +292,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
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
10 changes: 10 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,16 @@
views.RecycleBinCategoryListApi.as_view(),
name="recycle_bin.category.list",
),
path(
"categories/<int:id>/",
views.RecycleBinCategoryRevertHardDeleteApi.as_view(),
name="recycle_bin.category.revert_hard_delete",
),
path(
"categories/<int:id>/conflicts/",
views.RecycleBinCategoryRevertConflictApi.as_view(),
name="recycle_bin.category.revert.conflict",
),
path(
"departments/",
views.RecycleBinDepartmentListApi.as_view(),
Expand Down
44 changes: 44 additions & 0 deletions src/api/bkuser_core/api/web/recycle_bin/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- 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 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


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

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

if ProfileCategory.objects.filter(enabled=True, domain=category.domain).exists():
conflicts.append(_("目录登录域重复"))

return conflicts


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)
112 changes: 109 additions & 3 deletions src/api/bkuser_core/api/web/recycle_bin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,38 @@
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.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.serializers import (
RecycleBinCategoryOutputSlZ,
RecycleBinDepartmentOutputSlZ,
RecycleBinProfileOutputSlZ,
RecycleBinSearchInputSlZ,
)
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
from bkuser_core.departments.models import Department
from bkuser_core.profiles.constants import ProfileStatus
from bkuser_core.profiles.models import Profile
from bkuser_core.recycle_bin.constants import RecycleBinObjectType
from bkuser_core.recycle_bin.models import RecycleBin
from bkuser_core.recycle_bin.signals import post_category_hard_delete, post_category_revert
from bkuser_core.recycle_bin.tasks import hard_delete_category_related_resource

logger = logging.getLogger(__name__)


class RecycleBinCategoryListApi(generics.ListAPIView):
Expand All @@ -49,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 @@ -91,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 @@ -144,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 All @@ -169,3 +210,68 @@ def get_serializer_context(self):
}
)
return output_slz_context


class RecycleBinCategoryRevertHardDeleteApi(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [ManageCategoryPermission]

queryset = ProfileCategory.objects.filter(enabled=False, status=CategoryStatus.DELETED.value)
lookup_url_kwarg = "id"

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)

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)

# 还原
category.revert()
# 恢复原本的定时任务,增加审计日志
post_category_revert.send_robust(
sender=self, instance=category, operator=request.operator, extra_values={"request": request}
)

# 删除回收站记录
RecycleBin.objects.filter(object_type=RecycleBinObjectType.CATEGORY.value, object_id=category.id).delete()

return Response(status=status.HTTP_200_OK)

def delete(self, request, *args, **kwargs):
"""硬删除目录"""
category = self.get_object()
self._check_if_category_in_recycle_bin(category.id)

# 增加审计日志, 删除同步功能
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)

return Response(status=status.HTTP_200_OK)


class RecycleBinCategoryRevertConflictApi(generics.ListAPIView):
permission_classes = [ManageCategoryPermission]

queryset = ProfileCategory.objects.filter(enabled=False, status=CategoryStatus.DELETED.value)
lookup_url_kwarg = "id"

def get(self, request, *args, **kwargs):
"""检查还原目录是否会冲突"""
category = self.get_object()
# 查询冲突
conflicts = list_conflict_before_revert_category(category)
return Response(data=conflicts)
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 @@ -47,6 +47,8 @@ class OperationType(AutoLowerEnum):
CREATE = auto()
UPDATE = auto()
DELETE = auto()
HARD_DELETE = auto()
REVERT = auto()
RETRIEVE = auto()

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

0 comments on commit c808574

Please sign in to comment.