perf: optimize user operation logs (#13221)

This commit is contained in:
jiangweidong 2024-05-31 11:05:35 +08:00 committed by GitHub
parent cdfb11549e
commit dfd133cf5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 38 additions and 10 deletions

View File

@ -52,7 +52,7 @@ class OperateLogStore(object):
resource_map = { resource_map = {
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v 'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
} }
return resource_map.get(resource_type, lambda k, v: v) return resource_map.get(resource_type, lambda k, v: _(v))
@classmethod @classmethod
def convert_diff_friendly(cls, op_log): def convert_diff_friendly(cls, op_log):

View File

@ -37,6 +37,9 @@ class ActionChoices(TextChoices):
approve = 'approve', _('Approve') approve = 'approve', _('Approve')
close = 'close', _('Close') close = 'close', _('Close')
# Custom action
finished = 'finished', _('Finished')
class LoginTypeChoices(TextChoices): class LoginTypeChoices(TextChoices):
web = "W", _("Web") web = "W", _("Web")

View File

@ -58,7 +58,7 @@ class OperatorLogHandler(metaclass=Singleton):
return return
key = '%s_%s' % (self.CACHE_KEY, instance_id) key = '%s_%s' % (self.CACHE_KEY, instance_id)
cache.set(key, instance_dict, 3 * 60) cache.set(key, instance_dict, 3)
def get_instance_dict_from_cache(self, instance_id): def get_instance_dict_from_cache(self, instance_id):
if instance_id is None: if instance_id is None:

View File

@ -257,6 +257,8 @@ class UserLoginLog(models.Model):
class UserSession(models.Model): class UserSession(models.Model):
_OPERATE_LOG_ACTION = {'delete': ActionChoices.finished}
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(verbose_name=_("Login IP")) ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
key = models.CharField(max_length=128, verbose_name=_("Session key")) key = models.CharField(max_length=128, verbose_name=_("Session key"))

View File

@ -3,7 +3,9 @@
import uuid import uuid
from django.apps import apps from django.apps import apps
from django.db.models.signals import post_save, pre_save, m2m_changed, pre_delete from django.db.models.signals import (
pre_delete, pre_save, m2m_changed, post_delete, post_save
)
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import translation from django.utils import translation
@ -94,7 +96,7 @@ def signal_of_operate_log_whether_continue(
return condition return condition
@receiver(pre_save) @receiver([pre_save, pre_delete])
def on_object_pre_create_or_update( def on_object_pre_create_or_update(
sender, instance=None, raw=False, using=None, update_fields=None, **kwargs sender, instance=None, raw=False, using=None, update_fields=None, **kwargs
): ):
@ -103,6 +105,7 @@ def on_object_pre_create_or_update(
) )
if not ok: if not ok:
return return
with translation.override('en'): with translation.override('en'):
# users.PrivateToken Model 没有 id 有 pk字段 # users.PrivateToken Model 没有 id 有 pk字段
instance_id = getattr(instance, 'id', getattr(instance, 'pk', None)) instance_id = getattr(instance, 'id', getattr(instance, 'pk', None))
@ -145,7 +148,7 @@ def on_object_created_or_update(
) )
@receiver(pre_delete) @receiver(post_delete)
def on_object_delete(sender, instance=None, **kwargs): def on_object_delete(sender, instance=None, **kwargs):
ok = signal_of_operate_log_whether_continue(sender, instance, False) ok = signal_of_operate_log_whether_continue(sender, instance, False)
if not ok: if not ok:
@ -153,9 +156,15 @@ def on_object_delete(sender, instance=None, **kwargs):
with translation.override('en'): with translation.override('en'):
resource_type = sender._meta.verbose_name resource_type = sender._meta.verbose_name
action = getattr(sender, '_OPERATE_LOG_ACTION', {})
action = action.get('delete', ActionChoices.delete)
instance_id = getattr(instance, 'id', getattr(instance, 'pk', None))
log_id, before = get_instance_dict_from_cache(instance_id)
if not log_id:
log_id, before = None, model_to_dict(instance)
create_or_update_operate_log( create_or_update_operate_log(
ActionChoices.delete, resource_type, action, resource_type, log_id=log_id,
resource=instance, before=model_to_dict(instance) resource=instance, before=before,
) )
@ -166,7 +175,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'django_celery_beat', 'contenttypes', 'sessions', 'auth', 'django_celery_beat', 'contenttypes', 'sessions', 'auth',
} }
exclude_models = { exclude_models = {
'UserPasswordHistory', 'ContentType', 'UserPasswordHistory', 'ContentType', 'Asset',
'MessageContent', 'SiteMessage', 'MessageContent', 'SiteMessage',
'PlatformAutomation', 'PlatformProtocol', 'Protocol', 'PlatformAutomation', 'PlatformProtocol', 'Protocol',
'HistoricalAccount', 'GatheredUser', 'ApprovalRule', 'HistoricalAccount', 'GatheredUser', 'ApprovalRule',
@ -180,11 +189,13 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'ApplyCommandTicket', 'ApplyLoginAssetTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'FavoriteAsset',
} }
include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1): for i, app in enumerate(apps.get_models(), 1):
app_name = app._meta.app_label app_name = app._meta.app_label
model_name = app._meta.object_name model_name = app._meta.object_name
if app_name in exclude_apps or \ if app_name in exclude_apps or \
model_name in exclude_models or \ model_name in exclude_models or \
model_name.endswith('Execution'): model_name.endswith('Execution'):
continue if model_name not in include_models:
continue
MODELS_NEED_RECORD.add(model_name) MODELS_NEED_RECORD.add(model_name)

View File

@ -49,9 +49,15 @@ def _get_instance_field_value(
continue continue
value = getattr(instance, f.name, None) or getattr(instance, f.attname, None) value = getattr(instance, f.name, None) or getattr(instance, f.attname, None)
if not isinstance(value, bool) and not value: if not isinstance(value, (bool, int)) and not value:
continue continue
choices = getattr(f, 'choices', []) or []
for c_value, c_label in choices:
if c_value == value:
value = c_label
break
if getattr(f, 'primary_key', False): if getattr(f, 'primary_key', False):
f.verbose_name = 'id' f.verbose_name = 'id'
elif isinstance(value, list): elif isinstance(value, list):

View File

@ -75,6 +75,7 @@ class AuthMixin:
if self.can_update_ssh_key(): if self.can_update_ssh_key():
self.public_key = public_key self.public_key = public_key
self.save() self.save()
post_user_change_password.send(self.__class__, user=self)
def can_update_password(self): def can_update_password(self):
return self.is_local return self.is_local

View File

@ -17,6 +17,7 @@ from orgs.utils import current_org
from rbac.builtin import BuiltinRole from rbac.builtin import BuiltinRole
from rbac.models import OrgRoleBinding, SystemRoleBinding, Role from rbac.models import OrgRoleBinding, SystemRoleBinding, Role
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from users.signals import post_user_change_password
from ..const import PasswordStrategy from ..const import PasswordStrategy
from ..models import User from ..models import User
@ -268,6 +269,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa
instance = self.save_and_set_custom_m2m_fields( instance = self.save_and_set_custom_m2m_fields(
validated_data, save_handler, created=False validated_data, save_handler, created=False
) )
if validated_data.get('public_key'):
post_user_change_password.send(instance.__class__, user=instance)
return instance return instance
def create(self, validated_data): def create(self, validated_data):
@ -275,6 +278,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa
instance = self.save_and_set_custom_m2m_fields( instance = self.save_and_set_custom_m2m_fields(
validated_data, save_handler, created=True validated_data, save_handler, created=True
) )
if validated_data.get('public_key'):
post_user_change_password.send(instance.__class__, user=instance)
return instance return instance
@classmethod @classmethod