diff --git a/common/templates/base.html b/common/templates/base.html index 388703ed1e..b69b91873e 100644 --- a/common/templates/base.html +++ b/common/templates/base.html @@ -242,11 +242,24 @@ + + + + {% endif %} + {% if perms.sql.audit_user %} +
  • + 系统审计 + -
  • {% endif %} {% if perms.sql.menu_document %} diff --git a/sql/admin.py b/sql/admin.py index ec57f68462..2389956f10 100755 --- a/sql/admin.py +++ b/sql/admin.py @@ -252,6 +252,6 @@ class CloudAccessKeyAdmin(admin.ModelAdmin): # 登录审计日志 @admin.register(AuditEntry) class AuditEntryAdmin(admin.ModelAdmin): - list_display = ('user_id', 'user_name', 'action', 'ip', 'action_time') - list_filter = ('user_id', 'user_name', 'action', 'ip') + list_display = ('user_id', 'user_name', 'user_display', 'action', 'extra_info', 'action_time') + list_filter = ('user_id', 'user_name', 'user_display', 'action', 'extra_info') diff --git a/sql/audit_log.py b/sql/audit_log.py index c2c56cfe11..d1a5417045 100644 --- a/sql/audit_log.py +++ b/sql/audit_log.py @@ -1,30 +1,67 @@ # -*- coding: UTF-8 -*- import logging -from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +import datetime +import simplejson as json + from django.dispatch import receiver -from .models import AuditEntry, Users from django.utils import timezone -from common.utils.permission import superuser_required from django.http import HttpResponse -import simplejson as json -from common.utils.extend_json_encoder import ExtendJSONEncoder from django.db.models import Q +from django.contrib.auth.decorators import login_required,permission_required +from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed + +from .models import AuditEntry, Users +from common.utils.permission import superuser_required +from common.utils.extend_json_encoder import ExtendJSONEncoder log = logging.getLogger('default') -@superuser_required +@login_required +def audit_input(request): + """用户提交的操作信息""" + result = {} + action = request.POST.get('action') + extra_info = request.POST.get('extra_info','') + + result['user_id'] = request.user.id + result['user_name'] = request.user.username + result['user_display'] = request.user.display + result['action'] = action + result['extra_info'] = extra_info + + audit = AuditEntry(**result) + audit.save() + + return HttpResponse(json.dumps(result, cls=ExtendJSONEncoder, bigint_as_string=True), + content_type='application/json') + + +@permission_required('sql.audit_user', raise_exception=True) def audit_log(request): - """获取登录审计日志列表""" + """获取审计日志列表""" limit = int(request.POST.get('limit')) offset = int(request.POST.get('offset')) limit = offset + limit + limit = limit if limit else None search = request.POST.get('search', '') + action = request.POST.get('action','') + start_date = request.POST.get('start_date') + end_date = request.POST.get('end_date') + + filter_dict = dict() + if start_date and end_date: + end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') + datetime.timedelta(days=1) + filter_dict['action_time__range'] = (start_date, end_date) + if action: + filter_dict['action'] = action + + audit_log_obj = AuditEntry.objects.filter(**filter_dict) + if search: + audit_log_obj = audit_log_obj.filter(Q(user_name__icontains=search) | Q(action__icontains=search)| Q(extra_info__icontains=search)) - # 过滤搜索条件 - audit_log_obj = AuditEntry.objects.filter(Q(user_name__icontains=search) | Q(action__icontains=search)| Q(ip__icontains=search)) audit_log_count = audit_log_obj.count() - audit_log_list = audit_log_obj.order_by('-action_time')[offset:limit].values("user_id", "user_name", "ip", "action", "action_time") + audit_log_list = audit_log_obj.order_by('-action_time')[offset:limit].values('user_id', 'user_name', 'user_display', 'action', 'extra_info', 'action_time') # QuerySet 序列化 rows = [row for row in audit_log_list] @@ -48,14 +85,14 @@ def get_client_ip(request): def user_logged_in_callback(sender, request, user, **kwargs): ip = get_client_ip(request) now = timezone.now() - AuditEntry.objects.create(action=u'登入', ip=ip, user_id=user.id, user_name=user.username, action_time=now) + AuditEntry.objects.create(action=u'登入', extra_info=ip, user_id=user.id, user_name=user.username, user_display=user.display, action_time=now) @receiver(user_logged_out) def user_logged_out_callback(sender, request, user, **kwargs): ip = get_client_ip(request) now = timezone.now() - AuditEntry.objects.create(action=u'登出', ip=ip, user_id=user.id, user_name=user.username, action_time=now) + AuditEntry.objects.create(action=u'登出', extra_info=ip, user_id=user.id, user_name=user.username, user_display=user.display, action_time=now) @receiver(user_login_failed) @@ -65,8 +102,9 @@ def user_login_failed_callback(sender, credentials, **kwargs): user_obj = Users.objects.filter(username=user_name)[0:1] user_count = user_obj.count() user_id = 0 + user_display = '' if user_count > 0: user_id = user_obj[0].id - AuditEntry.objects.create(action=u'登入失败', user_id=user_id, user_name=user_name - , action_time=now) + user_display = user_obj[0].display + AuditEntry.objects.create(action=u'登入失败', user_id=user_id, user_name=user_name, user_display=user_display, action_time=now) diff --git a/sql/models.py b/sql/models.py index 796dcc4021..d6abf581a2 100755 --- a/sql/models.py +++ b/sql/models.py @@ -728,6 +728,7 @@ class Meta: ('archive_apply', '提交归档申请'), ('archive_review', '审核归档申请'), ('archive_mgt', '管理归档申请'), + ('audit_user','审计权限'), ) @@ -867,9 +868,10 @@ class AuditEntry(models.Model): 登录审计日志 """ user_id = models.IntegerField('用户ID') - user_name = models.CharField('用户名称', max_length=255, null=True) + user_name = models.CharField('用户名称', max_length=30, null=True) + user_display = models.CharField('用户中文名',max_length=50, null=True) action = models.CharField('动作', max_length=255) - ip = models.GenericIPAddressField('IP', null=True) + extra_info = models.CharField('额外的信息', max_length=255, null=True) action_time = models.DateTimeField('操作时间', auto_now_add=True) class Meta: @@ -879,9 +881,9 @@ class Meta: verbose_name_plural = u'审计日志' def __unicode__(self): - return '{0} - {1} - {2} - {3} - {4}'.format(self.user_id, self.user_name, self.ip + return '{0} - {1} - {2} - {3} - {4}'.format(self.user_id, self.user_name, self.extra_info , self.action, self.action_time) def __str__(self): - return '{0} - {1} - {2} - {3} - {4}'.format(self.user_id, self.user_name, self.ip + return '{0} - {1} - {2} - {3} - {4}'.format(self.user_id, self.user_name, self.extra_info , self.action, self.action_time) diff --git a/sql/query.py b/sql/query.py index 4b2a38ca08..137f5a9263 100644 --- a/sql/query.py +++ b/sql/query.py @@ -182,6 +182,13 @@ def query(request): @permission_required('sql.menu_sqlquery', raise_exception=True) def querylog(request): + return _querylog(request) + +@permission_required('sql.audit_user', raise_exception=True) +def querylog_audit(request): + return _querylog(request) + +def _querylog(request): """ 获取sql查询记录 :param request: @@ -196,6 +203,8 @@ def querylog(request): star = True if request.GET.get('star') == 'true' else False query_log_id = request.GET.get('query_log_id') search = request.GET.get('search', '') + start_date = request.GET.get('start_date','') + end_date = request.GET.get('end_date','') # 组合筛选项 filter_dict = dict() @@ -205,9 +214,14 @@ def querylog(request): # 语句别名 if query_log_id: filter_dict['id'] = query_log_id - # 管理员查看全部数据,普通用户查看自己的数据 - if not user.is_superuser: + + # 管理员、审计员查看全部数据,普通用户查看自己的数据 + if not (user.is_superuser or user.has_perm('sql.audit_user')): filter_dict['username'] = user.username + + if start_date and end_date: + end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') + datetime.timedelta(days=1) + filter_dict['create_time__range'] = (start_date, end_date) # 过滤组合筛选项 sql_log = QueryLog.objects.filter(**filter_dict) diff --git a/sql/sql_workflow.py b/sql/sql_workflow.py index b465f082c7..3d03c0d51a 100644 --- a/sql/sql_workflow.py +++ b/sql/sql_workflow.py @@ -33,6 +33,13 @@ @permission_required('sql.menu_sqlworkflow', raise_exception=True) def sql_workflow_list(request): + return _sql_workflow_list(request) + +@permission_required('sql.audit_user', raise_exception=True) +def sql_workflow_list_audit(request): + return _sql_workflow_list(request) + +def _sql_workflow_list(request): """ 获取审核列表 :param request: @@ -64,8 +71,8 @@ def sql_workflow_list(request): if start_date and end_date: end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') + datetime.timedelta(days=1) filter_dict['create_time__range'] = (start_date, end_date) - # 管理员,可查看所有工单 - if user.is_superuser: + # 管理员,审计员,可查看所有工单 + if user.is_superuser or user.has_perm('sql.audit_user'): pass # 非管理员,拥有审核权限、资源组粒度执行权限的,可以查看组内所有工单 elif user.has_perm('sql.sql_review') or user.has_perm('sql.sql_execute_for_resource_group'): diff --git a/sql/templates/audit.html b/sql/templates/audit.html index ed96c871ea..f3f2767c74 100644 --- a/sql/templates/audit.html +++ b/sql/templates/audit.html @@ -1,6 +1,24 @@ {% extends "base.html" %} {% block content %} +
    +
    + +
    +
    +
    +   + +
    +
    + +
    + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/sql/templates/audit_sqlworkflow.html b/sql/templates/audit_sqlworkflow.html new file mode 100644 index 0000000000..05fd935f43 --- /dev/null +++ b/sql/templates/audit_sqlworkflow.html @@ -0,0 +1,350 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +   + +
    +
    +
    + +
    +
    +
    + + +{% endblock content %} +{% block js %} + {% load static %} + + + + + + + +{% endblock %} diff --git a/sql/templates/sqlquery.html b/sql/templates/sqlquery.html index 651cf5957f..1ae4328b71 100644 --- a/sql/templates/sqlquery.html +++ b/sql/templates/sqlquery.html @@ -848,6 +848,44 @@ }] }) } + // 对下载的按钮附加其他动作 需要等渲染结束后才能绑定 + setTimeout("actionAppend()",100) + } + + // 通过前端元素查看确定要绑定事件的对象,有其他更好的实现方式? + function downloadAppendOpt(){ + $(".export.btn-group.open:first").children(".dropdown-menu:first")[0].onclick=function(){ + // 实际绑定的动作 + $.ajax({ + type: "post", + url: "/audit/input/", + dataType: "json", + data: { + action:"下载" + }, + complete: function () { + console.log("回调触发") + }, + success: function (data) { + console.log("回调成功") + console.log(data) + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + console.log("回调失败") + } + }); + + } + } + + // 对tab中的按钮添加事件,按钮触发下拉单,下拉单实际进行下载动作 + function actionAppend(){ + for (var i=0; i<$(".btn.btn-default.btn-undefined.dropdown-toggle").length; i++ ){ + $(".btn.btn-default.btn-undefined.dropdown-toggle")[i].onclick=function(){ + // 下拉单必须渲染后才能绑定事件 但不能太后绑定以免点击后已经关闭 + setTimeout("downloadAppendOpt()",10) + } + } } //将数据通过ajax提交给后端进行检查 diff --git a/sql/tests.py b/sql/tests.py index c283944e9d..a8aa6035a7 100644 --- a/sql/tests.py +++ b/sql/tests.py @@ -229,6 +229,18 @@ def test_audit(self): r = self.client.get(f'/audit/', data=data) self.assertEqual(r.status_code, 200) + def test_audit_sqlquery(self): + """测试audit_sqlquery页面""" + data = {} + r = self.client.get(f'/audit_sqlquery/', data=data) + self.assertEqual(r.status_code, 200) + + def test_audit_sqlworkflow(self): + """测试audit_sqlworkflow页面""" + data = {} + r = self.client.get(f'/audit_sqlworkflow/', data=data) + self.assertEqual(r.status_code, 200) + def test_groupmgmt(self): """测试groupmgmt页面""" data = {} diff --git a/sql/urls.py b/sql/urls.py index c21291484f..1fe968a9b9 100644 --- a/sql/urls.py +++ b/sql/urls.py @@ -57,9 +57,12 @@ path('archive//', views.archive_detail, name='archive_detail'), path('config/', views.config), path('audit/', views.audit), + path('audit_sqlquery/', views.audit_sqlquery), + path('audit_sqlworkflow/', views.audit_sqlworkflow), path('authenticate/', auth.authenticate_entry), path('sqlworkflow_list/', sql_workflow.sql_workflow_list), + path('sqlworkflow_list_audit/', sql_workflow.sql_workflow_list_audit), path('sqlworkflow/detail_content/', sql_workflow.detail_content), path('sqlworkflow/backup_sql/', sql_workflow.backup_sql), path('simplecheck/', sql_workflow.check), @@ -116,6 +119,7 @@ path('query/', query.query), path('query/querylog/', query.querylog), + path('query/querylog_audit/', query.querylog_audit), path('query/favorite/', query.favorite), path('query/explain/', sql.sql_optimize.explain), path('query/applylist/', sql.query_privileges.query_priv_apply_list), @@ -154,4 +158,5 @@ path('4admin/sync_ding_user/', ding_api.sync_ding_user), path('audit/log/', audit_log.audit_log), + path('audit/input/', audit_log.audit_input), ] diff --git a/sql/views.py b/sql/views.py index 4259f8eccd..d41a24e805 100644 --- a/sql/views.py +++ b/sql/views.py @@ -21,7 +21,7 @@ from sql.utils.tasks import task_info from .models import Users, SqlWorkflow, QueryPrivileges, ResourceGroup, \ - QueryPrivilegesApply, Config, SQL_WORKFLOW_CHOICES, InstanceTag, Instance, QueryLog, ArchiveConfig + QueryPrivilegesApply, Config, SQL_WORKFLOW_CHOICES, InstanceTag, Instance, QueryLog, ArchiveConfig, AuditEntry from sql.utils.workflow_audit import Audit from sql.utils.sql_review import can_execute, can_timingtask, can_cancel, can_view, can_rollback from common.utils.const import Const, WorkflowDict @@ -57,7 +57,7 @@ def sqlworkflow(request): # 过滤筛选项的数据 filter_dict = dict() # 管理员,可查看所有工单 - if user.is_superuser: + if user.is_superuser or user.has_perm('sql.audit_user'): pass # 非管理员,拥有审核权限、资源组粒度执行权限的,可以查看组内所有工单 elif user.has_perm('sql.sql_review') or user.has_perm('sql.sql_execute_for_resource_group'): @@ -435,7 +435,45 @@ def dbaprinciples(request): return render(request, 'dbaprinciples.html', {'md': md}) -@superuser_required +@permission_required('sql.audit_user', raise_exception=True) def audit(request): - """登录审计日志页面""" - return render(request, 'audit.html') + """通用审计日志页面""" + _action_types = AuditEntry.objects.values_list('action').distinct() + action_types = [ i[0] for i in _action_types ] + return render(request, 'audit.html', {'action_types': action_types}) + + +@permission_required('sql.audit_user', raise_exception=True) +def audit_sqlquery(request): + """SQL在线查询页面审计""" + user = request.user + favorites = QueryLog.objects.filter(username=user.username, favorite=True).values('id', 'alias') + is_download = 1 if user.has_perm('sql.download') or user.is_superuser else 0 + return render(request, 'audit_sqlquery.html', {'favorites': favorites, 'is_download':is_download}) + + +def audit_sqlworkflow(request): + """SQL上线工单列表页面""" + user = request.user + # 过滤筛选项的数据 + filter_dict = dict() + # 管理员,可查看所有工单 + if user.is_superuser or user.has_perm('sql.audit_user'): + pass + # 非管理员,拥有审核权限、资源组粒度执行权限的,可以查看组内所有工单 + elif user.has_perm('sql.sql_review') or user.has_perm('sql.sql_execute_for_resource_group'): + # 先获取用户所在资源组列表 + group_list = user_groups(user) + group_ids = [group.group_id for group in group_list] + filter_dict['group_id__in'] = group_ids + # 其他人只能查看自己提交的工单 + else: + filter_dict['engineer'] = user.username + instance_id = SqlWorkflow.objects.filter(**filter_dict).values('instance_id').distinct() + instance = Instance.objects.filter(pk__in=instance_id) + resource_group_id = SqlWorkflow.objects.filter(**filter_dict).values('group_id').distinct() + resource_group = ResourceGroup.objects.filter(group_id__in=resource_group_id) + + return render(request, 'audit_sqlworkflow.html', + {'status_list': SQL_WORKFLOW_CHOICES, + 'instance': instance, 'resource_group': resource_group}) \ No newline at end of file diff --git a/src/init_sql/v1.8.3.sql b/src/init_sql/v1.8.3.sql index a0df1445f5..1217808cfe 100644 --- a/src/init_sql/v1.8.3.sql +++ b/src/init_sql/v1.8.3.sql @@ -15,4 +15,12 @@ set @content_type_id=(select id from django_content_type where app_label='sql' a INSERT INTO auth_permission (name, content_type_id, codename) VALUES ('菜单 My2SQL', @content_type_id, 'menu_my2sql'); -- ssh 隧道功能修改 -ALTER TABLE `ssh_tunnel` ADD COLUMN pkey longtext NULL AFTER password; \ No newline at end of file +ALTER TABLE `ssh_tunnel` ADD COLUMN pkey longtext NULL AFTER password; + +-- 审计功能增强 +alter table audit_log change `ip` `extra_info` varchar(255) DEFAULT NULL COMMENT '额外的信息'; +alter table audit_log add `user_display` varchar(50) DEFAULT NULL COMMENT '用户中文名'; + +set @content_type_id=(select id from django_content_type where app_label='sql' and model='permission'); +insert IGNORE INTO auth_permission (name, content_type_id, codename) VALUES +('审计权限 ', @content_type_id, 'audit_user');