mirror of https://github.com/jumpserver/jumpserver
feat: 重构操作日志 (#8941)
* feat:重构操作日志模块 * feat: 改密计划增加操作日志记录 * feat: 支持操作日志接入ES,且接口limit支持自定义限制大小 * feat:翻译 * feat: 生成迁移文件 * feat: 优化迁移文件 * feat: 优化多对多日志记录 * feat: 命令存储ES部分和日志存储ES部分代码优化 * feat: 优化敏感字段脱敏 Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>pull/8891/head^2
parent
1e97a23bc5
commit
2029e9f8df
|
@ -21,8 +21,8 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(max_length=64, verbose_name='Name')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -50,8 +50,8 @@ class CommandFilter(OrgModelMixin):
|
|||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated'))
|
||||
created_by = models.CharField(
|
||||
max_length=128, blank=True, default='', verbose_name=_('Created by')
|
||||
)
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin
|
||||
from importlib import import_module
|
||||
|
||||
from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.conf import settings
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework import generics
|
||||
|
||||
from common.drf.api import JMSReadOnlyModelViewSet
|
||||
from common.plugins.es import QuerySet as ESQuerySet
|
||||
from common.drf.filters import DatetimeRangeFilter
|
||||
from common.api import CommonGenericViewSet
|
||||
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from . import filters
|
||||
from .backends import TYPE_ENGINE_MAPPING
|
||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
|
||||
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
|
||||
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
|
||||
from .serializers import (
|
||||
OperateLogSerializer, OperateLogActionDetailSerializer,
|
||||
PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
|
||||
)
|
||||
|
||||
|
||||
class FTPLogViewSet(CreateModelMixin,
|
||||
|
@ -68,7 +76,7 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView):
|
|||
return qs
|
||||
|
||||
|
||||
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet):
|
||||
model = OperateLog
|
||||
serializer_class = OperateLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
|
@ -79,6 +87,22 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
|
|||
search_fields = ['resource']
|
||||
ordering = ['-datetime']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.query_params.get('type') == 'action_detail':
|
||||
return OperateLogActionDetailSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
qs = OperateLog.objects.all()
|
||||
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
|
||||
if es_config:
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
|
||||
store = engine_mod.OperateLogStore(es_config)
|
||||
if store.ping(timeout=2):
|
||||
qs = ESQuerySet(store)
|
||||
qs.model = OperateLog
|
||||
return qs
|
||||
|
||||
|
||||
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
queryset = PasswordChangeLog.objects.all()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
TYPE_ENGINE_MAPPING = {
|
||||
'db': 'audits.backends.db',
|
||||
'es': 'audits.backends.es',
|
||||
}
|
||||
|
||||
|
||||
def get_operate_log_storage(default=False):
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING['db'])
|
||||
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
|
||||
if not default and es_config:
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
|
||||
storage = engine_mod.OperateLogStore(es_config)
|
||||
return storage
|
|
@ -0,0 +1,38 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from audits.models import OperateLog
|
||||
|
||||
|
||||
class OperateLogStore(object):
|
||||
def __init__(self, config):
|
||||
self.model = OperateLog
|
||||
self.max_length = 1024
|
||||
self.max_length_tip_msg = _(
|
||||
'The text content is too long. Use Elasticsearch to store operation logs'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ping(timeout=None):
|
||||
return True
|
||||
|
||||
def save(self, **kwargs):
|
||||
log_id = kwargs.get('id', '')
|
||||
before = kwargs.get('before') or {}
|
||||
after = kwargs.get('after') or {}
|
||||
if len(str(before)) > self.max_length:
|
||||
before = {_('Tips'): self.max_length_tip_msg}
|
||||
if len(str(after)) > self.max_length:
|
||||
after = {_('Tips'): self.max_length_tip_msg}
|
||||
|
||||
op_log = self.model.objects.filter(pk=log_id).first()
|
||||
if op_log is not None:
|
||||
raw_after = op_log.after or {}
|
||||
raw_before = op_log.before or {}
|
||||
raw_before.update(before)
|
||||
raw_after.update(after)
|
||||
op_log.before = raw_before
|
||||
op_log.after = raw_after
|
||||
op_log.save()
|
||||
else:
|
||||
self.model.objects.create(**kwargs)
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
|
||||
from common.utils.timezone import local_now_display
|
||||
from common.utils import get_logger
|
||||
from common.utils.encode import Singleton
|
||||
from common.plugins.es import ES
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OperateLogStore(ES, metaclass=Singleton):
|
||||
def __init__(self, config):
|
||||
properties = {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"user": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"action": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"resource_type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"org_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "date",
|
||||
"format": "yyyy-MM-dd HH:mm:ss"
|
||||
}
|
||||
}
|
||||
exact_fields = {}
|
||||
match_fields = {
|
||||
'id', 'user', 'action', 'resource_type',
|
||||
'resource', 'remote_addr', 'org_id'
|
||||
}
|
||||
keyword_fields = {
|
||||
'id', 'user', 'action', 'resource_type', 'org_id'
|
||||
}
|
||||
if not config.get('INDEX'):
|
||||
config['INDEX'] = 'jumpserver_operate_log'
|
||||
super().__init__(config, properties, keyword_fields, exact_fields, match_fields)
|
||||
self.pre_use_check()
|
||||
|
||||
@staticmethod
|
||||
def make_data(data):
|
||||
op_id = data.get('id', str(uuid.uuid4()))
|
||||
datetime_param = data.get('datetime', local_now_display())
|
||||
data = {
|
||||
'id': op_id, 'user': data['user'], 'action': data['action'],
|
||||
'resource_type': data['resource_type'], 'resource': data['resource'],
|
||||
'remote_addr': data['remote_addr'], 'datetime': datetime_param,
|
||||
'before': data['before'], 'after': data['after'], 'org_id': data['org_id']
|
||||
}
|
||||
return data
|
||||
|
||||
def save(self, **kwargs):
|
||||
log_id = kwargs.get('id', '')
|
||||
before = kwargs.get('before') or {}
|
||||
after = kwargs.get('after') or {}
|
||||
|
||||
op_log = self.get({'id': log_id})
|
||||
if op_log is not None:
|
||||
data = {'doc': {}}
|
||||
raw_after = op_log.get('after') or {}
|
||||
raw_before = op_log.get('before') or {}
|
||||
raw_before.update(before)
|
||||
raw_after.update(after)
|
||||
data['doc']['before'] = raw_before
|
||||
data['doc']['after'] = raw_after
|
||||
self.es.update(
|
||||
index=self.index, doc_type=self.doc_type,
|
||||
id=op_log.get('es_id'), body=data, refresh=True
|
||||
)
|
||||
else:
|
||||
data = self.make_data(kwargs)
|
||||
self.es.index(
|
||||
index=self.index, doc_type=self.doc_type, body=data,
|
||||
refresh=True
|
||||
)
|
|
@ -7,11 +7,13 @@ DEFAULT_CITY = _("Unknown")
|
|||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# authentication
|
||||
'AccessKey', 'TempToken',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
'CommandFilter', 'Platform', 'Label',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
|
@ -20,6 +22,13 @@ MODELS_NEED_RECORD = (
|
|||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# notifications
|
||||
'SystemMsgSubscription', 'UserMsgSubscription',
|
||||
# Terminal
|
||||
'Terminal', 'Endpoint', 'EndpointRule', 'CommandStorage', 'ReplayStorage',
|
||||
# rbac
|
||||
'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'ApplicationChangeAuthPlan',
|
||||
'GatherUserTask', 'Interface',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_request_ip, get_logger
|
||||
from common.utils.timezone import as_current_tz
|
||||
from common.utils.encode import Singleton
|
||||
from common.local import encrypted_field_set
|
||||
from settings.serializers import SettingsSerializer
|
||||
from jumpserver.utils import current_request
|
||||
from audits.models import OperateLog
|
||||
from orgs.utils import get_current_org_id
|
||||
|
||||
from .backends import get_operate_log_storage
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ModelClient:
|
||||
@staticmethod
|
||||
def save(**kwargs):
|
||||
log_id = kwargs.get('id', '')
|
||||
op_log = OperateLog.objects.filter(pk=log_id).first()
|
||||
if op_log is not None:
|
||||
raw_after = op_log.after or {}
|
||||
raw_before = op_log.before or {}
|
||||
cur_before = kwargs.get('before') or {}
|
||||
cur_after = kwargs.get('after') or {}
|
||||
raw_before.update(cur_before)
|
||||
raw_after.update(cur_after)
|
||||
op_log.before = raw_before
|
||||
op_log.after = raw_after
|
||||
op_log.save()
|
||||
else:
|
||||
OperateLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
class OperatorLogHandler(metaclass=Singleton):
|
||||
CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY'
|
||||
|
||||
def __init__(self):
|
||||
self.log_client = self.get_storage_client()
|
||||
|
||||
@staticmethod
|
||||
def get_storage_client():
|
||||
client = get_operate_log_storage()
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def _consistent_type_to_str(value1, value2):
|
||||
if isinstance(value1, datetime):
|
||||
value1 = as_current_tz(value1).strftime('%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(value2, datetime):
|
||||
value2 = as_current_tz(value2).strftime('%Y-%m-%d %H:%M:%S')
|
||||
return value1, value2
|
||||
|
||||
def _look_for_two_dict_change(self, left_dict, right_dict):
|
||||
# 以右边的字典为基础
|
||||
before, after = {}, {}
|
||||
for key, value in right_dict.items():
|
||||
pre_value = left_dict.get(key, '')
|
||||
pre_value, value = self._consistent_type_to_str(pre_value, value)
|
||||
if sorted(str(value)) == sorted(str(pre_value)):
|
||||
continue
|
||||
if pre_value:
|
||||
before[key] = pre_value
|
||||
if value:
|
||||
after[key] = value
|
||||
return before, after
|
||||
|
||||
def cache_instance_before_data(self, instance_dict):
|
||||
instance_id = instance_dict.get('id')
|
||||
if instance_id is None:
|
||||
return
|
||||
|
||||
key = '%s_%s' % (self.CACHE_KEY, instance_id)
|
||||
cache.set(key, instance_dict, 3 * 60)
|
||||
|
||||
def get_instance_dict_from_cache(self, instance_id):
|
||||
if instance_id is None:
|
||||
return None
|
||||
|
||||
key = '%s_%s' % (self.CACHE_KEY, instance_id)
|
||||
cache_instance = cache.get(key, {})
|
||||
log_id = cache_instance.get('operate_log_id')
|
||||
return log_id, cache_instance
|
||||
|
||||
def get_instance_current_with_cache_diff(self, current_instance):
|
||||
log_id, before, after = None, None, None
|
||||
instance_id = current_instance.get('id')
|
||||
if instance_id is None:
|
||||
return log_id, before, after
|
||||
|
||||
log_id, cache_instance = self.get_instance_dict_from_cache(instance_id)
|
||||
if not cache_instance:
|
||||
return log_id, before, after
|
||||
|
||||
before, after = self._look_for_two_dict_change(
|
||||
cache_instance, current_instance
|
||||
)
|
||||
return log_id, before, after
|
||||
|
||||
@staticmethod
|
||||
def get_resource_display_from_setting(resource):
|
||||
resource_display = None
|
||||
setting_serializer = SettingsSerializer()
|
||||
label = setting_serializer.get_field_label(resource)
|
||||
if label is not None:
|
||||
resource_display = label
|
||||
return resource_display
|
||||
|
||||
def get_resource_display(self, resource):
|
||||
resource_display = str(resource)
|
||||
return_value = self.get_resource_display_from_setting(resource_display)
|
||||
if return_value is not None:
|
||||
resource_display = return_value
|
||||
return resource_display
|
||||
|
||||
def __data_processing(self, dict_item, loop=True):
|
||||
encrypt_value = '******'
|
||||
for key, value in dict_item.items():
|
||||
if isinstance(value, bool):
|
||||
value = _('Yes') if value else _('No')
|
||||
elif isinstance(value, (list, tuple)):
|
||||
value = ','.join(value)
|
||||
elif isinstance(value, dict) and loop:
|
||||
self.__data_processing(value, loop=False)
|
||||
if key in encrypted_field_set:
|
||||
value = encrypt_value
|
||||
dict_item[key] = value
|
||||
return dict_item
|
||||
|
||||
def data_processing(self, before, after):
|
||||
if before:
|
||||
before = self.__data_processing(before)
|
||||
if after:
|
||||
after = self.__data_processing(after)
|
||||
return before, after
|
||||
|
||||
def create_or_update_operate_log(
|
||||
self, action, resource_type, resource=None,
|
||||
force=False, log_id=None, before=None, after=None
|
||||
):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
|
||||
remote_addr = get_request_ip(current_request)
|
||||
resource_display = self.get_resource_display(resource)
|
||||
before, after = self.data_processing(before, after)
|
||||
if not force and not any([before, after]):
|
||||
# 前后都没变化,没必要生成日志,除非手动强制保存
|
||||
return
|
||||
|
||||
data = {
|
||||
'id': log_id, "user": str(user), 'action': action,
|
||||
'resource_type': str(resource_type), 'resource': resource_display,
|
||||
'remote_addr': remote_addr, 'before': before, 'after': after,
|
||||
'org_id': get_current_org_id(),
|
||||
}
|
||||
with transaction.atomic():
|
||||
if self.log_client.ping(timeout=1):
|
||||
client = self.log_client
|
||||
else:
|
||||
logger.info('Switch default operate log storage save.')
|
||||
client = get_operate_log_storage(default=True)
|
||||
|
||||
try:
|
||||
client.save(**data)
|
||||
except Exception as e:
|
||||
error_msg = 'An error occurred saving OperateLog.' \
|
||||
'Error: %s, Data: %s' % (e, data)
|
||||
logger.error(error_msg)
|
||||
|
||||
|
||||
op_handler = OperatorLogHandler()
|
||||
create_or_update_operate_log = op_handler.create_or_update_operate_log
|
||||
cache_instance_before_data = op_handler.cache_instance_before_data
|
||||
get_instance_current_with_cache_diff = op_handler.get_instance_current_with_cache_diff
|
||||
get_instance_dict_from_cache = op_handler.get_instance_dict_from_cache
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.14 on 2022-10-11 09:45
|
||||
|
||||
import common.db.encoder
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0014_auto_20220505_1902'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='operatelog',
|
||||
name='after',
|
||||
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='operatelog',
|
||||
name='before',
|
||||
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True),
|
||||
),
|
||||
]
|
|
@ -4,8 +4,9 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
from django.utils.translation import gettext, ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from common.utils import lazyproperty
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from orgs.mixins.models import OrgModelMixin, Organization
|
||||
from orgs.utils import current_org
|
||||
|
||||
|
@ -65,6 +66,8 @@ class OperateLog(OrgModelMixin):
|
|||
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
|
||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True)
|
||||
before = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True)
|
||||
after = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||
|
@ -78,6 +81,21 @@ class OperateLog(OrgModelMixin):
|
|||
self.org_id = Organization.ROOT_ID
|
||||
return super(OperateLog, self).save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
self = cls()
|
||||
for k, v in d.items():
|
||||
setattr(self, k, v)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_multi_dict(cls, l):
|
||||
operate_logs = []
|
||||
for d in l:
|
||||
operate_log = cls.from_dict(d)
|
||||
operate_logs.append(operate_log)
|
||||
return operate_logs
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Operate log")
|
||||
|
||||
|
|
|
@ -47,6 +47,12 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class OperateLogActionDetailSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.OperateLog
|
||||
fields = ('before', 'after')
|
||||
|
||||
|
||||
class OperateLogSerializer(serializers.ModelSerializer):
|
||||
action_display = serializers.CharField(source='get_action_display', label=_('Action'))
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete
|
||||
post_save, m2m_changed, pre_delete, pre_save
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
|
@ -16,24 +16,32 @@ from django.utils import translation
|
|||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.request import Request
|
||||
|
||||
from assets.models import Asset, SystemUser
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser, CommandFilter
|
||||
from terminal.models import Session, Command
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from rbac.models import Role
|
||||
|
||||
from audits.utils import model_to_dict_for_operate_log as model_to_dict
|
||||
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 authentication.signals import post_auth_failed, post_auth_success
|
||||
from authentication.utils import check_different_city_login_if_need
|
||||
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 .utils import write_login_log, create_operate_log
|
||||
from .utils import write_login_log
|
||||
from . import models, serializers
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from .const import MODELS_NEED_RECORD
|
||||
from terminal.backends.command.serializers import SessionCommandSerializer
|
||||
from terminal.serializers import SessionSerializer
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
|
||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||
from common.utils.encode import data_to_json
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
sys_logger = get_syslogger(__name__)
|
||||
json_render = JSONRenderer()
|
||||
|
@ -62,70 +70,6 @@ class AuthBackendLabelMapping(LazyObject):
|
|||
|
||||
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
|
||||
|
||||
|
||||
M2M_NEED_RECORD = {
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
_('{User} JOINED {UserGroup}'),
|
||||
_('{User} LEFT {UserGroup}')
|
||||
),
|
||||
SystemUser.assets.through._meta.object_name: (
|
||||
_('Asset and SystemUser'),
|
||||
_('{Asset} ADD {SystemUser}'),
|
||||
_('{Asset} REMOVE {SystemUser}')
|
||||
),
|
||||
Asset.nodes.through._meta.object_name: (
|
||||
_('Node and Asset'),
|
||||
_('{Node} ADD {Asset}'),
|
||||
_('{Node} REMOVE {Asset}')
|
||||
),
|
||||
AssetPermission.users.through._meta.object_name: (
|
||||
_('User asset permissions'),
|
||||
_('{AssetPermission} ADD {User}'),
|
||||
_('{AssetPermission} REMOVE {User}'),
|
||||
),
|
||||
AssetPermission.user_groups.through._meta.object_name: (
|
||||
_('User group asset permissions'),
|
||||
_('{AssetPermission} ADD {UserGroup}'),
|
||||
_('{AssetPermission} REMOVE {UserGroup}'),
|
||||
),
|
||||
AssetPermission.assets.through._meta.object_name: (
|
||||
_('Asset permission'),
|
||||
_('{AssetPermission} ADD {Asset}'),
|
||||
_('{AssetPermission} REMOVE {Asset}'),
|
||||
),
|
||||
AssetPermission.nodes.through._meta.object_name: (
|
||||
_('Node permission'),
|
||||
_('{AssetPermission} ADD {Node}'),
|
||||
_('{AssetPermission} REMOVE {Node}'),
|
||||
),
|
||||
AssetPermission.system_users.through._meta.object_name: (
|
||||
_('Asset permission and SystemUser'),
|
||||
_('{AssetPermission} ADD {SystemUser}'),
|
||||
_('{AssetPermission} REMOVE {SystemUser}'),
|
||||
),
|
||||
ApplicationPermission.users.through._meta.object_name: (
|
||||
_('User application permissions'),
|
||||
_('{ApplicationPermission} ADD {User}'),
|
||||
_('{ApplicationPermission} REMOVE {User}'),
|
||||
),
|
||||
ApplicationPermission.user_groups.through._meta.object_name: (
|
||||
_('User group application permissions'),
|
||||
_('{ApplicationPermission} ADD {UserGroup}'),
|
||||
_('{ApplicationPermission} REMOVE {UserGroup}'),
|
||||
),
|
||||
ApplicationPermission.applications.through._meta.object_name: (
|
||||
_('Application permission'),
|
||||
_('{ApplicationPermission} ADD {Application}'),
|
||||
_('{ApplicationPermission} REMOVE {Application}'),
|
||||
),
|
||||
ApplicationPermission.system_users.through._meta.object_name: (
|
||||
_('Application permission and SystemUser'),
|
||||
_('{ApplicationPermission} ADD {SystemUser}'),
|
||||
_('{ApplicationPermission} REMOVE {SystemUser}'),
|
||||
),
|
||||
}
|
||||
|
||||
M2M_ACTION = {
|
||||
POST_ADD: OperateLog.ACTION_CREATE,
|
||||
POST_REMOVE: OperateLog.ACTION_DELETE,
|
||||
|
@ -137,60 +81,115 @@ M2M_ACTION = {
|
|||
def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
||||
if action not in M2M_ACTION:
|
||||
return
|
||||
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
if not instance:
|
||||
return
|
||||
|
||||
sender_name = sender._meta.object_name
|
||||
if sender_name in M2M_NEED_RECORD:
|
||||
org_id = current_org.id
|
||||
remote_addr = get_request_ip(current_request)
|
||||
user = str(user)
|
||||
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name]
|
||||
action = M2M_ACTION[action]
|
||||
if action == OperateLog.ACTION_CREATE:
|
||||
resource_tmpl = resource_tmpl_add
|
||||
elif action == OperateLog.ACTION_DELETE:
|
||||
resource_tmpl = resource_tmpl_remove
|
||||
resource_type = instance._meta.verbose_name
|
||||
current_instance = model_to_dict(instance, include_model_fields=False)
|
||||
|
||||
to_create = []
|
||||
objs = model.objects.filter(pk__in=pk_set)
|
||||
instance_id = current_instance.get('id')
|
||||
log_id, before_instance = get_instance_dict_from_cache(instance_id)
|
||||
|
||||
instance_name = instance._meta.object_name
|
||||
instance_value = str(instance)
|
||||
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, [])
|
||||
|
||||
model_name = model._meta.object_name
|
||||
after, before, before_value = None, None, None
|
||||
if action == OperateLog.ACTION_CREATE:
|
||||
before_value = list(set(changed_field) - set(objs_display))
|
||||
elif action == OperateLog.ACTION_DELETE:
|
||||
before_value = list(
|
||||
set(changed_field).symmetric_difference(set(objs_display))
|
||||
)
|
||||
|
||||
for obj in objs:
|
||||
resource = resource_tmpl.format(**{
|
||||
instance_name: instance_value,
|
||||
model_name: str(obj)
|
||||
})[:128] # `resource` 字段只有 128 个字符长 😔
|
||||
if changed_field:
|
||||
after = {field_name: changed_field}
|
||||
if before_value:
|
||||
before = {field_name: before_value}
|
||||
|
||||
to_create.append(OperateLog(
|
||||
user=user, action=action, resource_type=resource_type,
|
||||
resource=resource, remote_addr=remote_addr, org_id=org_id
|
||||
))
|
||||
OperateLog.objects.bulk_create(to_create)
|
||||
if sorted(str(before)) == sorted(str(after)):
|
||||
return
|
||||
|
||||
create_or_update_operate_log(
|
||||
OperateLog.ACTION_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
|
||||
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):
|
||||
# last_login 改变是最后登录日期, 每次登录都会改变
|
||||
if instance._meta.object_name == 'User' and \
|
||||
update_fields and 'last_login' in update_fields:
|
||||
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.OperateLog.ACTION_CREATE
|
||||
after = model_to_dict(instance)
|
||||
log_id = getattr(instance, 'operate_log_id', None)
|
||||
else:
|
||||
action = models.OperateLog.ACTION_UPDATE
|
||||
create_operate_log(action, sender, instance)
|
||||
current_instance = model_to_dict(instance)
|
||||
log_id, before, after = get_instance_current_with_cache_diff(current_instance)
|
||||
|
||||
resource_type = sender._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
action, resource_type, resource=instance,
|
||||
log_id=log_id, before=before, after=after
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
def on_object_delete(sender, instance=None, **kwargs):
|
||||
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance)
|
||||
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(
|
||||
models.OperateLog.ACTION_DELETE, resource_type,
|
||||
resource=instance, before=model_to_dict(instance)
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_user_change_password, sender=User)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import csv
|
||||
import codecs
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.db import transaction
|
||||
from django.utils import translation
|
||||
from itertools import chain
|
||||
|
||||
from audits.models import OperateLog
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger
|
||||
from jumpserver.utils import current_request
|
||||
from .const import DEFAULT_CITY, MODELS_NEED_RECORD
|
||||
from django.http import HttpResponse
|
||||
from django.db import models
|
||||
|
||||
from settings.serializers import SettingsSerializer
|
||||
from common.utils import validate_ip, get_ip_city, get_logger
|
||||
from common.db import fields
|
||||
from .const import DEFAULT_CITY
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -46,23 +47,60 @@ def write_login_log(*args, **kwargs):
|
|||
UserLoginLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
with translation.override('en'):
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
def get_resource_display(resource):
|
||||
resource_display = str(resource)
|
||||
setting_serializer = SettingsSerializer()
|
||||
label = setting_serializer.get_field_label(resource_display)
|
||||
if label is not None:
|
||||
resource_display = label
|
||||
return resource_display
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
def model_to_dict_for_operate_log(
|
||||
instance, include_model_fields=True, include_related_fields=True
|
||||
):
|
||||
need_continue_fields = ['date_updated']
|
||||
opts = instance._meta
|
||||
data = {}
|
||||
for f in chain(opts.concrete_fields, opts.private_fields):
|
||||
if isinstance(f, (models.FileField, models.ImageField)):
|
||||
continue
|
||||
|
||||
if getattr(f, 'attname', None) in need_continue_fields:
|
||||
continue
|
||||
|
||||
value = getattr(instance, f.name) or getattr(instance, f.attname)
|
||||
if not isinstance(value, bool) and not value:
|
||||
continue
|
||||
|
||||
if getattr(f, 'primary_key', False):
|
||||
f.verbose_name = 'id'
|
||||
elif isinstance(f, (
|
||||
fields.EncryptCharField, fields.EncryptTextField,
|
||||
fields.EncryptJsonDictCharField, fields.EncryptJsonDictTextField
|
||||
)) or getattr(f, 'attname', '') == 'password':
|
||||
value = 'encrypt|%s' % value
|
||||
elif isinstance(value, list):
|
||||
value = [str(v) for v in value]
|
||||
|
||||
if include_model_fields or getattr(f, 'primary_key', False):
|
||||
data[str(f.verbose_name)] = value
|
||||
|
||||
if include_related_fields:
|
||||
for f in chain(opts.many_to_many, opts.related_objects):
|
||||
value = []
|
||||
if instance.pk is not None:
|
||||
related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '')
|
||||
if related_name:
|
||||
try:
|
||||
value = [str(i) for i in getattr(instance, related_name).all()]
|
||||
except:
|
||||
pass
|
||||
if not value:
|
||||
continue
|
||||
try:
|
||||
field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name
|
||||
data[str(field_key)] = value
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Generated by Django 2.1.7 on 2019-02-28 08:07
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -27,7 +28,7 @@ class Migration(migrations.Migration):
|
|||
models.UUIDField(default=uuid.uuid4, editable=False,
|
||||
verbose_name='AccessKeySecret')),
|
||||
('user', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
on_delete=common.db.models.CASCADE_SIGNAL_SKIP,
|
||||
related_name='access_keys',
|
||||
to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by Django 3.1.13 on 2021-12-27 02:59
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -16,6 +16,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='ssotoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
field=models.ForeignKey(db_constraint=False, on_delete=common.db.models.CASCADE_SIGNAL_SKIP, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -16,10 +16,10 @@ class AccessKey(models.Model):
|
|||
default=uuid.uuid4, editable=False)
|
||||
secret = models.UUIDField(verbose_name='AccessKeySecret',
|
||||
default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
|
||||
on_delete=models.CASCADE, related_name='access_keys')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'),
|
||||
on_delete=models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||
|
||||
def get_id(self):
|
||||
return str(self.id)
|
||||
|
@ -51,7 +51,7 @@ class SSOToken(models.JMSBaseModel):
|
|||
"""
|
||||
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
|
||||
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False)
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE_SIGNAL_SKIP, verbose_name=_('User'), db_constraint=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('SSO token')
|
||||
|
|
|
@ -15,3 +15,5 @@ POST_CLEAR = 'post_clear'
|
|||
|
||||
POST_PREFIX = 'post'
|
||||
PRE_PREFIX = 'pre'
|
||||
|
||||
SKIP_SIGNAL = 'skip_signal'
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.utils.encoding import force_text
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from common.utils import signer, crypto
|
||||
from common.local import add_encrypted_field_set
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
@ -149,6 +150,10 @@ class EncryptMixin:
|
|||
class EncryptTextField(EncryptMixin, models.TextField):
|
||||
description = _("Encrypt field using Secret Key")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
add_encrypted_field_set(self.verbose_name)
|
||||
|
||||
|
||||
class EncryptCharField(EncryptMixin, models.CharField):
|
||||
@staticmethod
|
||||
|
@ -163,6 +168,7 @@ class EncryptCharField(EncryptMixin, models.CharField):
|
|||
def __init__(self, *args, **kwargs):
|
||||
self.change_max_length(kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
add_encrypted_field_set(self.verbose_name)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
|
@ -174,11 +180,15 @@ class EncryptCharField(EncryptMixin, models.CharField):
|
|||
|
||||
|
||||
class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
|
||||
pass
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
add_encrypted_field_set(self.verbose_name)
|
||||
|
||||
|
||||
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
|
||||
pass
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
add_encrypted_field_set(self.verbose_name)
|
||||
|
||||
|
||||
class PortField(models.IntegerField):
|
||||
|
|
|
@ -19,6 +19,8 @@ from django.db.models import QuerySet
|
|||
from django.db.models.functions import Concat
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..const.signals import SKIP_SIGNAL
|
||||
|
||||
|
||||
class Choice(str):
|
||||
def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label`
|
||||
|
@ -124,6 +126,9 @@ class JMSModel(JMSBaseModel):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
|
||||
def concated_display(name1, name2):
|
||||
return Concat(F(name1), Value('('), F(name2), Value(')'))
|
||||
|
@ -238,3 +243,14 @@ class MultiTableChildQueryset(QuerySet):
|
|||
self._batched_insert(objs, self.model._meta.local_fields, batch_size)
|
||||
|
||||
return objs
|
||||
|
||||
|
||||
def CASCADE_SIGNAL_SKIP(collector, field, sub_objs, using):
|
||||
# 级联删除时,操作日志标记不保存,以免用户混淆
|
||||
try:
|
||||
for obj in sub_objs:
|
||||
setattr(obj, SKIP_SIGNAL, True)
|
||||
except:
|
||||
pass
|
||||
|
||||
CASCADE(collector, field, sub_objs, using)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.utils import decrypt_password
|
||||
from common.local import add_encrypted_field_set
|
||||
|
||||
__all__ = [
|
||||
'ReadableHiddenField', 'EncryptedField'
|
||||
|
@ -32,6 +33,7 @@ class EncryptedField(serializers.CharField):
|
|||
write_only = True
|
||||
kwargs['write_only'] = write_only
|
||||
super().__init__(**kwargs)
|
||||
add_encrypted_field_set(self.label)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
value = super().to_internal_value(value)
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
from werkzeug.local import Local
|
||||
|
||||
thread_local = Local()
|
||||
encrypted_field_set = set()
|
||||
|
||||
|
||||
def _find(attr):
|
||||
return getattr(thread_local, attr, None)
|
||||
|
||||
|
||||
def add_encrypted_field_set(label):
|
||||
if label:
|
||||
encrypted_field_set.add(str(label))
|
||||
|
|
|
@ -7,7 +7,7 @@ from rest_framework import permissions
|
|||
from rest_framework.request import Request
|
||||
|
||||
from common.exceptions import UserConfirmRequired
|
||||
from audits.utils import create_operate_log
|
||||
from audits.handler import create_or_update_operate_log
|
||||
from audits.models import OperateLog
|
||||
|
||||
__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"]
|
||||
|
@ -62,10 +62,18 @@ class RecordViewLogMixin:
|
|||
def list(self, request, *args, **kwargs):
|
||||
response = super().list(request, *args, **kwargs)
|
||||
resource = self.get_resource_display(request)
|
||||
create_operate_log(self.ACTION, self.model, resource)
|
||||
resource_type = self.model._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
self.ACTION, resource_type, force=True,
|
||||
resource=resource
|
||||
)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
create_operate_log(self.ACTION, self.model, self.get_object())
|
||||
resource_type = self.model._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
self.ACTION, resource_type, force=True,
|
||||
resource=self.get_object()
|
||||
)
|
||||
return response
|
||||
|
|
|
@ -0,0 +1,428 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import datetime
|
||||
import inspect
|
||||
|
||||
from collections.abc import Iterable
|
||||
from functools import reduce, partial
|
||||
from itertools import groupby
|
||||
from uuid import UUID
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import QuerySet as DJQuerySet
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import bulk
|
||||
from elasticsearch.exceptions import RequestError, NotFoundError
|
||||
|
||||
from common.utils.common import lazyproperty
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_date_display
|
||||
from common.exceptions import JMSException
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class InvalidElasticsearch(JMSException):
|
||||
default_code = 'invalid_elasticsearch'
|
||||
default_detail = _('Invalid elasticsearch config')
|
||||
|
||||
|
||||
class NotSupportElasticsearch8(JMSException):
|
||||
default_code = 'not_support_elasticsearch8'
|
||||
default_detail = _('Not Support Elasticsearch8')
|
||||
|
||||
|
||||
class ES(object):
|
||||
def __init__(self, config, properties, keyword_fields, exact_fields=None, match_fields=None):
|
||||
|
||||
self.config = config
|
||||
hosts = self.config.get('HOSTS')
|
||||
kwargs = self.config.get('OTHER', {})
|
||||
|
||||
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
|
||||
if ignore_verify_certs:
|
||||
kwargs['verify_certs'] = None
|
||||
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
|
||||
self.index_prefix = self.config.get('INDEX') or 'jumpserver'
|
||||
self.is_index_by_date = bool(self.config.get('INDEX_BY_DATE', False))
|
||||
|
||||
self.index = None
|
||||
self.query_index = None
|
||||
self.properties = properties
|
||||
self.exact_fields, self.match_fields, self.keyword_fields = set(), set(), set()
|
||||
|
||||
if isinstance(keyword_fields, Iterable):
|
||||
self.keyword_fields.update(keyword_fields)
|
||||
if isinstance(exact_fields, Iterable):
|
||||
self.exact_fields.update(exact_fields)
|
||||
if isinstance(match_fields, Iterable):
|
||||
self.match_fields.update(match_fields)
|
||||
|
||||
self.init_index()
|
||||
self.doc_type = self.config.get("DOC_TYPE") or '_doc'
|
||||
if self.is_new_index_type():
|
||||
self.doc_type = '_doc'
|
||||
self.exact_fields.update(self.keyword_fields)
|
||||
else:
|
||||
self.match_fields.update(self.keyword_fields)
|
||||
|
||||
def init_index(self):
|
||||
if self.is_index_by_date:
|
||||
date = local_now_date_display()
|
||||
self.index = '%s-%s' % (self.index_prefix, date)
|
||||
self.query_index = '%s-alias' % self.index_prefix
|
||||
else:
|
||||
self.index = self.config.get("INDEX") or 'jumpserver'
|
||||
self.query_index = self.config.get("INDEX") or 'jumpserver'
|
||||
|
||||
def is_new_index_type(self):
|
||||
if not self.ping(timeout=2):
|
||||
return False
|
||||
|
||||
info = self.es.info()
|
||||
version = info['version']['number'].split('.')[0]
|
||||
|
||||
if version == '8':
|
||||
raise NotSupportElasticsearch8
|
||||
|
||||
try:
|
||||
# 获取索引信息,如果没有定义,直接返回
|
||||
data = self.es.indices.get_mapping(self.index)
|
||||
except NotFoundError:
|
||||
return False
|
||||
|
||||
try:
|
||||
if version == '6':
|
||||
# 检测索引是不是新的类型 es6
|
||||
properties = data[self.index]['mappings']['data']['properties']
|
||||
else:
|
||||
# 检测索引是不是新的类型 es7 default index type: _doc
|
||||
properties = data[self.index]['mappings']['properties']
|
||||
|
||||
for keyword in self.keyword_fields:
|
||||
if not properties[keyword]['type'] == 'keyword':
|
||||
break
|
||||
else:
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def pre_use_check(self):
|
||||
if not self.ping(timeout=3):
|
||||
raise InvalidElasticsearch
|
||||
self._ensure_index_exists()
|
||||
|
||||
def _ensure_index_exists(self):
|
||||
info = self.es.info()
|
||||
version = info['version']['number'].split('.')[0]
|
||||
if version == '6':
|
||||
mappings = {'mappings': {'data': {'properties': self.properties}}}
|
||||
else:
|
||||
mappings = {'mappings': {'properties': self.properties}}
|
||||
|
||||
if self.is_index_by_date:
|
||||
mappings['aliases'] = {
|
||||
self.query_index: {}
|
||||
}
|
||||
|
||||
try:
|
||||
self.es.indices.create(self.index, body=mappings)
|
||||
return
|
||||
except RequestError as e:
|
||||
if e.error == 'resource_already_exists_exception':
|
||||
logger.warning(e)
|
||||
else:
|
||||
logger.exception(e)
|
||||
|
||||
def make_data(self, data):
|
||||
return []
|
||||
|
||||
def save(self, **kwargs):
|
||||
data = self.make_data(kwargs)
|
||||
return self.es.index(index=self.index, doc_type=self.doc_type, body=data)
|
||||
|
||||
def bulk_save(self, command_set, raise_on_error=True):
|
||||
actions = []
|
||||
for command in command_set:
|
||||
data = dict(
|
||||
_index=self.index,
|
||||
_type=self.doc_type,
|
||||
_source=self.make_data(command),
|
||||
)
|
||||
actions.append(data)
|
||||
return bulk(self.es, actions, index=self.index, raise_on_error=raise_on_error)
|
||||
|
||||
def get(self, query: dict):
|
||||
item = None
|
||||
data = self.filter(query, size=1)
|
||||
if len(data) >= 1:
|
||||
item = data[0]
|
||||
return item
|
||||
|
||||
def filter(self, query: dict, from_=None, size=None, sort=None):
|
||||
try:
|
||||
data = self._filter(query, from_, size, sort)
|
||||
except Exception as e:
|
||||
logger.error('ES filter error: {}'.format(e))
|
||||
data = []
|
||||
return data
|
||||
|
||||
def _filter(self, query: dict, from_=None, size=None, sort=None):
|
||||
body = self.get_query_body(**query)
|
||||
|
||||
data = self.es.search(
|
||||
index=self.query_index, doc_type=self.doc_type, body=body,
|
||||
from_=from_, size=size, sort=sort
|
||||
)
|
||||
source_data = []
|
||||
for item in data['hits']['hits']:
|
||||
if item:
|
||||
item['_source'].update({'es_id': item['_id']})
|
||||
source_data.append(item['_source'])
|
||||
|
||||
return source_data
|
||||
|
||||
def count(self, **query):
|
||||
try:
|
||||
body = self.get_query_body(**query)
|
||||
data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body)
|
||||
count = data["count"]
|
||||
except Exception as e:
|
||||
logger.error('ES count error: {}'.format(e))
|
||||
count = 0
|
||||
return count
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.es, item)
|
||||
|
||||
def all(self):
|
||||
"""返回所有数据"""
|
||||
raise NotImplementedError("Not support")
|
||||
|
||||
def ping(self, timeout=None):
|
||||
try:
|
||||
return self.es.ping(request_timeout=timeout)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def handler_time_field(data):
|
||||
datetime__gte = data.get('datetime__gte')
|
||||
datetime__lte = data.get('datetime__lte')
|
||||
datetime_range = {}
|
||||
|
||||
if datetime__gte:
|
||||
if isinstance(datetime__gte, datetime.datetime):
|
||||
datetime__gte = datetime__gte.strftime('%Y-%m-%d %H:%M:%S')
|
||||
datetime_range['gte'] = datetime__gte
|
||||
if datetime__lte:
|
||||
if isinstance(datetime__lte, datetime.datetime):
|
||||
datetime__lte = datetime__lte.strftime('%Y-%m-%d %H:%M:%S')
|
||||
datetime_range['lte'] = datetime__lte
|
||||
return 'datetime', datetime_range
|
||||
|
||||
def get_query_body(self, **kwargs):
|
||||
new_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, UUID):
|
||||
v = str(v)
|
||||
if k == 'pk':
|
||||
k = 'id'
|
||||
new_kwargs[k] = v
|
||||
kwargs = new_kwargs
|
||||
|
||||
index_in_field = 'id__in'
|
||||
exact_fields = self.exact_fields
|
||||
match_fields = self.match_fields
|
||||
|
||||
match = {}
|
||||
exact = {}
|
||||
index = {}
|
||||
|
||||
if index_in_field in kwargs:
|
||||
index['values'] = kwargs[index_in_field]
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if k in exact_fields:
|
||||
exact[k] = v
|
||||
elif k in match_fields:
|
||||
match[k] = v
|
||||
|
||||
# 处理时间
|
||||
time_field_name, time_range = self.handler_time_field(kwargs)
|
||||
|
||||
# 处理组织
|
||||
should = []
|
||||
org_id = match.get('org_id')
|
||||
|
||||
real_default_org_id = '00000000-0000-0000-0000-000000000002'
|
||||
root_org_id = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
if org_id == root_org_id:
|
||||
match.pop('org_id')
|
||||
elif org_id in (real_default_org_id, ''):
|
||||
match.pop('org_id')
|
||||
should.append({
|
||||
'bool': {
|
||||
'must_not': [
|
||||
{
|
||||
'wildcard': {'org_id': '*'}
|
||||
}
|
||||
]}
|
||||
})
|
||||
should.append({'match': {'org_id': real_default_org_id}})
|
||||
|
||||
# 构建 body
|
||||
body = {
|
||||
'query': {
|
||||
'bool': {
|
||||
'must': [
|
||||
{'match': {k: v}} for k, v in match.items()
|
||||
],
|
||||
'should': should,
|
||||
'filter': [
|
||||
{
|
||||
'term': {k: v}
|
||||
} for k, v in exact.items()
|
||||
] + [
|
||||
{
|
||||
'range': {
|
||||
time_field_name: time_range
|
||||
}
|
||||
}
|
||||
] + [
|
||||
{
|
||||
'ids': {k: v}
|
||||
} for k, v in index.items()
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
return body
|
||||
|
||||
|
||||
class QuerySet(DJQuerySet):
|
||||
default_days_ago = 7
|
||||
max_result_window = 10000
|
||||
|
||||
def __init__(self, es_instance):
|
||||
self._method_calls = []
|
||||
self._slice = None # (from_, size)
|
||||
self._storage = es_instance
|
||||
|
||||
# 命令列表模糊搜索时报错
|
||||
super().__init__()
|
||||
|
||||
@lazyproperty
|
||||
def _grouped_method_calls(self):
|
||||
_method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])}
|
||||
return _method_calls
|
||||
|
||||
@lazyproperty
|
||||
def _filter_kwargs(self):
|
||||
_method_calls = self._grouped_method_calls
|
||||
filter_calls = _method_calls.get('filter')
|
||||
if not filter_calls:
|
||||
return {}
|
||||
names, multi_args, multi_kwargs = zip(*filter_calls)
|
||||
kwargs = reduce(lambda x, y: {**x, **y}, multi_kwargs, {})
|
||||
|
||||
striped_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
k = k.replace('__exact', '')
|
||||
k = k.replace('__startswith', '')
|
||||
k = k.replace('__icontains', '')
|
||||
striped_kwargs[k] = v
|
||||
return striped_kwargs
|
||||
|
||||
@lazyproperty
|
||||
def _sort(self):
|
||||
order_by = self._grouped_method_calls.get('order_by')
|
||||
if order_by:
|
||||
for call in reversed(order_by):
|
||||
fields = call[1]
|
||||
if fields:
|
||||
field = fields[-1]
|
||||
|
||||
if field.startswith('-'):
|
||||
direction = 'desc'
|
||||
else:
|
||||
direction = 'asc'
|
||||
field = field.lstrip('-+')
|
||||
sort = f'{field}:{direction}'
|
||||
return sort
|
||||
|
||||
def __execute(self):
|
||||
_filter_kwargs = self._filter_kwargs
|
||||
_sort = self._sort
|
||||
from_, size = self._slice or (None, None)
|
||||
data = self._storage.filter(_filter_kwargs, from_=from_, size=size, sort=_sort)
|
||||
return self.model.from_multi_dict(data)
|
||||
|
||||
def __stage_method_call(self, item, *args, **kwargs):
|
||||
_clone = self.__clone()
|
||||
_clone._method_calls.append((item, args, kwargs))
|
||||
return _clone
|
||||
|
||||
def __clone(self):
|
||||
uqs = QuerySet(self._storage)
|
||||
uqs._method_calls = self._method_calls.copy()
|
||||
uqs._slice = self._slice
|
||||
uqs.model = self.model
|
||||
return uqs
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs.update(self._filter_kwargs)
|
||||
return self._storage.get(kwargs)
|
||||
|
||||
def count(self, limit_to_max_result_window=True):
|
||||
filter_kwargs = self._filter_kwargs
|
||||
count = self._storage.count(**filter_kwargs)
|
||||
if limit_to_max_result_window:
|
||||
count = min(count, self.max_result_window)
|
||||
return count
|
||||
|
||||
def __getattribute__(self, item):
|
||||
if any((
|
||||
item.startswith('__'),
|
||||
item in QuerySet.__dict__,
|
||||
)):
|
||||
return object.__getattribute__(self, item)
|
||||
|
||||
origin_attr = object.__getattribute__(self, item)
|
||||
if not inspect.ismethod(origin_attr):
|
||||
return origin_attr
|
||||
|
||||
attr = partial(self.__stage_method_call, item)
|
||||
return attr
|
||||
|
||||
def __getitem__(self, item):
|
||||
max_window = self.max_result_window
|
||||
if isinstance(item, slice):
|
||||
if self._slice is None:
|
||||
clone = self.__clone()
|
||||
from_ = item.start or 0
|
||||
if item.stop is None:
|
||||
size = self.max_result_window - from_
|
||||
else:
|
||||
size = item.stop - from_
|
||||
|
||||
if from_ + size > max_window:
|
||||
if from_ >= max_window:
|
||||
from_ = max_window
|
||||
size = 0
|
||||
else:
|
||||
size = max_window - from_
|
||||
clone._slice = (from_, size)
|
||||
return clone
|
||||
return self.__execute()[item]
|
||||
|
||||
def __repr__(self):
|
||||
return self.__execute().__repr__()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__execute())
|
||||
|
||||
def __len__(self):
|
||||
return self.count()
|
|
@ -0,0 +1,6 @@
|
|||
from django.conf import settings
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
|
||||
class MaxLimitOffsetPagination(LimitOffsetPagination):
|
||||
max_limit = settings.MAX_LIMIT_PER_PAGE or 100
|
|
@ -178,5 +178,9 @@ HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
|
|||
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
|
||||
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
|
||||
|
||||
OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG
|
||||
|
||||
MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE
|
||||
|
||||
# Magnus DB Port
|
||||
MAGNUS_PORTS = CONFIG.MAGNUS_PORTS
|
||||
|
|
|
@ -47,7 +47,7 @@ REST_FRAMEWORK = {
|
|||
'SEARCH_PARAM': "search",
|
||||
'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z',
|
||||
'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PAGINATION_CLASS': 'jumpserver.rewriting.pagination.MaxLimitOffsetPagination',
|
||||
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -43,11 +43,11 @@ class Migration(migrations.Migration):
|
|||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('message_type', models.CharField(max_length=128)),
|
||||
('receive_backends', models.JSONField(default=list)),
|
||||
('receive_backends', models.JSONField(default=list, verbose_name='receive backend')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'abstract': False, 'verbose_name': 'User message'
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
@ -64,7 +64,7 @@ class Migration(migrations.Migration):
|
|||
('users', models.ManyToManyField(related_name='system_msg_subscriptions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'abstract': False, 'verbose_name': 'System message'
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by Django 3.1.12 on 2021-09-09 11:46
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def init_user_msg_subscription(apps, schema_editor):
|
||||
|
@ -49,7 +49,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='usermsgsubscription',
|
||||
name='user',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL),
|
||||
field=models.OneToOneField(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.RunPython(init_user_msg_subscription)
|
||||
]
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.db.models import JMSModel
|
||||
from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP
|
||||
|
||||
__all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
|
||||
|
||||
|
||||
class UserMsgSubscription(JMSModel):
|
||||
user = models.OneToOneField('users.User', related_name='user_msg_subscription', on_delete=models.CASCADE)
|
||||
receive_backends = models.JSONField(default=list)
|
||||
user = models.OneToOneField(
|
||||
'users.User', related_name='user_msg_subscription', on_delete=CASCADE_SIGNAL_SKIP,
|
||||
verbose_name=_('User')
|
||||
)
|
||||
receive_backends = models.JSONField(default=list, verbose_name=_('receive backend'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('User message')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user} subscription: {self.receive_backends}'
|
||||
return _('{} subscription').format(self.user)
|
||||
|
||||
|
||||
class SystemMsgSubscription(JMSModel):
|
||||
|
@ -21,11 +28,19 @@ class SystemMsgSubscription(JMSModel):
|
|||
|
||||
message_type_label = ''
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message_type}'
|
||||
class Meta:
|
||||
verbose_name = _('System message')
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
def set_message_type_label(self):
|
||||
# 采用手动调用,没设置成 property 的方式
|
||||
# 因为目前只有界面修改时会用到这个属性,避免实例化时占用资源计算
|
||||
from ..notifications import system_msgs
|
||||
msg_label = ''
|
||||
for msg in system_msgs:
|
||||
if msg.get('message_type') == self.message_type:
|
||||
msg_label = msg.get('message_type_label', '')
|
||||
break
|
||||
self.message_type_label = msg_label
|
||||
|
||||
@property
|
||||
def receivers(self):
|
||||
|
@ -47,3 +62,9 @@ class SystemMsgSubscription(JMSModel):
|
|||
receviers.append(recevier)
|
||||
|
||||
return receviers
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message_type_label}' or f'{self.message_type}'
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
|
|
@ -22,6 +22,10 @@ class SystemMsgSubscriptionSerializer(BulkModelSerializer):
|
|||
'receive_backends': {'required': True}
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.set_message_type_label()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
|
||||
category = serializers.CharField()
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from common.db.models import UnionQuerySet, BitOperationChoice
|
||||
from common.utils import date_expired_default, lazyproperty
|
||||
from orgs.mixins.models import OrgManager
|
||||
|
||||
|
||||
__all__ = [
|
||||
'BasePermission', 'BasePermissionQuerySet', 'Action'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Generated by Django 3.1.13 on 2021-11-19 08:29
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.contenttypes.models
|
||||
|
@ -84,7 +85,7 @@ class Migration(migrations.Migration):
|
|||
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')),
|
||||
('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='orgs.organization', verbose_name='Organization')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='rbac.role', verbose_name='Role')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Role binding',
|
||||
|
|
|
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='permission',
|
||||
options={'verbose_name': 'Permission'},
|
||||
options={'verbose_name': 'Permissions'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='role',
|
||||
|
|
|
@ -23,7 +23,7 @@ class Permission(DjangoPermission):
|
|||
""" 权限类 """
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = _('Permission')
|
||||
verbose_name = _('Permissions')
|
||||
|
||||
@classmethod
|
||||
def to_perms(cls, queryset):
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.conf import settings
|
|||
from django.core.exceptions import ValidationError
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.db.models import JMSModel
|
||||
from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP
|
||||
from common.utils import lazyproperty
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from .role import Role
|
||||
|
@ -38,7 +38,7 @@ class RoleBinding(JMSModel):
|
|||
verbose_name=_('Scope')
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
'users.User', related_name='role_bindings', on_delete=models.CASCADE, verbose_name=_('User')
|
||||
'users.User', related_name='role_bindings', on_delete=CASCADE_SIGNAL_SKIP, verbose_name=_('User')
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
Role, related_name='role_bindings', on_delete=models.CASCADE, verbose_name=_('Role')
|
||||
|
@ -56,7 +56,7 @@ class RoleBinding(JMSModel):
|
|||
]
|
||||
|
||||
def __str__(self):
|
||||
display = '{user} & {role}'.format(user=self.user, role=self.role)
|
||||
display = '{role} -> {user}'.format(user=self.user, role=self.role)
|
||||
if self.org:
|
||||
display += ' | {org}'.format(org=self.org)
|
||||
return display
|
||||
|
|
|
@ -21,8 +21,8 @@ class Migration(migrations.Migration):
|
|||
verbose_name='Name')),
|
||||
('value', models.TextField(verbose_name='Value')),
|
||||
('category',
|
||||
models.CharField(default='default', max_length=128)),
|
||||
('encrypted', models.BooleanField(default=False)),
|
||||
models.CharField(default='default', max_length=128, verbose_name='Category')),
|
||||
('encrypted', models.BooleanField(default=False, verbose_name='Encrypted')),
|
||||
('enabled',
|
||||
models.BooleanField(default=True, verbose_name='Enabled')),
|
||||
('comment', models.TextField(verbose_name='Comment')),
|
||||
|
|
|
@ -32,8 +32,8 @@ class SettingManager(models.Manager):
|
|||
class Setting(models.Model):
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
|
||||
value = models.TextField(verbose_name=_("Value"), null=True, blank=True)
|
||||
category = models.CharField(max_length=128, default="default")
|
||||
encrypted = models.BooleanField(default=False)
|
||||
category = models.CharField(max_length=128, default="default", verbose_name=_('Category'))
|
||||
encrypted = models.BooleanField(default=False, verbose_name=_('Encrypted'))
|
||||
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
|
||||
comment = models.TextField(verbose_name=_("Comment"))
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = [
|
|||
|
||||
|
||||
class AuthSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('Basic'))
|
||||
|
||||
AUTH_CAS = serializers.BooleanField(required=False, label=_('CAS Auth'))
|
||||
AUTH_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth'))
|
||||
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth'))
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = [
|
|||
|
||||
|
||||
class CASSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('CAS'))
|
||||
|
||||
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
|
||||
CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url'))
|
||||
CAS_ROOT_PROXIED_AS = serializers.CharField(
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = ['DingTalkSettingSerializer']
|
|||
|
||||
|
||||
class DingTalkSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('DingTalk'))
|
||||
|
||||
DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId')
|
||||
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey')
|
||||
DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='AppSecret')
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = ['FeiShuSettingSerializer']
|
|||
|
||||
|
||||
class FeiShuSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('FeiShu'))
|
||||
|
||||
FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
|
||||
FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
||||
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth'))
|
||||
|
|
|
@ -36,6 +36,7 @@ class LDAPUserSerializer(serializers.Serializer):
|
|||
|
||||
class LDAPSettingSerializer(serializers.Serializer):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('LDAP'))
|
||||
|
||||
AUTH_LDAP_SERVER_URI = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('LDAP server'),
|
||||
|
|
|
@ -16,6 +16,8 @@ class SettingImageField(serializers.ImageField):
|
|||
|
||||
|
||||
class OAuth2SettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('OAuth2'))
|
||||
|
||||
AUTH_OAUTH2 = serializers.BooleanField(
|
||||
default=False, label=_('Enable OAuth2 Auth')
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ __all__ = [
|
|||
|
||||
|
||||
class CommonSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('OIDC'))
|
||||
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
|
||||
BASE_SITE_URL = serializers.CharField(
|
||||
required=False, allow_null=True, allow_blank=True,
|
||||
|
|
|
@ -10,6 +10,8 @@ __all__ = ['RadiusSettingSerializer']
|
|||
|
||||
|
||||
class RadiusSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('Radius'))
|
||||
|
||||
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('Enable Radius Auth'))
|
||||
RADIUS_SERVER = serializers.CharField(required=False, allow_blank=True, max_length=1024, label=_('Host'))
|
||||
RADIUS_PORT = serializers.IntegerField(required=False, label=_('Port'))
|
||||
|
|
|
@ -8,6 +8,8 @@ __all__ = [
|
|||
|
||||
|
||||
class SAML2SettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('SAML2'))
|
||||
|
||||
AUTH_SAML2 = serializers.BooleanField(
|
||||
default=False, required=False, label=_('Enable SAML2 Auth')
|
||||
)
|
||||
|
|
|
@ -24,6 +24,8 @@ class SignTmplPairSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class BaseSMSSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('SMS')
|
||||
|
||||
SMS_TEST_PHONE = serializers.CharField(
|
||||
max_length=256, required=False, validators=[PhoneValidator(), ],
|
||||
allow_blank=True, label=_('Test phone')
|
||||
|
@ -38,7 +40,7 @@ class BaseSMSSettingSerializer(serializers.Serializer):
|
|||
class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId')
|
||||
ALIBABA_ACCESS_KEY_SECRET = EncryptedField(
|
||||
max_length=256, required=False, label='AccessKeySecret',
|
||||
max_length=256, required=False, label='access_key_secret',
|
||||
)
|
||||
ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = [
|
|||
|
||||
|
||||
class SSOSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('SSO'))
|
||||
|
||||
AUTH_SSO = serializers.BooleanField(
|
||||
required=False, label=_('Enable SSO auth'),
|
||||
help_text=_("Other service can using SSO token login to JumpServer without password")
|
||||
|
|
|
@ -7,6 +7,8 @@ __all__ = ['WeComSettingSerializer']
|
|||
|
||||
|
||||
class WeComSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('WeCom'))
|
||||
|
||||
WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid')
|
||||
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid')
|
||||
WECOM_SECRET = EncryptedField(max_length=256, required=False, label='secret')
|
||||
|
|
|
@ -24,6 +24,8 @@ class AnnouncementSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class BasicSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Basic')
|
||||
|
||||
SITE_URL = serializers.URLField(
|
||||
required=True, label=_("Site url"),
|
||||
help_text=_('eg: http://dev.jumpserver.org:8080')
|
||||
|
|
|
@ -5,6 +5,8 @@ __all__ = ['CleaningSerializer']
|
|||
|
||||
|
||||
class CleaningSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Period clean')
|
||||
|
||||
LOGIN_LOG_KEEP_DAYS = serializers.IntegerField(
|
||||
min_value=1, max_value=9999,
|
||||
label=_("Login log keep days"), help_text=_("Unit: day")
|
||||
|
|
|
@ -16,6 +16,7 @@ class MailTestSerializer(serializers.Serializer):
|
|||
|
||||
class EmailSettingSerializer(serializers.Serializer):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
PREFIX_TITLE = _('Email')
|
||||
|
||||
EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host"))
|
||||
EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port"))
|
||||
|
@ -46,6 +47,8 @@ class EmailSettingSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class EmailContentSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Email')
|
||||
|
||||
EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField(
|
||||
max_length=1024, allow_blank=True, required=False,
|
||||
label=_('Create user email subject'),
|
||||
|
|
|
@ -3,6 +3,8 @@ from rest_framework import serializers
|
|||
|
||||
|
||||
class OtherSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('More...')
|
||||
|
||||
EMAIL_SUFFIX = serializers.CharField(
|
||||
required=False, max_length=1024, label=_("Email suffix"),
|
||||
help_text=_('This is used by default if no email is returned during SSO authentication')
|
||||
|
|
|
@ -143,6 +143,8 @@ class SecurityAuthSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
|
||||
PREFIX_TITLE = _('Security')
|
||||
|
||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
|
||||
required=True, label=_('Enable terminal register'),
|
||||
help_text=_(
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# coding: utf-8
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .basic import BasicSettingSerializer
|
||||
from .other import OtherSettingSerializer
|
||||
|
@ -7,7 +9,8 @@ from .auth import (
|
|||
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
|
||||
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
|
||||
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer, CMPP2SMSSettingSerializer
|
||||
TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer,
|
||||
SAML2SettingSerializer, OAuth2SettingSerializer, SSOSettingSerializer
|
||||
)
|
||||
from .terminal import TerminalSettingSerializer
|
||||
from .security import SecuritySettingSerializer
|
||||
|
@ -22,6 +25,7 @@ __all__ = [
|
|||
class SettingsSerializer(
|
||||
BasicSettingSerializer,
|
||||
LDAPSettingSerializer,
|
||||
AuthSettingSerializer,
|
||||
TerminalSettingSerializer,
|
||||
SecuritySettingSerializer,
|
||||
WeComSettingSerializer,
|
||||
|
@ -31,13 +35,33 @@ class SettingsSerializer(
|
|||
EmailContentSettingSerializer,
|
||||
OtherSettingSerializer,
|
||||
OIDCSettingSerializer,
|
||||
SAML2SettingSerializer,
|
||||
OAuth2SettingSerializer,
|
||||
KeycloakSettingSerializer,
|
||||
CASSettingSerializer,
|
||||
RadiusSettingSerializer,
|
||||
SSOSettingSerializer,
|
||||
CleaningSerializer,
|
||||
AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer,
|
||||
CMPP2SMSSettingSerializer,
|
||||
):
|
||||
CACHE_KEY = 'SETTING_FIELDS_MAPPING'
|
||||
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
pass
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.fields_label_mapping = None
|
||||
|
||||
# 单次计算量不大,搞个缓存,以防操作日志大量写入时,这里影响性能
|
||||
def get_field_label(self, field_name):
|
||||
if self.fields_label_mapping is None:
|
||||
self.fields_label_mapping = {}
|
||||
for subclass in SettingsSerializer.__bases__:
|
||||
prefix = getattr(subclass, 'PREFIX_TITLE', _('Setting'))
|
||||
fields = subclass().get_fields()
|
||||
for name, item in fields.items():
|
||||
label = '[%s] %s' % (prefix, getattr(item, 'label', ''))
|
||||
self.fields_label_mapping[name] = label
|
||||
cache.set(self.CACHE_KEY, self.fields_label_mapping, 3600 * 24)
|
||||
return self.fields_label_mapping.get(field_name)
|
||||
|
|
|
@ -3,6 +3,8 @@ from rest_framework import serializers
|
|||
|
||||
|
||||
class TerminalSettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Terminal')
|
||||
|
||||
SORT_BY_CHOICES = (
|
||||
('hostname', _('Hostname')),
|
||||
('ip', _('IP'))
|
||||
|
|
|
@ -1,109 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import pytz
|
||||
import inspect
|
||||
|
||||
from datetime import datetime
|
||||
from functools import reduce, partial
|
||||
from itertools import groupby
|
||||
from uuid import UUID
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import QuerySet as DJQuerySet
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import bulk
|
||||
from elasticsearch.exceptions import RequestError, NotFoundError
|
||||
|
||||
from common.utils.common import lazyproperty
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_date_display, utc_now
|
||||
from common.exceptions import JMSException
|
||||
from terminal.models import Command
|
||||
from common.plugins.es import ES
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class InvalidElasticsearch(JMSException):
|
||||
default_code = 'invalid_elasticsearch'
|
||||
default_detail = _('Invalid elasticsearch config')
|
||||
|
||||
|
||||
class NotSupportElasticsearch8(JMSException):
|
||||
default_code = 'not_support_elasticsearch8'
|
||||
default_detail = _('Not Support Elasticsearch8')
|
||||
|
||||
|
||||
class CommandStore(object):
|
||||
class CommandStore(ES):
|
||||
def __init__(self, config):
|
||||
self.doc_type = config.get("DOC_TYPE") or '_doc'
|
||||
self.index_prefix = config.get('INDEX') or 'jumpserver'
|
||||
self.is_index_by_date = bool(config.get('INDEX_BY_DATE'))
|
||||
self.exact_fields = {}
|
||||
self.match_fields = {}
|
||||
hosts = config.get("HOSTS")
|
||||
kwargs = config.get("OTHER", {})
|
||||
|
||||
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
|
||||
if ignore_verify_certs:
|
||||
kwargs['verify_certs'] = None
|
||||
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
|
||||
|
||||
self.exact_fields = set()
|
||||
self.match_fields = {'input', 'risk_level', 'user', 'asset', 'system_user'}
|
||||
may_exact_fields = {'session', 'org_id'}
|
||||
|
||||
if self.is_new_index_type():
|
||||
self.exact_fields.update(may_exact_fields)
|
||||
self.doc_type = '_doc'
|
||||
else:
|
||||
self.match_fields.update(may_exact_fields)
|
||||
|
||||
self.init_index(config)
|
||||
|
||||
def init_index(self, config):
|
||||
if self.is_index_by_date:
|
||||
date = local_now_date_display()
|
||||
self.index = '%s-%s' % (self.index_prefix, date)
|
||||
self.query_index = '%s-alias' % self.index_prefix
|
||||
else:
|
||||
self.index = config.get("INDEX") or 'jumpserver'
|
||||
self.query_index = config.get("INDEX") or 'jumpserver'
|
||||
|
||||
def is_new_index_type(self):
|
||||
if not self.ping(timeout=3):
|
||||
return False
|
||||
|
||||
info = self.es.info()
|
||||
version = info['version']['number'].split('.')[0]
|
||||
|
||||
if version == '8':
|
||||
raise NotSupportElasticsearch8
|
||||
|
||||
try:
|
||||
# 获取索引信息,如果没有定义,直接返回
|
||||
data = self.es.indices.get_mapping(self.index)
|
||||
except NotFoundError:
|
||||
return False
|
||||
|
||||
try:
|
||||
if version == '6':
|
||||
# 检测索引是不是新的类型 es6
|
||||
properties = data[self.index]['mappings']['data']['properties']
|
||||
else:
|
||||
# 检测索引是不是新的类型 es7 default index type: _doc
|
||||
properties = data[self.index]['mappings']['properties']
|
||||
if properties['session']['type'] == 'keyword' \
|
||||
and properties['org_id']['type'] == 'keyword':
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def pre_use_check(self):
|
||||
if not self.ping(timeout=3):
|
||||
raise InvalidElasticsearch
|
||||
self._ensure_index_exists()
|
||||
|
||||
def _ensure_index_exists(self):
|
||||
properties = {
|
||||
"session": {
|
||||
"type": "keyword"
|
||||
|
@ -118,25 +27,11 @@ class CommandStore(object):
|
|||
"type": "long"
|
||||
}
|
||||
}
|
||||
info = self.es.info()
|
||||
version = info['version']['number'].split('.')[0]
|
||||
if version == '6':
|
||||
mappings = {'mappings': {'data': {'properties': properties}}}
|
||||
else:
|
||||
mappings = {'mappings': {'properties': properties}}
|
||||
exact_fields = {}
|
||||
match_fields = {'input', 'risk_level', 'user', 'asset', 'system_user'}
|
||||
keyword_fields = {'session', 'org_id'}
|
||||
|
||||
if self.is_index_by_date:
|
||||
mappings['aliases'] = {
|
||||
self.query_index: {}
|
||||
}
|
||||
try:
|
||||
self.es.indices.create(self.index, body=mappings)
|
||||
return
|
||||
except RequestError as e:
|
||||
if e.error == 'resource_already_exists_exception':
|
||||
logger.warning(e)
|
||||
else:
|
||||
logger.exception(e)
|
||||
super().__init__(config, properties, keyword_fields, exact_fields, match_fields)
|
||||
|
||||
@staticmethod
|
||||
def make_data(command):
|
||||
|
@ -150,274 +45,14 @@ class CommandStore(object):
|
|||
data["date"] = datetime.fromtimestamp(command['timestamp'], tz=pytz.UTC)
|
||||
return data
|
||||
|
||||
def bulk_save(self, command_set, raise_on_error=True):
|
||||
actions = []
|
||||
for command in command_set:
|
||||
data = dict(
|
||||
_index=self.index,
|
||||
_type=self.doc_type,
|
||||
_source=self.make_data(command),
|
||||
)
|
||||
actions.append(data)
|
||||
return bulk(self.es, actions, index=self.index, raise_on_error=raise_on_error)
|
||||
|
||||
def save(self, command):
|
||||
"""
|
||||
保存命令到数据库
|
||||
"""
|
||||
data = self.make_data(command)
|
||||
return self.es.index(index=self.index, doc_type=self.doc_type, body=data)
|
||||
|
||||
def filter(self, query: dict, from_=None, size=None, sort=None):
|
||||
try:
|
||||
data = self._filter(query, from_, size, sort)
|
||||
except Exception as e:
|
||||
logger.error('ES filter error: {}'.format(e))
|
||||
data = []
|
||||
return data
|
||||
|
||||
def _filter(self, query: dict, from_=None, size=None, sort=None):
|
||||
body = self.get_query_body(**query)
|
||||
|
||||
data = self.es.search(
|
||||
index=self.query_index, doc_type=self.doc_type, body=body, from_=from_, size=size,
|
||||
sort=sort
|
||||
)
|
||||
source_data = []
|
||||
for item in data['hits']['hits']:
|
||||
if item:
|
||||
item['_source'].update({'id': item['_id']})
|
||||
source_data.append(item['_source'])
|
||||
|
||||
return Command.from_multi_dict(source_data)
|
||||
|
||||
def count(self, **query):
|
||||
try:
|
||||
body = self.get_query_body(**query)
|
||||
data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body)
|
||||
count = data["count"]
|
||||
except Exception as e:
|
||||
logger.error('ES count error: {}'.format(e))
|
||||
count = 0
|
||||
return count
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.es, item)
|
||||
|
||||
def all(self):
|
||||
"""返回所有数据"""
|
||||
raise NotImplementedError("Not support")
|
||||
|
||||
def ping(self, timeout=None):
|
||||
try:
|
||||
return self.es.ping(request_timeout=timeout)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_query_body(self, **kwargs):
|
||||
new_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
new_kwargs[k] = str(v) if isinstance(v, UUID) else v
|
||||
kwargs = new_kwargs
|
||||
|
||||
index_in_field = 'id__in'
|
||||
exact_fields = self.exact_fields
|
||||
match_fields = self.match_fields
|
||||
|
||||
match = {}
|
||||
exact = {}
|
||||
index = {}
|
||||
|
||||
if index_in_field in kwargs:
|
||||
index['values'] = kwargs[index_in_field]
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if k in exact_fields:
|
||||
exact[k] = v
|
||||
elif k in match_fields:
|
||||
match[k] = v
|
||||
|
||||
# 处理时间
|
||||
timestamp__gte = kwargs.get('timestamp__gte')
|
||||
timestamp__lte = kwargs.get('timestamp__lte')
|
||||
@staticmethod
|
||||
def handler_time_field(data):
|
||||
timestamp__gte = data.get('timestamp__gte')
|
||||
timestamp__lte = data.get('timestamp__lte')
|
||||
timestamp_range = {}
|
||||
|
||||
if timestamp__gte:
|
||||
timestamp_range['gte'] = timestamp__gte
|
||||
if timestamp__lte:
|
||||
timestamp_range['lte'] = timestamp__lte
|
||||
|
||||
# 处理组织
|
||||
should = []
|
||||
org_id = match.get('org_id')
|
||||
|
||||
real_default_org_id = '00000000-0000-0000-0000-000000000002'
|
||||
root_org_id = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
if org_id == root_org_id:
|
||||
match.pop('org_id')
|
||||
elif org_id in (real_default_org_id, ''):
|
||||
match.pop('org_id')
|
||||
should.append({
|
||||
'bool': {
|
||||
'must_not': [
|
||||
{
|
||||
'wildcard': {'org_id': '*'}
|
||||
}
|
||||
]}
|
||||
})
|
||||
should.append({'match': {'org_id': real_default_org_id}})
|
||||
|
||||
# 构建 body
|
||||
body = {
|
||||
'query': {
|
||||
'bool': {
|
||||
'must': [
|
||||
{'match': {k: v}} for k, v in match.items()
|
||||
],
|
||||
'should': should,
|
||||
'filter': [
|
||||
{
|
||||
'term': {k: v}
|
||||
} for k, v in exact.items()
|
||||
] + [
|
||||
{
|
||||
'range': {
|
||||
'timestamp': timestamp_range
|
||||
}
|
||||
}
|
||||
] + [
|
||||
{
|
||||
'ids': {k: v}
|
||||
} for k, v in index.items()
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
return body
|
||||
|
||||
|
||||
class QuerySet(DJQuerySet):
|
||||
_method_calls = None
|
||||
_storage = None
|
||||
_command_store_config = None
|
||||
_slice = None # (from_, size)
|
||||
default_days_ago = 5
|
||||
max_result_window = 10000
|
||||
|
||||
def __init__(self, command_store_config):
|
||||
self._method_calls = []
|
||||
self._command_store_config = command_store_config
|
||||
self._storage = CommandStore(command_store_config)
|
||||
|
||||
# 命令列表模糊搜索时报错
|
||||
super().__init__()
|
||||
|
||||
@lazyproperty
|
||||
def _grouped_method_calls(self):
|
||||
_method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])}
|
||||
return _method_calls
|
||||
|
||||
@lazyproperty
|
||||
def _filter_kwargs(self):
|
||||
_method_calls = self._grouped_method_calls
|
||||
filter_calls = _method_calls.get('filter')
|
||||
if not filter_calls:
|
||||
return {}
|
||||
names, multi_args, multi_kwargs = zip(*filter_calls)
|
||||
kwargs = reduce(lambda x, y: {**x, **y}, multi_kwargs, {})
|
||||
|
||||
striped_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
k = k.replace('__exact', '')
|
||||
k = k.replace('__startswith', '')
|
||||
k = k.replace('__icontains', '')
|
||||
striped_kwargs[k] = v
|
||||
return striped_kwargs
|
||||
|
||||
@lazyproperty
|
||||
def _sort(self):
|
||||
order_by = self._grouped_method_calls.get('order_by')
|
||||
if order_by:
|
||||
for call in reversed(order_by):
|
||||
fields = call[1]
|
||||
if fields:
|
||||
field = fields[-1]
|
||||
|
||||
if field.startswith('-'):
|
||||
direction = 'desc'
|
||||
else:
|
||||
direction = 'asc'
|
||||
field = field.lstrip('-+')
|
||||
sort = f'{field}:{direction}'
|
||||
return sort
|
||||
|
||||
def __execute(self):
|
||||
_filter_kwargs = self._filter_kwargs
|
||||
_sort = self._sort
|
||||
from_, size = self._slice or (None, None)
|
||||
data = self._storage.filter(_filter_kwargs, from_=from_, size=size, sort=_sort)
|
||||
return data
|
||||
|
||||
def __stage_method_call(self, item, *args, **kwargs):
|
||||
_clone = self.__clone()
|
||||
_clone._method_calls.append((item, args, kwargs))
|
||||
return _clone
|
||||
|
||||
def __clone(self):
|
||||
uqs = QuerySet(self._command_store_config)
|
||||
uqs._method_calls = self._method_calls.copy()
|
||||
uqs._slice = self._slice
|
||||
uqs.model = self.model
|
||||
return uqs
|
||||
|
||||
def count(self, limit_to_max_result_window=True):
|
||||
filter_kwargs = self._filter_kwargs
|
||||
count = self._storage.count(**filter_kwargs)
|
||||
if limit_to_max_result_window:
|
||||
count = min(count, self.max_result_window)
|
||||
return count
|
||||
|
||||
def __getattribute__(self, item):
|
||||
if any((
|
||||
item.startswith('__'),
|
||||
item in QuerySet.__dict__,
|
||||
)):
|
||||
return object.__getattribute__(self, item)
|
||||
|
||||
origin_attr = object.__getattribute__(self, item)
|
||||
if not inspect.ismethod(origin_attr):
|
||||
return origin_attr
|
||||
|
||||
attr = partial(self.__stage_method_call, item)
|
||||
return attr
|
||||
|
||||
def __getitem__(self, item):
|
||||
max_window = self.max_result_window
|
||||
if isinstance(item, slice):
|
||||
if self._slice is None:
|
||||
clone = self.__clone()
|
||||
from_ = item.start or 0
|
||||
if item.stop is None:
|
||||
size = self.max_result_window - from_
|
||||
else:
|
||||
size = item.stop - from_
|
||||
|
||||
if from_ + size > max_window:
|
||||
if from_ >= max_window:
|
||||
from_ = max_window
|
||||
size = 0
|
||||
else:
|
||||
size = max_window - from_
|
||||
clone._slice = (from_, size)
|
||||
return clone
|
||||
return self.__execute()[item]
|
||||
|
||||
def __repr__(self):
|
||||
return self.__execute().__repr__()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__execute())
|
||||
|
||||
def __len__(self):
|
||||
return self.count()
|
||||
return 'timestamp', timestamp_range
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.plugins.es import QuerySet as ESQuerySet
|
||||
from common.utils import get_logger
|
||||
from common.db.fields import EncryptJsonDictTextField
|
||||
from common.utils.timezone import local_now_date_display
|
||||
|
@ -117,7 +118,8 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
|
|||
|
||||
if self.type in TYPE_ENGINE_MAPPING:
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
|
||||
qs = engine_mod.QuerySet(self.config)
|
||||
store = engine_mod.CommandStore(self.config)
|
||||
qs = ESQuerySet(store)
|
||||
qs.model = Command
|
||||
return qs
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.const.signals import SKIP_SIGNAL
|
||||
from users.models import User
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from .status import Status
|
||||
|
@ -107,8 +108,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model):
|
|||
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
|
||||
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
|
||||
replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default')
|
||||
user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE)
|
||||
is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted')
|
||||
user = models.OneToOneField(User, related_name='terminal', verbose_name=_('Application User'), null=True, on_delete=models.CASCADE)
|
||||
is_accepted = models.BooleanField(default=False, verbose_name=_('Is Accepted'))
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
@ -159,6 +160,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model):
|
|||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.user:
|
||||
setattr(self.user, SKIP_SIGNAL, True)
|
||||
self.user.delete()
|
||||
self.user = None
|
||||
self.is_deleted = True
|
||||
|
|
|
@ -70,6 +70,9 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
|
|||
def __init__(self, command):
|
||||
self.command = command
|
||||
|
||||
def __str__(self):
|
||||
return str(self.message_type_label)
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
command = Command.objects.first().to_dict()
|
||||
|
|
|
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_otp_secret_key',
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True, verbose_name='OTP secret key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by Django 3.1 on 2021-04-27 12:43
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@ class Migration(migrations.Migration):
|
|||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('password', models.CharField(max_length=128)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -20,7 +20,7 @@ from django.shortcuts import reverse
|
|||
from orgs.utils import current_org
|
||||
from orgs.models import Organization
|
||||
from rbac.const import Scope
|
||||
from common.db import fields
|
||||
from common.db import fields, models as jms_models
|
||||
from common.utils import (
|
||||
date_expired_default, get_logger, lazyproperty, random_string, bulk_create_with_signal
|
||||
)
|
||||
|
@ -691,7 +691,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
mfa_level = models.SmallIntegerField(
|
||||
default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA')
|
||||
)
|
||||
otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True)
|
||||
otp_secret_key = fields.EncryptCharField(
|
||||
max_length=128, blank=True, null=True, verbose_name=_('OTP secret key')
|
||||
)
|
||||
# Todo: Auto generate key, let user download
|
||||
private_key = fields.EncryptTextField(
|
||||
blank=True, null=True, verbose_name=_('Private key')
|
||||
|
@ -705,7 +707,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
comment = models.TextField(
|
||||
blank=True, null=True, verbose_name=_('Comment')
|
||||
)
|
||||
is_first_login = models.BooleanField(default=True)
|
||||
is_first_login = models.BooleanField(default=True, verbose_name=_('Is first login'))
|
||||
date_expired = models.DateTimeField(
|
||||
default=date_expired_default, blank=True, null=True,
|
||||
db_index=True, verbose_name=_('Date expired')
|
||||
|
@ -927,7 +929,7 @@ class UserPasswordHistory(models.Model):
|
|||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
password = models.CharField(max_length=128)
|
||||
user = models.ForeignKey("users.User", related_name='history_passwords',
|
||||
on_delete=models.CASCADE, verbose_name=_('User'))
|
||||
on_delete=jms_models.CASCADE_SIGNAL_SKIP, verbose_name=_('User'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
|
||||
def __str__(self):
|
||||
|
|
Loading…
Reference in New Issue