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):