mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-01-16 07:28:59 +00:00
[Update] Merge with dev
This commit is contained in:
@@ -101,7 +101,23 @@ class UserUpdatePKApi(generics.UpdateAPIView):
|
||||
user.save()
|
||||
|
||||
|
||||
class UserGroupViewSet(BulkModelViewSet):
|
||||
class UserUnblockPKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
username = user.username if user else ''
|
||||
key_limit = self.key_prefix_limit.format(username, '*')
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
cache.delete_pattern(key_limit)
|
||||
cache.delete(key_block)
|
||||
|
||||
|
||||
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -203,13 +219,15 @@ class UserAuthApi(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def post(self, request):
|
||||
# limit login
|
||||
username = request.data.get('username')
|
||||
ip = request.data.get('remote_addr', None)
|
||||
ip = ip if ip else get_login_ip(request)
|
||||
key_limit = self.key_prefix_limit.format(ip, username)
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
if is_block_login(key_limit):
|
||||
msg = _("Log in frequently and try again later")
|
||||
return Response({'msg': msg}, status=401)
|
||||
@@ -224,7 +242,7 @@ class UserAuthApi(APIView):
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
|
||||
set_user_login_failed_count_to_cache(key_limit)
|
||||
set_user_login_failed_count_to_cache(key_limit, key_block)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if not user.otp_enabled:
|
||||
|
||||
@@ -182,6 +182,14 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="{% if not unblock %}display:none{% endif %}">
|
||||
<td>{% trans 'Unblock user' %}</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-unblock-user" style="width: 54px">{% trans 'Unblock' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -275,7 +283,7 @@ $(document).ready(function() {
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id];
|
||||
})
|
||||
});
|
||||
})
|
||||
.on('click', '#is_active', function() {
|
||||
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
||||
@@ -293,7 +301,7 @@ $(document).ready(function() {
|
||||
.on('click', '#force_enable_otp', function() {
|
||||
{% if request.user == user_object %}
|
||||
toastr.error("{% trans 'Goto profile page enable MFA' %}");
|
||||
return
|
||||
return;
|
||||
{% endif %}
|
||||
|
||||
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
|
||||
@@ -426,6 +434,40 @@ $(document).ready(function() {
|
||||
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'users:user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
}).on('click', '#btn-unblock-user', function () {
|
||||
function doReset() {
|
||||
{#var the_url = '{% url "api-users:user-reset-password" pk=user_object.id %}';#}
|
||||
var the_url = '{% url "api-users:user-unblock" pk=user_object.id %}';
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans "Success" %}";
|
||||
{#swal("{% trans 'Unblock user' %}", msg, "success");#}
|
||||
swal({
|
||||
title: "{% trans 'Unblock user' %}",
|
||||
text: msg,
|
||||
type: "success"
|
||||
}, function() {
|
||||
location.reload()
|
||||
}
|
||||
);
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans "After unlocking the user, the user can log in normally."%}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -59,7 +59,7 @@ function initTable() {
|
||||
ele: $('#user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + escape(cellData) + '</a>';
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
|
||||
@@ -29,6 +29,8 @@ urlpatterns = [
|
||||
api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||
url(r'^users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$',
|
||||
api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
|
||||
url(r'^users/(?P<pk>[0-9a-zA-Z\-]{36})/unblock/$',
|
||||
api.UserUnblockPKApi.as_view(), name='user-unblock'),
|
||||
url(r'^users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$',
|
||||
api.UserUpdateGroupApi.as_view(), name='user-update-group'),
|
||||
url(r'^groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
|
||||
|
||||
@@ -8,13 +8,13 @@ app_name = 'users'
|
||||
|
||||
urlpatterns = [
|
||||
# Login view
|
||||
url(r'^login$', views.UserLoginView.as_view(), name='login'),
|
||||
url(r'^logout$', views.UserLogoutView.as_view(), name='logout'),
|
||||
url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'),
|
||||
url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||
url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
|
||||
url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
url(r'^password/reset/success$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
url(r'^login/$', views.UserLoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', views.UserLogoutView.as_view(), name='logout'),
|
||||
url(r'^login/otp/$', views.UserLoginOtpView.as_view(), name='login-otp'),
|
||||
url(r'^password/forgot/$', views.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||
url(r'^password/forgot/sendmail-success/$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
|
||||
url(r'^password/reset/$', views.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
url(r'^password/reset/success/$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
|
||||
# Profile
|
||||
url(r'^profile/$', views.UserProfileView.as_view(), name='user-profile'),
|
||||
@@ -29,23 +29,23 @@ urlpatterns = [
|
||||
url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
|
||||
|
||||
# User view
|
||||
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
||||
url(r'^user/export/', views.UserExportView.as_view(), name='user-export'),
|
||||
url(r'^user/$', views.UserListView.as_view(), name='user-list'),
|
||||
url(r'^user/export/$', views.UserExportView.as_view(), name='user-export'),
|
||||
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||
url(r'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'),
|
||||
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.UserUpdateView.as_view(), name='user-update'),
|
||||
url(r'^user/update$', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserDetailView.as_view(), name='user-detail'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'),
|
||||
url(r'^user/create/$', views.UserCreateView.as_view(), name='user-create'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.UserUpdateView.as_view(), name='user-update'),
|
||||
url(r'^user/update/$', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.UserDetailView.as_view(), name='user-detail'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
|
||||
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history/$', views.UserDetailView.as_view(), name='user-login-history'),
|
||||
|
||||
# User group view
|
||||
url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'),
|
||||
url(r'^user-group/create$', views.UserGroupCreateView.as_view(), name='user-group-create'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||
url(r'^user-group/$', views.UserGroupListView.as_view(), name='user-group-list'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.UserGroupDetailView.as_view(), name='user-group-detail'),
|
||||
url(r'^user-group/create/$', views.UserGroupCreateView.as_view(), name='user-group-create'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.UserGroupUpdateView.as_view(), name='user-group-update'),
|
||||
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||
|
||||
# Login log
|
||||
url(r'^login-log/$', views.LoginLogListView.as_view(), name='login-log-list'),
|
||||
|
||||
@@ -212,10 +212,10 @@ def write_login_log(*args, **kwargs):
|
||||
|
||||
|
||||
def get_ip_city(ip, timeout=10):
|
||||
# Taobao ip api: http://ip.taobao.com//service/getIpInfo.php?ip=8.8.8.8
|
||||
# Taobao ip api: http://ip.taobao.com/service/getIpInfo.php?ip=8.8.8.8
|
||||
# Sina ip api: http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=8.8.8.8&format=json
|
||||
|
||||
url = 'http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=%s&format=json' % ip
|
||||
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % ip
|
||||
try:
|
||||
r = requests.get(url, timeout=timeout)
|
||||
except:
|
||||
@@ -224,8 +224,8 @@ def get_ip_city(ip, timeout=10):
|
||||
if r and r.status_code == 200:
|
||||
try:
|
||||
data = r.json()
|
||||
if not isinstance(data, int) and data['ret'] == 1:
|
||||
city = data['country'] + ' ' + data['city']
|
||||
if not isinstance(data, int) and data['code'] == 0:
|
||||
city = data['data']['country'] + ' ' + data['data']['city']
|
||||
except ValueError:
|
||||
pass
|
||||
return city
|
||||
@@ -333,7 +333,7 @@ def check_password_rules(password):
|
||||
return bool(match_obj)
|
||||
|
||||
|
||||
def set_user_login_failed_count_to_cache(key_limit):
|
||||
def set_user_login_failed_count_to_cache(key_limit, key_block):
|
||||
count = cache.get(key_limit)
|
||||
count = count + 1 if count else 1
|
||||
|
||||
@@ -343,6 +343,15 @@ def set_user_login_failed_count_to_cache(key_limit):
|
||||
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
|
||||
else settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||
|
||||
setting_limit_count = Setting.objects.filter(
|
||||
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||
).first()
|
||||
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||
|
||||
if count >= limit_count:
|
||||
cache.set(key_block, 1, int(limit_time)*60)
|
||||
|
||||
cache.set(key_limit, count, int(limit_time)*60)
|
||||
|
||||
|
||||
@@ -357,3 +366,9 @@ def is_block_login(key_limit):
|
||||
|
||||
if count and count >= limit_count:
|
||||
return True
|
||||
|
||||
|
||||
def is_need_unblock(key_block):
|
||||
if not cache.get(key_block):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -52,6 +52,7 @@ class UserLoginView(FormView):
|
||||
redirect_field_name = 'next'
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
@@ -65,7 +66,7 @@ class UserLoginView(FormView):
|
||||
# limit login authentication
|
||||
ip = get_login_ip(request)
|
||||
username = self.request.POST.get('username')
|
||||
key_limit = self.key_prefix_limit.format(ip, username)
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
if is_block_login(key_limit):
|
||||
return self.render_to_response(self.get_context_data(block_login=True))
|
||||
|
||||
@@ -91,8 +92,9 @@ class UserLoginView(FormView):
|
||||
|
||||
# limit user login failed count
|
||||
ip = get_login_ip(self.request)
|
||||
key_limit = self.key_prefix_limit.format(ip, username)
|
||||
set_user_login_failed_count_to_cache(key_limit)
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
set_user_login_failed_count_to_cache(key_limit, key_block)
|
||||
|
||||
# show captcha
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
|
||||
@@ -37,7 +37,9 @@ from common.models import Setting
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from .. import forms
|
||||
from ..models import User, UserGroup
|
||||
from ..utils import generate_otp_uri, check_otp_code, get_user_or_tmp_user, get_password_check_rules, check_password_rules
|
||||
from ..utils import generate_otp_uri, check_otp_code, \
|
||||
get_user_or_tmp_user, get_password_check_rules, check_password_rules, \
|
||||
is_need_unblock
|
||||
from ..signals import post_user_create
|
||||
from ..tasks import write_login_log_async
|
||||
|
||||
@@ -169,13 +171,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'users/user_detail.html'
|
||||
context_object_name = "user_object"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.get_object()
|
||||
key_block = self.key_prefix_block.format(user.username)
|
||||
groups = UserGroup.objects.exclude(id__in=self.object.groups.all())
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('User detail'),
|
||||
'groups': groups
|
||||
'groups': groups,
|
||||
'unblock': is_need_unblock(key_block),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
Reference in New Issue
Block a user