diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 3e4cc95d9..2254b1683 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -10,9 +10,6 @@ from django.utils import timezone from django.utils.translation import gettext as _ from assets.automations.methods import platform_automation_methods -from audits.signals import post_activity_log -from audits.const import ActivityChoices -from common.utils import reverse from common.utils import get_logger, lazyproperty from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback @@ -150,21 +147,6 @@ class BasePlaybookManager: yaml.safe_dump(plays, f) return sub_playbook_path - def send_activity(self, assets, **kwargs): - user = kwargs.pop('user', _('Unknown')) - task_type = self.method_type().label - detail = 'User %s performs a task(%s) for this resource.' % ( - user, task_type - ) - detail_url = reverse( - 'ops:celery-task-log', kwargs={'pk': self.execution.id} - ) - for a in assets: - post_activity_log.send( - sender=self, resource_id=a.id, detail=detail, - detail_url=detail_url, type=ActivityChoices.task - ) - def get_runners(self, **kwargs): runners = [] for platform, assets in self.get_assets_group_by_platform().items(): @@ -184,7 +166,6 @@ class BasePlaybookManager: callback=PlaybookCallback(), ) runners.append(runer) - self.send_activity(assets, **kwargs) return runners def on_host_success(self, host, result): @@ -214,7 +195,7 @@ class BasePlaybookManager: pass def run(self, *args, **kwargs): - runners = self.get_runners(user=kwargs.pop('user')) + runners = self.get_runners(user=kwargs.pop('user', None)) if len(runners) > 1: print("### 分批次执行开始任务, 总共 {}\n".format(len(runners))) else: diff --git a/apps/audits/api.py b/apps/audits/api.py index 06eae0fa1..663527e1a 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -20,7 +20,7 @@ from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, Activit from .serializers import FTPLogSerializer, UserLoginLogSerializer, JobAuditLogSerializer from .serializers import ( OperateLogSerializer, OperateLogActionDetailSerializer, - PasswordChangeLogSerializer, ActivitiesOperatorLogSerializer, + PasswordChangeLogSerializer, ActivityOperatorLogSerializer, ) @@ -79,7 +79,7 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView): class ResourceActivityAPIView(generics.ListAPIView): - serializer_class = ActivitiesOperatorLogSerializer + serializer_class = ActivityOperatorLogSerializer rbac_perms = { 'GET': 'audits.view_activitylog', } @@ -88,8 +88,7 @@ class ResourceActivityAPIView(generics.ListAPIView): def get_operate_log_qs(fields, limit=30, **filters): queryset = OperateLog.objects.filter(**filters).annotate( r_type=Value(ActivityChoices.operate_log, CharField()), - r_detail_url=Value(None, CharField()), - r_detail=Value(None, CharField()), + r_detail_id=F('id'), r_detail=Value(None, CharField()), r_user=F('user'), r_action=F('action'), ).values(*fields)[:limit] return queryset @@ -97,8 +96,8 @@ class ResourceActivityAPIView(generics.ListAPIView): @staticmethod def get_activity_log_qs(fields, limit=30, **filters): queryset = ActivityLog.objects.filter(**filters).annotate( - r_type=F('type'), r_detail_url=F('detail_url'), r_detail=F('detail'), - r_user=Value(None, CharField()), + r_type=F('type'), r_detail_id=F('detail_id'), + r_detail=F('detail'), r_user=Value(None, CharField()), r_action=Value(None, CharField()), ).values(*fields)[:limit] return queryset @@ -106,7 +105,10 @@ class ResourceActivityAPIView(generics.ListAPIView): def get_queryset(self): limit = 30 resource_id = self.request.query_params.get('resource_id') - fields = ('id', 'datetime', 'r_detail', 'r_detail_url', 'r_user', 'r_action', 'r_type') + fields = ( + 'id', 'datetime', 'r_detail', 'r_detail_id', + 'r_user', 'r_action', 'r_type' + ) with tmp_to_root_org(): qs1 = self.get_operate_log_qs(fields, resource_id=resource_id) qs2 = self.get_activity_log_qs(fields, resource_id=resource_id) diff --git a/apps/audits/migrations/0021_auto_20230207_0857.py b/apps/audits/migrations/0021_auto_20230207_0857.py index 9df98ace5..e9e8b939e 100644 --- a/apps/audits/migrations/0021_auto_20230207_0857.py +++ b/apps/audits/migrations/0021_auto_20230207_0857.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): ('resource_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Resource')), ('datetime', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Datetime')), ('detail', models.TextField(blank=True, default='', verbose_name='Detail')), - ('detail_url', models.CharField(default=None, max_length=256, null=True, verbose_name='Detail url')), + ('detail_id', models.CharField(default=None, max_length=36, null=True, verbose_name='Detail ID')), ], options={ 'verbose_name': 'Activity log', diff --git a/apps/audits/models.py b/apps/audits/models.py index 8ae162c79..29a67911f 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -108,8 +108,8 @@ class ActivityLog(OrgModelMixin): auto_now=True, verbose_name=_('Datetime'), db_index=True ) detail = models.TextField(default='', blank=True, verbose_name=_('Detail')) - detail_url = models.CharField( - max_length=256, default=None, null=True, verbose_name=_('Detail url') + detail_id = models.CharField( + max_length=36, default=None, null=True, verbose_name=_('Detail ID') ) class Meta: diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 0eebce6f7..ebe7394d9 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -106,7 +106,7 @@ class SessionAuditSerializer(serializers.ModelSerializer): fields = "__all__" -class ActivitiesOperatorLogSerializer(serializers.Serializer): +class ActivityOperatorLogSerializer(serializers.Serializer): timestamp = serializers.SerializerMethodField() detail_url = serializers.SerializerMethodField() content = serializers.SerializerMethodField() @@ -126,11 +126,22 @@ class ActivitiesOperatorLogSerializer(serializers.Serializer): @staticmethod def get_detail_url(obj): - detail_url = obj['r_detail_url'] - if obj['r_type'] == ActivityChoices.operate_log: - detail_url = reverse( - view_name='audits:operate-log-detail', - kwargs={'pk': obj['id']}, - api_to_ui=True, is_audit=True - ) + detail_url = '' + detail_id, obj_type = obj['r_detail_id'], obj['r_type'] + if detail_id: + if obj_type == ActivityChoices.operate_log: + detail_url = reverse( + view_name='audits:operate-log-detail', + kwargs={'pk': obj['id']}, + api_to_ui=True, is_audit=True + ) + elif obj_type == ActivityChoices.task: + detail_url = reverse( + 'ops:celery-task-log', kwargs={'pk': detail_id} + ) + elif obj_type == ActivityChoices.login_log: + detail_url = '%s?id=%s' % ( + reverse('api-audits:login-log-list', api_to_ui=True, is_audit=True), + detail_id + ) return detail_url diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py deleted file mode 100644 index 2b76fac01..000000000 --- a/apps/audits/signal_handlers.py +++ /dev/null @@ -1,333 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid - -from django.apps import apps -from django.conf import settings -from django.contrib.auth import BACKEND_SESSION_KEY -from django.db import transaction -from django.db.models.signals import post_save, pre_save, m2m_changed, pre_delete -from django.dispatch import receiver -from django.utils import timezone, translation -from django.utils.functional import LazyObject -from django.utils.translation import ugettext_lazy as _ -from rest_framework.renderers import JSONRenderer -from rest_framework.request import Request - -from audits.handler import ( - get_instance_current_with_cache_diff, cache_instance_before_data, - create_or_update_operate_log, get_instance_dict_from_cache -) -from audits.models import ActivityLog -from audits.utils import model_to_dict_for_operate_log as model_to_dict -from authentication.signals import post_auth_failed, post_auth_success -from authentication.utils import check_different_city_login_if_need -from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL -from common.signals import django_ready -from common.utils import get_request_ip, get_logger, get_syslogger -from common.utils.encode import data_to_json -from jumpserver.utils import current_request -from terminal.models import Session, Command -from terminal.serializers import SessionSerializer, SessionCommandSerializer -from users.models import User -from users.signals import post_user_change_password -from . import models, serializers -from .const import MODELS_NEED_RECORD, ActionChoices -from .utils import write_login_log -from .signals import post_activity_log - - -logger = get_logger(__name__) -sys_logger = get_syslogger(__name__) -json_render = JSONRenderer() - - -class AuthBackendLabelMapping(LazyObject): - @staticmethod - def get_login_backends(): - backend_label_mapping = {} - for source, backends in User.SOURCE_BACKEND_MAPPING.items(): - for backend in backends: - backend_label_mapping[backend] = source.label - backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _("SSH Key") - backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _("Password") - backend_label_mapping[settings.AUTH_BACKEND_SSO] = _("SSO") - backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") - backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") - backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") - backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") - backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") - return backend_label_mapping - - def _setup(self): - self._wrapped = self.get_login_backends() - - -AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() - -M2M_ACTION = { - POST_ADD: ActionChoices.create, - POST_REMOVE: ActionChoices.delete, - POST_CLEAR: ActionChoices.delete, -} - - -@receiver(m2m_changed) -def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs): - if action not in M2M_ACTION: - return - if not instance: - return - - resource_type = instance._meta.verbose_name - current_instance = model_to_dict(instance, include_model_fields=False) - - instance_id = current_instance.get('id') - log_id, before_instance = get_instance_dict_from_cache(instance_id) - - field_name = str(model._meta.verbose_name) - objs = model.objects.filter(pk__in=pk_set) - objs_display = [str(o) for o in objs] - action = M2M_ACTION[action] - changed_field = current_instance.get(field_name, []) - - after, before, before_value = None, None, None - if action == ActionChoices.create: - before_value = list(set(changed_field) - set(objs_display)) - elif action == ActionChoices.delete: - before_value = list( - set(changed_field).symmetric_difference(set(objs_display)) - ) - - if changed_field: - after = {field_name: changed_field} - if before_value: - before = {field_name: before_value} - - if sorted(str(before)) == sorted(str(after)): - return - - create_or_update_operate_log( - ActionChoices.update, resource_type, - resource=instance, log_id=log_id, before=before, after=after - ) - - -def signal_of_operate_log_whether_continue(sender, instance, created, update_fields=None): - condition = True - if not instance: - condition = False - if instance and getattr(instance, SKIP_SIGNAL, False): - condition = False - # 终端模型的 create 事件由系统产生,不记录 - if instance._meta.object_name == 'Terminal' and created: - condition = False - # last_login 改变是最后登录日期, 每次登录都会改变 - if instance._meta.object_name == 'User' and update_fields and 'last_login' in update_fields: - condition = False - # 不在记录白名单中,跳过 - if sender._meta.object_name not in MODELS_NEED_RECORD: - condition = False - return condition - - -@receiver(pre_save) -def on_object_pre_create_or_update(sender, instance=None, raw=False, using=None, update_fields=None, **kwargs): - ok = signal_of_operate_log_whether_continue( - sender, instance, False, update_fields - ) - if not ok: - return - - # users.PrivateToken Model 没有 id 有 pk字段 - instance_id = getattr(instance, 'id', getattr(instance, 'pk', None)) - instance_before_data = {'id': instance_id} - raw_instance = type(instance).objects.filter(pk=instance_id).first() - - if raw_instance: - instance_before_data = model_to_dict(raw_instance) - operate_log_id = str(uuid.uuid4()) - instance_before_data['operate_log_id'] = operate_log_id - setattr(instance, 'operate_log_id', operate_log_id) - cache_instance_before_data(instance_before_data) - - -@receiver(post_save) -def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): - ok = signal_of_operate_log_whether_continue( - sender, instance, created, update_fields - ) - if not ok: - return - - log_id, before, after = None, None, None - if created: - action = models.ActionChoices.create - after = model_to_dict(instance) - log_id = getattr(instance, 'operate_log_id', None) - else: - action = ActionChoices.update - current_instance = model_to_dict(instance) - log_id, before, after = get_instance_current_with_cache_diff(current_instance) - - resource_type = sender._meta.verbose_name - object_name = sender._meta.object_name - create_or_update_operate_log( - action, resource_type, resource=instance, log_id=log_id, - before=before, after=after, object_name=object_name - ) - - -@receiver(pre_delete) -def on_object_delete(sender, instance=None, **kwargs): - ok = signal_of_operate_log_whether_continue(sender, instance, False) - if not ok: - return - - resource_type = sender._meta.verbose_name - create_or_update_operate_log( - ActionChoices.delete, resource_type, - resource=instance, before=model_to_dict(instance) - ) - - -@receiver(post_user_change_password, sender=User) -def on_user_change_password(sender, user=None, **kwargs): - if not current_request: - remote_addr = '127.0.0.1' - change_by = 'System' - else: - remote_addr = get_request_ip(current_request) - if not current_request.user.is_authenticated: - change_by = str(user) - else: - change_by = str(current_request.user) - with transaction.atomic(): - models.PasswordChangeLog.objects.create( - user=str(user), change_by=change_by, - remote_addr=remote_addr, - ) - - -def on_audits_log_create(sender, instance=None, **kwargs): - if sender == models.UserLoginLog: - category = "login_log" - serializer_cls = serializers.UserLoginLogSerializer - elif sender == models.FTPLog: - category = "ftp_log" - serializer_cls = serializers.FTPLogSerializer - elif sender == models.OperateLog: - category = "operation_log" - serializer_cls = serializers.OperateLogSerializer - elif sender == models.PasswordChangeLog: - category = "password_change_log" - serializer_cls = serializers.PasswordChangeLogSerializer - elif sender == Session: - category = "host_session_log" - serializer_cls = SessionSerializer - elif sender == Command: - category = "session_command_log" - serializer_cls = SessionCommandSerializer - else: - return - - serializer = serializer_cls(instance) - data = data_to_json(serializer.data, indent=None) - msg = "{} - {}".format(category, data) - sys_logger.info(msg) - - -def get_login_backend(request): - backend = request.session.get('auth_backend', '') or \ - request.session.get(BACKEND_SESSION_KEY, '') - - backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None) - if backend_label is None: - backend_label = '' - return backend_label - - -def generate_data(username, request, login_type=None, user_id=None): - user_agent = request.META.get('HTTP_USER_AGENT', '') - login_ip = get_request_ip(request) or '0.0.0.0' - - if login_type is None and isinstance(request, Request): - login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') - if login_type is None: - login_type = 'W' - - with translation.override('en'): - backend = str(get_login_backend(request)) - - if user_id is None: - user = User.objects.filter(username=username).first() - user_id = getattr(user, 'id', None) - - data = { - 'user_id': user_id, - 'username': username, - 'ip': login_ip, - 'type': login_type, - 'user_agent': user_agent[0:254], - 'datetime': timezone.now(), - 'backend': backend, - } - return data - - -@receiver(post_auth_success) -def on_user_auth_success(sender, user, request, login_type=None, **kwargs): - logger.debug('User login success: {}'.format(user.username)) - check_different_city_login_if_need(user, request) - data = generate_data( - user.username, request, login_type=login_type, user_id=user.id - ) - request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") - data.update({'mfa': int(user.mfa_enabled), 'status': True}) - write_login_log(**data) - - -@receiver(post_auth_failed) -def on_user_auth_failed(sender, username, request, reason='', **kwargs): - logger.debug('User login failed: {}'.format(username)) - data = generate_data(username, request) - data.update({'reason': reason[:128], 'status': False}) - write_login_log(**data) - - -@receiver(django_ready) -def on_django_start_set_operate_log_monitor_models(sender, **kwargs): - exclude_apps = { - 'django_cas_ng', 'captcha', 'admin', 'jms_oidc_rp', 'audits', - 'django_celery_beat', 'contenttypes', 'sessions', 'auth', - } - exclude_models = { - 'UserPasswordHistory', 'ContentType', - 'MessageContent', 'SiteMessage', - 'PlatformAutomation', 'PlatformProtocol', 'Protocol', - 'HistoricalAccount', 'GatheredUser', 'ApprovalRule', - 'BaseAutomation', 'CeleryTask', 'Command', 'JobAuditLog', - 'ConnectionToken', 'SessionJoinRecord', - 'HistoricalJob', 'Status', 'TicketStep', 'Ticket', - 'UserAssetGrantedTreeNodeRelation', 'TicketAssignee', - 'SuperTicket', 'SuperConnectionToken', 'PermNode', - 'PermedAsset', 'PermedAccount', 'MenuPermission', - 'Permission', 'TicketSession', 'ApplyLoginTicket', - 'ApplyCommandTicket', 'ApplyLoginAssetTicket', - } - for i, app in enumerate(apps.get_models(), 1): - app_name = app._meta.app_label - model_name = app._meta.object_name - if app_name in exclude_apps or \ - model_name in exclude_models or \ - model_name.endswith('Execution'): - continue - MODELS_NEED_RECORD.add(model_name) - - -@receiver(post_activity_log) -def on_activity_log_trigger(sender, **kwargs): - ActivityLog.objects.create( - resource_id=kwargs['resource_id'], - detail=kwargs.get('detail'), detail_url=kwargs.get('detail_url') - ) diff --git a/apps/audits/signal_handlers/__init__.py b/apps/audits/signal_handlers/__init__.py new file mode 100644 index 000000000..82e0f4917 --- /dev/null +++ b/apps/audits/signal_handlers/__init__.py @@ -0,0 +1,4 @@ +from .activity_log import * +from .login_log import * +from .operate_log import * +from .other import * diff --git a/apps/audits/signal_handlers/activity_log.py b/apps/audits/signal_handlers/activity_log.py new file mode 100644 index 000000000..8a09935ef --- /dev/null +++ b/apps/audits/signal_handlers/activity_log.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# +from celery import signals +from django.db.models.signals import post_save +from django.utils.translation import ugettext_lazy as _ + +from audits.models import ActivityLog +from assets.models import Asset, Node +from accounts.const import AutomationTypes +from accounts.models import AccountBackupAutomation +from common.utils import get_object_or_none +from ops.celery import app +from orgs.utils import tmp_to_root_org +from terminal.models import Session +from users.models import User +from jumpserver.utils import current_request + +from ..const import ActivityChoices + + +class ActivityLogHandler(object): + + @staticmethod + def _func_accounts_execute_automation(*args, **kwargs): + asset_ids = [] + pid, tp = kwargs.get('pid'), kwargs.get('tp') + model = AutomationTypes.get_type_model(tp) + task_type_label = tp.label + with tmp_to_root_org(): + instance = get_object_or_none(model, pk=pid) + if instance is not None: + asset_ids = instance.get_all_assets().values_list('id', flat=True) + return task_type_label, asset_ids + + @staticmethod + def _func_accounts_push_accounts_to_assets(*args, **kwargs): + return '', args[0][1] + + @staticmethod + def _func_accounts_execute_account_backup_plan(*args, **kwargs): + asset_ids, pid = [], kwargs.get('pid') + with tmp_to_root_org(): + instance = get_object_or_none(AccountBackupAutomation, pk=pid) + if instance is not None: + asset_ids = Asset.objects.filter( + platform__type__in=instance.types + ).values_list('id', flat=True) + return '', asset_ids + + @staticmethod + def _func_assets_verify_accounts_connectivity(*args, **kwargs): + return '', args[0][1] + + @staticmethod + def _func_accounts_verify_accounts_connectivity(*args, **kwargs): + return '', args[0][1] + + @staticmethod + def _func_assets_test_assets_connectivity_manual(*args, **kwargs): + return '', args[0][0] + + @staticmethod + def _func_assets_test_node_assets_connectivity_manual(*args, **kwargs): + asset_ids = [] + node = get_object_or_none(Node, pk=args[0][0]) + if node is not None: + asset_ids = node.get_all_assets().values_list('id', flat=True) + return '', asset_ids + + @staticmethod + def _func_assets_update_assets_hardware_info_manual(*args, **kwargs): + return '', args[0][0] + + @staticmethod + def _func_assets_update_node_assets_hardware_info_manual(*args, **kwargs): + asset_ids = [] + node = get_object_or_none(Node, pk=args[0][0]) + if node is not None: + asset_ids = node.get_all_assets().values_list('id', flat=True) + return '', asset_ids + + def get_celery_task_info(self, task_name, *args, **kwargs): + task_display, resource_ids = self.get_info_by_task_name( + task_name, *args, **kwargs + ) + return task_display, resource_ids + + @staticmethod + def get_task_display(task_name, **kwargs): + task = app.tasks.get(task_name) + return getattr(task, 'verbose_name', _('Unknown')) + + def get_info_by_task_name(self, task_name, *args, **kwargs): + resource_ids = [] + task_name_list = str(task_name).split('.') + if len(task_name_list) < 2: + return '', resource_ids + + task_display = self.get_task_display(task_name) + model, name = task_name_list[0], task_name_list[-1] + func_name = '_func_%s_%s' % (model, name) + handle_func = getattr(self, func_name, None) + if handle_func is not None: + task_type, resource_ids = handle_func(*args, **kwargs) + if task_type: + task_display = '%s-%s' % (task_display, task_type) + return task_display, resource_ids + + @staticmethod + def session_for_activity(obj): + detail = _( + '{} used account[{}], login method[{}] login the asset.' + ).format( + obj.user, obj.account, obj.login_from_display + ) + return obj.asset_id, detail, ActivityChoices.session_log + + @staticmethod + def login_log_for_activity(obj): + login_status = _('Success') if obj.status else _('Failed') + detail = _('User {} login into this service.[{}]').format( + obj.username, login_status + ) + user_id = User.objects.filter(username=obj.username).values('id').first() + return user_id['id'], detail, ActivityChoices.login_log + + +activity_handler = ActivityLogHandler() + + +@signals.before_task_publish.connect +def before_task_publish_for_activity_log(headers=None, **kwargs): + task_id, task_name = headers.get('id'), headers.get('task') + args, kwargs = kwargs['body'][:2] + task_display, resource_ids = activity_handler.get_celery_task_info( + task_name, args, **kwargs + ) + activities = [] + detail = _('User %s performs a task(%s) for this resource.') % ( + getattr(current_request, 'user', None), task_display + ) + for resource_id in resource_ids: + activities.append( + ActivityLog( + resource_id=resource_id, type=ActivityChoices.task, detail=detail + ) + ) + ActivityLog.objects.bulk_create(activities) + + activity_info = { + 'activity_ids': [a.id for a in activities] + } + kwargs['activity_info'] = activity_info + + +@signals.task_prerun.connect +def on_celery_task_pre_run_for_activity_log(task_id='', **kwargs): + activity_info = kwargs['kwargs'].pop('activity_info', None) + if activity_info is None: + return + + activities = [] + for activity_id in activity_info['activity_ids']: + activities.append( + ActivityLog(id=activity_id, detail_id=task_id) + ) + ActivityLog.objects.bulk_update(activities, ('detail_id', )) + + +@post_save.connect +def on_object_created( + sender, instance=None, created=False, update_fields=None, **kwargs +): + handler_mapping = { + 'Session': activity_handler.session_for_activity, + 'UserLoginLog': activity_handler.login_log_for_activity + } + model_name = sender._meta.object_name + if not created or model_name not in handler_mapping.keys(): + return + + resource_id, detail, a_type = handler_mapping[model_name](instance) + + ActivityLog.objects.create( + resource_id=resource_id, type=a_type, + detail=detail, detail_id=instance.id + ) + + + diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py new file mode 100644 index 000000000..34e8665ac --- /dev/null +++ b/apps/audits/signal_handlers/login_log.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +from django.utils.functional import LazyObject +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.contrib.auth import BACKEND_SESSION_KEY +from django.dispatch import receiver +from django.utils import timezone, translation +from rest_framework.request import Request + +from authentication.signals import post_auth_failed, post_auth_success +from authentication.utils import check_different_city_login_if_need +from common.utils import get_request_ip, get_logger +from users.models import User + +from ..utils import write_login_log + + +logger = get_logger(__name__) + + +class AuthBackendLabelMapping(LazyObject): + @staticmethod + def get_login_backends(): + backend_label_mapping = {} + for source, backends in User.SOURCE_BACKEND_MAPPING.items(): + for backend in backends: + backend_label_mapping[backend] = source.label + backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _("SSH Key") + backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _("Password") + backend_label_mapping[settings.AUTH_BACKEND_SSO] = _("SSO") + backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") + backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") + backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") + return backend_label_mapping + + def _setup(self): + self._wrapped = self.get_login_backends() + + +AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() + + +def get_login_backend(request): + backend = request.session.get('auth_backend', '') or \ + request.session.get(BACKEND_SESSION_KEY, '') + + backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None) + if backend_label is None: + backend_label = '' + return backend_label + + +def generate_data(username, request, login_type=None): + user_agent = request.META.get('HTTP_USER_AGENT', '') + login_ip = get_request_ip(request) or '0.0.0.0' + + if login_type is None and isinstance(request, Request): + login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') + if login_type is None: + login_type = 'W' + + with translation.override('en'): + backend = str(get_login_backend(request)) + + data = { + 'username': username, + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent[0:254], + 'datetime': timezone.now(), + 'backend': backend, + } + return data + + +@receiver(post_auth_success) +def on_user_auth_success(sender, user, request, login_type=None, **kwargs): + logger.debug('User login success: {}'.format(user.username)) + check_different_city_login_if_need(user, request) + data = generate_data( + user.username, request, login_type=login_type + ) + request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") + data.update({'mfa': int(user.mfa_enabled), 'status': True}) + write_login_log(**data) + + +@receiver(post_auth_failed) +def on_user_auth_failed(sender, username, request, reason='', **kwargs): + logger.debug('User login failed: {}'.format(username)) + data = generate_data(username, request) + data.update({'reason': reason[:128], 'status': False}) + write_login_log(**data) diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py new file mode 100644 index 000000000..0c0fb9f48 --- /dev/null +++ b/apps/audits/signal_handlers/operate_log.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +import uuid + +from django.apps import apps +from django.dispatch import receiver +from django.db.models.signals import post_save, pre_save, m2m_changed, pre_delete + +from audits.handler import ( + get_instance_current_with_cache_diff, cache_instance_before_data, + create_or_update_operate_log, get_instance_dict_from_cache +) +from audits.utils import model_to_dict_for_operate_log as model_to_dict +from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL +from common.signals import django_ready + +from ..const import MODELS_NEED_RECORD, ActionChoices + + +M2M_ACTION = { + POST_ADD: ActionChoices.create, + POST_REMOVE: ActionChoices.delete, + POST_CLEAR: ActionChoices.delete, +} + + +@receiver(m2m_changed) +def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs): + if action not in M2M_ACTION: + return + if not instance: + return + + resource_type = instance._meta.verbose_name + current_instance = model_to_dict(instance, include_model_fields=False) + + instance_id = current_instance.get('id') + log_id, before_instance = get_instance_dict_from_cache(instance_id) + + field_name = str(model._meta.verbose_name) + objs = model.objects.filter(pk__in=pk_set) + objs_display = [str(o) for o in objs] + action = M2M_ACTION[action] + changed_field = current_instance.get(field_name, []) + + after, before, before_value = None, None, None + if action == ActionChoices.create: + before_value = list(set(changed_field) - set(objs_display)) + elif action == ActionChoices.delete: + before_value = list( + set(changed_field).symmetric_difference(set(objs_display)) + ) + + if changed_field: + after = {field_name: changed_field} + if before_value: + before = {field_name: before_value} + + if sorted(str(before)) == sorted(str(after)): + return + + create_or_update_operate_log( + ActionChoices.update, resource_type, + resource=instance, log_id=log_id, before=before, after=after + ) + + +def signal_of_operate_log_whether_continue( + sender, instance, created, update_fields=None +): + condition = True + if not instance: + condition = False + if instance and getattr(instance, SKIP_SIGNAL, False): + condition = False + # 终端模型的 create 事件由系统产生,不记录 + if instance._meta.object_name == 'Terminal' and created: + condition = False + # last_login 改变是最后登录日期, 每次登录都会改变 + if instance._meta.object_name == 'User' and \ + update_fields and 'last_login' in update_fields: + condition = False + # 不在记录白名单中,跳过 + if sender._meta.object_name not in MODELS_NEED_RECORD: + condition = False + return condition + + +@receiver(pre_save) +def on_object_pre_create_or_update( + sender, instance=None, raw=False, using=None, update_fields=None, **kwargs +): + ok = signal_of_operate_log_whether_continue( + sender, instance, False, update_fields + ) + if not ok: + return + + # users.PrivateToken Model 没有 id 有 pk字段 + instance_id = getattr(instance, 'id', getattr(instance, 'pk', None)) + instance_before_data = {'id': instance_id} + raw_instance = type(instance).objects.filter(pk=instance_id).first() + + if raw_instance: + instance_before_data = model_to_dict(raw_instance) + operate_log_id = str(uuid.uuid4()) + instance_before_data['operate_log_id'] = operate_log_id + setattr(instance, 'operate_log_id', operate_log_id) + cache_instance_before_data(instance_before_data) + + +@receiver(post_save) +def on_object_created_or_update( + sender, instance=None, created=False, update_fields=None, **kwargs +): + ok = signal_of_operate_log_whether_continue( + sender, instance, created, update_fields + ) + if not ok: + return + + log_id, before, after = None, None, None + if created: + action = ActionChoices.create + after = model_to_dict(instance) + log_id = getattr(instance, 'operate_log_id', None) + else: + action = ActionChoices.update + current_instance = model_to_dict(instance) + log_id, before, after = get_instance_current_with_cache_diff(current_instance) + + resource_type = sender._meta.verbose_name + object_name = sender._meta.object_name + create_or_update_operate_log( + action, resource_type, resource=instance, log_id=log_id, + before=before, after=after, object_name=object_name + ) + + +@receiver(pre_delete) +def on_object_delete(sender, instance=None, **kwargs): + ok = signal_of_operate_log_whether_continue(sender, instance, False) + if not ok: + return + + resource_type = sender._meta.verbose_name + create_or_update_operate_log( + ActionChoices.delete, resource_type, + resource=instance, before=model_to_dict(instance) + ) + + +@receiver(django_ready) +def on_django_start_set_operate_log_monitor_models(sender, **kwargs): + exclude_apps = { + 'django_cas_ng', 'captcha', 'admin', 'jms_oidc_rp', 'audits', + 'django_celery_beat', 'contenttypes', 'sessions', 'auth', + } + exclude_models = { + 'UserPasswordHistory', 'ContentType', + 'MessageContent', 'SiteMessage', + 'PlatformAutomation', 'PlatformProtocol', 'Protocol', + 'HistoricalAccount', 'GatheredUser', 'ApprovalRule', + 'BaseAutomation', 'CeleryTask', 'Command', 'JobAuditLog', + 'ConnectionToken', 'SessionJoinRecord', + 'HistoricalJob', 'Status', 'TicketStep', 'Ticket', + 'UserAssetGrantedTreeNodeRelation', 'TicketAssignee', + 'SuperTicket', 'SuperConnectionToken', 'PermNode', + 'PermedAsset', 'PermedAccount', 'MenuPermission', + 'Permission', 'TicketSession', 'ApplyLoginTicket', + 'ApplyCommandTicket', 'ApplyLoginAssetTicket', + } + for i, app in enumerate(apps.get_models(), 1): + app_name = app._meta.app_label + model_name = app._meta.object_name + if app_name in exclude_apps or \ + model_name in exclude_models or \ + model_name.endswith('Execution'): + continue + MODELS_NEED_RECORD.add(model_name) diff --git a/apps/audits/signal_handlers/other.py b/apps/audits/signal_handlers/other.py new file mode 100644 index 000000000..07d3694fe --- /dev/null +++ b/apps/audits/signal_handlers/other.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +from django.dispatch import receiver +from django.db import transaction + +from audits.models import ( + PasswordChangeLog, UserLoginLog, FTPLog, OperateLog +) +from audits.serializers import ( + UserLoginLogSerializer, FTPLogSerializer, OperateLogSerializer, + PasswordChangeLogSerializer +) +from common.utils import get_request_ip, get_syslogger +from common.utils.encode import data_to_json +from jumpserver.utils import current_request +from users.models import User +from users.signals import post_user_change_password +from terminal.models import Session, Command +from terminal.serializers import SessionSerializer, SessionCommandSerializer + + +sys_logger = get_syslogger(__name__) + + +@receiver(post_user_change_password, sender=User) +def on_user_change_password(sender, user=None, **kwargs): + if not current_request: + remote_addr = '127.0.0.1' + change_by = 'System' + else: + remote_addr = get_request_ip(current_request) + if not current_request.user.is_authenticated: + change_by = str(user) + else: + change_by = str(current_request.user) + with transaction.atomic(): + PasswordChangeLog.objects.create( + user=str(user), change_by=change_by, + remote_addr=remote_addr, + ) + + +def on_audits_log_create(sender, instance=None, **kwargs): + if sender == UserLoginLog: + category = "login_log" + serializer_cls = UserLoginLogSerializer + elif sender == FTPLog: + category = "ftp_log" + serializer_cls = FTPLogSerializer + elif sender == OperateLog: + category = "operation_log" + serializer_cls = OperateLogSerializer + elif sender == PasswordChangeLog: + category = "password_change_log" + serializer_cls = PasswordChangeLogSerializer + elif sender == Session: + category = "host_session_log" + serializer_cls = SessionSerializer + elif sender == Command: + category = "session_command_log" + serializer_cls = SessionCommandSerializer + else: + return + + serializer = serializer_cls(instance) + data = data_to_json(serializer.data, indent=None) + msg = "{} - {}".format(category, data) + sys_logger.info(msg) diff --git a/apps/audits/signals.py b/apps/audits/signals.py deleted file mode 100644 index 229c63933..000000000 --- a/apps/audits/signals.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.dispatch import Signal - - -post_activity_log = Signal( - providing_args=('resource_id', 'detail', 'detail_url') -) diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 851ddae3b..3ff42177c 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -4,13 +4,10 @@ from itertools import chain from django.db import models from django.http import HttpResponse -from django.utils.translation import gettext_lazy as _ -from common.utils import validate_ip, get_ip_city, get_logger, reverse -from audits.const import ActivityChoices +from common.utils import validate_ip, get_ip_city, get_logger from settings.serializers import SettingsSerializer from .const import DEFAULT_CITY -from .signals import post_activity_log logger = get_logger(__name__) @@ -44,24 +41,7 @@ def write_login_log(*args, **kwargs): else: city = get_ip_city(ip) or DEFAULT_CITY kwargs.update({'ip': ip, 'city': city}) - user_id = kwargs.pop('user_id', None) - audit_log = UserLoginLog.objects.create(**kwargs) - - # 发送Activity信号 - if user_id is not None: - login_status = _('Success') if audit_log.status else _('Failed') - detail = _('User {} login into this service.[{}]').format( - audit_log.username, login_status - ) - detail_url = '%s?id=%s' % ( - reverse('api-audits:login-log-list', api_to_ui=True, is_audit=True,), - audit_log.id - ) - post_activity_log.send( - sender=UserLoginLog, resource_id=user_id, - detail=detail, detail_url=detail_url, - type=ActivityChoices.login_log - ) + UserLoginLog.objects.create(**kwargs) def get_resource_display(resource): diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 1284ae8ed..6f4f2863f 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -15,8 +15,6 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from audits.signals import post_activity_log -from audits.const import ActivityChoices from common.const.http import GET from common.drf.filters import DatetimeRangeFilter from common.drf.renders import PassthroughRenderer @@ -122,30 +120,10 @@ class SessionViewSet(OrgBulkModelViewSet): queryset = queryset.select_for_update() return queryset - @staticmethod - def send_activity(serializer): - # 发送Activity信号 - data = serializer.validated_data - user, asset_id = data['user'], data["asset_id"] - account, login_from = data['account'], data["login_from"] - login_from = Session(login_from=login_from).get_login_from_display() - detail = _( - '{} used account[{}], login method[{}] login the asset.' - ).format( - user, account, login_from - ) - post_activity_log.send( - sender=Session, resource_id=asset_id, detail=detail, - type=ActivityChoices.session_log - ) - def perform_create(self, serializer): if hasattr(self.request.user, 'terminal'): serializer.validated_data["terminal"] = self.request.user.terminal - - resp = super().perform_create(serializer) - self.send_activity(serializer) - return resp + return super().perform_create(serializer) class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):