Commit 2ecfecb0 authored by ibuler's avatar ibuler

[Update] Merge with dev

parents 2abb9efe e41add61
...@@ -24,9 +24,9 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点 ...@@ -24,9 +24,9 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
### 开始使用 ### 开始使用
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html) 快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html) 一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org) 也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.3.2" __version__ = "1.3.3"
...@@ -142,7 +142,7 @@ class Asset(OrgModelMixin): ...@@ -142,7 +142,7 @@ class Asset(OrgModelMixin):
return False, warning return False, warning
def is_unixlike(self): def is_unixlike(self):
if self.platform not in ("Windows",): if self.platform not in ("Windows", "Windows2016"):
return True return True
else: else:
return False return False
......
...@@ -53,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer): ...@@ -53,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
model = SystemUser model = SystemUser
fields = [ fields = [
"id", "name", "username", "protocol", "id", "name", "username", "protocol",
"password", "private_key", "login_mode", "password", "private_key",
] ]
......
...@@ -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>#}
......
...@@ -50,4 +50,3 @@ urlpatterns = [ ...@@ -50,4 +50,3 @@ urlpatterns = [
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'), url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'), url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
] ]
...@@ -92,7 +92,7 @@ class DatetimeSearchMixin: ...@@ -92,7 +92,7 @@ class DatetimeSearchMixin:
date_format = '%Y-%m-%d' date_format = '%Y-%m-%d'
date_from = date_to = None date_from = date_to = None
def get(self, request, *args, **kwargs): def get_date_range(self):
date_from_s = self.request.GET.get('date_from') date_from_s = self.request.GET.get('date_from')
date_to_s = self.request.GET.get('date_to') date_to_s = self.request.GET.get('date_to')
...@@ -112,6 +112,9 @@ class DatetimeSearchMixin: ...@@ -112,6 +112,9 @@ class DatetimeSearchMixin:
) )
else: else:
self.date_to = timezone.now() self.date_to = timezone.now()
def get(self, request, *args, **kwargs):
self.get_date_range()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
......
This diff is collapsed.
...@@ -341,6 +341,7 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch( ...@@ -341,6 +341,7 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_CONNECTION_OPTIONS = { AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5 ldap.OPT_TIMEOUT: 5
} }
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
......
...@@ -10,6 +10,7 @@ from users.models import User ...@@ -10,6 +10,7 @@ from users.models import User
from assets.models import Asset from assets.models import Asset
from terminal.models import Session from terminal.models import Session
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
class IndexView(AdminUserRequiredMixin, TemplateView): class IndexView(AdminUserRequiredMixin, TemplateView):
...@@ -27,7 +28,7 @@ class IndexView(AdminUserRequiredMixin, TemplateView): ...@@ -27,7 +28,7 @@ class IndexView(AdminUserRequiredMixin, TemplateView):
@staticmethod @staticmethod
def get_user_count(): def get_user_count():
return User.objects.filter(role__in=('Admin', 'User')).count() return current_org.get_org_users().count()
@staticmethod @staticmethod
def get_asset_count(): def get_asset_count():
...@@ -49,7 +50,6 @@ class IndexView(AdminUserRequiredMixin, TemplateView): ...@@ -49,7 +50,6 @@ class IndexView(AdminUserRequiredMixin, TemplateView):
def get_week_login_asset_count(self): def get_week_login_asset_count(self):
return self.session_week.count() return self.session_week.count()
# return self.session_week.values('asset').distinct().count()
def get_month_day_metrics(self): def get_month_day_metrics(self):
month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0'] month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0']
......
...@@ -55,9 +55,11 @@ class Organization(models.Model): ...@@ -55,9 +55,11 @@ class Organization(models.Model):
def get_org_users(self): def get_org_users(self):
from users.models import User from users.models import User
if self.is_default(): if self.is_default():
return User.objects.filter(orgs__isnull=True) users = User.objects.filter(orgs__isnull=True)
else: else:
return self.users.all() users = self.users.all()
users = users.exclude(role=User.ROLE_APP)
return users
def get_org_admins(self): def get_org_admins(self):
if self.is_real(): if self.is_real():
......
...@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404 ...@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.utils import set_or_append_attr_bulk, get_object_or_none from common.utils import set_or_append_attr_bulk, get_object_or_none
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
...@@ -72,10 +73,7 @@ class UserGrantedAssetsApi(ListAPIView): ...@@ -72,10 +73,7 @@ class UserGrantedAssetsApi(ListAPIView):
util = AssetPermissionUtil(user) util = AssetPermissionUtil(user)
for k, v in util.get_assets().items(): for k, v in util.get_assets().items():
if k.is_unixlike(): system_users_granted = [s for s in v if s.protocol == k.protocol]
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
else:
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
queryset.append(k) queryset.append(k)
return queryset return queryset
...@@ -123,10 +121,7 @@ class UserGrantedNodesWithAssetsApi(ListAPIView): ...@@ -123,10 +121,7 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
for node, _assets in nodes.items(): for node, _assets in nodes.items():
assets = _assets.keys() assets = _assets.keys()
for k, v in _assets.items(): for k, v in _assets.items():
if k.is_unixlike(): system_users_granted = [s for s in v if s.protocol == k.protocol]
system_users_granted = [s for s in v if s.protocol in ['ssh', 'telnet']]
else:
system_users_granted = [s for s in v if s.protocol in ['rdp', 'telnet']]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
node.assets_granted = assets node.assets_granted = assets
queryset.append(node) queryset.append(node)
......
...@@ -6,13 +6,11 @@ from .. import views ...@@ -6,13 +6,11 @@ from .. import views
app_name = 'perms' app_name = 'perms'
urlpatterns = [ urlpatterns = [
url(r'^asset-permission$', views.AssetPermissionListView.as_view(), name='asset-permission-list'), url(r'^asset-permission/$', views.AssetPermissionListView.as_view(), name='asset-permission-list'),
url(r'^asset-permission/create$', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'), url(r'^asset-permission/create/$', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'),
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update$', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'), url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'),
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})$', views.AssetPermissionDetailView.as_view(),name='asset-permission-detail'), url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AssetPermissionDetailView.as_view(),name='asset-permission-detail'),
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete$', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'), url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'),
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user$', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'), url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/user/$', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'),
url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'), url(r'^asset-permission/(?P<pk>[0-9a-zA-Z\-]{36})/asset/$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
] ]
<div class="footer fixed"> <div class="footer fixed">
<div class="pull-right"> <div class="pull-right">
Version <strong>1.3.2-{% include '_build.html' %}</strong> GPLv2. Version <strong>1.3.3-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">--> <!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div> </div>
<div> <div>
......
...@@ -4,6 +4,7 @@ from collections import OrderedDict ...@@ -4,6 +4,7 @@ from collections import OrderedDict
import logging import logging
import os import os
import uuid import uuid
import copy
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
...@@ -310,6 +311,7 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -310,6 +311,7 @@ class SessionReplayViewSet(viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) self.session = get_object_or_404(Session, id=session_id)
# 新版本和老版本的文件后缀不同 # 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径 session_path = self.get_session_path() # 存在外部存储上的路径
local_path = self.get_local_path() local_path = self.get_local_path()
......
...@@ -92,13 +92,26 @@ ...@@ -92,13 +92,26 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div id="actions" class="">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="export">{% trans 'Export command' %}</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
{% trans 'Submit' %}
</button>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static "js/plugins/footable/footable.all.min.js" %}"></script> <script src="{% static "js/plugins/footable/footable.all.min.js" %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$('.footable').footable(); $('.footable').footable();
$('.select2').select2({ $('.select2').select2({
dropdownAutoWidth : true, dropdownAutoWidth : true,
...@@ -112,7 +125,19 @@ ...@@ -112,7 +125,19 @@
calendarWeeks: true, calendarWeeks: true,
autoclose: true autoclose: true
}); });
}); })
.on('click', '#btn_bulk_update', function(){
var action = $('#slct_bulk_update').val();
var param_action = '&action=' + action;
var local_params = window.location.search;
if(!local_params){
param_action = '?action=' + action;
}
var params = local_params + param_action;
var pathname = window.location.pathname + 'export/';
var url = pathname + params;
window.open(url);
});
</script> </script>
{% endblock %} {% endblock %}
......
{% load common_tags %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Command Report</title>
<style>
*{
margin: 0;
padding: 0;
}
.background {
background-color: #535659;
padding-top: 50px;
padding-bottom: 50px;
}
.paper {
margin-left: 23%;
margin-right: 24%;
border: solid;
padding: 50px;
background-color: #fff;
}
h2 {
font-style: italic;
text-align: center;
}
.info {
width: 200px;
margin-left: 450px;
font-style: italic;
text-align: left;
padding-top: 20px;
padding-bottom: 20px;
}
.command {
margin-left: 10px;
}
.command-desc {
font-size: 12px;
}
.command-desc span {
float: right;
}
.command-input {
{#font-style: italic;#}
font-size: 15px;
margin-top: 10px;
margin-bottom: 10px;
}
.command-input span {
font-size: 13px;
}
.hr-line-dashed {
border-top: 1px dashed #000;
color: #000;
background-color: #fff;
height: 1px;
margin: 20px 0;
}
pre {
font-size: 12px;
}
</style>
</head>
<body>
<div class="background">
<div class="paper">
<h2>Command Report</h2>
<div class="info">
<p>total: {{ total_count }}</p>
<p>date: {{ now | ts_to_date }}</p>
</div>
<div class="hr-line-dashed"></div>
<div>
{% for command in queryset %}
<div class="command">
<p class="command-desc">
[{{ command.user}} {{ command.system_user }}@{{ command.asset }} {{ command.timestamp | ts_to_date }}]
<span>{{ forloop.counter }}</span>
</p>
<p class="command-input"><span>$ </span>{{ command.input }}</p>
<pre>{{ command.output }}</pre>
</div>
<div class="hr-line-dashed"></div>
{% endfor %}
</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
...@@ -24,5 +24,6 @@ urlpatterns = [ ...@@ -24,5 +24,6 @@ urlpatterns = [
# Command view # Command view
url(r'^command/$', views.CommandListView.as_view(), name='command-list'), url(r'^command/$', views.CommandListView.as_view(), name='command-list'),
url(r'^command/export/$', views.CommandExportView.as_view(), name='command-export')
] ]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.views.generic import ListView from django.views.generic import ListView, View
from django.conf import settings from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import HttpResponse
from django.template import loader
import time
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
...@@ -12,7 +14,7 @@ from ..models import Command ...@@ -12,7 +14,7 @@ from ..models import Command
from .. import utils from .. import utils
from ..backends import get_multi_command_storage from ..backends import get_multi_command_storage
__all__ = ['CommandListView'] __all__ = ['CommandListView', 'CommandExportView']
common_storage = get_multi_command_storage() common_storage = get_multi_command_storage()
...@@ -61,7 +63,43 @@ class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView): ...@@ -61,7 +63,43 @@ class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class CommandExportView(DatetimeSearchMixin, AdminUserRequiredMixin, View):
model = Command
command = user = asset = system_user = action = ''
date_from = date_to = None
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
template = 'terminal/command_report.html'
context = {
'queryset': queryset,
'total_count': len(queryset),
'now': time.time(),
}
content = loader.render_to_string(template, context, request)
content_type = 'application/octet-stream'
response = HttpResponse(content, content_type)
filename = 'command-report-{}.html'.format(int(time.time()))
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
return response
def get_queryset(self):
self.get_date_range()
self.action = self.request.GET.get('action', '')
self.command = self.request.GET.get('command', '')
self.user = self.request.GET.get("user", '')
self.asset = self.request.GET.get('asset', '')
self.system_user = self.request.GET.get('system_user', '')
filter_kwargs = dict()
filter_kwargs['date_from'] = self.date_from
filter_kwargs['date_to'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if self.asset:
filter_kwargs['asset'] = self.asset
if self.system_user:
filter_kwargs['system_user'] = self.system_user
if self.command:
filter_kwargs['input'] = self.command
queryset = common_storage.filter(**filter_kwargs)
return queryset
...@@ -101,7 +101,23 @@ class UserUpdatePKApi(generics.UpdateAPIView): ...@@ -101,7 +101,23 @@ class UserUpdatePKApi(generics.UpdateAPIView):
user.save() 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() queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer serializer_class = UserGroupSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -203,13 +219,15 @@ class UserAuthApi(APIView): ...@@ -203,13 +219,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)
...@@ -224,7 +242,7 @@ class UserAuthApi(APIView): ...@@ -224,7 +242,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 %}
...@@ -59,7 +59,7 @@ function initTable() { ...@@ -59,7 +59,7 @@ function initTable() {
ele: $('#user_list_table'), ele: $('#user_list_table'),
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {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)); $(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
......
...@@ -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'^users/(?P<pk>[0-9a-zA-Z\-]{36})/pubkey/update/$', url(r'^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'^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/$', url(r'^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'^groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$', url(r'^groups/(?P<pk>[0-9a-zA-Z\-]{36})/users/$',
......
...@@ -8,13 +8,13 @@ app_name = 'users' ...@@ -8,13 +8,13 @@ app_name = 'users'
urlpatterns = [ urlpatterns = [
# Login view # Login view
url(r'^login$', views.UserLoginView.as_view(), name='login'), url(r'^login/$', views.UserLoginView.as_view(), name='login'),
url(r'^logout$', views.UserLogoutView.as_view(), name='logout'), url(r'^logout/$', views.UserLogoutView.as_view(), name='logout'),
url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'), 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/$', 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/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/$', views.UserResetPasswordView.as_view(), name='reset-password'),
url(r'^password/reset/success$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), url(r'^password/reset/success/$', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
# Profile # Profile
url(r'^profile/$', views.UserProfileView.as_view(), name='user-profile'), url(r'^profile/$', views.UserProfileView.as_view(), name='user-profile'),
...@@ -29,23 +29,23 @@ urlpatterns = [ ...@@ -29,23 +29,23 @@ urlpatterns = [
url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'), url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
# User view # User view
url(r'^user$', views.UserListView.as_view(), name='user-list'), url(r'^user/$', views.UserListView.as_view(), name='user-list'),
url(r'^user/export/', views.UserExportView.as_view(), name='user-export'), 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'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
url(r'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'), 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/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/(?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/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})/$', 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})/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/(?P<pk>[0-9a-zA-Z\-]{36})/login-history/$', views.UserDetailView.as_view(), name='user-login-history'),
# User group view # User group view
url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), 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/(?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/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})/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/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
# Login log # Login log
url(r'^login-log/$', views.LoginLogListView.as_view(), name='login-log-list'), url(r'^login-log/$', views.LoginLogListView.as_view(), name='login-log-list'),
......
...@@ -212,10 +212,10 @@ def write_login_log(*args, **kwargs): ...@@ -212,10 +212,10 @@ def write_login_log(*args, **kwargs):
def get_ip_city(ip, timeout=10): 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 # 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: try:
r = requests.get(url, timeout=timeout) r = requests.get(url, timeout=timeout)
except: except:
...@@ -224,8 +224,8 @@ def get_ip_city(ip, timeout=10): ...@@ -224,8 +224,8 @@ def get_ip_city(ip, timeout=10):
if r and r.status_code == 200: if r and r.status_code == 200:
try: try:
data = r.json() data = r.json()
if not isinstance(data, int) and data['ret'] == 1: if not isinstance(data, int) and data['code'] == 0:
city = data['country'] + ' ' + data['city'] city = data['data']['country'] + ' ' + data['data']['city']
except ValueError: except ValueError:
pass pass
return city return city
...@@ -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
...@@ -52,6 +52,7 @@ class UserLoginView(FormView): ...@@ -52,6 +52,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:
...@@ -65,7 +66,7 @@ class UserLoginView(FormView): ...@@ -65,7 +66,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))
...@@ -91,8 +92,9 @@ class UserLoginView(FormView): ...@@ -91,8 +92,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)
......
...@@ -37,7 +37,9 @@ from common.models import Setting ...@@ -37,7 +37,9 @@ from common.models import Setting
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from .. import forms from .. import forms
from ..models import User, UserGroup 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 ..signals import post_user_create
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
...@@ -169,13 +171,17 @@ class UserDetailView(AdminUserRequiredMixin, DetailView): ...@@ -169,13 +171,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)
......
...@@ -228,7 +228,7 @@ Luna 已改为纯前端,需要 Nginx 来运行访问 ...@@ -228,7 +228,7 @@ Luna 已改为纯前端,需要 Nginx 来运行访问
-p 8081:8080 -v /opt/guacamole/key:/config/guacamole/key \ -p 8081:8080 -v /opt/guacamole/key:/config/guacamole/key \
-e JUMPSERVER_KEY_DIR=/config/guacamole/key \ -e JUMPSERVER_KEY_DIR=/config/guacamole/key \
-e JUMPSERVER_SERVER=http://<填写本机的IP地址>:8080 \ -e JUMPSERVER_SERVER=http://<填写本机的IP地址>:8080 \
registry.jumpserver.org/public/guacamole:1.0.0 registry.jumpserver.org/public/guacamole:latest
这里所需要注意的是 guacamole 暴露出来的端口是 8081,若与主机上其他端口冲突请自定义一下。 这里所需要注意的是 guacamole 暴露出来的端口是 8081,若与主机上其他端口冲突请自定义一下。
......
...@@ -122,8 +122,8 @@ def start_gunicorn(): ...@@ -122,8 +122,8 @@ def start_gunicorn():
cmd = [ cmd = [
'gunicorn', 'jumpserver.wsgi', 'gunicorn', 'jumpserver.wsgi',
'-b', bind, '-b', bind,
'-w', str(WORKERS),
'-k', 'eventlet', '-k', 'eventlet',
'-w', str(WORKERS),
'--access-logformat', log_format, '--access-logformat', log_format,
'-p', pid_file, '-p', pid_file,
] ]
......
...@@ -48,7 +48,7 @@ MarkupSafe==1.0 ...@@ -48,7 +48,7 @@ MarkupSafe==1.0
mysqlclient==1.3.12 mysqlclient==1.3.12
olefile==0.44 olefile==0.44
openapi-codec==1.3.2 openapi-codec==1.3.2
paramiko==2.4.0 paramiko==2.4.1
passlib==1.7.1 passlib==1.7.1
Pillow==4.3.0 Pillow==4.3.0
pyasn1==0.4.2 pyasn1==0.4.2
......
...@@ -5,4 +5,4 @@ python3 ../apps/manage.py makemigrations ...@@ -5,4 +5,4 @@ python3 ../apps/manage.py makemigrations
python3 ../apps/manage.py migrate python3 ../apps/manage.py migrate
python3 ../apps/manage.py makemigrations -merge python3 ../apps/manage.py makemigrations --merge
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