Unverified Commit ff9e109a authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #1533 from jumpserver/update_unblockuser

[Update] 取消系统用户-清除认证信息,取消-网关rdp协议认证信息,添加解除用户登录限制功能
parents 8c587a13 09e63649
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
{% bootstrap_field form.domain layout="horizontal" %} {% bootstrap_field form.domain layout="horizontal" %}
{% block auth %} {% block auth %}
<h3>{% trans 'Auth' %}</h3> <h3 id="auth_title">{% trans 'Auth' %}</h3>
<div class="auth-fields"> <div class="auth-fields">
{% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %} {% bootstrap_field form.password layout="horizontal" %}
...@@ -72,14 +72,23 @@ ...@@ -72,14 +72,23 @@
var protocol_id = '#' + '{{ form.protocol.id_for_label }}'; var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}'; var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
var port = '#' + '{{ form.port.id_for_label }}'; var port = '#' + '{{ form.port.id_for_label }}';
var username = '#' + '{{ form.username.id_for_label }}';
var password = '#' + '{{ form.password.id_for_label }}';
var auth_title = '#auth_title';
function protocolChange() { function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') { if ($(protocol_id + " option:selected").text() === 'rdp') {
{#$(port).val(3389);#} {#$(port).val(3389);#}
$(private_key_id).closest('.form-group').addClass('hidden') $(private_key_id).closest('.form-group').addClass('hidden');
$(username).closest('.form-group').addClass('hidden');
$(password).closest('.form-group').addClass('hidden');
$(auth_title).addClass('hidden');
} else { } else {
{#$(port).val(22);#} {#$(port).val(22);#}
$(private_key_id).closest('.form-group').removeClass('hidden') $(private_key_id).closest('.form-group').removeClass('hidden');
$(username).closest('.form-group').removeClass('hidden');
$(password).closest('.form-group').removeClass('hidden');
$(auth_title).removeClass('hidden');
} }
} }
......
...@@ -152,15 +152,14 @@ ...@@ -152,15 +152,14 @@
</span> </span>
</td> </td>
</tr> </tr>
{# <tr>#}
<tr> {# <td width="50%">{% trans 'Clear auth' %}:</td>#}
<td width="50%">{% trans 'Clear auth' %}:</td> {# <td>#}
<td> {# <span style="float: right">#}
<span style="float: right"> {# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button> {# </span>#}
</span> {# </td>#}
</td> {# </tr>#}
</tr>
{# <tr>#} {# <tr>#}
{# <td width="50%">{% trans 'Change auth period' %}:</td>#} {# <td width="50%">{% trans 'Change auth period' %}:</td>#}
......
This diff is collapsed.
...@@ -95,6 +95,22 @@ class UserUpdatePKApi(generics.UpdateAPIView): ...@@ -95,6 +95,22 @@ class UserUpdatePKApi(generics.UpdateAPIView):
user.save() user.save()
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): class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer serializer_class = UserGroupSerializer
...@@ -197,13 +213,15 @@ class UserAuthApi(APIView): ...@@ -197,13 +213,15 @@ class UserAuthApi(APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = UserSerializer serializer_class = UserSerializer
key_prefix_limit = "_LOGIN_LIMIT_{}_{}" key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def post(self, request): def post(self, request):
# limit login # limit login
username = request.data.get('username') username = request.data.get('username')
ip = request.data.get('remote_addr', None) ip = request.data.get('remote_addr', None)
ip = ip if ip else get_login_ip(request) 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): if is_block_login(key_limit):
msg = _("Log in frequently and try again later") msg = _("Log in frequently and try again later")
return Response({'msg': msg}, status=401) return Response({'msg': msg}, status=401)
...@@ -218,7 +236,7 @@ class UserAuthApi(APIView): ...@@ -218,7 +236,7 @@ class UserAuthApi(APIView):
} }
self.write_login_log(request, data) 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) return Response({'msg': msg}, status=401)
if not user.otp_enabled: if not user.otp_enabled:
......
...@@ -182,6 +182,14 @@ ...@@ -182,6 +182,14 @@
</span> </span>
</td> </td>
</tr> </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> </tbody>
</table> </table>
</div> </div>
...@@ -275,7 +283,7 @@ $(document).ready(function() { ...@@ -275,7 +283,7 @@ $(document).ready(function() {
.on('select2:unselect', function(evt) { .on('select2:unselect', function(evt) {
var data = evt.params.data; var data = evt.params.data;
delete jumpserver.nodes_selected[data.id]; delete jumpserver.nodes_selected[data.id];
}) });
}) })
.on('click', '#is_active', function() { .on('click', '#is_active', function() {
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}"; var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
...@@ -293,7 +301,7 @@ $(document).ready(function() { ...@@ -293,7 +301,7 @@ $(document).ready(function() {
.on('click', '#force_enable_otp', function() { .on('click', '#force_enable_otp', function() {
{% if request.user == user_object %} {% if request.user == user_object %}
toastr.error("{% trans 'Goto profile page enable MFA' %}"); toastr.error("{% trans 'Goto profile page enable MFA' %}");
return return;
{% endif %} {% endif %}
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}"; var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
...@@ -426,6 +434,40 @@ $(document).ready(function() { ...@@ -426,6 +434,40 @@ $(document).ready(function() {
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
var redirect_url = "{% url 'users:user-list' %}"; var redirect_url = "{% url 'users:user-list' %}";
objectDelete($this, name, the_url, redirect_url); 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> </script>
{% endblock %} {% endblock %}
...@@ -29,6 +29,8 @@ urlpatterns = [ ...@@ -29,6 +29,8 @@ urlpatterns = [
api.UserResetPKApi.as_view(), name='user-public-key-reset'), api.UserResetPKApi.as_view(), name='user-public-key-reset'),
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$', url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$',
api.UserUpdatePKApi.as_view(), name='user-public-key-update'), api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/unblock/$',
api.UserUnblockPKApi.as_view(), name='user-unblock'),
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$', url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/groups/$',
api.UserUpdateGroupApi.as_view(), name='user-update-group'), api.UserUpdateGroupApi.as_view(), name='user-update-group'),
url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$', url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
......
...@@ -333,7 +333,7 @@ def check_password_rules(password): ...@@ -333,7 +333,7 @@ def check_password_rules(password):
return bool(match_obj) 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 = cache.get(key_limit)
count = count + 1 if count else 1 count = count + 1 if count else 1
...@@ -343,6 +343,15 @@ def set_user_login_failed_count_to_cache(key_limit): ...@@ -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 \ limit_time = setting_limit_time.cleaned_value if setting_limit_time \
else settings.DEFAULT_LOGIN_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) cache.set(key_limit, count, int(limit_time)*60)
...@@ -357,3 +366,9 @@ def is_block_login(key_limit): ...@@ -357,3 +366,9 @@ def is_block_login(key_limit):
if count and count >= limit_count: if count and count >= limit_count:
return True return True
def is_need_unblock(key_block):
if not cache.get(key_block):
return False
return True
...@@ -51,6 +51,7 @@ class UserLoginView(FormView): ...@@ -51,6 +51,7 @@ class UserLoginView(FormView):
redirect_field_name = 'next' redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}" key_prefix_captcha = "_LOGIN_INVALID_{}"
key_prefix_limit = "_LOGIN_LIMIT_{}_{}" key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_staff: if request.user.is_staff:
...@@ -64,7 +65,7 @@ class UserLoginView(FormView): ...@@ -64,7 +65,7 @@ class UserLoginView(FormView):
# limit login authentication # limit login authentication
ip = get_login_ip(request) ip = get_login_ip(request)
username = self.request.POST.get('username') 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): if is_block_login(key_limit):
return self.render_to_response(self.get_context_data(block_login=True)) return self.render_to_response(self.get_context_data(block_login=True))
...@@ -90,8 +91,9 @@ class UserLoginView(FormView): ...@@ -90,8 +91,9 @@ class UserLoginView(FormView):
# limit user login failed count # limit user login failed count
ip = get_login_ip(self.request) ip = get_login_ip(self.request)
key_limit = self.key_prefix_limit.format(ip, username) key_limit = self.key_prefix_limit.format(username, ip)
set_user_login_failed_count_to_cache(key_limit) key_block = self.key_prefix_block.format(username)
set_user_login_failed_count_to_cache(key_limit, key_block)
# show captcha # show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600) cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
......
...@@ -36,7 +36,9 @@ from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen ...@@ -36,7 +36,9 @@ from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.models import Setting from common.models import Setting
from .. import forms from .. import forms
from ..models import User, UserGroup from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user_or_tmp_user, get_password_check_rules, check_password_rules from ..utils import AdminUserRequiredMixin, 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 ..signals import post_user_create
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
...@@ -168,13 +170,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView): ...@@ -168,13 +170,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
model = User model = User
template_name = 'users/user_detail.html' template_name = 'users/user_detail.html'
context_object_name = "user_object" context_object_name = "user_object"
key_prefix_block = "_LOGIN_BLOCK_{}"
def get_context_data(self, **kwargs): 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()) groups = UserGroup.objects.exclude(id__in=self.object.groups.all())
context = { context = {
'app': _('Users'), 'app': _('Users'),
'action': _('User detail'), 'action': _('User detail'),
'groups': groups 'groups': groups,
'unblock': is_need_unblock(key_block),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment