diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index d689675b0..ee2fba4d5 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -61,7 +61,7 @@ class LoginAssetCheckAPI(CreateAPIView): 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'ticket_detail_url': ticket_detail_url, - 'reviewers': [str(user) for user in ticket.assignees.all()], + 'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()], } return data diff --git a/apps/applications/api/account.py b/apps/applications/api/account.py index 3193467c3..8b28bcc37 100644 --- a/apps/applications/api/account.py +++ b/apps/applications/api/account.py @@ -12,7 +12,7 @@ from .. import serializers class AccountFilterSet(BaseFilterSet): - username = filters.CharFilter(field_name='username') + username = filters.CharFilter(method='do_nothing') type = filters.CharFilter(field_name='type', lookup_expr='exact') category = filters.CharFilter(field_name='category', lookup_expr='exact') app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact') diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 1090e0095..245379630 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -6,14 +6,15 @@ from rest_framework.decorators import action from rest_framework.response import Response from common.tree import TreeNodeSerializer -from ..hands import IsOrgAdminOrAppUser, IsValidUser +from common.mixins.views import SuggestionMixin +from ..hands import IsOrgAdminOrAppUser from .. import serializers from ..models import Application __all__ = ['ApplicationViewSet'] -class ApplicationViewSet(OrgBulkModelViewSet): +class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet): model = Application filterset_fields = { 'name': ['exact'], @@ -24,7 +25,8 @@ class ApplicationViewSet(OrgBulkModelViewSet): permission_classes = (IsOrgAdminOrAppUser,) serializer_classes = { 'default': serializers.AppSerializer, - 'get_tree': TreeNodeSerializer + 'get_tree': TreeNodeSerializer, + 'suggestion': serializers.MiniAppSerializer } @action(methods=['GET'], detail=False, url_path='tree') @@ -34,9 +36,3 @@ class ApplicationViewSet(OrgBulkModelViewSet): tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count) serializer = self.get_serializer(tree_nodes, many=True) return Response(serializer.data) - - @action(methods=['get'], detail=False, permission_classes=(IsValidUser,)) - def suggestion(self, request): - queryset = self.filter_queryset(self.get_queryset())[:3] - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 79cbdd3bc..0eba3e3e9 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -15,7 +15,7 @@ from .. import models from .. import const __all__ = [ - 'AppSerializer', 'AppSerializerMixin', + 'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin', 'AppAccountSerializer', 'AppAccountSecretSerializer' ] @@ -78,6 +78,12 @@ class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer): return _attrs +class MiniAppSerializer(serializers.ModelSerializer): + class Meta: + model = models.Application + fields = AppSerializer.Meta.fields_mini + + class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True) category_display = serializers.SerializerMethodField(label=_('Category display')) diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 35023c39d..5fe0152d7 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- # from assets.api import FilterAssetByNodeMixin -from rest_framework.decorators import action from rest_framework.viewsets import ModelViewSet from rest_framework.generics import RetrieveAPIView -from rest_framework.response import Response from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser, IsValidUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser +from common.mixins.views import SuggestionMixin from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from ..models import Asset, Node, Platform @@ -27,7 +26,7 @@ __all__ = [ ] -class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet): +class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ @@ -64,12 +63,6 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet): assets = serializer.save() self.set_assets_node(assets) - @action(methods=['get'], detail=False, permission_classes=(IsValidUser,)) - def suggestion(self, request): - queryset = self.filter_queryset(self.get_queryset())[:3] - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - class AssetPlatformRetrieveApi(RetrieveAPIView): queryset = Platform.objects.all() diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index ca22c494f..948300d93 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -77,7 +77,7 @@ class CommandConfirmAPI(CreateAPIView): 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'ticket_detail_url': ticket_detail_url, - 'reviewers': [str(user) for user in ticket.assignees.all()] + 'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()] } @lazyproperty diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 03a506640..674ff2208 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -6,6 +6,7 @@ from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics +from common.mixins.views import SuggestionMixin from orgs.utils import tmp_to_root_org from ..models import SystemUser, Asset from .. import serializers @@ -24,7 +25,7 @@ __all__ = [ ] -class SystemUserViewSet(OrgBulkModelViewSet): +class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): """ System user api set, for add,delete,update,list,retrieve resource """ @@ -39,6 +40,7 @@ class SystemUserViewSet(OrgBulkModelViewSet): serializer_class = serializers.SystemUserSerializer serializer_classes = { 'default': serializers.SystemUserSerializer, + 'suggestion': serializers.MiniSystemUserSerializer } permission_classes = (IsOrgAdminOrAppUser,) diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 83b145249..7aafab504 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -120,6 +120,7 @@ class Gateway(BaseUser): except(paramiko.AuthenticationException, paramiko.BadAuthenticationType, paramiko.SSHException, + paramiko.ChannelException, paramiko.ssh_exception.NoValidConnectionsError, socket.gaierror) as e: err = str(e) @@ -128,6 +129,8 @@ class Gateway(BaseUser): err = err.format(port=self.port, ip=self.ip) elif err == 'Authentication failed.': err = _('Authentication failed') + elif err == 'Connect failed': + err = _('Connect failed') self.is_connective = False return False, err diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 69848ce20..8c4ebf98d 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -10,7 +10,7 @@ from .utils import validate_password_contains_left_double_curly_bracket from .base import AuthSerializerMixin __all__ = [ - 'SystemUserSerializer', + 'SystemUserSerializer', 'MiniSystemUserSerializer', 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', @@ -185,6 +185,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): return queryset +class MiniSystemUserSerializer(serializers.ModelSerializer): + class Meta: + model = SystemUser + fields = SystemUserSerializer.Meta.fields_mini + + class SystemUserWithAuthInfoSerializer(SystemUserSerializer): class Meta(SystemUserSerializer.Meta): fields_mini = ['id', 'name', 'username'] @@ -208,6 +214,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): """ 系统用户最基本信息的数据结构 """ + class Meta: model = SystemUser fields = ('id', 'name', 'username') diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 70f8a863c..c793481e8 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import post_save, post_delete, m2m_changed +from django.db.models.signals import ( + post_save, post_delete, m2m_changed, pre_delete +) from django.dispatch import receiver from django.conf import settings from django.db import transaction @@ -35,7 +37,7 @@ MODELS_NEED_RECORD = ( # users 'User', 'UserGroup', # acls - 'LoginACL', 'LoginAssetACL', + 'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', # assets 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', 'CommandFilter', 'Platform', 'AuthBook', @@ -98,68 +100,68 @@ def create_operate_log(action, sender, resource): M2M_NEED_RECORD = { 'OrganizationMember': ( _('User and Organization'), - _('{User} *JOINED* {Organization}'), - _('{User} *LEFT* {Organization}') + _('{User} JOINED {Organization}'), + _('{User} LEFT {Organization}') ), User.groups.through._meta.object_name: ( _('User and Group'), - _('{User} *JOINED* {UserGroup}'), - _('{User} *LEFT* {UserGroup}') + _('{User} JOINED {UserGroup}'), + _('{User} LEFT {UserGroup}') ), SystemUser.assets.through._meta.object_name: ( _('Asset and SystemUser'), - _('{Asset} *ADD* {SystemUser}'), - _('{Asset} *REMOVE* {SystemUser}') + _('{Asset} ADD {SystemUser}'), + _('{Asset} REMOVE {SystemUser}') ), Asset.nodes.through._meta.object_name: ( _('Node and Asset'), - _('{Node} *ADD* {Asset}'), - _('{Node} *REMOVE* {Asset}') + _('{Node} ADD {Asset}'), + _('{Node} REMOVE {Asset}') ), AssetPermission.users.through._meta.object_name: ( _('User asset permissions'), - _('{AssetPermission} *ADD* {User}'), - _('{AssetPermission} *REMOVE* {User}'), + _('{AssetPermission} ADD {User}'), + _('{AssetPermission} REMOVE {User}'), ), AssetPermission.user_groups.through._meta.object_name: ( _('User group asset permissions'), - _('{AssetPermission} *ADD* {UserGroup}'), - _('{AssetPermission} *REMOVE* {UserGroup}'), + _('{AssetPermission} ADD {UserGroup}'), + _('{AssetPermission} REMOVE {UserGroup}'), ), AssetPermission.assets.through._meta.object_name: ( _('Asset permission'), - _('{AssetPermission} *ADD* {Asset}'), - _('{AssetPermission} *REMOVE* {Asset}'), + _('{AssetPermission} ADD {Asset}'), + _('{AssetPermission} REMOVE {Asset}'), ), AssetPermission.nodes.through._meta.object_name: ( _('Node permission'), - _('{AssetPermission} *ADD* {Node}'), - _('{AssetPermission} *REMOVE* {Node}'), + _('{AssetPermission} ADD {Node}'), + _('{AssetPermission} REMOVE {Node}'), ), AssetPermission.system_users.through._meta.object_name: ( _('Asset permission and SystemUser'), - _('{AssetPermission} *ADD* {SystemUser}'), - _('{AssetPermission} *REMOVE* {SystemUser}'), + _('{AssetPermission} ADD {SystemUser}'), + _('{AssetPermission} REMOVE {SystemUser}'), ), ApplicationPermission.users.through._meta.object_name: ( _('User application permissions'), - _('{ApplicationPermission} *ADD* {User}'), - _('{ApplicationPermission} *REMOVE* {User}'), + _('{ApplicationPermission} ADD {User}'), + _('{ApplicationPermission} REMOVE {User}'), ), ApplicationPermission.user_groups.through._meta.object_name: ( _('User group application permissions'), - _('{ApplicationPermission} *ADD* {UserGroup}'), - _('{ApplicationPermission} *REMOVE* {UserGroup}'), + _('{ApplicationPermission} ADD {UserGroup}'), + _('{ApplicationPermission} REMOVE {UserGroup}'), ), ApplicationPermission.applications.through._meta.object_name: ( _('Application permission'), - _('{ApplicationPermission} *ADD* {Application}'), - _('{ApplicationPermission} *REMOVE* {Application}'), + _('{ApplicationPermission} ADD {Application}'), + _('{ApplicationPermission} REMOVE {Application}'), ), ApplicationPermission.system_users.through._meta.object_name: ( _('Application permission and SystemUser'), - _('{ApplicationPermission} *ADD* {SystemUser}'), - _('{ApplicationPermission} *REMOVE* {SystemUser}'), + _('{ApplicationPermission} ADD {SystemUser}'), + _('{ApplicationPermission} REMOVE {SystemUser}'), ), } @@ -226,7 +228,7 @@ def on_object_created_or_update(sender, instance=None, created=False, update_fie create_operate_log(action, sender, instance) -@receiver(post_delete) +@receiver(pre_delete) def on_object_delete(sender, instance=None, **kwargs): create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index c8005ba95..f2346ab6d 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -71,7 +71,7 @@ sms_failed_msg = _( "(The account will be temporarily locked for {block_time} minutes)" ) mfa_type_failed_msg = _( - "The MFA type({mfa_type}) is not supported" + "The MFA type({mfa_type}) is not supported, " "You can also try {times_try} times " "(The account will be temporarily locked for {block_time} minutes)" ) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 3a2f9c09b..79f747ee0 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -346,6 +346,10 @@ class AuthMixin(PasswordEncryptionViewMixin): def check_user_mfa_if_need(self, user): if self.request.session.get('auth_mfa'): return + + if settings.OTP_IN_RADIUS: + return + if not user.mfa_enabled: return unset, url = user.mfa_enabled_but_not_set() @@ -411,11 +415,11 @@ class AuthMixin(PasswordEncryptionViewMixin): return elif ticket.state_reject: raise errors.LoginConfirmOtherError( - ticket.id, ticket.get_action_display() + ticket.id, ticket.get_state_display() ) elif ticket.state_close: raise errors.LoginConfirmOtherError( - ticket.id, ticket.get_action_display() + ticket.id, ticket.get_state_display() ) else: raise errors.LoginConfirmOtherError( diff --git a/apps/authentication/models.py b/apps/authentication/models.py index c74d06953..62bd74392 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -45,6 +45,9 @@ class LoginConfirmSetting(CommonModelMixin): reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + class Meta: + verbose_name = _('Login Confirm') + @classmethod def get_user_confirm_setting(cls, user): return get_object_or_none(cls, user=user) @@ -83,7 +86,8 @@ class LoginConfirmSetting(CommonModelMixin): return ticket def __str__(self): - return '{} confirm'.format(self.user.username) + reviewers = [u.username for u in self.reviewers.all()] + return _('{} need confirm by {}').format(self.user.username, reviewers) class SSOToken(models.JMSBaseModel): diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index c6c1db680..87b177ff5 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -14,7 +14,8 @@ from .signals import post_auth_success, post_auth_failed @receiver(user_logged_in) def on_user_auth_login_success(sender, user, request, **kwargs): # 开启了 MFA,且没有校验过 - if user.mfa_enabled and not request.session.get('auth_mfa'): + + if user.mfa_enabled and not settings.OTP_IN_RADIUS and not request.session.get('auth_mfa'): request.session['auth_mfa_required'] = 1 if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED: diff --git a/apps/authentication/sms_verify_code.py b/apps/authentication/sms_verify_code.py index 33d17b207..0c3e986ce 100644 --- a/apps/authentication/sms_verify_code.py +++ b/apps/authentication/sms_verify_code.py @@ -52,10 +52,13 @@ class VerifyCodeUtil: ttl = self.ttl() if ttl > 0: raise CodeSendTooFrequently(ttl) - - self.generate() - self.save() - self.send() + try: + self.generate() + self.save() + self.send() + except JMSException: + self.clear() + raise def generate(self): code = ''.join(random.sample('0123456789', 4)) diff --git a/apps/authentication/templates/authentication/login_otp.html b/apps/authentication/templates/authentication/login_otp.html index 858c2737d..ff4d4e28b 100644 --- a/apps/authentication/templates/authentication/login_otp.html +++ b/apps/authentication/templates/authentication/login_otp.html @@ -19,19 +19,25 @@ {% endfor %} -
- - - {% trans 'Please enter the verification code' %} - -
- - +
+ + + +
+ +
{% trans "Can't provide security? Please contact the administrator!" %}
+