diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index 536061082..f759da435 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -8,34 +8,51 @@ from users.models import User from ..models import LoginACL from .rules import RuleSerializer -__all__ = ['LoginACLSerializer', ] +__all__ = [ + "LoginACLSerializer", +] -common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') +common_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " +) class LoginACLSerializer(BulkModelSerializer): - user = ObjectRelatedField(queryset=User.objects, label=_('User')) + user = ObjectRelatedField(queryset=User.objects, label=_("User")) reviewers = ObjectRelatedField( - queryset=User.objects, label=_('Reviewers'), many=True, required=False + queryset=User.objects, label=_("Reviewers"), many=True, required=False + ) + action_display = serializers.ReadOnlyField( + source="get_action_display", label=_("Action") + ) + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count" ) - action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) - reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') rules = MethodSerializer() class Meta: model = LoginACL - fields_mini = ['id', 'name'] + fields_mini = ["id", "name"] fields_small = fields_mini + [ - 'priority', 'rules', 'action', 'action_display', 'is_active', 'user', - 'date_created', 'date_updated', 'reviewers_amount', 'comment', 'created_by', + "priority", + "rules", + "action", + "action_display", + "is_active", + "user", + "date_created", + "date_updated", + "reviewers_amount", + "comment", + "created_by", ] - fields_fk = ['user'] - fields_m2m = ['reviewers'] + fields_fk = ["user"] + fields_m2m = ["reviewers"] fields = fields_small + fields_fk + fields_m2m extra_kwargs = { - 'priority': {'default': 50}, - 'is_active': {'default': True}, - "reviewers": {'allow_null': False, 'required': True}, + "priority": {"default": 50}, + "is_active": {"default": True}, + "reviewers": {"allow_null": False, "required": True}, } def __init__(self, *args, **kwargs): @@ -43,7 +60,7 @@ class LoginACLSerializer(BulkModelSerializer): self.set_action_choices() def set_action_choices(self): - action = self.fields.get('action') + action = self.fields.get("action") if not action: return choices = action._choices @@ -53,6 +70,3 @@ class LoginACLSerializer(BulkModelSerializer): def get_rules_serializer(self): return RuleSerializer() - - def get_reviewers_display(self, obj): - return ','.join([str(user) for user in obj.reviewers.all()]) diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 7282bf1a9..84bab6cc3 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -3,54 +3,66 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.models import Organization -from assets.const import Protocol +from common.drf.fields import LabeledChoiceField from acls import models -__all__ = ['LoginAssetACLSerializer'] +__all__ = ["LoginAssetACLSerializer"] -common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') +common_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " +) class LoginAssetACLUsersSerializer(serializers.Serializer): username_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), - help_text=common_help_text + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, ) class LoginAssetACLAssestsSerializer(serializers.Serializer): ip_group_help_text = _( - 'Format for comma-delimited string, with * indicating a match all. ' - 'Such as: ' - '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 ' - '(Domain name support)' + "Format for comma-delimited string, with * indicating a match all. " + "Such as: " + "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 " + "(Domain name support)" ) ip_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'), - help_text=ip_group_help_text + default=["*"], + child=serializers.CharField(max_length=1024), + label=_("IP"), + help_text=ip_group_help_text, ) hostname_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'), - help_text=common_help_text + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Hostname"), + help_text=common_help_text, ) class LoginAssetACLAccountsSerializer(serializers.Serializer): protocol_group_help_text = _( - 'Format for comma-delimited string, with * indicating a match all. ' - 'Protocol options: {}' + "Format for comma-delimited string, with * indicating a match all. " + "Protocol options: {}" ) name_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Name'), - help_text=common_help_text + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Name"), + help_text=common_help_text, ) username_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), - help_text=common_help_text + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, ) @@ -58,34 +70,48 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): users = LoginAssetACLUsersSerializer() assets = LoginAssetACLAssestsSerializer() accounts = LoginAssetACLAccountsSerializer() - reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') - action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count" + ) + action = LabeledChoiceField( + choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action") + ) class Meta: model = models.LoginAssetACL - fields_mini = ['id', 'name'] + fields_mini = ["id", "name"] fields_small = fields_mini + [ - 'users', 'accounts', 'assets', - 'is_active', 'date_created', 'date_updated', - 'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' + "users", + "accounts", + "assets", + "is_active", + "date_created", + "date_updated", + "priority", + "action", + "comment", + "created_by", + "org_id", ] - fields_m2m = ['reviewers', 'reviewers_amount'] + fields_m2m = ["reviewers", "reviewers_amount"] fields = fields_small + fields_m2m extra_kwargs = { - "reviewers": {'allow_null': False, 'required': True}, - 'priority': {'default': 50}, - 'is_active': {'default': True}, + "reviewers": {"allow_null": False, "required": True}, + "priority": {"default": 50}, + "is_active": {"default": True}, } def validate_reviewers(self, reviewers): - org_id = self.fields['org_id'].default() + org_id = self.fields["org_id"].default() org = Organization.get_instance(org_id) if not org: - error = _('The organization `{}` does not exist'.format(org_id)) + error = _("The organization `{}` does not exist".format(org_id)) raise serializers.ValidationError(error) users = org.get_members() valid_reviewers = list(set(reviewers) & set(users)) if not valid_reviewers: - error = _('None of the reviewers belong to Organization `{}`'.format(org.name)) + error = _( + "None of the reviewers belong to Organization `{}`".format(org.name) + ) raise serializers.ValidationError(error) return valid_reviewers diff --git a/apps/assets/serializers/mixin.py b/apps/assets/serializers/mixin.py index 45943dc2a..e69de29bb 100644 --- a/apps/assets/serializers/mixin.py +++ b/apps/assets/serializers/mixin.py @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import gettext_lazy as _ - - -class CategoryDisplayMixin(serializers.Serializer): - category_display = serializers.ReadOnlyField( - source='get_category_display', label=_("Category display") - ) - type_display = serializers.ReadOnlyField( - source='get_type_display', label=_("Type display") - ) diff --git a/apps/audits/const.py b/apps/audits/const.py index 18033ee78..7a0a6bc6e 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -1,24 +1,74 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices, IntegerChoices DEFAULT_CITY = _("Unknown") MODELS_NEED_RECORD = ( # users - 'User', 'UserGroup', + "User", + "UserGroup", # acls - 'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', + "LoginACL", + "LoginAssetACL", + "LoginConfirmSetting", # assets - 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', - 'CommandFilter', 'Platform', 'Account', + "Asset", + "Node", + "AdminUser", + "SystemUser", + "Domain", + "Gateway", + "CommandFilterRule", + "CommandFilter", + "Platform", + "Account", # applications # orgs - 'Organization', + "Organization", # settings - 'Setting', + "Setting", # perms - 'AssetPermission', + "AssetPermission", # xpack - 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', + "License", + "Account", + "SyncInstanceTask", + "ChangeAuthPlan", + "GatherUserTask", ) + + +class OperateChoices(TextChoices): + mkdir = "mkdir", _("Mkdir") + rmdir = "rmdir", _("Rmdir") + delete = "delete", _("Delete") + upload = "upload", _("Upload") + rename = "rename", _("Rename") + symlink = "symlink", _("Symlink") + download = "download", _("Download") + + +class ActionChoices(TextChoices): + view = "view", _("View") + update = "update", _("Update") + delete = "delete", _("Delete") + create = "create", _("Create") + + +class LoginTypeChoices(TextChoices): + web = "W", _("Web") + terminal = "T", _("Terminal") + unknown = "U", _("Unknown") + + +class MFAChoices(IntegerChoices): + disabled = 0, _("Disabled") + enabled = 1, _("Enabled") + unknown = 2, _("-") + + +class LoginStatusChoices(IntegerChoices): + success = True, _("Success") + failed = False, _("Failed") diff --git a/apps/audits/models.py b/apps/audits/models.py index 5dd8eb0a2..6f0f8fcc0 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -8,63 +8,55 @@ from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org +from .const import ( + OperateChoices, + ActionChoices, + LoginTypeChoices, + MFAChoices, + LoginStatusChoices, +) __all__ = [ - 'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog', + "FTPLog", + "OperateLog", + "PasswordChangeLog", + "UserLoginLog", ] class FTPLog(OrgModelMixin): - OPERATE_DELETE = 'Delete' - OPERATE_UPLOAD = 'Upload' - OPERATE_DOWNLOAD = 'Download' - OPERATE_RMDIR = 'Rmdir' - OPERATE_RENAME = 'Rename' - OPERATE_MKDIR = 'Mkdir' - OPERATE_SYMLINK = 'Symlink' - - OPERATE_CHOICES = ( - (OPERATE_DELETE, _('Delete')), - (OPERATE_UPLOAD, _('Upload')), - (OPERATE_DOWNLOAD, _('Download')), - (OPERATE_RMDIR, _('Rmdir')), - (OPERATE_RENAME, _('Rename')), - (OPERATE_MKDIR, _('Mkdir')), - (OPERATE_SYMLINK, _('Symlink')) - ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) - remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) + user = models.CharField(max_length=128, verbose_name=_("User")) + remote_addr = models.CharField( + max_length=128, verbose_name=_("Remote addr"), blank=True, null=True + ) asset = models.CharField(max_length=1024, verbose_name=_("Asset")) system_user = models.CharField(max_length=128, verbose_name=_("System user")) - operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES) + operate = models.CharField( + max_length=16, verbose_name=_("Operate"), choices=OperateChoices.choices + ) filename = models.CharField(max_length=1024, verbose_name=_("Filename")) is_success = models.BooleanField(default=True, verbose_name=_("Success")) - date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start')) + date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start")) class Meta: verbose_name = _("File transfer log") class OperateLog(OrgModelMixin): - ACTION_CREATE = 'create' - ACTION_VIEW = 'view' - ACTION_UPDATE = 'update' - ACTION_DELETE = 'delete' - ACTION_CHOICES = ( - (ACTION_CREATE, _("Create")), - (ACTION_VIEW, _("View")), - (ACTION_UPDATE, _("Update")), - (ACTION_DELETE, _("Delete")) - ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) - action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) + user = models.CharField(max_length=128, verbose_name=_("User")) + action = models.CharField( + max_length=16, choices=ActionChoices.choices, verbose_name=_("Action") + ) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) 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) + 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 + ) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -84,50 +76,48 @@ class OperateLog(OrgModelMixin): class PasswordChangeLog(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) + user = models.CharField(max_length=128, verbose_name=_("User")) change_by = models.CharField(max_length=128, verbose_name=_("Change by")) - remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) + remote_addr = models.CharField( + max_length=128, verbose_name=_("Remote addr"), blank=True, null=True + ) + datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime")) def __str__(self): return "{} change {}'s password".format(self.change_by, self.user) class Meta: - verbose_name = _('Password change log') + verbose_name = _("Password change log") class UserLoginLog(models.Model): - LOGIN_TYPE_CHOICE = ( - ('W', 'Web'), - ('T', 'Terminal'), - ('U', 'Unknown'), - ) - - MFA_DISABLED = 0 - MFA_ENABLED = 1 - MFA_UNKNOWN = 2 - - MFA_CHOICE = ( - (MFA_DISABLED, _('Disabled')), - (MFA_ENABLED, _('Enabled')), - (MFA_UNKNOWN, _('-')), - ) - - STATUS_CHOICE = ( - (True, _('Success')), - (False, _('Failed')) - ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - username = models.CharField(max_length=128, verbose_name=_('Username')) - type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) - ip = models.GenericIPAddressField(verbose_name=_('Login ip')) - city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city')) - user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent')) - mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA')) - reason = models.CharField(default='', max_length=128, blank=True, verbose_name=_('Reason')) - status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status')) - datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login')) - backend = models.CharField(max_length=32, default='', verbose_name=_('Authentication backend')) + username = models.CharField(max_length=128, verbose_name=_("Username")) + type = models.CharField( + choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type") + ) + ip = models.GenericIPAddressField(verbose_name=_("Login ip")) + city = models.CharField( + max_length=254, blank=True, null=True, verbose_name=_("Login city") + ) + user_agent = models.CharField( + max_length=254, blank=True, null=True, verbose_name=_("User agent") + ) + mfa = models.SmallIntegerField( + default=MFAChoices.unknown, choices=MFAChoices.choices, verbose_name=_("MFA") + ) + reason = models.CharField( + default="", max_length=128, blank=True, verbose_name=_("Reason") + ) + status = models.BooleanField( + default=LoginStatusChoices.success, + choices=LoginStatusChoices.choices, + verbose_name=_("Status"), + ) + datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Date login")) + backend = models.CharField( + max_length=32, default="", verbose_name=_("Authentication backend") + ) @property def backend_display(self): @@ -137,8 +127,8 @@ class UserLoginLog(models.Model): def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None): login_logs = cls.objects.all() if date_from and date_to: - date_from = "{} {}".format(date_from, '00:00:00') - date_to = "{} {}".format(date_to, '23:59:59') + date_from = "{} {}".format(date_from, "00:00:00") + date_to = "{} {}".format(date_to, "23:59:59") login_logs = login_logs.filter( datetime__gte=date_from, datetime__lte=date_to ) @@ -146,18 +136,19 @@ class UserLoginLog(models.Model): login_logs = login_logs.filter(username=user) if keyword: login_logs = login_logs.filter( - Q(ip__contains=keyword) | - Q(city__contains=keyword) | - Q(username__contains=keyword) + Q(ip__contains=keyword) + | Q(city__contains=keyword) + | Q(username__contains=keyword) ) if not current_org.is_root(): - username_list = current_org.get_members().values_list('username', flat=True) + username_list = current_org.get_members().values_list("username", flat=True) login_logs = login_logs.filter(username__in=username_list) return login_logs @property def reason_display(self): from authentication.errors import reason_choices, old_reason_choices + reason = reason_choices.get(self.reason) if reason: return reason @@ -165,5 +156,5 @@ class UserLoginLog(models.Model): return reason class Meta: - ordering = ['-datetime', 'username'] - verbose_name = _('User login log') + ordering = ["-datetime", "username"] + verbose_name = _("User login log") diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 0f595be25..46e383f6a 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -3,77 +3,99 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.serializers import BulkSerializerMixin +from common.drf.fields import LabeledChoiceField from terminal.models import Session from . import models +from .const import ( + ActionChoices, + OperateChoices, + MFAChoices, + LoginStatusChoices, + LoginTypeChoices, +) class FTPLogSerializer(serializers.ModelSerializer): - operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate display')) + operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate")) class Meta: model = models.FTPLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'user', 'remote_addr', 'asset', 'system_user', 'org_id', - 'operate', 'filename', 'operate_display', - 'is_success', - 'date_start', + "user", + "remote_addr", + "asset", + "system_user", + "org_id", + "operate", + "filename", + "is_success", + "date_start", ] fields = fields_small class UserLoginLogSerializer(serializers.ModelSerializer): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display')) - mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA display')) + mfa = LabeledChoiceField(choices=MFAChoices.choices, label=_("MFA")) + type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type")) + status = LabeledChoiceField(choices=LoginStatusChoices.choices, label=_("Status")) class Meta: model = models.UserLoginLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', - 'mfa', 'mfa_display', 'reason', 'reason_display', 'backend', 'backend_display', - 'status', 'status_display', - 'datetime', + "username", + "type", + "ip", + "city", + "user_agent", + "mfa", + "reason", + "reason_display", + "backend", + "backend_display", + "status", + "datetime", ] fields = fields_small extra_kwargs = { - "user_agent": {'label': _('User agent')}, - "reason_display": {'label': _('Reason display')}, - 'backend_display': {'label': _('Authentication backend')} + "user_agent": {"label": _("User agent")}, + "reason_display": {"label": _("Reason display")}, + "backend_display": {"label": _("Authentication backend")}, } class OperateLogSerializer(serializers.ModelSerializer): - action_display = serializers.CharField(source='get_action_display', label=_('Action')) + action = LabeledChoiceField(choices=ActionChoices.choices, label=_("Action")) class Meta: model = models.OperateLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'user', 'action', 'action_display', - 'resource_type', 'resource_type_display', 'resource', - 'remote_addr', 'datetime', 'org_id' + "user", + "action", + "resource_type", + "resource_type_display", + "resource", + "remote_addr", + "datetime", + "org_id", ] fields = fields_small - extra_kwargs = { - 'resource_type_display': {'label': _('Resource Type')} - } + extra_kwargs = {"resource_type_display": {"label": _("Resource Type")}} class PasswordChangeLogSerializer(serializers.ModelSerializer): class Meta: model = models.PasswordChangeLog - fields = ( - 'id', 'user', 'change_by', 'remote_addr', 'datetime' - ) + fields = ("id", "user", "change_by", "remote_addr", "datetime") class SessionAuditSerializer(serializers.ModelSerializer): class Meta: model = Session - fields = '__all__' + fields = "__all__" + # # class CommandExecutionSerializer(serializers.ModelSerializer): diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index f395965c3..7b07b3f1b 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -1,38 +1,34 @@ # -*- coding: utf-8 -*- # -import time - -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete -) -from django.dispatch import receiver from django.conf import settings from django.db import transaction -from django.utils import timezone +from django.dispatch import receiver +from django.utils import timezone, translation from django.utils.functional import LazyObject from django.contrib.auth import BACKEND_SESSION_KEY from django.utils.translation import ugettext_lazy as _ -from django.utils import translation -from rest_framework.renderers import JSONRenderer +from django.db.models.signals import post_save, m2m_changed, pre_delete from rest_framework.request import Request +from rest_framework.renderers import JSONRenderer -from assets.models import Asset -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 . import models, serializers -from .models import OperateLog from orgs.utils import current_org from perms.models import AssetPermission -from terminal.backends.command.serializers import SessionCommandSerializer +from users.models import User +from users.signals import post_user_change_password +from assets.models import Asset +from jumpserver.utils import current_request +from authentication.signals import post_auth_failed, post_auth_success +from authentication.utils import check_different_city_login_if_need +from terminal.models import Session, Command from terminal.serializers import SessionSerializer +from terminal.backends.command.serializers import SessionCommandSerializer from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json +from . import models, serializers +from .const import ActionChoices +from .utils import write_login_log, create_operate_log + logger = get_logger(__name__) sys_logger = get_syslogger(__name__) @@ -46,14 +42,14 @@ class AuthBackendLabelMapping(LazyObject): for source, backends in User.SOURCE_BACKEND_MAPPING.items(): for backend in backends: backend_label_mapping[backend] = source.label - backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') - backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') - backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') - backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token') - backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') - backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu') - backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') - backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token') + backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _("SSH Key") + backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _("Password") + backend_label_mapping[settings.AUTH_BACKEND_SSO] = _("SSO") + backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") + backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") + backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") return backend_label_mapping def _setup(self): @@ -65,41 +61,41 @@ AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() M2M_NEED_RECORD = { User.groups.through.__name__: ( - _('User and Group'), - _('{User} JOINED {UserGroup}'), - _('{User} LEFT {UserGroup}') + _("User and Group"), + _("{User} JOINED {UserGroup}"), + _("{User} LEFT {UserGroup}"), ), Asset.nodes.through.__name__: ( - _('Node and Asset'), - _('{Node} ADD {Asset}'), - _('{Node} REMOVE {Asset}') + _("Node and Asset"), + _("{Node} ADD {Asset}"), + _("{Node} REMOVE {Asset}"), ), AssetPermission.users.through.__name__: ( - _('User asset permissions'), - _('{AssetPermission} ADD {User}'), - _('{AssetPermission} REMOVE {User}'), + _("User asset permissions"), + _("{AssetPermission} ADD {User}"), + _("{AssetPermission} REMOVE {User}"), ), AssetPermission.user_groups.through.__name__: ( - _('User group asset permissions'), - _('{AssetPermission} ADD {UserGroup}'), - _('{AssetPermission} REMOVE {UserGroup}'), + _("User group asset permissions"), + _("{AssetPermission} ADD {UserGroup}"), + _("{AssetPermission} REMOVE {UserGroup}"), ), AssetPermission.assets.through.__name__: ( - _('Asset permission'), - _('{AssetPermission} ADD {Asset}'), - _('{AssetPermission} REMOVE {Asset}'), + _("Asset permission"), + _("{AssetPermission} ADD {Asset}"), + _("{AssetPermission} REMOVE {Asset}"), ), AssetPermission.nodes.through.__name__: ( - _('Node permission'), - _('{AssetPermission} ADD {Node}'), - _('{AssetPermission} REMOVE {Node}'), + _("Node permission"), + _("{AssetPermission} ADD {Node}"), + _("{AssetPermission} REMOVE {Node}"), ), } M2M_ACTION_MAPER = { - POST_ADD: OperateLog.ACTION_CREATE, - POST_REMOVE: OperateLog.ACTION_DELETE, - POST_CLEAR: OperateLog.ACTION_DELETE, + POST_ADD: ActionChoices.create, + POST_REMOVE: ActionChoices.delete, + POST_CLEAR: ActionChoices.delete, } @@ -117,12 +113,14 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs): 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] + resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[ + sender_name + ] action = M2M_ACTION_MAPER[action] - if action == OperateLog.ACTION_CREATE: + if action == ActionChoices.create: resource_tmpl = resource_tmpl_add - elif action == OperateLog.ACTION_DELETE: + elif action == ActionChoices.delete: resource_tmpl = resource_tmpl_remove else: return @@ -139,41 +137,53 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs): print("Instace name: ", instance_name, instance_value) for obj in objs: - resource = resource_tmpl.format(**{ - instance_name: instance_value, - model_name: str(obj) - })[:128] # `resource` 字段只有 128 个字符长 😔 + resource = resource_tmpl.format( + **{instance_name: instance_value, model_name: str(obj)} + )[ + :128 + ] # `resource` 字段只有 128 个字符长 😔 - 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) + to_create.append( + models.OperateLog( + user=user, + action=action, + resource_type=resource_type, + resource=resource, + remote_addr=remote_addr, + org_id=org_id, + ) + ) + models.OperateLog.objects.bulk_create(to_create) @receiver(post_save) -def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): +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: + if ( + instance._meta.object_name == "User" + and update_fields + and "last_login" in update_fields + ): return if created: - action = models.OperateLog.ACTION_CREATE + action = ActionChoices.create else: - action = models.OperateLog.ACTION_UPDATE + action = ActionChoices.update create_operate_log(action, sender, instance) @receiver(pre_delete) def on_object_delete(sender, instance=None, **kwargs): - create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) + create_operate_log(ActionChoices.delete, sender, instance) @receiver(post_user_change_password, sender=User) def on_user_change_password(sender, user=None, **kwargs): if not current_request: - remote_addr = '127.0.0.1' - change_by = 'System' + remote_addr = "127.0.0.1" + change_by = "System" else: remote_addr = get_request_ip(current_request) if not current_request.user.is_authenticated: @@ -182,7 +192,8 @@ def on_user_change_password(sender, user=None, **kwargs): change_by = str(current_request.user) with transaction.atomic(): models.PasswordChangeLog.objects.create( - user=str(user), change_by=change_by, + user=str(user), + change_by=change_by, remote_addr=remote_addr, ) @@ -216,51 +227,52 @@ def on_audits_log_create(sender, instance=None, **kwargs): def get_login_backend(request): - backend = request.session.get('auth_backend', '') or \ - request.session.get(BACKEND_SESSION_KEY, '') + backend = request.session.get("auth_backend", "") or request.session.get( + BACKEND_SESSION_KEY, "" + ) backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None) if backend_label is None: - backend_label = '' + backend_label = "" return backend_label def generate_data(username, request, login_type=None): - user_agent = request.META.get('HTTP_USER_AGENT', '') - login_ip = get_request_ip(request) or '0.0.0.0' + user_agent = request.META.get("HTTP_USER_AGENT", "") + login_ip = get_request_ip(request) or "0.0.0.0" if login_type is None and isinstance(request, Request): - login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') + login_type = request.META.get("HTTP_X_JMS_LOGIN_TYPE", "U") if login_type is None: - login_type = 'W' + login_type = "W" - with translation.override('en'): + with translation.override("en"): backend = str(get_login_backend(request)) data = { - 'username': username, - 'ip': login_ip, - 'type': login_type, - 'user_agent': user_agent[0:254], - 'datetime': timezone.now(), - 'backend': backend, + "username": username, + "ip": login_ip, + "type": login_type, + "user_agent": user_agent[0:254], + "datetime": timezone.now(), + "backend": backend, } return data @receiver(post_auth_success) def on_user_auth_success(sender, user, request, login_type=None, **kwargs): - logger.debug('User login success: {}'.format(user.username)) + logger.debug("User login success: {}".format(user.username)) check_different_city_login_if_need(user, request) data = generate_data(user.username, request, login_type=login_type) - request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") - data.update({'mfa': int(user.mfa_enabled), 'status': True}) + request.session["login_time"] = data["datetime"].strftime("%Y-%m-%d %H:%M:%S") + data.update({"mfa": int(user.mfa_enabled), "status": True}) write_login_log(**data) @receiver(post_auth_failed) -def on_user_auth_failed(sender, username, request, reason='', **kwargs): - logger.debug('User login failed: {}'.format(username)) +def on_user_auth_failed(sender, username, request, reason="", **kwargs): + logger.debug("User login failed: {}".format(username)) data = generate_data(username, request) - data.update({'reason': reason[:128], 'status': False}) + data.update({"reason": reason[:128], "status": False}) write_login_log(**data) diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index edca62d5b..22640fb5d 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -9,13 +9,24 @@ from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import signer, crypto - __all__ = [ - 'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin', - 'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', - 'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', - 'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField', - 'EncryptJsonDictCharField', 'PortField', 'BitChoices', + "JsonMixin", + "JsonDictMixin", + "JsonListMixin", + "JsonTypeMixin", + "JsonCharField", + "JsonTextField", + "JsonListCharField", + "JsonListTextField", + "JsonDictCharField", + "JsonDictTextField", + "EncryptCharField", + "EncryptTextField", + "EncryptMixin", + "EncryptJsonDictTextField", + "EncryptJsonDictCharField", + "PortField", + "BitChoices", ] @@ -116,7 +127,7 @@ class EncryptMixin: """ def decrypt_from_signer(self, value): - return signer.unsign(value) or '' + return signer.unsign(value) or "" def from_db_value(self, value, expression, connection, context=None): if not value: @@ -131,7 +142,7 @@ class EncryptMixin: # 可能和Json mix,所以要先解密,再json sp = super() - if hasattr(sp, 'from_db_value'): + if hasattr(sp, "from_db_value"): plain_value = sp.from_db_value(plain_value, expression, connection, context) return plain_value @@ -141,7 +152,7 @@ class EncryptMixin: # 先 json 再解密 sp = super() - if hasattr(sp, 'get_prep_value'): + if hasattr(sp, "get_prep_value"): value = sp.get_prep_value(value) value = force_text(value) # 替换新的加密方式 @@ -155,12 +166,12 @@ class EncryptTextField(EncryptMixin, models.TextField): class EncryptCharField(EncryptMixin, models.CharField): @staticmethod def change_max_length(kwargs): - kwargs.setdefault('max_length', 1024) - max_length = kwargs.get('max_length') + kwargs.setdefault("max_length", 1024) + max_length = kwargs.get("max_length") if max_length < 129: max_length = 128 max_length = max_length * 2 - kwargs['max_length'] = max_length + kwargs["max_length"] = max_length def __init__(self, *args, **kwargs): self.change_max_length(kwargs) @@ -168,10 +179,10 @@ class EncryptCharField(EncryptMixin, models.CharField): def deconstruct(self): name, path, args, kwargs = super().deconstruct() - max_length = kwargs.pop('max_length') + max_length = kwargs.pop("max_length") if max_length > 255: max_length = max_length // 2 - kwargs['max_length'] = max_length + kwargs["max_length"] = max_length return name, path, args, kwargs @@ -185,11 +196,13 @@ class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): class PortField(models.IntegerField): def __init__(self, *args, **kwargs): - kwargs.update({ - 'blank': False, - 'null': False, - 'validators': [MinValueValidator(0), MaxValueValidator(65535)] - }) + kwargs.update( + { + "blank": False, + "null": False, + "validators": [MinValueValidator(0), MaxValueValidator(65535)], + } + ) super().__init__(*args, **kwargs) @@ -200,22 +213,22 @@ class BitChoices(models.IntegerChoices): @classmethod def tree(cls): - root = [_('All'), cls.branches()] + root = [_("All"), cls.branches()] return cls.render_node(root) @classmethod def render_node(cls, node): if isinstance(node, BitChoices): return { - 'id': node.name, - 'label': node.label, + "id": node.name, + "label": node.label, } else: name, children = node return { - 'id': name, - 'label': name, - 'children': [cls.render_node(child) for child in children] + "id": name, + "label": name, + "children": [cls.render_node(child) for child in children], } @classmethod @@ -224,5 +237,3 @@ class BitChoices(models.IntegerChoices): for c in cls: value |= c.value return value - - diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 9f824e5e2..3d1131c7d 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -9,14 +9,20 @@ from rest_framework.request import Request from common.exceptions import UserConfirmRequired from audits.utils import create_operate_log from audits.models import OperateLog +from audits.const import ActionChoices -__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"] +__all__ = [ + "PermissionsMixin", + "RecordViewLogMixin", + "UserConfirmRequiredExceptionMixin", +] class UserConfirmRequiredExceptionMixin: """ 异常处理 """ + def dispatch(self, request, *args, **kwargs): try: return super().dispatch(request, *args, **kwargs) @@ -40,23 +46,23 @@ class PermissionsMixin(UserPassesTestMixin): class RecordViewLogMixin: - ACTION = OperateLog.ACTION_VIEW + ACTION = ActionChoices.view @staticmethod def get_resource_display(request): query_params = dict(request.query_params) - if query_params.get('format'): - query_params.pop('format') - spm_filter = query_params.pop('spm') if query_params.get('spm') else None + if query_params.get("format"): + query_params.pop("format") + spm_filter = query_params.pop("spm") if query_params.get("spm") else None if not query_params and not spm_filter: - display_message = _('Export all') + display_message = _("Export all") elif spm_filter: - display_message = _('Export only selected items') + display_message = _("Export only selected items") else: - query = ','.join( - ['%s=%s' % (key, value) for key, value in query_params.items()] + query = ",".join( + ["%s=%s" % (key, value) for key, value in query_params.items()] ) - display_message = _('Export filtered: %s') % query + display_message = _("Export filtered: %s") % query return display_message def list(self, request, *args, **kwargs): diff --git a/apps/terminal/automations/deploy_applet_host/__init__.py b/apps/terminal/automations/deploy_applet_host/__init__.py index 8b415ece4..2946a6410 100644 --- a/apps/terminal/automations/deploy_applet_host/__init__.py +++ b/apps/terminal/automations/deploy_applet_host/__init__.py @@ -21,38 +21,40 @@ class DeployAppletHostManager: @staticmethod def get_run_dir(): - base = os.path.join(settings.ANSIBLE_DIR, 'applet_host_deploy') - now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + base = os.path.join(settings.ANSIBLE_DIR, "applet_host_deploy") + now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") return os.path.join(base, now) def generate_playbook(self): - playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml') + playbook_src = os.path.join(CURRENT_DIR, "playbook.yml") base_site_url = settings.BASE_SITE_URL bootstrap_token = settings.BOOTSTRAP_TOKEN host_id = str(self.deployment.host.id) if not base_site_url: - base_site_url = "localhost:8080" + base_site_url = "http://localhost:8080" with open(playbook_src) as f: plays = yaml.safe_load(f) for play in plays: - play['vars'].update(self.deployment.host.deploy_options) - play['vars']['DownloadHost'] = base_site_url + '/download/' - play['vars']['CORE_HOST'] = base_site_url - play['vars']['BOOTSTRAP_TOKEN'] = bootstrap_token - play['vars']['HOST_ID'] = host_id - play['vars']['HOST_NAME'] = self.deployment.host.name + play["vars"].update(self.deployment.host.deploy_options) + play["vars"]["DownloadHost"] = base_site_url + "/download" + play["vars"]["CORE_HOST"] = base_site_url + play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token + play["vars"]["HOST_ID"] = host_id + play["vars"]["HOST_NAME"] = self.deployment.host.name - playbook_dir = os.path.join(self.run_dir, 'playbook') - playbook_dst = os.path.join(playbook_dir, 'main.yml') + playbook_dir = os.path.join(self.run_dir, "playbook") + playbook_dst = os.path.join(playbook_dir, "main.yml") os.makedirs(playbook_dir, exist_ok=True) - with open(playbook_dst, 'w') as f: + with open(playbook_dst, "w") as f: yaml.safe_dump(plays, f) return playbook_dst def generate_inventory(self): - inventory = JMSInventory([self.deployment.host], account_policy='privileged_only') - inventory_dir = os.path.join(self.run_dir, 'inventory') - inventory_path = os.path.join(inventory_dir, 'hosts.yml') + inventory = JMSInventory( + [self.deployment.host], account_policy="privileged_only" + ) + inventory_dir = os.path.join(self.run_dir, "inventory") + inventory_path = os.path.join(inventory_dir, "hosts.yml") inventory.write_to_file(inventory_path) return inventory_path @@ -71,7 +73,7 @@ class DeployAppletHostManager: self.deployment.status = cb.status except Exception as e: logger.error("Error: {}".format(e)) - self.deployment.status = 'error' + self.deployment.status = "error" finally: self.deployment.date_finished = timezone.now() with safe_db_connection():