Commit 2ecfecb0 authored by ibuler's avatar ibuler

[Update] Merge with dev

parents 2abb9efe e41add61
......@@ -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)
......
......@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
__version__ = "1.3.2"
__version__ = "1.3.3"
......@@ -142,7 +142,7 @@ class Asset(OrgModelMixin):
return False, warning
def is_unixlike(self):
if self.platform not in ("Windows",):
if self.platform not in ("Windows", "Windows2016"):
return True
else:
return False
......
......@@ -53,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
model = SystemUser
fields = [
"id", "name", "username", "protocol",
"password", "private_key",
"login_mode", "password", "private_key",
]
......
......@@ -42,7 +42,7 @@
{% bootstrap_field form.domain layout="horizontal" %}
{% block auth %}
<h3>{% trans 'Auth' %}</h3>
<h3 id="auth_title">{% trans 'Auth' %}</h3>
<div class="auth-fields">
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %}
......@@ -72,14 +72,23 @@
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
var private_key_id = '#' + '{{ form.private_key_file.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() {
if ($(protocol_id + " option:selected").text() === 'rdp') {
{#$(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 {
{#$(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 @@
</span>
</td>
</tr>
<tr>
<td width="50%">{% trans 'Clear auth' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
</span>
</td>
</tr>
{# <tr>#}
{# <td width="50%">{% trans 'Clear auth' %}:</td>#}
{# <td>#}
{# <span style="float: right">#}
{# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
{# </span>#}
{# </td>#}
{# </tr>#}
{# <tr>#}
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
......
......@@ -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/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
]
......@@ -92,7 +92,7 @@ class DatetimeSearchMixin:
date_format = '%Y-%m-%d'
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_to_s = self.request.GET.get('date_to')
......@@ -112,6 +112,9 @@ class DatetimeSearchMixin:
)
else:
self.date_to = timezone.now()
def get(self, request, *args, **kwargs):
self.get_date_range()
return super().get(request, *args, **kwargs)
......
This diff is collapsed.
......@@ -341,6 +341,7 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5
}
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
......
......@@ -10,6 +10,7 @@ from users.models import User
from assets.models import Asset
from terminal.models import Session
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
class IndexView(AdminUserRequiredMixin, TemplateView):
......@@ -27,7 +28,7 @@ class IndexView(AdminUserRequiredMixin, TemplateView):
@staticmethod
def get_user_count():
return User.objects.filter(role__in=('Admin', 'User')).count()
return current_org.get_org_users().count()
@staticmethod
def get_asset_count():
......@@ -49,7 +50,6 @@ class IndexView(AdminUserRequiredMixin, TemplateView):
def get_week_login_asset_count(self):
return self.session_week.count()
# return self.session_week.values('asset').distinct().count()
def get_month_day_metrics(self):
month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0']
......
......@@ -55,9 +55,11 @@ class Organization(models.Model):
def get_org_users(self):
from users.models import User
if self.is_default():
return User.objects.filter(orgs__isnull=True)
users = User.objects.filter(orgs__isnull=True)
else:
return self.users.all()
users = self.users.all()
users = users.exclude(role=User.ROLE_APP)
return users
def get_org_admins(self):
if self.is_real():
......
......@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
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.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
......@@ -72,10 +73,7 @@ class UserGrantedAssetsApi(ListAPIView):
util = AssetPermissionUtil(user)
for k, v in util.get_assets().items():
if k.is_unixlike():
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']]
system_users_granted = [s for s in v if s.protocol == k.protocol]
k.system_users_granted = system_users_granted
queryset.append(k)
return queryset
......@@ -123,10 +121,7 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
for node, _assets in nodes.items():
assets = _assets.keys()
for k, v in _assets.items():
if k.is_unixlike():
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']]
system_users_granted = [s for s in v if s.protocol == k.protocol]
k.system_users_granted = system_users_granted
node.assets_granted = assets
queryset.append(node)
......
......@@ -6,13 +6,11 @@ from .. import views
app_name = 'perms'
urlpatterns = [
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/(?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})/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})/asset$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-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/(?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})/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})/asset/$', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
]
<div class="footer fixed">
<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">-->
</div>
<div>
......
......@@ -4,6 +4,7 @@ from collections import OrderedDict
import logging
import os
import uuid
import copy
from django.core.cache import cache
from django.shortcuts import get_object_or_404, redirect
......@@ -310,6 +311,7 @@ class SessionReplayViewSet(viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs):
session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id)
# 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径
local_path = self.get_local_path()
......
......@@ -92,13 +92,26 @@
{% endfor %}
</tbody>
</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 %}
{% block custom_foot_js %}
<script src="{% static "js/plugins/footable/footable.all.min.js" %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function () {
$(document).ready(function () {
$('.footable').footable();
$('.select2').select2({
dropdownAutoWidth : true,
......@@ -112,7 +125,19 @@
calendarWeeks: 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>
{% 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 = [
# Command view
url(r'^command/$', views.CommandListView.as_view(), name='command-list'),
url(r'^command/export/$', views.CommandExportView.as_view(), name='command-export')
]
# -*- coding: utf-8 -*-
#
from django.views.generic import ListView
from django.views.generic import ListView, View
from django.conf import settings
from django.utils import timezone
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.permissions import AdminUserRequiredMixin
......@@ -12,7 +14,7 @@ from ..models import Command
from .. import utils
from ..backends import get_multi_command_storage
__all__ = ['CommandListView']
__all__ = ['CommandListView', 'CommandExportView']
common_storage = get_multi_command_storage()
......@@ -61,7 +63,43 @@ class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView):
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):
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)
......
......@@ -228,7 +228,7 @@ Luna 已改为纯前端,需要 Nginx 来运行访问
-p 8081:8080 -v /opt/guacamole/key:/config/guacamole/key \
-e JUMPSERVER_KEY_DIR=/config/guacamole/key \
-e JUMPSERVER_SERVER=http://<填写本机的IP地址>:8080 \
registry.jumpserver.org/public/guacamole:1.0.0
registry.jumpserver.org/public/guacamole:latest
这里所需要注意的是 guacamole 暴露出来的端口是 8081,若与主机上其他端口冲突请自定义一下。
......
......@@ -122,8 +122,8 @@ def start_gunicorn():
cmd = [
'gunicorn', 'jumpserver.wsgi',
'-b', bind,
'-w', str(WORKERS),
'-k', 'eventlet',
'-w', str(WORKERS),
'--access-logformat', log_format,
'-p', pid_file,
]
......
......@@ -48,7 +48,7 @@ MarkupSafe==1.0
mysqlclient==1.3.12
olefile==0.44
openapi-codec==1.3.2
paramiko==2.4.0
paramiko==2.4.1
passlib==1.7.1
Pillow==4.3.0
pyasn1==0.4.2
......
......@@ -5,4 +5,4 @@ python3 ../apps/manage.py makemigrations
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