diff --git a/apps/audits/api.py b/apps/audits/api.py index fb1efe989..ca0e4d86a 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -126,9 +126,12 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): serializer_class = CommandExecutionHostsRelationSerializer m2m_field = CommandExecution.hosts.field - filterset_fields = [ - 'id', 'asset', 'commandexecution' - ] + filterset_fields = { + 'id': ['exact'], + 'asset': ['exact'], + 'asset__hostname': ['icontains'], + 'commandexecution': ['exact'], + } search_fields = ('asset__hostname', ) http_method_names = ['options', 'get'] rbac_perms = { diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 5bddda4e3..866964677 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -22,7 +22,10 @@ class TicketStatusApi(mixins.AuthMixin, APIView): self.request.session['auth_third_party_done'] = 1 return Response({"msg": "ok"}) except errors.LoginConfirmOtherError as e: - self.send_auth_signal(success=False, user=request.user, username=request.user.name, reason=e.as_data().get('msg')) + reason = e.msg + username = e.username + self.send_auth_signal(success=False, username=username, reason=reason) + # 若为三方登录,此时应退出登录 auth_logout(request) return Response(e.as_data(), status=200) except errors.NeedMoreInfoError as e: diff --git a/apps/authentication/backends/cas/urls.py b/apps/authentication/backends/cas/urls.py index 39a838b6a..376ce2332 100644 --- a/apps/authentication/backends/cas/urls.py +++ b/apps/authentication/backends/cas/urls.py @@ -3,9 +3,10 @@ from django.urls import path import django_cas_ng.views +from .views import CASLoginView urlpatterns = [ - path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'), + path('login/', CASLoginView.as_view(), name='cas-login'), path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'), path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'), ] diff --git a/apps/authentication/backends/cas/views.py b/apps/authentication/backends/cas/views.py new file mode 100644 index 000000000..f74e46e9c --- /dev/null +++ b/apps/authentication/backends/cas/views.py @@ -0,0 +1,15 @@ +from django_cas_ng.views import LoginView +from django.core.exceptions import PermissionDenied +from django.http import HttpResponseRedirect + +__all__ = ['LoginView'] + + +class CASLoginView(LoginView): + def get(self, request): + try: + return super().get(request) + except PermissionDenied: + return HttpResponseRedirect('/') + + diff --git a/apps/authentication/errors/failed.py b/apps/authentication/errors/failed.py index ec43d1b73..c85695fe5 100644 --- a/apps/authentication/errors/failed.py +++ b/apps/authentication/errors/failed.py @@ -56,7 +56,8 @@ class BlockGlobalIpLoginError(AuthFailedError): error = 'block_global_ip_login' def __init__(self, username, ip, **kwargs): - self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME) + if not self.msg: + self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME) LoginIpBlockUtil(ip).set_block_if_need() super().__init__(username=username, ip=ip, **kwargs) @@ -66,22 +67,21 @@ class CredentialError( BlockGlobalIpLoginError, AuthFailedError ): def __init__(self, error, username, ip, request): - super().__init__(error=error, username=username, ip=ip, request=request) util = LoginBlockUtil(username, ip) times_remainder = util.get_remainder_times() block_time = settings.SECURITY_LOGIN_LIMIT_TIME - if times_remainder < 1: self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) - return - - default_msg = const.invalid_login_msg.format( - times_try=times_remainder, block_time=block_time - ) - if error == const.reason_password_failed: - self.msg = default_msg else: - self.msg = const.reason_choices.get(error, default_msg) + default_msg = const.invalid_login_msg.format( + times_try=times_remainder, block_time=block_time + ) + if error == const.reason_password_failed: + self.msg = default_msg + else: + self.msg = const.reason_choices.get(error, default_msg) + # 先处理 msg 在 super,记录日志时原因才准确 + super().__init__(error=error, username=username, ip=ip, request=request) class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError): diff --git a/apps/authentication/errors/redirect.py b/apps/authentication/errors/redirect.py index bf334133d..466ec708d 100644 --- a/apps/authentication/errors/redirect.py +++ b/apps/authentication/errors/redirect.py @@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError): class LoginConfirmOtherError(LoginConfirmBaseError): error = 'login_confirm_error' - def __init__(self, ticket_id, status): + def __init__(self, ticket_id, status, username): + self.username = username msg = const.login_confirm_error_msg.format(status) super().__init__(ticket_id=ticket_id, msg=msg) + def as_data(self): + ret = super().as_data() + ret['data']['username'] = self.username + return ret + class PasswordTooSimple(NeedRedirectError): default_code = 'passwd_too_simple' diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 739048d75..7341b4bd1 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -377,7 +377,10 @@ class AuthACLMixin: raise errors.LoginConfirmWaitError(ticket.id) else: # rejected, closed - raise errors.LoginConfirmOtherError(ticket.id, ticket.get_state_display()) + ticket_id = ticket.id + status = ticket.get_state_display() + username = ticket.applicant.username + raise errors.LoginConfirmOtherError(ticket_id, status, username) def get_ticket(self): from tickets.models import ApplyLoginTicket diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index 836cb55bf..a7f18648b 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # +import ipaddress from urllib.parse import urljoin from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from common.utils import validate_ip, get_ip_city, get_request_ip from common.utils import get_logger @@ -23,8 +25,9 @@ def check_different_city_login_if_need(user, request): else: city = get_ip_city(ip) or DEFAULT_CITY - city_white = ['LAN', ] - if city not in city_white: + city_white = [_('LAN'), 'LAN'] + is_private = ipaddress.ip_address(ip).is_private + if not is_private: last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \ .filter(username=user.username, status=True).first() diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index 569854722..5abdf1d35 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -67,7 +67,7 @@ class SimpleMetadataWithFilters(SimpleMetadata): default = getattr(field, 'default', None) if default is not None and default != empty: - if isinstance(default, (str, int, bool, datetime.datetime, list)): + if isinstance(default, (str, int, bool, float, datetime.datetime, list)): field_info['default'] = default for attr in self.attrs: diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 3406271b6..83e4aeac2 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -40,12 +40,11 @@ class OrgManager(models.Manager): set_current_org(org) return self - def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): org = get_current_org() for obj in objs: if org.is_root(): - if not self.org_id: + if not obj.org_id: raise ValidationError('Please save in a organization') else: obj.org_id = org.id diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py index 2c620d331..279f3c34f 100644 --- a/apps/settings/serializers/auth/oauth2.py +++ b/apps/settings/serializers/auth/oauth2.py @@ -17,39 +17,39 @@ class SettingImageField(serializers.ImageField): class OAuth2SettingSerializer(serializers.Serializer): AUTH_OAUTH2 = serializers.BooleanField( - default=False, required=False, label=_('Enable OAuth2 Auth') + default=False, label=_('Enable OAuth2 Auth') ) AUTH_OAUTH2_LOGO_PATH = SettingImageField( allow_null=True, required=False, label=_('Logo') ) AUTH_OAUTH2_PROVIDER = serializers.CharField( - required=False, max_length=16, label=_('Service provider') + required=True, max_length=16, label=_('Service provider') ) AUTH_OAUTH2_CLIENT_ID = serializers.CharField( - required=False, max_length=1024, label=_('Client Id') + required=True, max_length=1024, label=_('Client Id') ) AUTH_OAUTH2_CLIENT_SECRET = EncryptedField( required=False, max_length=1024, label=_('Client Secret') ) AUTH_OAUTH2_SCOPE = serializers.CharField( - required=False, max_length=1024, label=_('Scope'), allow_blank=True + required=True, max_length=1024, label=_('Scope'), allow_blank=True ) AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField( - required=False, max_length=1024, label=_('Provider auth endpoint') + required=True, max_length=1024, label=_('Provider auth endpoint') ) AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = serializers.CharField( - required=False, max_length=1024, label=_('Provider token endpoint') + required=True, max_length=1024, label=_('Provider token endpoint') ) AUTH_OAUTH2_ACCESS_TOKEN_METHOD = serializers.ChoiceField( default='GET', label=_('Client authentication method'), choices=(('GET', 'GET'), ('POST', 'POST')) ) AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField( - required=False, max_length=1024, label=_('Provider userinfo endpoint') + required=True, max_length=1024, label=_('Provider userinfo endpoint') ) AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField( - required=False, label=_('User attr map') + required=True, label=_('User attr map') ) AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField( - required=False, label=_('Always update user') + default=True, label=_('Always update user') ) diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index d3f96b33f..1278dea06 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.drf.fields import EncryptedField +from common.validators import PhoneValidator from common.sdk.sms import BACKENDS __all__ = [ @@ -23,7 +24,10 @@ class SignTmplPairSerializer(serializers.Serializer): class BaseSMSSettingSerializer(serializers.Serializer): - SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, allow_blank=True, label=_('Test phone')) + SMS_TEST_PHONE = serializers.CharField( + max_length=256, required=False, validators=[PhoneValidator(), ], + allow_blank=True, label=_('Test phone') + ) def to_representation(self, instance): data = super().to_representation(instance) diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index 3fa0e5a82..26997b0dc 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -87,7 +87,7 @@ class BaseTicketMessage(UserMessage): @property def spec_items(self): fields = self.ticket._meta.local_fields + self.ticket._meta.local_many_to_many - excludes = ['ticket_ptr'] + excludes = ['ticket_ptr', 'flow'] item_names = [field.name for field in fields if field.name not in excludes] return self._get_fields_items(item_names) diff --git a/apps/tickets/templates/tickets/ticket_approve_diff.html b/apps/tickets/templates/tickets/ticket_approve_diff.html index 9fe6b80e0..8426b34ed 100644 --- a/apps/tickets/templates/tickets/ticket_approve_diff.html +++ b/apps/tickets/templates/tickets/ticket_approve_diff.html @@ -2,6 +2,7 @@

{{ approve_info }}

+{% if content %}
@@ -20,5 +21,6 @@
+{% endif %}