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

feat: added audit operation for data source and user #1981

Closed
wants to merge 2 commits into from
Closed
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
16 changes: 16 additions & 0 deletions src/bk-user/bkuser/apis/web/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - 用户管理 (bk-user) 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.
47 changes: 47 additions & 0 deletions src/bk-user/bkuser/apis/web/audit/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - 用户管理 (bk-user) 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 rest_framework import serializers

from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum


class OperationAuditRecordListInputSerializer(serializers.Serializer):
operator = serializers.CharField(help_text="操作人", required=False, allow_blank=True)
operation = serializers.ChoiceField(help_text="操作行为", choices=OperationEnum.get_choices(), required=False)
object_type = serializers.ChoiceField(
help_text="操作对象类型", choices=ObjectTypeEnum.get_choices(), required=False
)
object_name = serializers.CharField(help_text="操作对象名称", required=False, allow_blank=True)
created_at = serializers.DateTimeField(help_text="操作时间", required=False)


class OperationAuditRecordListOutputSerializer(serializers.Serializer):
operator = serializers.CharField(help_text="操作人", source="creator")
operation = serializers.SerializerMethodField(help_text="操作行为")
object_type = serializers.SerializerMethodField(help_text="操作对象类型")
object_name = serializers.CharField(help_text="操作对象名称", required=False)
created_at = serializers.DateTimeField(help_text="操作时间")

def get_operation(self, obj):
# 从 operation_map 中提取 operation 对应的中文标识
return self.context["operation_map"][obj.operation]

def get_object_type(self, obj):
# 从 object_type_map 中提取 object_type 对应的中文标识
return self.context["object_type_map"][obj.object_type]
29 changes: 29 additions & 0 deletions src/bk-user/bkuser/apis/web/audit/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - 用户管理 (bk-user) 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 bkuser.apis.web.audit import views

urlpatterns = [
# 操作审计列表
path(
"",
views.AuditRecordListAPIView.as_view(),
name="audit.list",
),
]
72 changes: 72 additions & 0 deletions src/bk-user/bkuser/apis/web/audit/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - 用户管理 (bk-user) 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.db.models import Q
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated

from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum
from bkuser.apps.audit.models import OperationAuditRecord
from bkuser.apps.permission.constants import PermAction
from bkuser.apps.permission.permissions import perm_class

from .serializers import OperationAuditRecordListInputSerializer, OperationAuditRecordListOutputSerializer


class AuditRecordListAPIView(generics.ListAPIView):
"""操作审计记录列表"""

permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]

serializer_class = OperationAuditRecordListOutputSerializer

def get_queryset(self):
slz = OperationAuditRecordListInputSerializer(data=self.request.query_params)
slz.is_valid(raise_exception=True)
params = slz.validated_data

filters = Q()
if params.get("operator"):
filters &= Q(creator=params["operator"])
if params.get("operation"):
filters &= Q(operation=params["operation"])
if params.get("object_type"):
filters &= Q(object_type=params["object_type"])
if params.get("created_at"):
start_time = params["created_at"].replace(microsecond=0)
end_time = params["created_at"].replace(microsecond=999999)
filters &= Q(created_at__range=(start_time, end_time))
if params.get("object_name"):
filters &= Q(object_name__icontains=params["object_name"])

return OperationAuditRecord.objects.filter(filters)

def get_serializer_context(self):
context = super().get_serializer_context()
context["operation_map"] = dict(OperationEnum.get_choices())
context["object_type_map"] = dict(ObjectTypeEnum.get_choices())
return context

@swagger_auto_schema(
tags=["audit"],
operation_description="操作审计列表",
query_serializer=OperationAuditRecordListInputSerializer(),
responses={status.HTTP_200_OK: OperationAuditRecordListOutputSerializer(many=True)},
)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
67 changes: 37 additions & 30 deletions src/bk-user/bkuser/apis/web/data_source/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
)
from bkuser.apis.web.mixins import CurrentUserTenantMixin
from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum
from bkuser.apps.audit.recorder import add_audit_record
from bkuser.apps.audit.data_model import AuditObject
from bkuser.apps.audit.recorder import add_audit_record, batch_add_audit_records
from bkuser.apps.data_source.constants import DataSourceTypeEnum
from bkuser.apps.data_source.models import (
DataSource,
Expand Down Expand Up @@ -80,6 +81,7 @@
from bkuser.idp_plugins.constants import BuiltinIdpPluginEnum
from bkuser.plugins.base import get_default_plugin_cfg, get_plugin_cfg_schema_map, get_plugin_cls
from bkuser.plugins.constants import DataSourcePluginEnum
from bkuser.utils.django import get_model_dict

from .schema import get_data_source_plugin_cfg_json_schema

Expand Down Expand Up @@ -193,11 +195,9 @@ def post(self, request, *args, **kwargs):
operation=OperationEnum.CREATE_DATA_SOURCE,
object_type=ObjectTypeEnum.DATA_SOURCE,
object_id=ds.id,
extras={
"plugin_config": ds.plugin_config,
"field_mapping": ds.field_mapping,
"sync_config": ds.sync_config,
},
data_before={},
data_after=get_model_dict(ds),
extras={},
)

return Response(
Expand Down Expand Up @@ -253,11 +253,7 @@ def put(self, request, *args, **kwargs):
data = slz.validated_data

# 【审计】记录变更前数据
data_before = {
"plugin_config": data_source.plugin_config,
"field_mapping": data_source.field_mapping,
"sync_config": data_source.sync_config,
}
data_before_data_source = get_model_dict(data_source)

with transaction.atomic():
data_source.field_mapping = data["field_mapping"]
Expand All @@ -274,7 +270,9 @@ def put(self, request, *args, **kwargs):
operation=OperationEnum.MODIFY_DATA_SOURCE,
object_type=ObjectTypeEnum.DATA_SOURCE,
object_id=data_source.id,
extras={"data_before": data_before},
data_before=data_before_data_source,
data_after=get_model_dict(data_source),
extras={},
)

return Response(status=status.HTTP_204_NO_CONTENT)
Expand Down Expand Up @@ -308,14 +306,27 @@ def delete(self, request, *args, **kwargs):
# 待删除的认证源
waiting_delete_idps = Idp.objects.filter(**idp_filters)

# 【审计】记录变更前数据,数据删除后便无法获取
idps_before_delete = list(
waiting_delete_idps.values("id", "name", "status", "plugin_config", "data_source_match_rules")
# 记录 data_source 删除前数据
data_source_audit_object = AuditObject(
id=data_source.id,
type=ObjectTypeEnum.DATA_SOURCE,
operation=OperationEnum.RESET_DATA_SOURCE,
data_before=get_model_dict(data_source),
data_after={},
extras={},
)
data_source_id = data_source.id
plugin_config = data_source.plugin_config
field_mapping = data_source.field_mapping
sync_config = data_source.sync_config
# 记录 idp 删除前数据
idp_audit_objects = [
AuditObject(
id=data_before_idp.id,
type=ObjectTypeEnum.IDP,
operation=OperationEnum.RESET_IDP,
data_before=get_model_dict(data_before_idp),
data_after={},
extras={},
)
for data_before_idp in list(waiting_delete_idps)
]

with transaction.atomic():
# 删除认证源敏感信息
Expand All @@ -334,20 +345,12 @@ def delete(self, request, *args, **kwargs):
# 删除数据源 & 关联资源数据
DataSourceHandler.delete_data_source_and_related_resources(data_source)

audit_objects = [data_source_audit_object] + idp_audit_objects
# 审计记录
add_audit_record(
batch_add_audit_records(
operator=request.user.username,
tenant_id=self.get_current_tenant_id(),
operation=OperationEnum.DELETE_DATA_SOURCE,
object_type=ObjectTypeEnum.DATA_SOURCE,
object_id=data_source_id,
extras={
"is_delete_idp": is_delete_idp,
"plugin_config": plugin_config,
"field_mapping": field_mapping,
"sync_config": sync_config,
"idps_before_delete": idps_before_delete,
},
objects=audit_objects,
)

return Response(status=status.HTTP_204_NO_CONTENT)
Expand Down Expand Up @@ -550,6 +553,8 @@ def post(self, request, *args, **kwargs):
operation=OperationEnum.SYNC_DATA_SOURCE,
object_type=ObjectTypeEnum.DATA_SOURCE,
object_id=data_source.id,
data_before={},
data_after={},
extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger},
)

Expand Down Expand Up @@ -603,6 +608,8 @@ def post(self, request, *args, **kwargs):
operation=OperationEnum.SYNC_DATA_SOURCE,
object_type=ObjectTypeEnum.DATA_SOURCE,
object_id=data_source.id,
data_before={},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 data_before, data_after, extras 都做成可选参数

data_after={},
extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger},
)

Expand Down
39 changes: 38 additions & 1 deletion src/bk-user/bkuser/apis/web/organization/views/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
#
# 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 collections import defaultdict
from typing import Dict, List

from django.utils.translation import gettext_lazy as _

from bkuser.apis.web.mixins import CurrentUserTenantMixin
from bkuser.apps.data_source.constants import DataSourceTypeEnum
from bkuser.apps.data_source.models import DataSource
from bkuser.apps.data_source.models import DataSource, DataSourceDepartmentUserRelation, DataSourceUserLeaderRelation
from bkuser.common.error_codes import error_codes


Expand All @@ -40,3 +43,37 @@ def get_current_tenant_local_real_data_source(self) -> DataSource:
raise error_codes.DATA_SOURCE_NOT_EXIST.f(_("当前租户不存在本地实名用户数据源"))

return real_data_source


class CurrentUserDepartmentRelationMixin:
"""获取用户与部门之间的映射关系"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

需要讨论:加这个 mixin 来算数据,是否合适?


def get_user_department_map(self, data_source_user_ids: List[int]) -> Dict:
# 记录用户与部门之间的映射关系
user_department_relations = DataSourceDepartmentUserRelation.objects.filter(
user_id__in=data_source_user_ids
).values("department_id", "user_id")
user_department_map = defaultdict(list)

# 将用户的所有部门存储在列表中
for relation in user_department_relations:
user_department_map[relation["user_id"]].append(relation["department_id"])

return user_department_map


class CurrentUserLeaderRelationMixin:
"""获取用户与上级之间的映射关系"""

def get_user_leader_map(self, data_source_user_ids: List[int]) -> Dict:
# 记录用户与上级之间的映射关系
user_leader_relations = DataSourceUserLeaderRelation.objects.filter(user_id__in=data_source_user_ids).values(
"leader_id", "user_id"
)
user_leader_map = defaultdict(list)

# 将用户的所有上级存储在列表中
for relation in user_leader_relations:
user_leader_map[relation["user_id"]].append(relation["leader_id"])

return user_leader_map
Loading
Loading