diff --git a/README.md b/README.md index f93198e1a..07e6f6c91 100644 --- a/README.md +++ b/README.md @@ -24,23 +24,20 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 ## 版本说明 -从 2.0 开始 JumpServer 版本号规则将发生变更,如下所示 - -大版本.功能版本.bug修复版本 +自 v2.0.0 发布后, JumpServer 版本号命名将变更为:v大版本.功能版本.Bug修复版本。比如: ``` -如 2.0.1 修复bug 将是 2.0.2 -如 2.0.2 添加新功能后 将会是 2.1.0 +v2.0.1 是 v2.0.0 之后的Bug修复版本; +v2.1.0 是 v2.0.0 之后的功能版本。 ``` -并且 JumpServer 以后也会像其它优秀开源项目一样,同时维护多个版本,目前计划同时维护 3 个版本 +像其它优秀开源项目一样,JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如: ``` -如 2.1, 2.2, 2.3 版本我们会同时维护, 发布 2.4 版本后,2.1 停止维护 +在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3; +在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4;v2.1 会停止维护。 ``` -JumpServer 每个月会发布一个功能版本, 修复版本视情况而定 - ## 功能列表 diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py index e024fedb1..896dab7e1 100644 --- a/apps/assets/api/gathered_user.py +++ b/apps/assets/api/gathered_user.py @@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet): permission_classes = [IsOrgAdmin] extra_filter_backends = [AssetRelatedByNodeFilterBackend] - filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname'] + filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id'] search_fields = ['username', 'asset__ip', 'asset__hostname'] diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index dc134ee97..ff3f81f36 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -28,7 +28,7 @@ class SystemUserViewSet(OrgBulkModelViewSet): System user api set, for add,delete,update,list,retrieve resource """ model = SystemUser - filter_fields = ("name", "username") + filter_fields = ("name", "username", "protocol") search_fields = filter_fields serializer_class = serializers.SystemUserSerializer serializer_classes = { diff --git a/apps/audits/api.py b/apps/audits/api.py index 9d6d0339e..7397b3596 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -27,6 +27,7 @@ class FTPLogViewSet(CreateModelMixin, ] filter_fields = ['user', 'asset', 'system_user', 'filename'] search_fields = filter_fields + ordering = ['-date_start'] class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet): diff --git a/apps/common/drf/renders/csv.py b/apps/common/drf/renders/csv.py index 0d90af359..435e3d4a6 100644 --- a/apps/common/drf/renders/csv.py +++ b/apps/common/drf/renders/csv.py @@ -30,7 +30,7 @@ class JMSCSVRender(BaseRenderer): @staticmethod def _gen_table(data, fields): - data = data[:100] + data = data[:10000] yield ['*{}'.format(f.label) if f.required else f.label for f in fields] for item in data: diff --git a/apps/common/http.py b/apps/common/http.py index df6b9a78f..f3a743045 100644 --- a/apps/common/http.py +++ b/apps/common/http.py @@ -10,3 +10,7 @@ class HttpResponseTemporaryRedirect(HttpResponse): def __init__(self, redirect_to): HttpResponse.__init__(self) self['Location'] = iri_to_uri(redirect_to) + + +def get_remote_addr(request): + return request.META.get("HTTP_X_FORWARDED_HOST") or request.META.get("REMOTE_ADDR") diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 13f25d492..2b4a5b65f 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -259,7 +259,8 @@ class Config(dict): 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, 'ORG_CHANGE_TO_URL': '', 'LANGUAGE_CODE': 'zh', - 'TIME_ZONE': 'Asia/Shanghai' + 'TIME_ZONE': 'Asia/Shanghai', + 'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True } def compatible_auth_openid_of_key(self): diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 0be476d96..1e552345b 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -86,7 +86,11 @@ TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD +AUTH_EXPIRED_SECONDS = 60 * 5 + # XPACK XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID LOGO_URLS = DYNAMIC.LOGO_URLS + +CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 1fa128c6a..1c50f6958 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index d2612f98a..3274e04b7 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -3755,7 +3755,7 @@ msgstr "腾讯云" #: xpack/plugins/cloud/serializers.py:53 msgid "History count" -msgstr "用户数量" +msgstr "执行次数" #: xpack/plugins/cloud/serializers.py:54 msgid "Instance count" diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index c5e227980..b78d5f930 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -351,7 +351,7 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): self.add_favorite_node_if_need(user_tree) self.set_user_tree_to_cache_if_need(user_tree) self.set_user_tree_to_local(user_tree) - print(user_tree) + # print(user_tree) return user_tree # Todo: 是否可以获取多个资产的系统用户 diff --git a/apps/settings/api.py b/apps/settings/api.py index 35ccd5f5b..786c43bad 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -275,6 +275,7 @@ class PublicSettingApi(generics.RetrieveAPIView): "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE, "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA, "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, + "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, "LOGO_URLS": settings.LOGO_URLS, "PASSWORD_RULE": { 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, diff --git a/apps/users/utils.py b/apps/users/utils.py index eb53c6607..af6e0197b 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -5,8 +5,8 @@ import re import pyotp import base64 import logging +import time -from django.http import Http404 from django.conf import settings from django.utils.translation import ugettext as _ from django.core.cache import cache @@ -333,3 +333,15 @@ def get_source_choices(): if settings.AUTH_CAS: choices.append((User.SOURCE_CAS, choices_all[User.SOURCE_CAS])) return choices + + +def is_auth_time_valid(session, key): + return True if session.get(key, 0) > time.time() else False + + +def is_auth_password_time_valid(session): + return is_auth_time_valid(session, 'auth_password_expired_at') + + +def is_auth_otp_time_valid(session): + return is_auth_time_valid(session, 'auth_opt_expired_at') diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index 067775e91..caed50532 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -1,4 +1,5 @@ # ~*~ coding: utf-8 ~*~ +import time from django.urls import reverse_lazy, reverse from django.utils.translation import ugettext as _ @@ -6,13 +7,17 @@ from django.views.generic.base import TemplateView from django.views.generic.edit import FormView from django.contrib.auth import logout as auth_logout from django.conf import settings +from django.http.response import HttpResponseForbidden -from common.utils import get_logger +from authentication.mixins import AuthMixin +from users.models import User +from common.utils import get_logger, get_object_or_none from common.permissions import IsValidUser from ... import forms from .password import UserVerifyPasswordView from ...utils import ( generate_otp_uri, check_otp_code, get_user_or_pre_auth_user, + is_auth_password_time_valid, is_auth_otp_time_valid ) __all__ = [ @@ -46,11 +51,50 @@ class UserOtpEnableInstallAppView(TemplateView): return super().get_context_data(**kwargs) -class UserOtpEnableBindView(TemplateView, FormView): +class UserOtpEnableBindView(AuthMixin, TemplateView, FormView): template_name = 'users/user_otp_enable_bind.html' form_class = forms.UserCheckOtpCodeForm success_url = reverse_lazy('authentication:user-otp-settings-success') + def get(self, request, *args, **kwargs): + if self._check_can_bind(): + return super().get(request, *args, **kwargs) + return HttpResponseForbidden() + + def post(self, request, *args, **kwargs): + if self._check_can_bind(): + return super().post(request, *args, **kwargs) + return HttpResponseForbidden() + + def _check_authenticated_user_can_bind(self): + user = self.request.user + session = self.request.session + + if not user.mfa_enabled: + return is_auth_password_time_valid(session) + + if not user.otp_secret_key: + return is_auth_password_time_valid(session) + + return is_auth_otp_time_valid(session) + + def _check_unauthenticated_user_can_bind(self): + session_user = None + if not self.request.session.is_empty(): + user_id = self.request.session.get('user_id') + session_user = get_object_or_none(User, pk=user_id) + + if session_user: + if all((is_auth_password_time_valid(self.request.session), session_user.mfa_enabled, not session_user.otp_secret_key)): + return True + return False + + def _check_can_bind(self): + if self.request.user.is_authenticated: + return self._check_authenticated_user_can_bind() + else: + return self._check_unauthenticated_user_can_bind() + def form_valid(self, form): otp_code = form.cleaned_data.get('otp_code') otp_secret_key = self.request.session.get('otp_secret_key', '') @@ -116,6 +160,7 @@ class UserOtpUpdateView(FormView): valid = user.check_mfa(otp_code) if valid: + self.request.session['auth_opt_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS return super().form_valid(form) else: error = _('MFA code invalid, or ntp sync server time') diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py index bb7caa9a1..1fbbd64a7 100644 --- a/apps/users/views/profile/password.py +++ b/apps/users/views/profile/password.py @@ -1,8 +1,10 @@ # ~*~ coding: utf-8 ~*~ +import time +from django.conf import settings from django.contrib.auth import authenticate from django.shortcuts import redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.utils.translation import ugettext as _ from django.views.generic.edit import UpdateView, FormView from django.contrib.auth import logout as auth_logout @@ -76,6 +78,7 @@ class UserVerifyPasswordView(FormView): user.save() self.request.session['user_id'] = str(user.id) self.request.session['auth_password'] = 1 + self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS return redirect(self.get_success_url()) def get_success_url(self):