Unverified Commit 1194932b authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #1770 from jumpserver/dev

settings加密存储
单独推送系统用户到某个资产
支持了 改密日志 操作日志
翻译更加完善,支持切换语言
parents 9f96f1c5 bb13003a
......@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
__version__ = "1.4.0"
__version__ = "1.4.1"
......@@ -2,9 +2,8 @@
#
import random
import time
from rest_framework import generics, permissions
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
......@@ -14,8 +13,8 @@ from django.db.models import Q
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
from ..models import Asset, SystemUser, AdminUser, Node
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual
......@@ -40,7 +39,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
permission_classes = (permissions.AllowAny,)
permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self):
node_id = self.request.query_params.get("node_id")
......
......@@ -55,7 +55,7 @@ class NodeViewSet(viewsets.ModelViewSet):
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You cant update the root node name")},
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
......@@ -218,7 +218,8 @@ class RefreshNodeHardwareInfoApi(APIView):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
task_name = _("更新节点资产硬件信息: {}".format(node.name))
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
task_name = _("Update node asset hardware information: {}").format(node.name)
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
......@@ -231,6 +232,7 @@ class TestNodeConnectiveApi(APIView):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
task_name = _("测试节点下资产是否可连接: {}".format(node.name))
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
......@@ -13,22 +13,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser
from ..models import SystemUser, Asset
from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset
logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi'
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi',
]
......@@ -82,3 +87,43 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def get_queryset(self):
system_user = self.get_object()
return system_user.assets.all()
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = push_system_user_a_asset_manual.delay(system_user, asset)
return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset)
return Response({"task": task.id})
\ No newline at end of file
......@@ -98,15 +98,18 @@ class SystemUserForm(PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys()
if login_mode == SystemUser.MANUAL_LOGIN or protocol == SystemUser.TELNET_PROTOCOL:
if login_mode == SystemUser.MANUAL_LOGIN or \
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]:
system_user.auto_push = 0
auto_generate_key = False
system_user.save()
if auto_generate_key:
logger.info('Auto generate key and set system user auth')
system_user.auto_gen_auth()
else:
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
system_user.set_auth(password=password, private_key=private_key,
public_key=public_key)
return system_user
......
......@@ -52,7 +52,8 @@ class Cluster(models.Model):
contact=forgery_py.name.full_name(),
phone=forgery_py.address.phone(),
address=forgery_py.address.city() + forgery_py.address.street_address(),
operator=choice(['北京联通', '北京电信', 'BGP全网通']),
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
......
......@@ -20,6 +20,9 @@ class Domain(OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
def __str__(self):
return self.name
......@@ -53,3 +56,4 @@ class Gateway(AssetUser):
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
......@@ -24,6 +24,9 @@ class Node(OrgModelMixin):
is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
class Meta:
verbose_name = _("Node")
def __str__(self):
return self.full_value
......
......@@ -93,8 +93,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
"""
from ops.utils import update_or_create_ansible_task
if task_name is None:
# task_name = _("Update some assets hardware info")
task_name = _("更新资产硬件信息")
task_name = _("Update some assets hardware info")
# task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hostname_list:
......@@ -113,8 +113,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task
def update_asset_hardware_info_manual(asset):
# task_name = _("Update asset hardware info")
task_name = _("更新资产硬件信息")
task_name = _("Update asset hardware info")
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util([asset], task_name=task_name)
......@@ -132,8 +132,8 @@ def update_assets_hardware_info_period():
return
from ops.utils import update_or_create_ansible_task
# task_name = _("Update assets hardware info period")
task_name = _("定期更新资产硬件信息")
task_name = _("Update assets hardware info period")
# task_name = _("定期更新资产硬件信息")
hostname_list = [
asset.fullname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike()
......@@ -210,15 +210,15 @@ def test_admin_user_connectability_period():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
# task_name = _("Test admin user connectability period: {}".format(admin_user.name))
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_admin_user_connectability_manual(admin_user):
# task_name = _("Test admin user connectability: {}").format(admin_user.name)
task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
task_name = _("Test admin user connectability: {}").format(admin_user.name)
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name)
......@@ -227,8 +227,8 @@ def test_asset_connectability_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
# task_name = _("Test assets connectability")
task_name = _("测试资产可连接性")
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts:
logger.info("No hosts, passed")
......@@ -272,15 +272,16 @@ def set_system_user_connectablity_info(result, **kwargs):
@shared_task
def test_system_user_connectability_util(system_user, task_name):
def test_system_user_connectability_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
:param system_user:
:param assets:
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
assets = system_user.get_assets()
# assets = system_user.get_assets()
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts:
......@@ -299,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name):
@shared_task
def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user)
return test_system_user_connectability_util(system_user, task_name)
assets = system_user.get_assets()
return test_system_user_connectability_util(system_user, assets, task_name)
@shared_task
def test_system_user_connectability_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format(
system_user, asset
)
return test_system_user_connectability_util(system_user, [asset], task_name)
@shared_task
......@@ -313,8 +323,8 @@ def test_system_user_connectability_period():
system_users = SystemUser.objects.all()
for system_user in system_users:
# task_name = _("Test system user connectability period: {}".format(system_user))
task_name = _("定期测试系统用户可连接性: {}".format(system_user))
task_name = _("Test system user connectability period: {}".format(system_user))
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name)
......@@ -393,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name):
@shared_task
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets()
task_name = "推送系统用户到入资产: {}".format(system_user.name)
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name)
@shared_task
def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset.fullname
)
return push_system_user_util([system_user], [asset], task_name=task_name)
@shared_task
def push_system_user_to_assets(system_user, assets):
task_name = _("推送系统用户到入资产: {}").format(system_user.name)
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util.delay([system_user], assets, task_name)
......
......@@ -5,8 +5,11 @@
{% block help_message %}
<div class="alert alert-info help-message">
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
Windows或其它硬件可以随意设置一个
{# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
{# Windows或其它硬件可以随意设置一个#}
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
{% trans 'You can set any one for Windows or other hardware.' %}
</div>
{% endblock %}
......
......@@ -5,9 +5,9 @@
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
<div class="tagBtnList">
<a class="label label-primary" id="change_all" value="1">全选</a>
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
{% for field in form %}
{% if field.name != 'assets' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
......
......@@ -4,7 +4,8 @@
{% block help_message %}
<div class="alert alert-info help-message">
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
</div>
{% endblock %}
......@@ -627,6 +628,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected assets !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......
......@@ -120,8 +120,8 @@ $(document).ready(function(){
APIUpdateAttr({
url: the_url,
method: "GET",
success_message: "可连接",
fail_message: "连接失败"
success_message: "{% trans 'Can be connected' %}",
fail_message: "{% trans 'The connection fails' %}"
})
});
</script>
......
......@@ -4,8 +4,11 @@
{% block help_message %}
<div class="alert alert-info help-message">
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>
JMS => 网域网关 => 目标资产
{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
{# JMS => 网域网关 => 目标资产#}
{% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
<br>
{% trans 'JMS => Domain gateway => Target assets' %}
</div>
{% endblock %}
......
......@@ -50,6 +50,7 @@
<th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
......@@ -67,21 +68,59 @@
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td width="50%">{% trans 'Push system user now' %}:</td>
<td width="50%">{% trans 'Test assets connective' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
{% if system_user.auto_push %}
<tr>
<td width="50%">{% trans 'Test assets connective' %}:</td>
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
......@@ -96,7 +135,7 @@
{% block custom_foot_js %}
<script>
function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe}};
var unreachable = {{ system_user.unreachable_assets|safe }};
var options = {
ele: $('#system_user_list'),
buttons: [],
......@@ -112,27 +151,64 @@ function initAssetsTable() {
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var push_btn = '';
{% if system_user.auto_push %}
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{% endif %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
$(td).html(push_btn + test_btn);
}}
],
ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }, {data: "id"}],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
function updateSystemUserNode(nodes) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = {
nodes: Object.assign([], nodes)
};
var success = function(data) {
// remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty();
$('#node_selected').val('');
$.map(jumpserver.nodes_selected, function(node_name, index) {
$('#opt_' + index).remove();
// change tr html of user groups.
$('.node_edit tbody').append(
'<tr>' +
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>'
)
});
// clear jumpserver.nodes_selected
jumpserver.nodes_selected = {};
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
jumpserver.nodes_selected = {};
$(document).ready(function () {
$('.select2').select2()
.on("select2:select", function (evt) {
var data = evt.params.data;
jumpserver.assets_selected[data.id] = data.text;
jumpserver.asset_groups_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.assets_selected[data.id];
delete jumpserver.asset_groups_selected[data.id];
});
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
initAssetsTable();
})
.on('click', '.btn-push', function () {
......@@ -140,11 +216,16 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
success: success
});
})
.on('click', '.btn-test-connective', function () {
......@@ -152,15 +233,85 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, seen left assets status" %}"
success: success
});
})
.on('click', '.btn-remove-from-node', function() {
var $this = $(this);
var $tr = $this.closest('tr');
var $badge = $tr.find('.bdg_node');
var gid = $badge.data('gid');
var node_name = $badge.html() || $badge.text();
$('#groups_selected').append(
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
);
$tr.remove();
var nodes = $('.bdg_node').map(function () {
return $(this).data('gid');
}).get();
updateSystemUserNode(nodes);
})
.on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false;
}
var nodes = $('.bdg_node').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
updateSystemUserNode(nodes);
})
.on('click', '.btn-push-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
.on('click', '.btn-test-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-test-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
</script>
{% endblock %}
......@@ -17,11 +17,11 @@
<li class="active">
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
{# <li>#}
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#}
{# <i class="fa fa-bar-chart-o"></i> {% trans 'Attached assets' %}#}
{# </a>#}
{# </li>#}
<li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
......@@ -152,63 +152,46 @@
</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>#}
{# <td>#}
{# <span style="float: right">#}
{# <button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>#}
{# </span>#}
{# </td>#}
{# </tr>#}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# <div class="panel panel-info">#}
{# <div class="panel-heading">#}
{# <i class="fa fa-info-circle"></i> {% trans 'Nodes' %}#}
{# </div>#}
{# <div class="panel-body">#}
{# <table class="table node_edit" id="add-asset2group">#}
{# <tbody>#}
{# <form>#}
{# <tr>#}
{# <td colspan="2" class="no-borders">#}
{# <select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">#}
{# {% for node in nodes_remain %}#}
{# <option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>#}
{# {% endfor %}#}
{# </select>#}
{# </td>#}
{# </tr>#}
{# <tr>#}
{# <td colspan="2" class="no-borders">#}
{# <button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>#}
{# </td>#}
{# </tr>#}
{# </form>#}
{##}
{# {% for node in system_user.nodes.all %}#}
{# <tr>#}
{# <td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>#}
{# <td>#}
{# <button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>#}
{# </td>#}
{# </tr>#}
{# {% endfor %}#}
{# </tbody>#}
{# </table>#}
{# </div>#}
{# </div>#}
</div>
</div>
</div>
......@@ -338,13 +321,13 @@ $(document).ready(function () {
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
var name = '{{ system_user.name }}';
swal({
title: '你确定清除该系统用户的认证信息吗 ?',
title: "{% trans 'Are you sure to remove authentication information for the system user ?' %}",
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: true
}, function () {
APIUpdateAttr({
......
......@@ -3,10 +3,13 @@
{% block help_message %}
<div class="alert alert-info help-message">
系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
目前还不支持Windows的自动推送
{# 系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);#}
{# 简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。#}
{# 系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。#}
{# 目前还不支持Windows的自动推送#}
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%}
{% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %}
</div>
{% endblock %}
......@@ -135,6 +138,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected System Users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......
......@@ -19,6 +19,8 @@ urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',
......@@ -31,10 +33,16 @@ urlpatterns = [
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
......
......@@ -94,6 +94,7 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
context = {
'app': _('assets'),
'action': _('System user asset'),
'nodes_remain': Node.objects.exclude(systemuser=self.object)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -3,3 +3,6 @@ from django.apps import AppConfig
class AuditsConfig(AppConfig):
name = 'audits'
def ready(self):
from . import signals_handler
# -*- coding: utf-8 -*-
#
from users.models import LoginLog
\ No newline at end of file
......@@ -4,6 +4,11 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
]
class FTPLog(OrgModelMixin):
......@@ -16,3 +21,40 @@ class FTPLog(OrgModelMixin):
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True)
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
ACTION_CHOICES = (
(ACTION_CREATE, _("Create")),
(ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete"))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action"))
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog):
class Meta:
proxy = True
# -*- coding: utf-8 -*-
#
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from jumpserver.utils import current_request
from common.utils import get_request_ip
from users.models import User
from .models import OperateLog, PasswordChangeLog
MODELS_NEED_RECORD = (
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
'Domain', 'Gateway', 'Organization', 'AssetPermission',
)
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
with transaction.atomic():
OperateLog.objects.create(
user=user, action=action, resource_type=resource_type,
resource=resource, remote_addr=remote_addr
)
@receiver(post_save, dispatch_uid="my_unique_identifier")
def on_object_created_or_update(sender, instance=None, created=False, **kwargs):
if created:
action = OperateLog.ACTION_CREATE
else:
action = OperateLog.ACTION_UPDATE
create_operate_log(action, sender, instance)
@receiver(post_delete, dispatch_uid="my_unique_identifier")
def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(OperateLog.ACTION_DELETE, sender, instance)
@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier")
def on_user_change_password(sender, instance=None, **kwargs):
if hasattr(instance, '_set_password'):
if not current_request or not current_request.user.is_authenticated:
return
with transaction.atomic():
PasswordChangeLog.objects.create(
user=instance, change_by=current_request.user,
remote_addr=get_request_ip(current_request),
)
......@@ -66,7 +66,6 @@
{% endblock %}
{% block table_head %}
<th class="text-center"></th>
{# <th class="text-center">{% trans 'ID' %}</th>#}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
......@@ -82,7 +81,6 @@
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}
......
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load terminal_tags %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<option value="">{% trans 'Action' %}</option>
{% for k, v in actions.items %}
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<option value="">{% trans 'Resource Type' %}</option>
{% for r in resource_type_list %}
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
<th class="text-center">{% trans 'Resource Type' %}</th>
<th class="text-center">{% trans 'Resource' %}</th>
<th class="text-center">{% trans 'Remote addr' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
{% endblock %}
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}
<td class="text-center">{{ object.user }}</td>
<td class="text-center">{{ object.get_action_display }}</td>
<td class="text-center">{{ object.resource_type }}</td>
<td class="text-center">{{ object.resource }}</td>
<td class="text-center">{{ object.remote_addr }}</td>
<td class="text-center">{{ object.datetime }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"paging": false,
"bInfo" : false,
"order": [],
"language": jumpserver.language
});
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
})
</script>
{% endblock %}
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load terminal_tags %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Change by' %}</th>
<th class="text-center">{% trans 'Remote addr' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
{% endblock %}
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center">{{ object.user }}</td>
<td class="text-center">{{ object.change_by }}</td>
<td class="text-center">{{ object.remote_addr }}</td>
<td class="text-center">{{ object.datetime }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"paging": false,
"bInfo" : false,
"order": [],
"language": jumpserver.language
});
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
})
</script>
{% endblock %}
......@@ -9,5 +9,8 @@ __all__ = ["urlpatterns"]
app_name = "audits"
urlpatterns = [
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
]
from django.conf import settings
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from django.db.models import Q
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from .models import FTPLog
from orgs.utils import current_org
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
def get_resource_type_list():
from users.models import User, UserGroup
from assets.models import Asset, Node, AdminUser, SystemUser, Domain, Gateway
from orgs.models import Organization
from perms.models import AssetPermission
models = [
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
Gateway, Organization, AssetPermission
]
return [model._meta.verbose_name for model in models]
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
......@@ -53,3 +68,125 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = OperateLog
template_name = 'audits/operate_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = action = resource_type = ''
date_from = date_to = None
actions_dict = dict(OperateLog.ACTION_CHOICES)
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
self.action = self.request.GET.get('action')
self.resource_type = self.request.GET.get('resource_type')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if self.action:
filter_kwargs['action'] = self.action
if self.resource_type:
filter_kwargs['resource_type'] = self.resource_type
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'actions': self.actions_dict,
'resource_type_list': get_resource_type_list(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'action': self.action,
'resource_type': self.resource_type,
"app": _("Audits"),
"action": _("Operate log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = PasswordChangeLog
template_name = 'audits/password_change_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = ''
date_from = date_to = None
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
"app": _("Audits"),
"action": _("Password change log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'audits/login_log_list.html'
model = UserLoginLog
paginate_by = settings.DISPLAY_PER_PAGE
user = keyword = ""
date_to = date_from = None
@staticmethod
def get_org_users():
users = current_org.get_org_users().values_list('username', flat=True)
return users
def get_queryset(self):
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '')
queryset = queryset.filter(
datetime__gt=self.date_from, datetime__lt=self.date_to
)
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Login log'),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'keyword': self.keyword,
'user_list': self.get_org_users(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
\ No newline at end of file
......@@ -13,7 +13,7 @@ from .utils import get_signer
signer = get_signer()
class DictField(forms.Field):
class FormDictField(forms.Field):
widget = forms.Textarea
def to_python(self, value):
......@@ -23,6 +23,7 @@ class DictField(forms.Field):
# RadioSelect will provide. Because bool("True") == bool('1') == True,
# we don't need to handle that explicitly.
if isinstance(value, six.string_types):
value = value.replace("'", '"')
try:
value = json.loads(value)
return value
......@@ -74,3 +75,14 @@ class EncryptCharField(EncryptMixin, models.CharField):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)
class FormEncryptMixin:
pass
class FormEncryptCharField(FormEncryptMixin, forms.CharField):
pass
class FormEncryptDictField(FormEncryptMixin, FormDictField):
pass
# -*- coding: utf-8 -*-
#
import json
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape
from django.db import transaction
from django.conf import settings
from .models import Setting
from .fields import DictField
def to_model_value(value):
try:
return json.dumps(value)
except json.JSONDecodeError:
return None
def to_form_value(value):
try:
data = json.loads(value)
if isinstance(data, dict):
data = value
return data
except json.JSONDecodeError:
return ""
from .models import Setting, common_settings
from .fields import FormDictField, FormEncryptCharField, \
FormEncryptMixin, FormEncryptDictField
class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
db_settings = Setting.objects.all()
for name, field in self.fields.items():
db_value = getattr(db_settings, name).value
db_value = getattr(common_settings, name)
django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is False or db_value:
field.initial = to_form_value(db_value)
if isinstance(db_value, dict):
db_value = json.dumps(db_value)
initial_value = db_value
elif django_value is False or django_value:
field.initial = to_form_value(to_model_value(django_value))
initial_value = django_value
else:
initial_value = ''
field.initial = initial_value
def save(self, category="default"):
if not self.is_bound:
raise ValueError("Form is not bound")
db_settings = Setting.objects.all()
if self.is_valid():
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == to_form_value(getattr(db_settings, name).value):
continue
defaults = {
'name': name,
'category': category,
'value': to_model_value(value)
}
Setting.objects.update_or_create(defaults=defaults, name=name)
else:
# db_settings = Setting.objects.all()
if not self.is_valid():
raise ValueError(self.errors)
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == getattr(common_settings, name):
continue
encrypted = True if isinstance(field, FormEncryptMixin) else False
try:
setting = Setting.objects.get(name=name)
except Setting.DoesNotExist:
setting = Setting()
setting.name = name
setting.category = category
setting.encrypted = encrypted
setting.cleaned_value = value
setting.save()
class BasicSettingForm(BaseForm):
SITE_URL = forms.URLField(
......@@ -88,7 +79,7 @@ class EmailSettingForm(BaseForm):
EMAIL_HOST_USER = forms.CharField(
max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
)
EMAIL_HOST_PASSWORD = forms.CharField(
EMAIL_HOST_PASSWORD = FormEncryptCharField(
max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
required=False, help_text=_("Some provider use token except password")
)
......@@ -109,7 +100,7 @@ class LDAPSettingForm(BaseForm):
AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
)
AUTH_LDAP_BIND_PASSWORD = forms.CharField(
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='',
widget=forms.PasswordInput, required=False
)
......@@ -121,15 +112,12 @@ class LDAPSettingForm(BaseForm):
label=_("User search filter"), initial='(cn=%(user)s)',
help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
)
AUTH_LDAP_USER_ATTR_MAP = DictField(
AUTH_LDAP_USER_ATTR_MAP = FormDictField(
label=_("User attr map"),
initial=json.dumps({
"username": "cn",
"name": "sn",
"email": "mail"
}),
help_text=_(
"User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr")
"User attr map present how to map LDAP user attr to jumpserver, "
"username,name,email is jumpserver attr"
)
)
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
......@@ -156,13 +144,13 @@ class TerminalSettingForm(BaseForm):
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth")
)
TERMINAL_COMMAND_STORAGE = DictField(
TERMINAL_COMMAND_STORAGE = FormEncryptDictField(
label=_("Command storage"), help_text=_(
"Set terminal storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
)
TERMINAL_REPLAY_STORAGE = DictField(
TERMINAL_REPLAY_STORAGE = FormEncryptDictField(
label=_("Replay storage"), help_text=_(
"Set replay storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
......@@ -194,6 +182,14 @@ class SecuritySettingForm(BaseForm):
"number of times, no login is allowed during this time interval."
)
)
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False,
label=_("Connection max idle time"),
help_text=_(
'If idle time more than it, disconnect connection(only ssh now) '
'Unit: minute'
),
)
# min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"),
......@@ -223,9 +219,10 @@ class SecuritySettingForm(BaseForm):
'and resets must contain numeric characters')
)
# special char
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
initial=False, required=False,
label=_("Must contain special characters"),
help_text=_('After opening, the user password changes '
'and resets must contain special characters')
)
......@@ -7,6 +7,10 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer
signer = get_signer()
class SettingQuerySet(models.QuerySet):
def __getattr__(self, item):
......@@ -26,6 +30,7 @@ class Setting(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
value = models.TextField(verbose_name=_("Value"))
category = models.CharField(max_length=128, default="default")
encrypted = models.BooleanField(default=False)
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
comment = models.TextField(verbose_name=_("Comment"))
......@@ -34,10 +39,21 @@ class Setting(models.Model):
def __str__(self):
return self.name
def __getattr__(self, item):
instances = self.__class__.objects.filter(name=item)
if len(instances) == 1:
return instances[0].cleaned_value
else:
return None
@property
def cleaned_value(self):
try:
return json.loads(self.value)
value = self.value
if self.encrypted:
value = signer.unsign(value)
value = json.loads(value)
return value
except json.JSONDecodeError:
return None
......@@ -45,6 +61,8 @@ class Setting(models.Model):
def cleaned_value(self, item):
try:
v = json.dumps(item)
if self.encrypted:
v = signer.sign(v)
self.value = v
except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e)))
......@@ -59,11 +77,7 @@ class Setting(models.Model):
pass
def refresh_setting(self):
try:
value = json.loads(self.value)
except json.JSONDecodeError:
return
setattr(settings, self.name, value)
setattr(settings, self.name, self.cleaned_value)
if self.name == "AUTH_LDAP":
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
......@@ -81,3 +95,5 @@ class Setting(models.Model):
class Meta:
db_table = "settings"
common_settings = Setting()
......@@ -92,3 +92,9 @@ class AdminUserRequiredMixin(UserPassesTestMixin):
return redirect('orgs:switch-a-org')
return HttpResponseForbidden()
return super().dispatch(request, *args, **kwargs)
class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser:
return True
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_save
from django.conf import settings
from django.db.utils import ProgrammingError, OperationalError
from jumpserver.utils import current_request
from .models import Setting
from .utils import get_logger
from .signals import django_ready, ldap_auth_enable
......@@ -42,3 +43,9 @@ def ldap_auth_on_changed(sender, enabled=True, **kwargs):
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
@receiver(pre_save, dispatch_uid="my_unique_identifier")
def on_create_set_created_by(sender, instance=None, **kwargs):
if hasattr(instance, 'created_by') and not instance.created_by:
if current_request and current_request.user.is_authenticated:
instance.created_by = current_request.user.name
......@@ -41,7 +41,7 @@
<h3>{% trans "User login settings" %}</h3>
{% for field in form %}
{% if forloop.counter == 4 %}
{% if forloop.counter == 5 %}
<div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3>
{% endif %}
......
......@@ -378,6 +378,15 @@ def get_signer():
return signer
def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get('REMOTE_ADDR', '')
return login_ip
class TeeObj:
origin_stdout = sys.stdout
......
......@@ -6,11 +6,11 @@ from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable
class BasicSettingView(AdminUserRequiredMixin, TemplateView):
class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form_class = BasicSettingForm
template_name = "common/basic_setting.html"
......@@ -36,7 +36,7 @@ class BasicSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class EmailSettingView(AdminUserRequiredMixin, TemplateView):
class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form_class = EmailSettingForm
template_name = "common/email_setting.html"
......@@ -62,7 +62,7 @@ class EmailSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form_class = LDAPSettingForm
template_name = "common/ldap_setting.html"
......@@ -90,7 +90,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form_class = TerminalSettingForm
template_name = "common/terminal_setting.html"
......@@ -120,7 +120,7 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class SecuritySettingView(AdminUserRequiredMixin, TemplateView):
class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm
template_name = "common/security_setting.html"
......
......@@ -6,6 +6,8 @@ import pytz
from django.utils import timezone
from django.shortcuts import HttpResponse
from .utils import set_current_request
class TimezoneMiddleware:
def __init__(self, get_response):
......@@ -45,3 +47,13 @@ class DemoMiddleware:
else:
response = self.get_response(request)
return response
class RequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
set_current_request(request)
response = self.get_response(request)
return response
......@@ -95,6 +95,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware',
'orgs.middleware.OrgMiddleware',
]
......@@ -127,7 +128,6 @@ TEMPLATES = [
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'jumpserver.context_processor.jumpserver_processor',
'django.template.context_processors.i18n',
'django.template.context_processors.debug',
'django.template.context_processors.request',
......@@ -136,6 +136,7 @@ TEMPLATES = [
'django.template.context_processors.static',
'django.template.context_processors.request',
'django.template.context_processors.media',
'jumpserver.context_processor.jumpserver_processor',
'orgs.context_processor.org_processor',
*get_xpack_context_processor(),
],
......@@ -214,7 +215,10 @@ LOGGING = {
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'class': 'logging.handlers.TimedRotatingFileHandler',
'when': "D",
'interval': 1,
"backupCount": 7,
'formatter': 'main',
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
},
......@@ -270,7 +274,8 @@ LOGGING = {
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en'
# LANGUAGE_CODE = 'en'
LANGUAGE_CODE = 'zh'
TIME_ZONE = 'Asia/Shanghai'
......@@ -281,7 +286,9 @@ USE_L10N = True
USE_TZ = True
# I18N translation
LOCALE_PATHS = [os.path.join(BASE_DIR, 'i18n'), ]
LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale'),
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
......@@ -441,7 +448,8 @@ TERMINAL_REPLAY_STORAGE = {
DEFAULT_PASSWORD_MIN_LENGTH = 6
DEFAULT_LOGIN_LIMIT_COUNT = 7
DEFAULT_LOGIN_LIMIT_TIME = 30
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = {
......@@ -465,4 +473,4 @@ SWAGGER_SETTINGS = {
'type': 'basic'
}
},
}
\ No newline at end of file
}
......@@ -6,6 +6,8 @@ import os
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
......@@ -14,7 +16,7 @@ from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from .views import IndexView, LunaView
from .views import IndexView, LunaView, I18NView
schema_view = get_schema_view(
openapi.Info(
......@@ -78,10 +80,14 @@ app_view_patterns = [
if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
js_i18n_patterns = i18n_patterns(
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
)
urlpatterns = [
path('', IndexView.as_view(), name='index'),
path('luna/', LunaView.as_view(), name='luna-error'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api),
......@@ -93,6 +99,7 @@ urlpatterns = [
urlpatterns += app_view_patterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += js_i18n_patterns
if settings.DEBUG:
urlpatterns += [
......
# -*- coding: utf-8 -*-
#
from functools import partial
from common.utils import LocalProxy
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def set_current_request(request):
setattr(_thread_locals, 'current_request', request)
def _find(attr):
return getattr(_thread_locals, attr, None)
def get_current_request():
return _find('current_request')
current_request = LocalProxy(partial(_find, 'current_request'))
import datetime
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings
from django.views.generic import TemplateView, View
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin
......@@ -85,7 +87,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
return self.session_month.values('user').distinct().count()
def get_month_inactive_user_total(self):
return User.objects.all().count() - self.get_month_active_user_total()
return current_org.get_org_users().count() - self.get_month_active_user_total()
def get_month_active_asset_total(self):
return self.session_month.values('asset').distinct().count()
......@@ -95,7 +97,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
@staticmethod
def get_user_disabled_total():
return User.objects.filter(is_active=False).count()
return current_org.get_org_users().filter(is_active=False).count()
@staticmethod
def get_asset_disabled_total():
......@@ -173,11 +175,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
class LunaView(View):
def get(self, request):
msg = """
Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发,
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运
"""
msg = _("<div>Luna is a separately deployed program, you need to deploy Luna, coco, configure nginx for url distribution,</div> "
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
return HttpResponse(msg)
class I18NView(View):
def get(self, request, lang):
referer_url = request.META.get('HTTP_REFERER', '/')
response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
return response
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-16 16:28+0800\n"
"POT-Creation-Date: 2018-08-27 18:29+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
......@@ -18,32 +18,32 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
#: assets/api/node.py:58
msgid "You cant update the root node name"
msgstr ""
msgid "You can't update the root node name"
msgstr "不能修改根节点名称"
#: assets/api/node.py:82
msgid "New node {}"
msgstr "新节点 {}"
#: assets/api/node.py:221
msgid "更新节点资产硬件信息: {}"
msgstr ""
#: assets/api/node.py:222
msgid "Update node asset hardware information: {}"
msgstr "更新节点资产硬件信息: {}"
#: assets/api/node.py:234
msgid "测试节点下资产是否可连接: {}"
msgstr ""
#: assets/api/node.py:236
msgid "Test if the assets under the node are connectable: {}"
msgstr "测试节点下资产是否可连接: {}"
#: assets/forms/asset.py:27 assets/models/asset.py:82 assets/models/user.py:113
#: assets/templates/assets/asset_detail.html:183
#: assets/templates/assets/asset_detail.html:191
#: assets/templates/assets/system_user_detail.html:178 perms/models.py:32
#: assets/templates/assets/system_user_asset.html:95 perms/models.py:32
msgid "Nodes"
msgstr "节点管理"
#: assets/forms/asset.py:30 assets/forms/asset.py:69 assets/forms/asset.py:112
#: assets/forms/asset.py:116 assets/models/asset.py:87
#: assets/models/cluster.py:19 assets/models/user.py:73
#: assets/templates/assets/asset_detail.html:73 templates/_nav.html:25
#: assets/templates/assets/asset_detail.html:73 templates/_nav.html:24
#: xpack/plugins/orgs/templates/orgs/org_list.html:18
msgid "Admin user"
msgstr "管理用户"
......@@ -51,7 +51,7 @@ msgstr "管理用户"
#: assets/forms/asset.py:33 assets/forms/asset.py:72 assets/forms/asset.py:128
#: assets/templates/assets/asset_create.html:36
#: assets/templates/assets/asset_create.html:38
#: assets/templates/assets/asset_list.html:80
#: assets/templates/assets/asset_list.html:81
#: assets/templates/assets/asset_update.html:41
#: assets/templates/assets/asset_update.html:43
#: assets/templates/assets/user_asset_list.html:34
......@@ -60,15 +60,17 @@ msgid "Label"
msgstr "标签"
#: assets/forms/asset.py:37 assets/forms/asset.py:76 assets/models/asset.py:78
#: assets/models/domain.py:47 assets/templates/assets/user_asset_list.html:168
#: assets/models/domain.py:24 assets/models/domain.py:50
#: assets/templates/assets/user_asset_list.html:168
#: xpack/plugins/orgs/templates/orgs/org_list.html:17
msgid "Domain"
msgstr "网域"
#: assets/forms/asset.py:41 assets/forms/asset.py:66 assets/forms/asset.py:80
#: assets/forms/asset.py:131 assets/templates/assets/asset_create.html:30
#: assets/templates/assets/asset_update.html:35 perms/forms.py:50
#: perms/forms.py:57 perms/models.py:78
#: assets/forms/asset.py:131 assets/models/node.py:28
#: assets/templates/assets/asset_create.html:30
#: assets/templates/assets/asset_update.html:35 perms/forms.py:37
#: perms/forms.py:44 perms/models.py:79
#: perms/templates/perms/asset_permission_list.html:57
#: perms/templates/perms/asset_permission_list.html:151
msgid "Node"
......@@ -94,7 +96,7 @@ msgid "Select assets"
msgstr "选择资产"
#: assets/forms/asset.py:108 assets/models/asset.py:75
#: assets/models/domain.py:45 assets/templates/assets/admin_user_assets.html:53
#: assets/models/domain.py:48 assets/templates/assets/admin_user_assets.html:53
#: assets/templates/assets/asset_detail.html:69
#: assets/templates/assets/domain_gateway_list.html:58
#: assets/templates/assets/system_user_asset.html:51
......@@ -103,13 +105,13 @@ msgid "Port"
msgstr "端口"
#: assets/forms/domain.py:15 assets/forms/label.py:13
#: assets/models/asset.py:242 assets/templates/assets/admin_user_list.html:25
#: assets/models/asset.py:242 assets/templates/assets/admin_user_list.html:28
#: assets/templates/assets/domain_detail.html:60
#: assets/templates/assets/domain_list.html:23
#: assets/templates/assets/domain_list.html:26
#: assets/templates/assets/label_list.html:16
#: assets/templates/assets/system_user_list.html:30 audits/models.py:13
#: assets/templates/assets/system_user_list.html:33 audits/models.py:18
#: audits/templates/audits/ftp_log_list.html:41
#: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:47
#: audits/templates/audits/ftp_log_list.html:71 perms/forms.py:34
#: perms/models.py:31
#: perms/templates/perms/asset_permission_create_update.html:40
#: perms/templates/perms/asset_permission_list.html:56
......@@ -125,21 +127,21 @@ msgstr "资产"
#: assets/forms/domain.py:42
msgid "Password should not contain special characters"
msgstr "密码不能包含特殊字符"
msgstr "不能包含特殊字符"
#: assets/forms/domain.py:59 assets/forms/user.py:79 assets/forms/user.py:139
#: assets/models/base.py:22 assets/models/cluster.py:18
#: assets/models/domain.py:18 assets/models/group.py:20
#: assets/models/label.py:18 assets/templates/assets/admin_user_detail.html:56
#: assets/templates/assets/admin_user_list.html:23
#: assets/templates/assets/admin_user_list.html:26
#: assets/templates/assets/domain_detail.html:56
#: assets/templates/assets/domain_gateway_list.html:56
#: assets/templates/assets/domain_list.html:22
#: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/label_list.html:14
#: assets/templates/assets/system_user_detail.html:58
#: assets/templates/assets/system_user_list.html:26 common/models.py:26
#: assets/templates/assets/system_user_list.html:29 common/models.py:26
#: common/templates/common/terminal_setting.html:72
#: common/templates/common/terminal_setting.html:90 ops/models/adhoc.py:36
#: common/templates/common/terminal_setting.html:90 ops/models/adhoc.py:37
#: ops/templates/ops/task_detail.html:59 ops/templates/ops/task_list.html:35
#: orgs/models.py:12 perms/models.py:28
#: perms/templates/perms/asset_permission_detail.html:62
......@@ -162,15 +164,15 @@ msgstr "名称"
#: assets/forms/domain.py:60 assets/forms/user.py:80 assets/forms/user.py:140
#: assets/models/base.py:23 assets/templates/assets/admin_user_detail.html:60
#: assets/templates/assets/admin_user_list.html:24
#: assets/templates/assets/admin_user_list.html:27
#: assets/templates/assets/domain_gateway_list.html:60
#: assets/templates/assets/system_user_detail.html:62
#: assets/templates/assets/system_user_list.html:27
#: assets/templates/assets/system_user_list.html:30
#: audits/templates/audits/login_log_list.html:49
#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15
#: users/forms.py:33 users/models/authentication.py:70 users/models/user.py:49
#: users/templates/users/_select_user_modal.html:14
#: users/templates/users/login.html:60
#: users/templates/users/login_log_list.html:49
#: users/templates/users/login.html:62
#: users/templates/users/user_detail.html:67
#: users/templates/users/user_list.html:24
#: users/templates/users/user_profile.html:47
......@@ -183,11 +185,11 @@ msgstr "密码或密钥密码"
#: assets/forms/user.py:25 assets/models/base.py:24 common/forms.py:113
#: users/forms.py:17 users/forms.py:35 users/forms.py:47
#: users/templates/users/login.html:63
#: users/templates/users/login.html:65
#: users/templates/users/reset_password.html:53
#: users/templates/users/user_create.html:10
#: users/templates/users/user_password_authentication.html:14
#: users/templates/users/user_password_update.html:42
#: users/templates/users/user_password_authentication.html:18
#: users/templates/users/user_password_update.html:43
#: users/templates/users/user_profile_update.html:40
#: users/templates/users/user_pubkey_update.html:40
msgid "Password"
......@@ -225,17 +227,17 @@ msgid ""
"password."
msgstr "如果选择手动登录模式,用户名和密码则不需要填写"
#: assets/models/asset.py:72 assets/models/domain.py:44
#: assets/models/asset.py:72 assets/models/domain.py:47
#: assets/templates/assets/_asset_list_modal.html:46
#: assets/templates/assets/admin_user_assets.html:52
#: assets/templates/assets/asset_detail.html:61
#: assets/templates/assets/asset_list.html:92
#: assets/templates/assets/asset_list.html:93
#: assets/templates/assets/domain_gateway_list.html:57
#: assets/templates/assets/system_user_asset.html:50
#: assets/templates/assets/user_asset_list.html:46
#: assets/templates/assets/user_asset_list.html:162 common/forms.py:145
#: assets/templates/assets/user_asset_list.html:162
#: audits/templates/audits/login_log_list.html:52 common/forms.py:145
#: perms/templates/perms/asset_permission_asset.html:55
#: users/templates/users/login_log_list.html:52
#: users/templates/users/user_granted_asset.html:45
#: users/templates/users/user_group_granted_asset.html:45
msgid "IP"
......@@ -244,7 +246,7 @@ msgstr "IP"
#: assets/models/asset.py:73 assets/templates/assets/_asset_list_modal.html:45
#: assets/templates/assets/admin_user_assets.html:51
#: assets/templates/assets/asset_detail.html:57
#: assets/templates/assets/asset_list.html:91
#: assets/templates/assets/asset_list.html:92
#: assets/templates/assets/system_user_asset.html:49
#: assets/templates/assets/user_asset_list.html:45
#: assets/templates/assets/user_asset_list.html:161 common/forms.py:144
......@@ -254,11 +256,11 @@ msgstr "IP"
msgid "Hostname"
msgstr "主机名"
#: assets/models/asset.py:74 assets/models/domain.py:46
#: assets/models/asset.py:74 assets/models/domain.py:49
#: assets/models/user.py:116
#: assets/templates/assets/domain_gateway_list.html:59
#: assets/templates/assets/system_user_detail.html:70
#: assets/templates/assets/system_user_list.html:28
#: assets/templates/assets/system_user_list.html:31
#: assets/templates/assets/user_asset_list.html:164
#: terminal/templates/terminal/session_list.html:75
msgid "Protocol"
......@@ -269,7 +271,7 @@ msgstr "协议"
msgid "Platform"
msgstr "系统平台"
#: assets/models/asset.py:83 assets/models/domain.py:49
#: assets/models/asset.py:83 assets/models/domain.py:52
#: assets/models/label.py:21 assets/templates/assets/asset_detail.html:105
#: assets/templates/assets/user_asset_list.html:169
msgid "Is active"
......@@ -308,10 +310,8 @@ msgid "CPU cores"
msgstr "CPU核数"
#: assets/models/asset.py:105
#, fuzzy
#| msgid "CPU count"
msgid "CPU vcpus"
msgstr "CPU数量"
msgstr "CPU总数"
#: assets/models/asset.py:107 assets/templates/assets/asset_detail.html:89
msgid "Memory"
......@@ -344,7 +344,7 @@ msgstr "主机名原始"
#: assets/models/asset.py:124 assets/templates/assets/asset_create.html:34
#: assets/templates/assets/asset_detail.html:220
#: assets/templates/assets/asset_update.html:39 templates/_nav.html:27
#: assets/templates/assets/asset_update.html:39 templates/_nav.html:26
msgid "Labels"
msgstr "标签管理"
......@@ -355,7 +355,7 @@ msgstr "标签管理"
#: assets/templates/assets/domain_detail.html:72
#: assets/templates/assets/system_user_detail.html:100
#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37
#: perms/models.py:83 perms/templates/perms/asset_permission_detail.html:98
#: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98
#: users/models/user.py:92 users/templates/users/user_detail.html:111
msgid "Created by"
msgstr "创建者"
......@@ -366,7 +366,7 @@ msgstr "创建者"
#: assets/templates/assets/domain_detail.html:68
#: assets/templates/assets/system_user_detail.html:96
#: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:63
#: orgs/models.py:16 perms/models.py:38 perms/models.py:84
#: orgs/models.py:16 perms/models.py:38 perms/models.py:85
#: perms/templates/perms/asset_permission_detail.html:94
#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17
#: users/templates/users/user_group_detail.html:63
......@@ -376,18 +376,18 @@ msgstr "创建日期"
#: assets/models/asset.py:131 assets/models/base.py:27
#: assets/models/cluster.py:29 assets/models/domain.py:19
#: assets/models/domain.py:48 assets/models/group.py:23
#: assets/models/domain.py:51 assets/models/group.py:23
#: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72
#: assets/templates/assets/admin_user_list.html:29
#: assets/templates/assets/admin_user_list.html:32
#: assets/templates/assets/asset_detail.html:125
#: assets/templates/assets/domain_detail.html:76
#: assets/templates/assets/domain_gateway_list.html:61
#: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/domain_list.html:28
#: assets/templates/assets/system_user_detail.html:104
#: assets/templates/assets/system_user_list.html:34
#: assets/templates/assets/system_user_list.html:37
#: assets/templates/assets/user_asset_list.html:170 common/models.py:30
#: ops/models/adhoc.py:42 orgs/models.py:17 perms/models.py:39
#: perms/models.py:85 perms/templates/perms/asset_permission_detail.html:102
#: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39
#: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102
#: terminal/models.py:27 terminal/templates/terminal/terminal_detail.html:63
#: users/models/group.py:15 users/models/user.py:84
#: users/templates/users/user_detail.html:123
......@@ -442,7 +442,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:360
#: users/models/user.py:363
msgid "System"
msgstr "系统"
......@@ -454,6 +454,25 @@ msgstr "默认Cluster"
msgid "Cluster"
msgstr "集群"
#: assets/models/cluster.py:56
msgid "Beijing unicom"
msgstr "北京联通"
#: assets/models/cluster.py:56
msgid "Beijing telecom"
msgstr "北京电信"
#: assets/models/cluster.py:56
msgid "BGP full netcom"
msgstr "BGP全网通"
#: assets/models/domain.py:59 assets/templates/assets/domain_detail.html:21
#: assets/templates/assets/domain_detail.html:64
#: assets/templates/assets/domain_gateway_list.html:21
#: assets/templates/assets/domain_list.html:27
msgid "Gateway"
msgstr "网关"
#: assets/models/group.py:30
msgid "Asset group"
msgstr "资产组"
......@@ -462,19 +481,23 @@ msgstr "资产组"
msgid "Default asset group"
msgstr "默认资产组"
#: assets/models/label.py:15 audits/models.py:11
#: audits/templates/audits/ftp_log_list.html:33
#: audits/templates/audits/ftp_log_list.html:71 perms/forms.py:16
#: perms/forms.py:41 perms/models.py:29
#: assets/models/label.py:15 audits/models.py:16 audits/models.py:36
#: audits/models.py:49 audits/templates/audits/ftp_log_list.html:33
#: audits/templates/audits/ftp_log_list.html:70
#: audits/templates/audits/operate_log_list.html:33
#: audits/templates/audits/operate_log_list.html:66
#: audits/templates/audits/password_change_log_list.html:33
#: audits/templates/audits/password_change_log_list.html:50 perms/forms.py:28
#: perms/models.py:29
#: perms/templates/perms/asset_permission_create_update.html:36
#: perms/templates/perms/asset_permission_list.html:54
#: perms/templates/perms/asset_permission_list.html:142
#: perms/templates/perms/asset_permission_list.html:142 templates/index.html:87
#: terminal/backends/command/models.py:12 terminal/models.py:127
#: terminal/templates/terminal/command_list.html:32
#: terminal/templates/terminal/command_list.html:72
#: terminal/templates/terminal/session_list.html:33
#: terminal/templates/terminal/session_list.html:71 users/forms.py:312
#: users/models/user.py:33 users/models/user.py:348
#: users/models/user.py:33 users/models/user.py:351
#: users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:13 users/views/user.py:378
#: xpack/plugins/orgs/forms.py:26
......@@ -508,6 +531,7 @@ msgstr "手动登录"
#: assets/models/user.py:114
#: assets/templates/assets/_asset_group_bulk_update_modal.html:11
#: assets/templates/assets/system_user_asset.html:21
#: assets/templates/assets/system_user_detail.html:22
#: assets/views/admin_user.py:29 assets/views/admin_user.py:47
#: assets/views/admin_user.py:63 assets/views/admin_user.py:78
#: assets/views/admin_user.py:102 assets/views/asset.py:53
......@@ -519,7 +543,7 @@ msgstr "手动登录"
#: assets/views/label.py:26 assets/views/label.py:42 assets/views/label.py:58
#: assets/views/system_user.py:28 assets/views/system_user.py:44
#: assets/views/system_user.py:60 assets/views/system_user.py:74
#: templates/_nav.html:20
#: templates/_nav.html:19
msgid "Assets"
msgstr "资产管理"
......@@ -542,17 +566,17 @@ msgid "Shell"
msgstr "Shell"
#: assets/models/user.py:120 assets/templates/assets/system_user_detail.html:66
#: assets/templates/assets/system_user_list.html:29
#: assets/templates/assets/system_user_list.html:32
msgid "Login mode"
msgstr "登录模式"
#: assets/models/user.py:181 assets/templates/assets/user_asset_list.html:167
#: audits/models.py:14 audits/templates/audits/ftp_log_list.html:49
#: audits/templates/audits/ftp_log_list.html:73 perms/forms.py:53
#: perms/models.py:33 perms/models.py:80
#: audits/models.py:19 audits/templates/audits/ftp_log_list.html:49
#: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:40
#: perms/models.py:33 perms/models.py:81
#: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:58
#: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:26
#: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:25
#: terminal/backends/command/models.py:14 terminal/models.py:129
#: terminal/templates/terminal/command_list.html:48
#: terminal/templates/terminal/command_list.html:74
......@@ -567,37 +591,45 @@ msgstr "系统用户"
msgid "%(value)s is not an even number"
msgstr "%(value)s is not an even number"
#: assets/tasks.py:97 assets/tasks.py:117
msgid "更新资产硬件信息"
msgstr ""
#: assets/tasks.py:96
msgid "Update some assets hardware info"
msgstr "更新资产硬件信息"
#: assets/tasks.py:136
msgid "定期更新资产硬件信息"
msgstr ""
#: assets/tasks.py:116
msgid "Update asset hardware info"
msgstr "更新资产硬件信息"
#: assets/tasks.py:214
msgid "定期测试管理账号可连接性: {}"
msgstr ""
#: assets/tasks.py:135
msgid "Update assets hardware info period"
msgstr "定期更新资产硬件信息"
#: assets/tasks.py:221
msgid "测试管理行号可连接性: {}"
msgstr ""
#: assets/tasks.py:213
msgid "Test admin user connectability period: {}"
msgstr "定期测试管理账号可连接性: {}"
#: assets/tasks.py:231
msgid "测试资产可连接性"
msgstr ""
#: assets/tasks.py:220
msgid "Test admin user connectability: {}"
msgstr "测试管理行号可连接性: {}"
#: assets/tasks.py:230
msgid "Test assets connectability"
msgstr "测试资产可连接性"
#: assets/tasks.py:301
msgid "Test system user connectability: {}"
msgstr "测试系统用户可连接性: {}"
#: assets/tasks.py:317
msgid "定期测试系统用户可连接性: {}"
msgstr ""
#: assets/tasks.py:316
msgid "Test system user connectability period: {}"
msgstr "定期测试系统用户可连接性: {}"
#: assets/tasks.py:402
msgid "推送系统用户到入资产: {}"
msgstr ""
#: assets/tasks.py:397 assets/tasks.py:412
msgid "Push system users to assets: {}"
msgstr "推送系统用户到入资产: {}"
#: assets/tasks.py:403
msgid "Push system users to asset: {} => {}"
msgstr "推送系统用户到入资产: {} => {}"
#: assets/templates/assets/_asset_group_bulk_update_modal.html:5
msgid "Update asset group"
......@@ -648,7 +680,7 @@ msgid "If set id, will use this id update asset existed"
msgstr "如果设置了id,则会使用该行信息更新该id的资产"
#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:54
#: templates/_nav.html:23
#: templates/_nav.html:22
msgid "Asset list"
msgstr "资产列表"
......@@ -698,8 +730,9 @@ msgstr "其它"
#: terminal/templates/terminal/terminal_update.html:47
#: users/templates/users/_user.html:46
#: users/templates/users/user_bulk_update.html:23
#: users/templates/users/user_password_update.html:70
#: users/templates/users/user_profile.html:188
#: users/templates/users/user_detail.html:172
#: users/templates/users/user_password_update.html:71
#: users/templates/users/user_profile.html:198
#: users/templates/users/user_profile_update.html:63
#: users/templates/users/user_pubkey_update.html:70
#: users/templates/users/user_pubkey_update.html:76
......@@ -710,7 +743,7 @@ msgstr "重置"
#: assets/templates/assets/admin_user_create_update.html:46
#: assets/templates/assets/asset_bulk_update.html:24
#: assets/templates/assets/asset_create.html:68
#: assets/templates/assets/asset_list.html:113
#: assets/templates/assets/asset_list.html:114
#: assets/templates/assets/asset_update.html:72
#: assets/templates/assets/domain_create_update.html:17
#: assets/templates/assets/gateway_create_update.html:59
......@@ -725,10 +758,10 @@ msgstr "重置"
#: terminal/templates/terminal/session_list.html:126
#: terminal/templates/terminal/terminal_update.html:48
#: users/templates/users/_user.html:47
#: users/templates/users/forgot_password.html:44
#: users/templates/users/forgot_password.html:45
#: users/templates/users/user_bulk_update.html:24
#: users/templates/users/user_list.html:45
#: users/templates/users/user_password_update.html:71
#: users/templates/users/user_password_update.html:72
#: users/templates/users/user_profile_update.html:64
#: users/templates/users/user_pubkey_update.html:77
msgid "Submit"
......@@ -770,15 +803,15 @@ msgid "Asset list of "
msgstr "资产列表"
#: assets/templates/assets/admin_user_assets.html:54
#: assets/templates/assets/admin_user_list.html:26
#: assets/templates/assets/admin_user_list.html:29
#: assets/templates/assets/system_user_asset.html:52
#: assets/templates/assets/system_user_list.html:31
#: assets/templates/assets/system_user_list.html:34
#: users/templates/users/user_group_granted_asset.html:47
msgid "Reachable"
msgstr "可连接"
#: assets/templates/assets/admin_user_assets.html:66
#: assets/templates/assets/system_user_asset.html:64
#: assets/templates/assets/system_user_asset.html:65
#: assets/templates/assets/system_user_detail.html:116
#: perms/templates/perms/asset_permission_detail.html:114
msgid "Quick update"
......@@ -791,22 +824,22 @@ msgstr "测试可连接性"
#: assets/templates/assets/admin_user_assets.html:75
#: assets/templates/assets/asset_detail.html:171
#: assets/templates/assets/system_user_asset.html:81
#: assets/templates/assets/system_user_asset.html:74
#: assets/templates/assets/system_user_detail.html:151
msgid "Test"
msgstr "测试"
#: assets/templates/assets/admin_user_detail.html:24
#: assets/templates/assets/admin_user_list.html:85
#: assets/templates/assets/admin_user_list.html:88
#: assets/templates/assets/asset_detail.html:24
#: assets/templates/assets/asset_list.html:170
#: assets/templates/assets/asset_list.html:171
#: assets/templates/assets/domain_detail.html:24
#: assets/templates/assets/domain_detail.html:103
#: assets/templates/assets/domain_gateway_list.html:85
#: assets/templates/assets/domain_list.html:50
#: assets/templates/assets/domain_list.html:53
#: assets/templates/assets/label_list.html:38
#: assets/templates/assets/system_user_detail.html:26
#: assets/templates/assets/system_user_list.html:89
#: assets/templates/assets/system_user_list.html:92 audits/models.py:32
#: perms/templates/perms/asset_permission_detail.html:30
#: perms/templates/perms/asset_permission_list.html:200
#: terminal/templates/terminal/terminal_detail.html:16
......@@ -816,7 +849,8 @@ msgstr "测试"
#: users/templates/users/user_group_list.html:43
#: users/templates/users/user_list.html:77
#: users/templates/users/user_profile.html:151
#: users/templates/users/user_profile.html:180
#: users/templates/users/user_profile.html:181
#: users/templates/users/user_profile.html:190
#: xpack/plugins/orgs/templates/orgs/org_detail.html:25
#: xpack/plugins/orgs/templates/orgs/org_list.html:85
#: xpack/templates/orgs/org_list.html:43
......@@ -824,16 +858,16 @@ msgid "Update"
msgstr "更新"
#: assets/templates/assets/admin_user_detail.html:28
#: assets/templates/assets/admin_user_list.html:86
#: assets/templates/assets/admin_user_list.html:89
#: assets/templates/assets/asset_detail.html:28
#: assets/templates/assets/asset_list.html:171
#: assets/templates/assets/asset_list.html:172
#: assets/templates/assets/domain_detail.html:28
#: assets/templates/assets/domain_detail.html:104
#: assets/templates/assets/domain_gateway_list.html:86
#: assets/templates/assets/domain_list.html:51
#: assets/templates/assets/domain_list.html:54
#: assets/templates/assets/label_list.html:39
#: assets/templates/assets/system_user_detail.html:30
#: assets/templates/assets/system_user_list.html:90
#: assets/templates/assets/system_user_list.html:93 audits/models.py:33
#: ops/templates/ops/task_list.html:72
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:201
......@@ -860,46 +894,67 @@ msgstr "选择节点"
#: assets/templates/assets/admin_user_detail.html:100
#: assets/templates/assets/asset_detail.html:200
#: assets/templates/assets/asset_list.html:631
#: assets/templates/assets/system_user_detail.html:195
#: assets/templates/assets/system_user_list.html:139 templates/_modal.html:22
#: assets/templates/assets/asset_list.html:633
#: assets/templates/assets/system_user_asset.html:112
#: assets/templates/assets/system_user_detail.html:330
#: assets/templates/assets/system_user_list.html:143 templates/_modal.html:22
#: terminal/templates/terminal/session_detail.html:108
#: users/templates/users/user_detail.html:374
#: users/templates/users/user_detail.html:399
#: users/templates/users/user_detail.html:422
#: users/templates/users/user_detail.html:466
#: users/templates/users/user_detail.html:382
#: users/templates/users/user_detail.html:408
#: users/templates/users/user_detail.html:431
#: users/templates/users/user_detail.html:476
#: users/templates/users/user_group_create_update.html:32
#: users/templates/users/user_group_list.html:86
#: users/templates/users/user_list.html:200
#: users/templates/users/user_profile.html:222
#: users/templates/users/user_group_list.html:87
#: users/templates/users/user_list.html:201
#: users/templates/users/user_profile.html:232
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:33
#: xpack/templates/orgs/org_list.html:86
msgid "Confirm"
msgstr "确认"
#: assets/templates/assets/admin_user_list.html:15
#: assets/templates/assets/admin_user_list.html:10
msgid ""
"Admin users are asset (charged server) on the root, or have NOPASSWD: ALL "
"sudo permissions users, "
msgstr ""
"管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,"
#: assets/templates/assets/admin_user_list.html:11
msgid ""
"Jumpserver users of the system using the user to `push system user`, `get "
"assets hardware information`, etc. "
msgstr "Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。"
#: assets/templates/assets/admin_user_list.html:12
msgid "You can set any one for Windows or other hardware."
msgstr "Windows或其它硬件可以随意设置一个"
#: assets/templates/assets/admin_user_list.html:18
#: assets/views/admin_user.py:48
msgid "Create admin user"
msgstr "创建管理用户"
#: assets/templates/assets/admin_user_list.html:27
#: assets/templates/assets/system_user_list.html:32
#: assets/templates/assets/admin_user_list.html:30
#: assets/templates/assets/system_user_list.html:35
msgid "Unreachable"
msgstr "不可达"
#: assets/templates/assets/admin_user_list.html:28
#: assets/templates/assets/system_user_list.html:33
#: assets/templates/assets/admin_user_list.html:31
#: assets/templates/assets/system_user_list.html:36
#: ops/templates/ops/adhoc_history.html:54
#: ops/templates/ops/task_history.html:60
msgid "Ratio"
msgstr "比例"
#: assets/templates/assets/admin_user_list.html:30
#: assets/templates/assets/asset_list.html:96
#: assets/templates/assets/admin_user_list.html:33
#: assets/templates/assets/asset_list.html:97
#: assets/templates/assets/domain_gateway_list.html:62
#: assets/templates/assets/domain_list.html:26
#: assets/templates/assets/domain_list.html:29
#: assets/templates/assets/label_list.html:17
#: assets/templates/assets/system_user_list.html:35
#: assets/templates/assets/system_user_asset.html:53
#: assets/templates/assets/system_user_list.html:38 audits/models.py:37
#: audits/templates/audits/operate_log_list.html:41
#: audits/templates/audits/operate_log_list.html:67
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42
#: perms/templates/perms/asset_permission_list.html:60
......@@ -912,6 +967,16 @@ msgstr "比例"
msgid "Action"
msgstr "动作"
#: assets/templates/assets/asset_bulk_update.html:8
#: users/templates/users/user_bulk_update.html:8
msgid "Select properties that need to be modified"
msgstr "选择需要修改属性"
#: assets/templates/assets/asset_bulk_update.html:10
#: users/templates/users/user_bulk_update.html:10
msgid "Select all"
msgstr "全选"
#: assets/templates/assets/asset_detail.html:85
msgid "CPU"
msgstr "CPU"
......@@ -934,9 +999,9 @@ msgid "Quick modify"
msgstr "快速修改"
#: assets/templates/assets/asset_detail.html:143
#: assets/templates/assets/asset_list.html:94
#: assets/templates/assets/asset_list.html:95
#: assets/templates/assets/user_asset_list.html:47 perms/models.py:34
#: perms/models.py:81
#: perms/models.py:82
#: perms/templates/perms/asset_permission_create_update.html:47
#: perms/templates/perms/asset_permission_detail.html:120
#: perms/templates/perms/asset_permission_list.html:59
......@@ -959,134 +1024,156 @@ msgid "Refresh"
msgstr "刷新"
#: assets/templates/assets/asset_detail.html:300
#: users/templates/users/user_detail.html:294
#: users/templates/users/user_detail.html:321
#: users/templates/users/user_detail.html:301
#: users/templates/users/user_detail.html:328
msgid "Update successfully!"
msgstr "更新成功"
#: assets/templates/assets/asset_list.html:68 assets/views/asset.py:93
#: assets/templates/assets/asset_list.html:8
msgid ""
"The left side is the asset tree, right click to create, delete, and change "
"the tree node, authorization asset is also organized as a node, and the "
"right side is the asset under that node"
msgstr ""
"左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,"
"右侧是属于该节点下的资产"
#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:93
msgid "Create asset"
msgstr "创建资产"
#: assets/templates/assets/asset_list.html:72
#: assets/templates/assets/asset_list.html:73
#: users/templates/users/user_list.html:7
msgid "Import"
msgstr "导入"
#: assets/templates/assets/asset_list.html:75
#: assets/templates/assets/asset_list.html:76
#: users/templates/users/user_list.html:10
msgid "Export"
msgstr "导出"
#: assets/templates/assets/asset_list.html:93
#: assets/templates/assets/asset_list.html:94
msgid "Hardware"
msgstr "硬件"
#: assets/templates/assets/asset_list.html:105
#: assets/templates/assets/asset_list.html:106
#: users/templates/users/user_list.html:38
msgid "Delete selected"
msgstr "批量删除"
#: assets/templates/assets/asset_list.html:106
#: assets/templates/assets/asset_list.html:107
#: users/templates/users/user_list.html:39
msgid "Update selected"
msgstr "批量更新"
#: assets/templates/assets/asset_list.html:107
#: assets/templates/assets/asset_list.html:108
msgid "Remove from this node"
msgstr "从节点移除"
#: assets/templates/assets/asset_list.html:108
#: assets/templates/assets/asset_list.html:109
#: users/templates/users/user_list.html:40
msgid "Deactive selected"
msgstr "禁用所选"
#: assets/templates/assets/asset_list.html:109
#: assets/templates/assets/asset_list.html:110
#: users/templates/users/user_list.html:41
msgid "Active selected"
msgstr "激活所选"
#: assets/templates/assets/asset_list.html:126
#: assets/templates/assets/asset_list.html:127
msgid "Add node"
msgstr "新建节点"
#: assets/templates/assets/asset_list.html:127
#: assets/templates/assets/asset_list.html:128
msgid "Rename node"
msgstr "重命名节点"
#: assets/templates/assets/asset_list.html:128
#: assets/templates/assets/asset_list.html:129
msgid "Delete node"
msgstr "删除节点"
#: assets/templates/assets/asset_list.html:130
#: assets/templates/assets/asset_list.html:131
msgid "Add assets to node"
msgstr "添加资产到节点"
#: assets/templates/assets/asset_list.html:131
#: assets/templates/assets/asset_list.html:132
msgid "Move assets to node"
msgstr "移动资产到节点"
#: assets/templates/assets/asset_list.html:133
#: assets/templates/assets/asset_list.html:134
msgid "Refresh node hardware info"
msgstr "更新节点资产硬件信息"
#: assets/templates/assets/asset_list.html:134
#: assets/templates/assets/asset_list.html:135
msgid "Test node connective"
msgstr "测试节点资产可连接性"
#: assets/templates/assets/asset_list.html:136
#: assets/templates/assets/asset_list.html:137
msgid "Display only current node assets"
msgstr "仅显示当前节点资产"
#: assets/templates/assets/asset_list.html:137
#: assets/templates/assets/asset_list.html:138
msgid "Displays all child node assets"
msgstr "显示所有子节点资产"
#: assets/templates/assets/asset_list.html:207
#: assets/templates/assets/asset_list.html:208
msgid "Create node failed"
msgstr "创建节点失败"
#: assets/templates/assets/asset_list.html:219
#: assets/templates/assets/asset_list.html:220
msgid "Have child node, cancel"
msgstr "存在子节点,不能删除"
#: assets/templates/assets/asset_list.html:221
#: assets/templates/assets/asset_list.html:222
msgid "Have assets, cancel"
msgstr "存在资产,不能删除"
#: assets/templates/assets/asset_list.html:292
#: assets/templates/assets/asset_list.html:293
msgid "Rename success"
msgstr "重命名成功"
#: assets/templates/assets/asset_list.html:293
#: assets/templates/assets/asset_list.html:294
msgid "Rename failed, do not change the root node name"
msgstr "重命名失败,不可以更改根节点名称"
msgstr "重命名失败,不能更改root节点的名称"
#: assets/templates/assets/asset_list.html:626
#: assets/templates/assets/system_user_list.html:134
#: users/templates/users/user_detail.html:369
#: users/templates/users/user_detail.html:394
#: users/templates/users/user_detail.html:461
#: assets/templates/assets/asset_list.html:627
#: assets/templates/assets/system_user_list.html:137
#: users/templates/users/user_detail.html:376
#: users/templates/users/user_detail.html:402
#: users/templates/users/user_detail.html:470
#: users/templates/users/user_group_list.html:81
#: users/templates/users/user_list.html:195
#: xpack/templates/orgs/org_list.html:81
msgid "Are you sure?"
msgstr "你确认吗?"
#: assets/templates/assets/asset_list.html:627
#: assets/templates/assets/asset_list.html:628
msgid "This will delete the selected assets !!!"
msgstr "删除选择资产"
#: assets/templates/assets/asset_list.html:635
#: assets/templates/assets/asset_list.html:631
#: assets/templates/assets/system_user_detail.html:328
#: assets/templates/assets/system_user_list.html:141
#: users/templates/users/user_detail.html:380
#: users/templates/users/user_detail.html:406
#: users/templates/users/user_detail.html:474
#: users/templates/users/user_group_create_update.html:31
#: users/templates/users/user_group_list.html:85
#: users/templates/users/user_list.html:199
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:32
msgid "Cancel"
msgstr "取消"
#: assets/templates/assets/asset_list.html:637
msgid "Asset Deleted."
msgstr "已被删除"
#: assets/templates/assets/asset_list.html:636
#: assets/templates/assets/asset_list.html:641
#: assets/templates/assets/asset_list.html:638
#: assets/templates/assets/asset_list.html:643
msgid "Asset Delete"
msgstr "删除"
#: assets/templates/assets/asset_list.html:640
#: assets/templates/assets/asset_list.html:642
msgid "Asset Deleting failed."
msgstr "删除失败"
......@@ -1104,13 +1191,6 @@ msgstr "确认删除"
msgid "Are you sure delete"
msgstr "您确定删除吗?"
#: assets/templates/assets/domain_detail.html:21
#: assets/templates/assets/domain_detail.html:64
#: assets/templates/assets/domain_gateway_list.html:21
#: assets/templates/assets/domain_list.html:24
msgid "Gateway"
msgstr "网关"
#: assets/templates/assets/domain_gateway_list.html:31
msgid "Gateway list"
msgstr "网关列表"
......@@ -1127,7 +1207,28 @@ msgstr "创建网关"
msgid "Test connection"
msgstr "测试连接"
#: assets/templates/assets/domain_list.html:14 assets/views/domain.py:46
#: assets/templates/assets/domain_gateway_list.html:123
msgid "Can be connected"
msgstr "可连接"
#: assets/templates/assets/domain_gateway_list.html:124
msgid "The connection fails"
msgstr "连接失败"
#: assets/templates/assets/domain_list.html:9
msgid ""
"The domain function is added to address the fact that some environments "
"(such as the hybrid cloud) cannot be connected directly by jumping on the "
"gateway server."
msgstr ""
"网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过"
"网关服务器进行跳转登录。"
#: assets/templates/assets/domain_list.html:11
msgid "JMS => Domain gateway => Target assets"
msgstr "JMS => 网域网关 => 目标资产"
#: assets/templates/assets/domain_list.html:17 assets/views/domain.py:46
msgid "Create domain"
msgstr "创建网域"
......@@ -1139,26 +1240,32 @@ msgstr "创建标签"
msgid "Assets of "
msgstr "资产"
#: assets/templates/assets/system_user_asset.html:70
#: assets/templates/assets/system_user_asset.html:71
#: assets/templates/assets/system_user_detail.html:148
msgid "Test assets connective"
msgstr "测试资产可连接性"
#: assets/templates/assets/system_user_asset.html:80
#: assets/templates/assets/system_user_detail.html:139
msgid "Push system user now"
msgstr "立刻推送系统"
#: assets/templates/assets/system_user_asset.html:73
#: assets/templates/assets/system_user_asset.html:83
#: assets/templates/assets/system_user_asset.html:159
#: assets/templates/assets/system_user_detail.html:142
msgid "Push"
msgstr "推送"
#: assets/templates/assets/system_user_asset.html:78
#: assets/templates/assets/system_user_detail.html:148
msgid "Test assets connective"
msgstr "测试资产可连接性"
#: assets/templates/assets/system_user_asset.html:103
msgid "Add to node"
msgstr "添加到节点"
#: assets/templates/assets/system_user_asset.html:147
#: assets/templates/assets/system_user_asset.html:223
msgid "Task has been send, Go to ops task list seen result"
msgstr "任务已下发,查看ops任务列表"
#: assets/templates/assets/system_user_asset.html:159
#: assets/templates/assets/system_user_asset.html:235
#: assets/templates/assets/system_user_asset.html:273
msgid "Task has been send, seen left assets status"
msgstr "任务已下发,查看左侧资产状态"
......@@ -1170,37 +1277,67 @@ msgstr "家目录"
msgid "Uid"
msgstr "Uid"
#: assets/templates/assets/system_user_detail.html:186
msgid "Add to node"
msgstr "添加到节点"
#: assets/templates/assets/system_user_detail.html:324
msgid "Are you sure to remove authentication information for the system user ?"
msgstr "你确定清除该系统用户的认证信息吗 ?"
#: assets/templates/assets/system_user_detail.html:353
#: assets/templates/assets/system_user_detail.html:336
msgid "Clear auth"
msgstr "清除认证信息"
#: assets/templates/assets/system_user_detail.html:353
#: assets/templates/assets/system_user_detail.html:336
msgid "success"
msgstr "成功"
#: assets/templates/assets/system_user_list.html:18
#: assets/templates/assets/system_user_list.html:10
msgid ""
"System user is Jumpserver jump login assets used by the users, can be "
"understood as the user login assets, such as web, sa, the dba (` ssh "
"web@some-host `), rather than using a user the username login server jump (` "
"ssh xiaoming@some-host `); "
msgstr ""
"系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 "
"web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器"
"(`ssh xiaoming@some-host`);"
#: assets/templates/assets/system_user_list.html:11
msgid ""
"In simple terms, users log into Jumpserver using their own username, and "
"Jumpserver uses system users to log into assets. "
msgstr ""
"简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资"
"产。"
#: assets/templates/assets/system_user_list.html:12
msgid ""
"When system users are created, if you choose auto push Jumpserver to use "
"ansible push system users into the asset, if the asset (Switch, Windows) "
"does not support ansible, please manually fill in the account password. "
"Automatic push for Windows is not currently supported."
msgstr ""
"系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到"
"资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。目前还不"
"支持Windows的自动推送"
#: assets/templates/assets/system_user_list.html:21
#: assets/views/system_user.py:45
msgid "Create system user"
msgstr "创建系统用户"
#: assets/templates/assets/system_user_list.html:135
#: assets/templates/assets/system_user_list.html:138
msgid "This will delete the selected System Users !!!"
msgstr "删除选择系统用户"
#: assets/templates/assets/system_user_list.html:143
#: assets/templates/assets/system_user_list.html:147
msgid "System Users Deleted."
msgstr "已被删除"
#: assets/templates/assets/system_user_list.html:144
#: assets/templates/assets/system_user_list.html:149
#: assets/templates/assets/system_user_list.html:148
#: assets/templates/assets/system_user_list.html:153
msgid "System Users Delete"
msgstr "删除系统用户"
#: assets/templates/assets/system_user_list.html:148
#: assets/templates/assets/system_user_list.html:152
msgid "System Users Deleting failed."
msgstr "系统用户删除失败"
......@@ -1236,7 +1373,7 @@ msgstr "更新资产"
msgid "already exists"
msgstr "已经存在"
#: assets/views/domain.py:30 templates/_nav.html:24
#: assets/views/domain.py:30 templates/_nav.html:23
msgid "Domain list"
msgstr "网域列表"
......@@ -1284,28 +1421,48 @@ msgstr "资产管理"
msgid "System user asset"
msgstr "系统用户集群资产"
#: audits/models.py:12 audits/templates/audits/ftp_log_list.html:74
#: audits/models.py:17 audits/models.py:40 audits/models.py:51
#: audits/templates/audits/ftp_log_list.html:73
#: audits/templates/audits/operate_log_list.html:70
#: audits/templates/audits/password_change_log_list.html:52
#: terminal/models.py:131 terminal/templates/terminal/session_list.html:74
#: terminal/templates/terminal/terminal_detail.html:47
msgid "Remote addr"
msgstr "远端地址"
#: audits/models.py:15 audits/templates/audits/ftp_log_list.html:75
#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:74
msgid "Operate"
msgstr "操作"
#: audits/models.py:16 audits/templates/audits/ftp_log_list.html:56
#: audits/templates/audits/ftp_log_list.html:76
#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:56
#: audits/templates/audits/ftp_log_list.html:75
msgid "Filename"
msgstr "文件名"
#: audits/models.py:17 audits/templates/audits/ftp_log_list.html:77
#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:66
#: users/templates/users/user_detail.html:443
#: users/templates/users/user_detail.html:452
msgid "Success"
msgstr "成功"
#: audits/templates/audits/ftp_log_list.html:78
#: audits/models.py:31
msgid "Create"
msgstr "创建"
#: audits/models.py:38 audits/templates/audits/operate_log_list.html:49
#: audits/templates/audits/operate_log_list.html:68
msgid "Resource Type"
msgstr "资源类型"
#: audits/models.py:39 audits/templates/audits/operate_log_list.html:69
msgid "Resource"
msgstr "资源"
#: audits/models.py:50 audits/templates/audits/password_change_log_list.html:51
msgid "Change by"
msgstr "修改者"
#: audits/templates/audits/ftp_log_list.html:77
#: ops/templates/ops/adhoc_history.html:52
#: ops/templates/ops/adhoc_history_detail.html:61
#: ops/templates/ops/task_history.html:58 perms/models.py:35
......@@ -1314,14 +1471,102 @@ msgstr "成功"
msgid "Date start"
msgstr "开始日期"
#: audits/views.py:51 templates/_nav.html:68
#: audits/templates/audits/login_log_list.html:28
#: perms/templates/perms/asset_permission_user.html:88
msgid "Select user"
msgstr "选择用户"
#: audits/templates/audits/login_log_list.html:35
#: audits/templates/audits/login_log_list.html:40
#: audits/templates/audits/operate_log_list.html:58
#: audits/templates/audits/password_change_log_list.html:42
#: ops/templates/ops/task_list.html:21 ops/templates/ops/task_list.html:26
#: templates/_base_list.html:43 templates/_header_bar.html:8
#: terminal/templates/terminal/command_list.html:60
#: terminal/templates/terminal/session_list.html:61
msgid "Search"
msgstr "搜索"
#: audits/templates/audits/login_log_list.html:48
#: ops/templates/ops/adhoc_detail.html:49
#: ops/templates/ops/adhoc_history_detail.html:49
#: ops/templates/ops/task_detail.html:55
#: terminal/templates/terminal/session_list.html:70
msgid "ID"
msgstr "ID"
#: audits/templates/audits/login_log_list.html:50
#: common/templates/common/terminal_setting.html:73
#: common/templates/common/terminal_setting.html:91
msgid "Type"
msgstr "类型"
#: audits/templates/audits/login_log_list.html:51
msgid "UA"
msgstr "Agent"
#: audits/templates/audits/login_log_list.html:53
msgid "City"
msgstr "城市"
#: audits/templates/audits/login_log_list.html:54 users/forms.py:169
#: users/models/authentication.py:75 users/models/user.py:73
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
#: audits/templates/audits/login_log_list.html:55
#: users/models/authentication.py:76
msgid "Reason"
msgstr "原因"
#: audits/templates/audits/login_log_list.html:56
#: users/models/authentication.py:77
msgid "Status"
msgstr "状态"
#: audits/templates/audits/login_log_list.html:57
#: ops/templates/ops/task_list.html:40
msgid "Date"
msgstr "日期"
#: audits/templates/audits/operate_log_list.html:71
#: audits/templates/audits/password_change_log_list.html:53
#: ops/templates/ops/task_adhoc.html:63
#: terminal/templates/terminal/command_list.html:76
#: terminal/templates/terminal/session_detail.html:50
msgid "Datetime"
msgstr "日期"
#: audits/views.py:66 audits/views.py:110 audits/views.py:143
#: templates/_nav.html:67
msgid "Audits"
msgstr "日志审计"
#: audits/views.py:52 templates/_nav.html:71
#: audits/views.py:67 templates/_nav.html:71
msgid "FTP log"
msgstr "FTP日志"
#: audits/views.py:111 templates/_nav.html:72
msgid "Operate log"
msgstr "操作日志"
#: audits/views.py:144 templates/_nav.html:73
msgid "Password change log"
msgstr "改密日志"
#: audits/views.py:183 templates/_nav.html:10 users/views/group.py:28
#: users/views/group.py:44 users/views/group.py:60 users/views/group.py:76
#: users/views/group.py:92 users/views/login.py:327 users/views/user.py:67
#: users/views/user.py:82 users/views/user.py:110 users/views/user.py:192
#: users/views/user.py:347 users/views/user.py:397 users/views/user.py:432
msgid "Users"
msgstr "用户管理"
#: audits/views.py:184 templates/_nav.html:70
msgid "Login log"
msgstr "登录日志"
#: common/api.py:18
msgid "Test mail sent to {}, please check"
msgstr "邮件已经发送{}, 请检查"
......@@ -1332,7 +1577,7 @@ msgstr "连接LDAP成功"
#: common/api.py:72
msgid "Search no entry matched in ou {}"
msgstr ""
msgstr "在ou:{}中没有匹配条目"
#: common/api.py:81
msgid "Match {} s users"
......@@ -1426,7 +1671,7 @@ msgstr "用户OU"
#: common/forms.py:118
msgid "Use | split User OUs"
msgstr ""
msgstr "使用|分隔各OU"
#: common/forms.py:121
msgid "User search filter"
......@@ -1461,7 +1706,7 @@ msgstr "资产列表排序"
msgid "Heartbeat interval"
msgstr "心跳间隔"
#: common/forms.py:151 ops/models/adhoc.py:37
#: common/forms.py:151 ops/models/adhoc.py:38
msgid "Units: seconds"
msgstr "单位: 秒"
......@@ -1626,18 +1871,12 @@ msgstr "用户登录设置"
msgid "Password check rule"
msgstr "密码校验规则"
#: common/templates/common/terminal_setting.html:73
#: common/templates/common/terminal_setting.html:91
#: users/templates/users/login_log_list.html:50
msgid "Type"
msgstr "类型"
#: common/validators.py:7
msgid "Special char not allowed"
msgstr "不能包含特殊字符"
#: common/views.py:19 common/views.py:45 common/views.py:71 common/views.py:101
#: common/views.py:129 templates/_nav.html:98
#: common/views.py:129 templates/_nav.html:100
msgid "Settings"
msgstr "系统设置"
......@@ -1646,93 +1885,103 @@ msgstr "系统设置"
msgid "Update setting successfully, please restart program"
msgstr "更新设置成功, 请手动重启程序"
#: jumpserver/views.py:178
msgid ""
"<div>Luna is a separately deployed program, you need to deploy Luna, coco, "
"configure nginx for url distribution,</div> </div>If you see this page, "
"prove that you are not accessing the nginx listening port. Good luck.</div>"
msgstr ""
"<div>Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发, </"
"div><div>如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运</"
"div>"
#: ops/api.py:81
msgid "Waiting ..."
msgstr ""
#: ops/models/adhoc.py:37
#: ops/models/adhoc.py:38
msgid "Interval"
msgstr "间隔"
#: ops/models/adhoc.py:38
#: ops/models/adhoc.py:39
msgid "Crontab"
msgstr "Crontab"
#: ops/models/adhoc.py:38
#: ops/models/adhoc.py:39
msgid "5 * * * *"
msgstr ""
#: ops/models/adhoc.py:40
#: ops/models/adhoc.py:41
msgid "Callback"
msgstr "回调"
#: ops/models/adhoc.py:154 ops/templates/ops/adhoc_detail.html:114
#: ops/models/adhoc.py:155 ops/templates/ops/adhoc_detail.html:114
msgid "Tasks"
msgstr "任务"
#: ops/models/adhoc.py:155 ops/templates/ops/adhoc_detail.html:57
#: ops/models/adhoc.py:156 ops/templates/ops/adhoc_detail.html:57
#: ops/templates/ops/task_adhoc.html:60
msgid "Pattern"
msgstr ""
#: ops/models/adhoc.py:156 ops/templates/ops/adhoc_detail.html:61
#: ops/models/adhoc.py:157 ops/templates/ops/adhoc_detail.html:61
msgid "Options"
msgstr "选项"
#: ops/models/adhoc.py:157 ops/templates/ops/adhoc_detail.html:53
#: ops/models/adhoc.py:158 ops/templates/ops/adhoc_detail.html:53
#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38
msgid "Hosts"
msgstr "主机"
#: ops/models/adhoc.py:158
#: ops/models/adhoc.py:159
msgid "Run as admin"
msgstr "再次执行"
#: ops/models/adhoc.py:159 ops/templates/ops/adhoc_detail.html:72
#: ops/models/adhoc.py:160 ops/templates/ops/adhoc_detail.html:72
#: ops/templates/ops/adhoc_detail.html:77 ops/templates/ops/task_adhoc.html:61
msgid "Run as"
msgstr "用户"
#: ops/models/adhoc.py:160 ops/templates/ops/adhoc_detail.html:82
#: ops/models/adhoc.py:161 ops/templates/ops/adhoc_detail.html:82
#: ops/templates/ops/task_adhoc.html:62
msgid "Become"
msgstr "Become"
#: ops/models/adhoc.py:161 users/templates/users/user_group_detail.html:59
#: ops/models/adhoc.py:162 users/templates/users/user_group_detail.html:59
#: xpack/plugins/orgs/templates/orgs/org_detail.html:56
msgid "Create by"
msgstr "创建者"
#: ops/models/adhoc.py:324
#: ops/models/adhoc.py:326
msgid "Start time"
msgstr "开始时间"
#: ops/models/adhoc.py:325
#: ops/models/adhoc.py:327
msgid "End time"
msgstr "完成时间"
#: ops/models/adhoc.py:326 ops/templates/ops/adhoc_history.html:57
#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_history.html:57
#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:41
msgid "Time"
msgstr "时间"
#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_detail.html:106
#: ops/models/adhoc.py:329 ops/templates/ops/adhoc_detail.html:106
#: ops/templates/ops/adhoc_history.html:55
#: ops/templates/ops/adhoc_history_detail.html:69
#: ops/templates/ops/task_detail.html:83 ops/templates/ops/task_history.html:61
msgid "Is finished"
msgstr "是否完成"
#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_history.html:56
#: ops/models/adhoc.py:330 ops/templates/ops/adhoc_history.html:56
#: ops/templates/ops/task_history.html:62
msgid "Is success"
msgstr "是否成功"
#: ops/models/adhoc.py:329
#: ops/models/adhoc.py:331
msgid "Adhoc raw result"
msgstr "结果"
#: ops/models/adhoc.py:330
#: ops/models/adhoc.py:332
msgid "Adhoc result summary"
msgstr "汇总"
......@@ -1746,14 +1995,6 @@ msgstr "版本详情"
msgid "Version run history"
msgstr "执行历史"
#: ops/templates/ops/adhoc_detail.html:49
#: ops/templates/ops/adhoc_history_detail.html:49
#: ops/templates/ops/task_detail.html:55
#: terminal/templates/terminal/session_list.html:70
#: users/templates/users/login_log_list.html:48
msgid "ID"
msgstr "ID"
#: ops/templates/ops/adhoc_detail.html:94 ops/templates/ops/task_list.html:36
msgid "Run times"
msgstr "执行次数"
......@@ -1861,12 +2102,6 @@ msgstr "输出"
msgid "Versions of "
msgstr "版本"
#: ops/templates/ops/task_adhoc.html:63
#: terminal/templates/terminal/command_list.html:76
#: terminal/templates/terminal/session_detail.html:50
msgid "Datetime"
msgstr "日期"
#: ops/templates/ops/task_detail.html:67
msgid "Total versions"
msgstr "版本数量"
......@@ -1879,24 +2114,10 @@ msgstr "最新版本"
msgid "Contents"
msgstr "内容"
#: ops/templates/ops/task_list.html:21 ops/templates/ops/task_list.html:26
#: templates/_base_list.html:43 templates/_header_bar.html:8
#: terminal/templates/terminal/command_list.html:60
#: terminal/templates/terminal/session_list.html:61
#: users/templates/users/login_log_list.html:35
#: users/templates/users/login_log_list.html:40
msgid "Search"
msgstr "搜索"
#: ops/templates/ops/task_list.html:37
msgid "Versions"
msgstr "版本"
#: ops/templates/ops/task_list.html:40
#: users/templates/users/login_log_list.html:57
msgid "Date"
msgstr "日期"
#: ops/templates/ops/task_list.html:71
msgid "Run"
msgstr "执行"
......@@ -1910,7 +2131,7 @@ msgstr "任务开始: "
msgid "Ops"
msgstr "作业中心"
#: ops/views.py:37 templates/_nav.html:62
#: ops/views.py:37 templates/_nav.html:61
msgid "Task list"
msgstr "任务列表"
......@@ -1918,42 +2139,37 @@ msgstr "任务列表"
msgid "Task run history"
msgstr "执行历史"
#: orgs/mixins.py:79
#: orgs/mixins.py:79 orgs/models.py:24
msgid "Organization"
msgstr "组织"
#: perms/forms.py:20 users/forms.py:265 users/forms.py:270 users/forms.py:316
#: xpack/plugins/orgs/forms.py:30
msgid "Select users"
msgstr "选择用户"
msgstr "组织管理"
#: perms/forms.py:44 perms/models.py:30 perms/models.py:79
#: perms/forms.py:31 perms/models.py:30 perms/models.py:80
#: perms/templates/perms/asset_permission_list.html:55
#: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14
#: users/forms.py:282 users/models/group.py:26 users/models/user.py:57
#: users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:200
#: users/templates/users/user_detail.html:207
#: users/templates/users/user_list.html:26
#: xpack/plugins/orgs/templates/orgs/org_list.html:15
msgid "User group"
msgstr "用户组"
#: perms/forms.py:66
#: perms/forms.py:53
msgid "User or group at least one required"
msgstr ""
msgstr "用户和用户组至少选一个"
#: perms/forms.py:75
#: perms/forms.py:62
msgid "Asset or group at least one required"
msgstr ""
msgstr "资产和节点至少选一个"
#: perms/models.py:36 perms/models.py:82
#: perms/models.py:36 perms/models.py:83
#: perms/templates/perms/asset_permission_detail.html:90
#: users/models/user.py:89 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:112
msgid "Date expired"
msgstr "失效日期"
#: perms/models.py:91 templates/_nav.html:34
#: perms/models.py:45 perms/models.py:92 templates/_nav.html:33
msgid "Asset permission"
msgstr "资产授权"
......@@ -1988,7 +2204,7 @@ msgid "Add node to this permission"
msgstr "添加节点"
#: perms/templates/perms/asset_permission_asset.html:125
#: users/templates/users/user_detail.html:217
#: users/templates/users/user_detail.html:224
msgid "Join"
msgstr "加入"
......@@ -2032,11 +2248,6 @@ msgstr "用户列表"
msgid "Add user to asset permission"
msgstr "添加用户"
#: perms/templates/perms/asset_permission_user.html:88
#: users/templates/users/login_log_list.html:28
msgid "Select user"
msgstr "选择用户"
#: perms/templates/perms/asset_permission_user.html:108
msgid "Add user group to asset permission"
msgstr "添加用户组"
......@@ -2046,7 +2257,7 @@ msgid "Select user groups"
msgstr "选择用户组"
#: perms/views.py:23 perms/views.py:53 perms/views.py:68 perms/views.py:83
#: perms/views.py:118 perms/views.py:150 templates/_nav.html:31
#: perms/views.py:118 perms/views.py:150 templates/_nav.html:30
#: xpack/plugins/orgs/templates/orgs/org_list.html:21
msgid "Perms"
msgstr "权限管理"
......@@ -2071,18 +2282,26 @@ msgstr "资产授权用户列表"
msgid "Asset permission asset list"
msgstr "资产授权资产列表"
#: templates/_header_bar.html:18
msgid "Supports"
msgstr "商业支持"
#: templates/_copyright.html:2 templates/_footer.html:8
msgid " Beijing Duizhan Tech, Inc. "
msgstr " 北京堆栈科技有限公司 "
#: templates/_header_bar.html:23
#: templates/_header_bar.html:31
msgid "Help"
msgstr "帮助"
#: templates/_header_bar.html:38 users/templates/users/_base_otp.html:29
msgid "Docs"
msgstr "文档"
#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:148
#: templates/_header_bar.html:44
msgid "Commercial support"
msgstr "商业支持"
#: templates/_header_bar.html:89 templates/_nav_user.html:9 users/forms.py:148
#: users/templates/users/_user.html:39
#: users/templates/users/first_login.html:39
#: users/templates/users/user_password_update.html:39
#: users/templates/users/user_password_update.html:40
#: users/templates/users/user_profile.html:17
#: users/templates/users/user_profile_update.html:37
#: users/templates/users/user_profile_update.html:57
......@@ -2090,24 +2309,24 @@ msgstr "文档"
msgid "Profile"
msgstr "个人信息"
#: templates/_header_bar.html:40
#: templates/_header_bar.html:92
msgid "Admin page"
msgstr "管理页面"
#: templates/_header_bar.html:42
#: templates/_header_bar.html:94
msgid "User page"
msgstr "用户页面"
#: templates/_header_bar.html:45
#: templates/_header_bar.html:97
msgid "Logout"
msgstr "注销登录"
#: templates/_header_bar.html:49 users/templates/users/login.html:44
#: users/templates/users/login.html:68
#: templates/_header_bar.html:101 users/templates/users/login.html:46
#: users/templates/users/login.html:70
msgid "Login"
msgstr "登录"
#: templates/_header_bar.html:62 templates/_nav.html:4
#: templates/_header_bar.html:114 templates/_nav.html:4
msgid "Dashboard"
msgstr "仪表盘"
......@@ -2137,57 +2356,50 @@ msgstr ""
"\"%(user_pubkey_update)s\"> 链接 </a> 更新\n"
" "
#: templates/_nav.html:10 users/views/group.py:27 users/views/group.py:43
#: users/views/group.py:61 users/views/group.py:78 users/views/group.py:94
#: users/views/login.py:333 users/views/login.py:397 users/views/user.py:67
#: users/views/user.py:82 users/views/user.py:110 users/views/user.py:192
#: users/views/user.py:347 users/views/user.py:397 users/views/user.py:432
msgid "Users"
msgstr "用户管理"
#: templates/_nav.html:13 users/views/user.py:68
msgid "User list"
msgstr "用户列表"
#: templates/_nav.html:15
msgid "Login logs"
msgstr "登录日志"
#: templates/_nav.html:40
#: templates/_nav.html:39
msgid "Sessions"
msgstr "会话管理"
#: templates/_nav.html:43
#: templates/_nav.html:42
msgid "Session online"
msgstr "在线会话"
#: templates/_nav.html:44
#: templates/_nav.html:43
msgid "Session offline"
msgstr "历史会话"
#: templates/_nav.html:45
#: templates/_nav.html:44
msgid "Commands"
msgstr "命令记录"
#: templates/_nav.html:48 templates/_nav_user.html:14
#: templates/_nav.html:47 templates/_nav_user.html:14
msgid "Web terminal"
msgstr "Web终端"
#: templates/_nav.html:52 terminal/views/command.py:50
#: templates/_nav.html:51 terminal/views/command.py:50
#: terminal/views/session.py:75 terminal/views/session.py:93
#: terminal/views/session.py:115 terminal/views/terminal.py:31
#: terminal/views/terminal.py:46 terminal/views/terminal.py:58
msgid "Terminal"
msgstr "终端管理"
#: templates/_nav.html:59
#: templates/_nav.html:58
msgid "Job Center"
msgstr "作业中心"
#: templates/_nav.html:86
#: templates/_nav.html:88
msgid "XPack"
msgstr ""
#: templates/_pagination.html:59
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: templates/captcha/image.html:3
msgid "Play CAPTCHA as audio file"
msgstr "语言播放验证码"
......@@ -2196,6 +2408,164 @@ msgstr "语言播放验证码"
msgid "Captcha"
msgstr "验证码"
#: templates/flash_message_standalone.html:45
msgid "Return"
msgstr "返回"
#: templates/index.html:11
msgid "Total users"
msgstr "用户总数"
#: templates/index.html:23
msgid "Total hosts"
msgstr "主机总数"
#: templates/index.html:36
msgid "Online users"
msgstr "在线用户"
#: templates/index.html:49
msgid "Online sessions"
msgstr "在线会话"
#: templates/index.html:61
msgid " Top 5 Active user"
msgstr "活跃用户Top5"
#: templates/index.html:62
msgid "In the past week, a total of "
msgstr "过去一周, 共有 "
#: templates/index.html:62
msgid " users have logged in "
msgstr " 位用户登录 "
#: templates/index.html:62
msgid " times asset."
msgstr " 次资产."
#: templates/index.html:67
msgid " times/week"
msgstr " 次/周"
#: templates/index.html:78
msgid "Active user asset ratio"
msgstr "活跃用户资产占比"
#: templates/index.html:81
msgid ""
"The following graphs describe the percentage of active users per month and "
"assets per user host per month, respectively."
msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比"
#: templates/index.html:91
msgid "Host"
msgstr "主机"
#: templates/index.html:106 templates/index.html:121
msgid "Top 10 assets in a week"
msgstr "一周Top10资产"
#: templates/index.html:122
msgid "Login frequency and last login record."
msgstr "登录次数及最近一次登录记录."
#: templates/index.html:133 templates/index.html:221
msgid " times"
msgstr " 次"
#: templates/index.html:136
msgid "The last time a user logged in"
msgstr "最近一次登录用户"
#: templates/index.html:138 templates/index.html:226
msgid "At "
msgstr "于"
#: templates/index.html:144 templates/index.html:183 templates/index.html:232
msgid "(No)"
msgstr "(暂无)"
#: templates/index.html:152
msgid "Last 10 login"
msgstr "最近十次登录"
#: templates/index.html:158
msgid "Login record"
msgstr "登录记录"
#: templates/index.html:159
msgid "Last 10 login records."
msgstr "最近十次登录记录."
#: templates/index.html:172 templates/index.html:174
msgid "Before"
msgstr "前"
#: templates/index.html:176
msgid "Login in "
msgstr "登录了"
#: templates/index.html:194 templates/index.html:209
msgid "Top 10 users in a week"
msgstr "一周Top10用户"
#: templates/index.html:210
msgid "User login frequency and last login record."
msgstr "用户登录次数及最近一次登录记录"
#: templates/index.html:224
msgid "The last time logged on to the host"
msgstr "最近一次登录主机"
#: templates/index.html:268
msgid "Monthly data overview"
msgstr "月数据总览"
#: templates/index.html:269
msgid "History summary in one month"
msgstr "一个月内历史汇总"
#: templates/index.html:277 templates/index.html:301
msgid "Login count"
msgstr "登陆次数"
#: templates/index.html:277 templates/index.html:308
msgid "Active users"
msgstr "活跃用户"
#: templates/index.html:277 templates/index.html:315
msgid "Active assets"
msgstr "活跃资产"
#: templates/index.html:342 templates/index.html:392
msgid "Monthly active users"
msgstr "月活跃用户"
#: templates/index.html:342 templates/index.html:393
msgid "Disable user"
msgstr "禁用用户"
#: templates/index.html:342 templates/index.html:394
msgid "Month not logged in user"
msgstr "月未登陆用户"
#: templates/index.html:368 templates/index.html:444
msgid "Access to the source"
msgstr "访问来源"
#: templates/index.html:418 templates/index.html:468
msgid "Month is logged into the host"
msgstr "月被登陆主机"
#: templates/index.html:418 templates/index.html:469
msgid "Disable host"
msgstr "禁用主机"
#: templates/index.html:418 templates/index.html:470
msgid "Month not logged on host"
msgstr "月未登陆主机"
#: templates/rest_framework/base.html:128
msgid "Filters"
msgstr "过滤"
......@@ -2412,10 +2782,26 @@ msgid ""
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
msgstr "你可以使用ssh客户端工具连接终端"
#: users/api.py:235 users/templates/users/login.html:50
#: users/api/auth.py:38 users/templates/users/login.html:52
msgid "Log in frequently and try again later"
msgstr "登录频繁, 稍后重试"
#: users/api/auth.py:77
msgid "Please carry seed value and conduct MFA secondary certification"
msgstr "请携带seed值, 进行MFA二次认证"
#: users/api/auth.py:190
msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证"
#: users/api/auth.py:202
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: users/api/user.py:134
msgid "Could not reset self otp, use profile reset instead"
msgstr ""
#: users/authentication.py:56
msgid "Invalid signature header. No credentials provided."
msgstr ""
......@@ -2491,7 +2877,7 @@ msgstr ""
msgid "Paste user id_rsa.pub here."
msgstr "复制用户公钥到这里"
#: users/forms.py:76 users/templates/users/user_detail.html:208
#: users/forms.py:76 users/templates/users/user_detail.html:215
msgid "Join user groups"
msgstr "添加到用户组"
......@@ -2516,12 +2902,6 @@ msgstr ""
msgid "* Enable MFA authentication to make the account more secure."
msgstr "* 启用MFA认证,使账号更加安全."
#: users/forms.py:169 users/models/authentication.py:75 users/models/user.py:73
#: users/templates/users/first_login.html:45
#: users/templates/users/login_log_list.html:54
msgid "MFA"
msgstr "MFA"
#: users/forms.py:174
msgid ""
"In order to protect you and your company, please keep your account, password "
......@@ -2567,13 +2947,18 @@ msgstr "复制你的公钥到这里"
#: users/forms.py:258 users/models/user.py:81
#: users/templates/users/first_login.html:42
#: users/templates/users/user_password_update.html:45
#: users/templates/users/user_password_update.html:46
#: users/templates/users/user_profile.html:68
#: users/templates/users/user_profile_update.html:43
#: users/templates/users/user_pubkey_update.html:43
msgid "Public key"
msgstr "ssh公钥"
#: users/forms.py:265 users/forms.py:270 users/forms.py:316
#: xpack/plugins/orgs/forms.py:30
msgid "Select users"
msgstr "选择用户"
#: users/models/authentication.py:36
msgid "Private Token"
msgstr "ssh密钥"
......@@ -2614,21 +2999,11 @@ msgstr "登录城市"
msgid "User agent"
msgstr "Agent"
#: users/models/authentication.py:76
#: users/templates/users/login_log_list.html:55
msgid "Reason"
msgstr "原因"
#: users/models/authentication.py:77
#: users/templates/users/login_log_list.html:56
msgid "Status"
msgstr "状态"
#: users/models/authentication.py:78
msgid "Date login"
msgstr "登录日期"
#: users/models/user.py:32 users/models/user.py:356
#: users/models/user.py:32 users/models/user.py:359
msgid "Administrator"
msgstr "管理员"
......@@ -2670,10 +3045,27 @@ msgstr "微信"
msgid "Source"
msgstr "用户来源"
#: users/models/user.py:359
#: users/models/user.py:362
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
#: users/templates/users/_base_otp.html:27
msgid "Home page"
msgstr "首页"
#: users/templates/users/_base_otp.html:44
msgid "Security token validation"
msgstr "安全令牌验证"
#: users/templates/users/_base_otp.html:44 users/templates/users/_user.html:13
#: users/templates/users/user_profile_update.html:51
msgid "Account"
msgstr "账户"
#: users/templates/users/_base_otp.html:44
msgid "Follow these steps to complete the binding operation"
msgstr "请按照以下步骤完成绑定操作"
#: users/templates/users/_select_user_modal.html:5
msgid "Please Select User"
msgstr "选择用户"
......@@ -2682,11 +3074,6 @@ msgstr "选择用户"
msgid "Asset num"
msgstr "资产数量"
#: users/templates/users/_user.html:13
#: users/templates/users/user_profile_update.html:51
msgid "Account"
msgstr "账户"
#: users/templates/users/_user.html:26
msgid "Security and Role"
msgstr "角色安全"
......@@ -2729,11 +3116,11 @@ msgid "Previous"
msgstr "上一步"
#: users/templates/users/first_login.html:105
#: users/templates/users/login_otp.html:66
#: users/templates/users/user_otp_authentication.html:22
#: users/templates/users/user_otp_enable_bind.html:19
#: users/templates/users/user_otp_enable_install_app.html:22
#: users/templates/users/user_password_authentication.html:17
#: users/templates/users/login_otp.html:67
#: users/templates/users/user_otp_authentication.html:26
#: users/templates/users/user_otp_enable_bind.html:23
#: users/templates/users/user_otp_enable_install_app.html:26
#: users/templates/users/user_password_authentication.html:21
msgid "Next"
msgstr "下一步"
......@@ -2749,56 +3136,129 @@ msgstr "向导"
msgid " for more information"
msgstr "获取更多信息"
#: users/templates/users/forgot_password.html:26
#: users/templates/users/login.html:77
#: users/templates/users/forgot_password.html:11
#: users/templates/users/forgot_password.html:27
#: users/templates/users/login.html:79
msgid "Forgot password"
msgstr "忘记密码"
#: users/templates/users/forgot_password.html:33
#: users/templates/users/forgot_password.html:34
msgid "Input your email, that will send a mail to your"
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
#: users/templates/users/login.html:53
msgid "Captcha invalid"
msgstr "验证码错误"
#: users/templates/users/login.html:27 users/templates/users/login_otp.html:27
#: users/templates/users/reset_password.html:25
msgid "Welcome to the Jumpserver open source fortress"
msgstr "欢迎使用Jumpserver开源堡垒机"
#: users/templates/users/login_log_list.html:51
msgid "UA"
msgstr "Agent"
#: users/templates/users/login.html:29 users/templates/users/login_otp.html:29
msgid ""
"The world's first fully open source fortress, using the GNU GPL v2.0 open "
"source protocol, is a professional operation and maintenance audit system in "
"compliance with 4A."
msgstr ""
"全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计"
"系统。"
#: users/templates/users/login_log_list.html:53
msgid "City"
msgstr "城市"
#: users/templates/users/login.html:32 users/templates/users/login_otp.html:32
msgid ""
"Developed using Python/Django, following the Web 2.0 specification and "
"equipped with industry-leading Web Terminal solutions, with beautiful "
"interactive interface and good user experience."
msgstr ""
"使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web "
"Terminal 解决方案,交互界面美观、用户体验好。"
#: users/templates/users/login.html:35 users/templates/users/login_otp.html:35
msgid ""
"Distributed architecture is adopted to support multi-machine room deployment "
"across regions, central node provides API, and each machine room deploys "
"login node, which can be extended horizontally and without concurrent access "
"restrictions."
msgstr ""
"采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,"
"可横向扩展、无并发访问限制。"
#: users/templates/users/login_otp.html:45
#: users/templates/users/login.html:38 users/templates/users/login_otp.html:38
msgid "Changes the world, starting with a little bit."
msgstr "改变世界,从一点点开始。"
#: users/templates/users/login.html:55
msgid "Captcha invalid"
msgstr "验证码错误"
#: users/templates/users/login_otp.html:46
#: users/templates/users/user_detail.html:91
#: users/templates/users/user_profile.html:85
msgid "MFA certification"
msgstr "MFA认证"
#: users/templates/users/login_otp.html:64
#: users/templates/users/user_otp_authentication.html:19
#: users/templates/users/user_otp_enable_bind.html:16
#: users/templates/users/login_otp.html:51
#: users/templates/users/user_otp_authentication.html:11
msgid ""
"The account protection has been opened, please complete the following "
"operations according to the prompts"
msgstr "账号保护已开启,请根据提示完成以下操作"
#: users/templates/users/login_otp.html:55
#: users/templates/users/user_otp_authentication.html:13
msgid "Open Authenticator and enter the 6-bit dynamic code"
msgstr "请打开手机Google Authenticator应用,输入6位动态码"
#: users/templates/users/login_otp.html:65
#: users/templates/users/user_otp_authentication.html:23
#: users/templates/users/user_otp_enable_bind.html:20
msgid "Six figures"
msgstr "6位数字"
#: users/templates/users/login_otp.html:69
#: users/templates/users/login_otp.html:70
msgid "Can't provide security? Please contact the administrator!"
msgstr "如果不能提供MFA验证码,请联系管理员!"
#: users/templates/users/reset_password.html:28
msgid ""
"Jumpserver is an open source desktop system developed using Python and "
"Django that helps Internet businesses with efficient users, assets, "
"permissions, and audit management"
msgstr ""
"Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用"
"户、资产、权限、审计 管理"
#: users/templates/users/reset_password.html:32
msgid ""
"We are from all over the world, we have great admiration and worship for the "
"spirit of open source, we have endless pursuit for perfection, neatness and "
"elegance"
msgstr ""
"我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的"
"追求"
#: users/templates/users/reset_password.html:36
msgid ""
"We focus on automatic operation and maintenance, and strive to build an easy-"
"to-use, stable, safe and automatic board hopping machine, which is our "
"unremitting pursuit and power"
msgstr ""
"专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的"
"追求和动力"
#: users/templates/users/reset_password.html:40
msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry"
msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry"
#: users/templates/users/reset_password.html:46
#: users/templates/users/user_detail.html:360 users/utils.py:80
#: users/templates/users/user_detail.html:367 users/utils.py:80
msgid "Reset password"
msgstr "重置密码"
#: users/templates/users/reset_password.html:59
#: users/templates/users/user_password_update.html:60
#: users/templates/users/user_password_update.html:61
#: users/templates/users/user_update.html:12
msgid "Your password must satisfy"
msgstr "您的密码必须满足:"
#: users/templates/users/reset_password.html:60
#: users/templates/users/user_password_update.html:61
#: users/templates/users/user_password_update.html:62
#: users/templates/users/user_update.html:13
msgid "Password strength"
msgstr "密码强度:"
......@@ -2813,37 +3273,37 @@ msgid "Setting"
msgstr "设置"
#: users/templates/users/reset_password.html:105
#: users/templates/users/user_password_update.html:98
#: users/templates/users/user_password_update.html:99
#: users/templates/users/user_update.html:34
msgid "Very weak"
msgstr "很弱"
#: users/templates/users/reset_password.html:106
#: users/templates/users/user_password_update.html:99
#: users/templates/users/user_password_update.html:100
#: users/templates/users/user_update.html:35
msgid "Weak"
msgstr "弱"
#: users/templates/users/reset_password.html:107
#: users/templates/users/user_password_update.html:100
#: users/templates/users/user_password_update.html:101
#: users/templates/users/user_update.html:36
msgid "Normal"
msgstr "正常"
#: users/templates/users/reset_password.html:108
#: users/templates/users/user_password_update.html:101
#: users/templates/users/user_password_update.html:102
#: users/templates/users/user_update.html:37
msgid "Medium"
msgstr "一般"
#: users/templates/users/reset_password.html:109
#: users/templates/users/user_password_update.html:102
#: users/templates/users/user_password_update.html:103
#: users/templates/users/user_update.html:38
msgid "Strong"
msgstr "强"
#: users/templates/users/reset_password.html:110
#: users/templates/users/user_password_update.html:103
#: users/templates/users/user_password_update.html:104
#: users/templates/users/user_update.html:39
msgid "Very strong"
msgstr "很强"
......@@ -2878,82 +3338,87 @@ msgstr "强制启用"
msgid "Last login"
msgstr "最后登录"
#: users/templates/users/user_detail.html:155
#: users/templates/users/user_detail.html:154
msgid "Force enabled MFA"
msgstr "强制启用MFA"
#: users/templates/users/user_detail.html:170
#: users/templates/users/user_detail.html:169
msgid "Reset MFA"
msgstr "重置MFA"
#: users/templates/users/user_detail.html:177
msgid "Send reset password mail"
msgstr "发送重置密码邮件"
#: users/templates/users/user_detail.html:173
#: users/templates/users/user_detail.html:181
#: users/templates/users/user_detail.html:180
#: users/templates/users/user_detail.html:188
msgid "Send"
msgstr "发送"
#: users/templates/users/user_detail.html:178
#: users/templates/users/user_detail.html:185
msgid "Send reset ssh key mail"
msgstr "发送重置密钥邮件"
#: users/templates/users/user_detail.html:186
#: users/templates/users/user_detail.html:446
#: users/templates/users/user_detail.html:193
#: users/templates/users/user_detail.html:455
msgid "Unblock user"
msgstr "解除登录限制"
#: users/templates/users/user_detail.html:189
#: users/templates/users/user_detail.html:196
msgid "Unblock"
msgstr "解除"
#: users/templates/users/user_detail.html:303
#: users/templates/users/user_detail.html:310
msgid "Goto profile page enable MFA"
msgstr "请去个人信息页面启用自己的MFA"
#: users/templates/users/user_detail.html:359
#: users/templates/users/user_detail.html:366
msgid "An e-mail has been sent to the user`s mailbox."
msgstr "已发送邮件到用户邮箱"
#: users/templates/users/user_detail.html:370
#: users/templates/users/user_detail.html:377
msgid "This will reset the user password and send a reset mail"
msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱"
#: users/templates/users/user_detail.html:384
#: users/templates/users/user_detail.html:392
msgid ""
"The reset-ssh-public-key E-mail has been sent successfully. Please inform "
"the user to update his new ssh public key."
msgstr "重设密钥邮件将会发送到用户邮箱"
#: users/templates/users/user_detail.html:385
#: users/templates/users/user_detail.html:393
msgid "Reset SSH public key"
msgstr "重置SSH密钥"
#: users/templates/users/user_detail.html:395
#: users/templates/users/user_detail.html:403
msgid "This will reset the user public key and send a reset mail"
msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱"
#: users/templates/users/user_detail.html:412
#: users/templates/users/user_profile.html:211
#: users/templates/users/user_detail.html:421
#: users/templates/users/user_profile.html:221
msgid "Successfully updated the SSH public key."
msgstr "更新ssh密钥成功"
#: users/templates/users/user_detail.html:413
#: users/templates/users/user_detail.html:417
#: users/templates/users/user_profile.html:212
#: users/templates/users/user_profile.html:217
#: users/templates/users/user_detail.html:422
#: users/templates/users/user_detail.html:426
#: users/templates/users/user_profile.html:222
#: users/templates/users/user_profile.html:227
msgid "User SSH public key update"
msgstr "ssh密钥"
#: users/templates/users/user_detail.html:462
#: users/templates/users/user_detail.html:471
msgid "After unlocking the user, the user can log in normally."
msgstr "解除用户登录限制后,此用户即可正常登录"
#: users/templates/users/user_group_create_update.html:31
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:32
msgid "Cancel"
msgstr "取消"
#: users/templates/users/user_detail.html:485
#, fuzzy
#| msgid "Reset password success"
msgid "Reset user MFA success"
msgstr "重置密码成功"
#: users/templates/users/user_group_detail.html:22
#: users/templates/users/user_group_granted_asset.html:18
#: users/views/group.py:79
#: users/views/group.py:77
msgid "User group detail"
msgstr "用户组详情"
......@@ -2962,7 +3427,7 @@ msgstr "用户组详情"
msgid "Add user"
msgstr "添加用户"
#: users/templates/users/user_group_list.html:5 users/views/group.py:44
#: users/templates/users/user_group_list.html:5 users/views/group.py:45
#: xpack/templates/orgs/org_list.html:5
msgid "Create user group"
msgstr "创建用户组"
......@@ -2972,18 +3437,18 @@ msgstr "创建用户组"
msgid "This will delete the selected groups !!!"
msgstr "删除选择组"
#: users/templates/users/user_group_list.html:90
#: users/templates/users/user_group_list.html:91
#: xpack/templates/orgs/org_list.html:90
msgid "UserGroups Deleted."
msgstr "用户组删除"
#: users/templates/users/user_group_list.html:91
#: users/templates/users/user_group_list.html:96
#: users/templates/users/user_group_list.html:92
#: users/templates/users/user_group_list.html:97
#: xpack/templates/orgs/org_list.html:91 xpack/templates/orgs/org_list.html:96
msgid "UserGroups Delete"
msgstr "用户组删除"
#: users/templates/users/user_group_list.html:95
#: users/templates/users/user_group_list.html:96
#: xpack/templates/orgs/org_list.html:95
msgid "UserGroup Deleting failed."
msgstr "用户组删除失败"
......@@ -2992,19 +3457,60 @@ msgstr "用户组删除失败"
msgid "This will delete the selected users !!!"
msgstr "删除选中用户 !!!"
#: users/templates/users/user_list.html:204
#: users/templates/users/user_list.html:205
msgid "User Deleted."
msgstr "已被删除"
#: users/templates/users/user_list.html:205
#: users/templates/users/user_list.html:210
#: users/templates/users/user_list.html:206
#: users/templates/users/user_list.html:211
msgid "User Delete"
msgstr "删除"
#: users/templates/users/user_list.html:209
#: users/templates/users/user_list.html:210
msgid "User Deleting failed."
msgstr "用户删除失败"
#: users/templates/users/user_otp_authentication.html:6
#: users/templates/users/user_password_authentication.html:6
msgid "Authenticate"
msgstr "验证身份"
#: users/templates/users/user_otp_authentication.html:32
msgid "Unbind"
msgstr "解绑 MFA"
#: users/templates/users/user_otp_enable_bind.html:6
msgid "Bind"
msgstr "绑定 MFA"
#: users/templates/users/user_otp_enable_bind.html:12
msgid ""
"Use the mobile Google Authenticator application to scan the following qr "
"code for a 6-bit verification code"
msgstr "使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码"
#: users/templates/users/user_otp_enable_install_app.html:6
msgid "Install"
msgstr "安装应用"
#: users/templates/users/user_otp_enable_install_app.html:11
msgid "Download and install the Google Authenticator application on your phone"
msgstr "请在手机端下载并安装 Google Authenticator 应用"
#: users/templates/users/user_otp_enable_install_app.html:14
msgid "Android downloads"
msgstr "Android手机下载"
#: users/templates/users/user_otp_enable_install_app.html:19
msgid "iPhone downloads"
msgstr "iPhone手机下载"
#: users/templates/users/user_otp_enable_install_app.html:23
msgid ""
"After installation, click the next step to enter the binding page (if "
"installed, go to the next step directly)."
msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步"
#: users/templates/users/user_profile.html:95
msgid "Administrator Settings force MFA login"
msgstr "管理员设置强制使用MFA登录"
......@@ -3019,18 +3525,22 @@ msgid "Update password"
msgstr "更改密码"
#: users/templates/users/user_profile.html:156
msgid "Update MFA settings"
msgstr "更改MFA设置"
msgid "Set MFA"
msgstr "设置MFA"
#: users/templates/users/user_profile.html:177
#: users/templates/users/user_profile.html:178
msgid "Update MFA"
msgstr "更改MFA"
#: users/templates/users/user_profile.html:187
msgid "Update SSH public key"
msgstr "更改SSH密钥"
#: users/templates/users/user_profile.html:185
#: users/templates/users/user_profile.html:195
msgid "Reset public key and download"
msgstr "重置并下载SSH密钥"
#: users/templates/users/user_profile.html:215
#: users/templates/users/user_profile.html:225
msgid "Failed to update SSH public key."
msgstr "更新密钥失败"
......@@ -3194,79 +3704,75 @@ msgstr "禁用或失效"
msgid "Password or SSH public key invalid"
msgstr "密码或密钥不合法"
#: users/utils.py:289 users/utils.py:299
#: users/utils.py:286 users/utils.py:296
msgid "Bit"
msgstr " 位"
#: users/views/group.py:28
#: users/views/group.py:29
msgid "User group list"
msgstr "用户组列表"
#: users/views/group.py:62
#: users/views/group.py:61
msgid "Update user group"
msgstr "更新用户组"
#: users/views/group.py:95
#: users/views/group.py:93
msgid "User group granted asset"
msgstr "用户组授权资产"
#: users/views/login.py:77
#: users/views/login.py:69
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: users/views/login.py:181 users/views/user.py:517 users/views/user.py:542
msgid "MFA code invalid"
msgstr "MFA码认证失败"
#: users/views/login.py:175 users/views/user.py:517 users/views/user.py:542
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对"
#: users/views/login.py:210
#: users/views/login.py:204
msgid "Logout success"
msgstr "退出登录成功"
#: users/views/login.py:211
#: users/views/login.py:205
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
#: users/views/login.py:227
#: users/views/login.py:221
msgid "Email address invalid, please input again"
msgstr "邮箱地址错误,重新输入"
#: users/views/login.py:240
#: users/views/login.py:234
msgid "Send reset password message"
msgstr "发送重置密码邮件"
#: users/views/login.py:241
#: users/views/login.py:235
msgid "Send reset password mail success, login your mail box and follow it "
msgstr ""
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
#: users/views/login.py:254
#: users/views/login.py:248
msgid "Reset password success"
msgstr "重置密码成功"
#: users/views/login.py:255
#: users/views/login.py:249
msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面"
#: users/views/login.py:276 users/views/login.py:289
#: users/views/login.py:270 users/views/login.py:283
msgid "Token invalid or expired"
msgstr "Token错误或失效"
#: users/views/login.py:285
#: users/views/login.py:279
msgid "Password not same"
msgstr "密码不一致"
#: users/views/login.py:295 users/views/user.py:126 users/views/user.py:415
#: users/views/login.py:289 users/views/user.py:126 users/views/user.py:415
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
#: users/views/login.py:333
#: users/views/login.py:327
msgid "First login"
msgstr "首次登陆"
#: users/views/login.py:398
msgid "Login log list"
msgstr "登录日志"
#: users/views/user.py:143
msgid "Bulk update user success"
msgstr "批量更新用户成功"
......@@ -3299,19 +3805,19 @@ msgstr "密钥更新"
msgid "Password invalid"
msgstr "用户名或密码无效"
#: users/views/user.py:568
#: users/views/user.py:572
msgid "MFA enable success"
msgstr "MFA 绑定成功"
#: users/views/user.py:569
#: users/views/user.py:573
msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:571
#: users/views/user.py:575
msgid "MFA disable success"
msgstr "MFA 解绑成功"
#: users/views/user.py:572
#: users/views/user.py:576
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
......@@ -3363,6 +3869,18 @@ msgstr "创建组织"
msgid "Update org"
msgstr "更新组织"
#~ msgid "OK"
#~ msgstr "完成"
#~ msgid "Update MFA settings"
#~ msgstr "更改MFA设置"
#~ msgid "MFA code invalid"
#~ msgstr "MFA码认证失败"
#~ msgid "Chinese"
#~ msgstr "中文"
#~ msgid "* required Must set exact system platform, Windows, Linux ..."
#~ msgstr "* required 必须准确设置操作系统平台,如Windows, Linux ..."
......
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-08 14:48+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:158
msgid "Update is successful!"
msgstr "更新成功"
#: static/js/jumpserver.js:160
msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:205 static/js/jumpserver.js:247
#: static/js/jumpserver.js:252
msgid "Error"
msgstr "错误"
#: static/js/jumpserver.js:205
msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被资产使用中,请先解除资产绑定"
#: static/js/jumpserver.js:212 static/js/jumpserver.js:260
msgid "Delete the success"
msgstr "删除成功"
#: static/js/jumpserver.js:219
msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:224 static/js/jumpserver.js:273
msgid "Cancel"
msgstr "取消"
#: static/js/jumpserver.js:227 static/js/jumpserver.js:276
msgid "Confirm"
msgstr "确认"
#: static/js/jumpserver.js:247
msgid ""
"The organization contains undeleted information. Please try again after "
"deleting"
msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:252
msgid ""
"Do not perform this operation under this organization. Try again after "
"switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:267
msgid ""
"Please ensure that the following information in the organization has been "
"deleted"
msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:269
msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"规则"
#: static/js/jumpserver.js:311
msgid "Loading ..."
msgstr "加载中 ..."
#: static/js/jumpserver.js:313
msgid "Search"
msgstr "搜索"
#: static/js/jumpserver.js:317
#, javascript-format
msgid "Selected item %d"
msgstr "选中 %d 项"
#: static/js/jumpserver.js:322
msgid "Per page _MENU_"
msgstr "每页 _MENU_"
#: static/js/jumpserver.js:324
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:328
msgid "No match"
msgstr "没有匹配项"
#: static/js/jumpserver.js:330
msgid "No record"
msgstr "没有记录"
......@@ -207,6 +207,7 @@ class AdHoc(models.Model):
return {}
def run(self, record=True):
set_to_root_org()
if record:
return self._run_and_record()
else:
......
......@@ -6,10 +6,10 @@ from django.views.generic import ListView, DetailView, TemplateView
from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
paginate_by = settings.DISPLAY_PER_PAGE
model = Task
ordering = ('-date_created',)
......@@ -43,7 +43,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
return super().get_context_data(**kwargs)
class TaskDetailView(AdminUserRequiredMixin, DetailView):
class TaskDetailView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_detail.html'
......@@ -56,7 +56,7 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_adhoc.html'
......@@ -69,7 +69,7 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_history.html'
......@@ -82,7 +82,7 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
model = AdHoc
template_name = 'ops/adhoc_detail.html'
......@@ -95,7 +95,7 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
model = AdHoc
template_name = 'ops/adhoc_history.html'
......@@ -108,7 +108,7 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
model = AdHocRunHistory
template_name = 'ops/adhoc_history_detail.html'
......@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
class CeleryTaskLogView(SuperUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask
......@@ -20,6 +20,9 @@ class Organization(models.Model):
ROOT_ID_NAME = 'ROOT'
DEFAULT_ID_NAME = 'DEFAULT'
class Meta:
verbose_name = _("Organization")
def __str__(self):
return self.name
......@@ -63,15 +66,18 @@ class Organization(models.Model):
org = cls.default() if default else None
return org
def get_org_users(self):
def get_org_users(self, include_app=False):
from users.models import User
if self.is_default():
users = User.objects.filter(orgs__isnull=True)
elif not self.is_real():
users = User.objects.all()
elif self.is_root():
users = User.objects.all()
else:
users = self.users.all()
users = users.exclude(role=User.ROLE_APP)
if not include_app:
users = users.exclude(role=User.ROLE_APP)
return users
def get_org_admins(self):
......
# -*- coding: utf-8 -*-
#
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Organization
from .hands import set_current_org, current_org, Node
from perms.models import AssetPermission
from users.models import UserGroup
@receiver(post_save, sender=Organization)
......@@ -21,3 +24,20 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
if instance and not created:
instance.expire_cache()
@receiver(m2m_changed, sender=Organization.users.through)
def on_org_user_changed(sender, instance=None, **kwargs):
if isinstance(instance, Organization):
old_org = current_org
set_current_org(instance)
if kwargs['action'] == 'pre_remove':
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
for user in users:
perms = AssetPermission.objects.filter(users=user)
user_groups = UserGroup.objects.filter(users=user)
for perm in perms:
perm.users.remove(user)
for user_group in user_groups:
user_group.users.remove(user)
set_current_org(old_org)
......@@ -40,5 +40,3 @@ def get_current_org():
current_org = LocalProxy(partial(_find, 'current_org'))
current_user = LocalProxy(partial(_find, 'current_user'))
current_request = LocalProxy(partial(_find, 'current_request'))
......@@ -6,23 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelForm
from orgs.utils import current_org
from .hands import User
from .models import AssetPermission
class AssetPermissionForm(OrgModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.exclude(role=User.ROLE_APP),
label=_("User"),
widget=forms.SelectMultiple(
attrs={
'class': 'select2',
'data-placeholder': _('Select users')
}
),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'initial' not in kwargs:
......
......@@ -42,6 +42,7 @@ class AssetPermission(OrgModelMixin):
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Asset permission")
def __str__(self):
return self.name
......
......@@ -154,8 +154,8 @@ function activeNav() {
function APIUpdateAttr(props) {
// props = {url: .., body: , success: , error: , method: ,}
props = props || {};
var success_message = props.success_message || '更新成功!';
var fail_message = props.fail_message || '更新时发生未知错误.';
var success_message = props.success_message || gettext('Update is successful!');
var fail_message = props.fail_message || gettext('An unknown error occurred while updating..');
var flash_message = props.flash_message || true;
if (props.flash_message === false){
flash_message = false;
......@@ -199,25 +199,25 @@ function objectDelete(obj, name, url, redirectTo) {
};
var fail = function() {
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
swal(gettext('Error'), "[ "+name+" ]" + gettext("Being used by the asset, please unbind the asset first."), "error");
};
APIUpdateAttr({
url: url,
body: JSON.stringify(body),
method: 'DELETE',
success_message: "删除成功",
success_message: gettext("Delete the success"),
success: success,
error: fail
});
}
swal({
title: '你确定删除吗 ?',
title: gettext('Are you sure about deleting it?'),
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: gettext('Confirm'),
closeOnConfirm: true,
}, function () {
doDelete()
......@@ -236,29 +236,29 @@ function orgDelete(obj, name, url, redirectTo){
};
var fail = function(responseText, status) {
if (status === 400){
swal("错误", "[ " + name + " ] 组织中包含未删除信息,请删除后重试", "error");
swal(gettext("Error"), "[ " + name + " ] " + gettext("The organization contains undeleted information. Please try again after deleting"), "error");
}
else if (status === 405){
swal("错误", "请勿在组织 [ "+ name + " ] 下执行此操作,切换到其他组织后重试", "error");
swal(gettext("Error"), " [ "+ name + " ] " + gettext("Do not perform this operation under this organization. Try again after switching to another organization"), "error");
}
};
APIUpdateAttr({
url: url,
body: JSON.stringify(body),
method: 'DELETE',
success_message: "删除成功",
success_message: gettext("Delete the success"),
success: success,
error: fail
});
}
swal({
title: "请确保组织内的以下信息已删除",
text: "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则",
title: gettext("Please ensure that the following information in the organization has been deleted"),
text: gettext("User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission"),
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: gettext('Confirm'),
closeOnConfirm: true
}, function () {
doDelete();
......@@ -292,20 +292,20 @@ var jumpserver = {};
jumpserver.checked = false;
jumpserver.selected = {};
jumpserver.language = {
processing: "加载中",
search: "搜索",
processing: gettext('Loading ...'),
search: gettext('Search'),
select: {
rows: {
_: "选中 %d 项",
_: gettext("Selected item %d"),
0: ""
}
},
lengthMenu: "每页 _MENU_",
info: "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项",
lengthMenu: gettext("Per page _MENU_"),
info: gettext('Displays the results of items _START_ to _END_; A total of _TOTAL_ entries'),
infoFiltered: "",
infoEmpty: "",
zeroRecords: "没有匹配项",
emptyTable: "没有记录",
zeroRecords: gettext("No match"),
emptyTable: gettext('No record'),
paginate: {
first: "«",
previous: "‹",
......
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018
\ No newline at end of file
{% load i18n %}
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2018
\ No newline at end of file
......@@ -6,6 +6,7 @@
<!-- Custom and plugin javascript -->
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
<script src="{% static "js/inspinia.js" %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script>
activeNav();
......
{% load i18n %}
<div class="footer fixed">
<div class="pull-right">
Version <strong>1.4.0-{% include '_build.html' %}</strong> GPLv2.
Version <strong>1.4.1-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div>
<div>
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2018
</div>
</div>
......@@ -13,16 +13,68 @@
{# <li>#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
{# </li>#}
{# <li class="dropdown">#}
{# <a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>#}
{# </a>#}
{# </li>#}
{# <li class="dropdown">#}
{# <a class="count-info" href="http://docs.jumpserver.org/" target="_blank">#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>#}
{# </a>#}
{# </li>#}
<li class="dropdown">
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-handshake-o"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Help' %} <b class="caret"></b></span>
</a>
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
<li>
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
<i class="fa fa-file-text"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
</a>
</li>
<li>
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
<i class="fa fa-suitcase"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Commercial support' %}</span>
</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-globe"></i>
{% ifequal request.COOKIES.django_language 'en' %}
<span class="m-r-sm text-muted welcome-message">English<b class="caret"></b></span>
{% else %}
<span class="m-r-sm text-muted welcome-message">中文<b class="caret"></b></span>
{% endifequal %}
</a>
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
<li>
<a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}">
<i class="fa fa-flag"></i>
<span> 中文</span>
</a>
</li>
<li>
<a id="switch_en" href="{% url 'i18n-switch' lang='en' %}">
<i class="fa fa-flag-checkered"></i>
<span> English</span>
</a>
</li>
</ul>
</li>
<li class="dropdown">
{% if request.user.is_authenticated %}
<a data-toggle="dropdown" class="dropdown-toggle" href="#">
......
......@@ -12,7 +12,6 @@
<ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
<li id="user-group"><a href="{% url 'users:user-group-list' %}">{% trans 'User group' %}</a></li>
<li id="login-log"><a href="{% url 'users:login-log-list' %}">{% trans 'Login logs' %}</a></li>
</ul>
</li>
<li id="assets">
......@@ -68,7 +67,10 @@
<i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="login-log"><a href="{% url 'audits:login-log-list' %}">{% trans 'Login log' %}</a></li>
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
</ul>
</li>
{#<li id="">#}
......
{% load i18n %}
{% load common_tags %}
{% if is_paginated %}
<div class="col-sm-4">
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite">
显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项
{# 显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项#}
</div>
</div>
<div class="col-sm-4">
......@@ -10,7 +11,7 @@
<ul class="pagination" style="margin-top: 0; float: right">
{% if page_obj.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
<a data-page="next" href="?page={{ page_obj.previous_page_number}}"></a>
<a data-page="next" class="page" href="?page={{ page_obj.previous_page_number}}"></a>
</li>
{% endif %}
......@@ -26,7 +27,7 @@
{% if page_obj.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
<a data-page="next" href="?page={{ page_obj.next_page_number }}"></a>
<a data-page="next" class="page" href="?page={{ page_obj.next_page_number }}"></a>
</li>
{% endif %}
</ul>
......@@ -40,7 +41,7 @@
var old_href = $(this).attr('href').replace('?', '');
var searchArray = searchStr.split('&');
if (searchStr == '') {
if (searchStr === '') {
searchStr = '?page=1'
}
......@@ -53,6 +54,13 @@
$(this).attr('href', searchArray.join('&'));
}
})
$('#editable_info').html(
"{% trans 'Displays the results of items _START_ to _END_; A total of _TOTAL_ entries' %}"
.replace('_START_', {{ page_obj.start_index }})
.replace('_END_', {{ page_obj.end_index }})
.replace('_TOTAL_', {{ paginator.count }})
)
});
</script>
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
......@@ -10,6 +11,7 @@
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>
......@@ -40,7 +42,7 @@
{% endif %}
<div class="row">
<div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">返回</a>
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">{% trans 'Return' %}</a>
</div>
</div>
</div>
......
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<div class="wrapper wrapper-content">
......@@ -7,7 +8,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-success pull-right">Users</span>
<h5>用户总数</h5>
<h5>{% trans 'Total users' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1>
......@@ -19,7 +20,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-info pull-right">Hosts</span>
<h5>主机总数</h5>
<h5>{% trans 'Total hosts' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1>
......@@ -32,7 +33,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-primary pull-right">Online</span>
<h5>在线用户</h5>
<h5>{% trans 'Online users' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_users"></span>{{ online_user_count }}</a></h1>
......@@ -45,7 +46,8 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-danger pull-right">Connected</span>
<h5>在线会话</h5>
<h5>{% trans 'Online sessions' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_hosts"></span>{{ online_asset_count }}</a></h1>
......@@ -56,13 +58,13 @@
</div>
<div class="row">
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
<h2>活跃用户TOP5</h2>
<small>过去一周共有<span class="text-info">{{ user_visit_count_weekly }}</span>位用户登录<span class="text-success">{{ asset_visit_count_weekly }}</span>次资产.</small>
<h2>{% trans ' Top 5 Active user' %}</h2>
<small>{% trans 'In the past week, a total of ' %}<span class="text-info">{{ user_visit_count_weekly }}</span>{% trans ' users have logged in ' %}<span class="text-success">{{ asset_visit_count_weekly }}</span>{% trans ' times asset.' %}</small>
<ul class="list-group clear-list m-t">
{% for data in user_visit_count_top_five %}
<li class="list-group-item fist-item">
<span class="pull-right">
{{ data.total }}次/周
{{ data.total }}{% trans ' times/week' %}
</span>
<span class="label ">{{ forloop.counter }}</span> {{ data.user }}
</li>
......@@ -73,21 +75,20 @@
<div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px">
<div class="statistic-box">
<h4>
活跃用户资产占比
{% trans 'Active user asset ratio' %}
</h4>
<p>
以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比
{% trans 'The following graphs describe the percentage of active users per month and assets per user host per month, respectively.' %}
</p>
<div class="row text-center">
<div class="col-sm-6">
<div id="activeUser" style="width: 140px; height: 140px;">
</div>
<h5>用户</h5>
<h5>{% trans 'User' %}</h5>
</div>
<div class="col-sm-6">
<div id="activeAsset" style="width: 140px; height: 140px;"></div>
<h5>主机</h5>
<h5>{% trans 'Host' %}</h5>
</div>
</div>
<div class="m-t">
......@@ -102,7 +103,7 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>一周Top10资产</h5>
<h5>{% trans 'Top 10 assets in a week' %}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
......@@ -117,8 +118,8 @@
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-inbox"></i> 一周Top10资产 </h3>
<small><i class="fa fa-map-marker"></i> 登录次数及最近一次登录记录. </small>
<h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small>
</div>
<div class="ibox-content inspinia-timeline">
{% if week_asset_hot_ten %}
......@@ -129,18 +130,18 @@
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.asset }}">{{ data.asset }}</strong>
<br/>
<small class="text-navy">{{ data.total }}</small>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">最近一次登录用户</p>
<p class="m-b-xs">{% trans 'The last time a user logged in' %}</p>
<p>{{ data.last.user }}</p>
<p>{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
......@@ -148,14 +149,14 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>最近十次登录</h5>
<h5>{% trans 'Last 10 login' %}</h5>
<div class="ibox-tools">
<span class="label label-info-light">10 Messages</span>
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-paper-plane-o"></i> 登录记录 </h3>
<small><i class="fa fa-map-marker"></i> 最近十次登录记录. </small>
<h3><i class="fa fa-paper-plane-o"></i> {% trans 'Login record' %}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'Last 10 login records.' %}</small>
</div>
<div class="ibox-content">
<div>
......@@ -168,18 +169,18 @@
</a>
<div class="media-body ">
{% ifequal login.is_finished 0 %}
<small class="pull-right text-navy">{{ login.date_start|timesince }} </small>
<small class="pull-right text-navy">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% else %}
<small class="pull-right">{{ login.date_start|timesince }} </small>
<small class="pull-right">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% endifequal %}
<strong>{{ login.user }}</strong> 登录了{{ login.asset }} <br>
<strong>{{ login.user }}</strong> {% trans 'Login in ' %}{{ login.asset }} <br>
<small class="text-muted">{{ login.date_start }}</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
......@@ -190,7 +191,7 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>一周Top10用户</h5>
<h5>{% trans 'Top 10 users in a week' %}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
......@@ -205,8 +206,8 @@
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-user"></i> 一周Top10用户 </h3>
<small><i class="fa fa-map-marker"></i> 用户登录次数及最近一次登录记录. </small>
<h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small>
</div>
<div class="ibox-content inspinia-timeline">
{% if week_user_hot_ten %}
......@@ -217,18 +218,18 @@
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.user }}">{{ data.user }}</strong>
<br/>
<small class="text-navy">{{ data.total }}</small>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">最近一次登录主机</p>
<p class="m-b-xs">{% trans 'The last time logged on to the host' %}</p>
<p>{{ data.last.asset }}</p>
<p>{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
......@@ -264,8 +265,8 @@ $(document).ready(function(){
var top10Chart = ec.init(document.getElementById('top10'));
var option = {
title : {
text: '月数据总览',
subtext: '一个月内历史汇总',
text: "{% trans 'Monthly data overview' %}",
subtext: "{% trans 'History summary in one month' %}",
x: 'center'
},
tooltip : {
......@@ -273,7 +274,7 @@ $(document).ready(function(){
},
backgroundColor: '#fff',
legend: {
data:['登陆次数', '活跃用户','活跃资产'],
data:["{% trans 'Login count' %}", "{% trans 'Active users' %}", "{% trans 'Active assets' %}"],
y: 'bottom'
},
toolbox: {
......@@ -297,21 +298,21 @@ $(document).ready(function(){
],
series : [
{
name:'登陆次数',
name:"{% trans 'Login count' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_total_visit_count|safe}}
},
{
name:'活跃用户',
name:"{% trans 'Active users' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_user|safe }}
},
{
name:'活跃资产',
name:"{% trans 'Active assets' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
......@@ -338,7 +339,7 @@ $(document).ready(function(){
show: false,
orient : 'vertical',
x : 'left',
data:['月活跃用户','禁用用户','月未登陆用户']
data:["{% trans 'Monthly active users' %}", "{% trans 'Disable user' %}", "{% trans 'Month not logged in user' %}"]
},
toolbox: {
show : false,
......@@ -364,7 +365,7 @@ $(document).ready(function(){
calculable : true,
series : [
{
name:'访问来源',
name:"{% trans 'Access to the source' %}",
type:'pie',
radius : ['50%', '70%'],
itemStyle : {
......@@ -388,9 +389,9 @@ $(document).ready(function(){
}
},
data:[
{value:{{ month_user_active }}, name:'月活跃用户'},
{value:{{ month_user_disabled }}, name:'禁用用户'},
{value:{{ month_user_inactive }}, name:'月未登陆用户'}
{value:{{ month_user_active }}, name:"{% trans 'Monthly active users' %}"},
{value:{{ month_user_disabled }}, name:"{% trans 'Disable user' %}"},
{value:{{ month_user_inactive }}, name:"{% trans 'Month not logged in user' %}"}
]
}
]
......@@ -414,7 +415,7 @@ $(document).ready(function(){
show: false,
orient : 'vertical',
x : 'left',
data:['月被登陆主机','禁用主机','月未登陆主机']
data:["{% trans 'Month is logged into the host' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"]
},
toolbox: {
show : false,
......@@ -440,7 +441,7 @@ $(document).ready(function(){
calculable : true,
series : [
{
name:'访问来源',
name:"{% trans 'Access to the source' %}",
type:'pie',
radius : ['50%', '70%'],
itemStyle : {
......@@ -464,9 +465,9 @@ $(document).ready(function(){
}
},
data:[
{value:{{ month_asset_active }}, name:'月被登陆主机'},
{value:{{ month_asset_disabled }}, name:'禁用主机'},
{value:{{ month_asset_inactive }}, name:'月未登陆主机'}
{value:{{ month_asset_active }}, name:"{% trans 'Month is logged into the host' %}"},
{value:{{ month_asset_disabled }}, name:"{% trans 'Disable host' %}"},
{value:{{ month_asset_inactive }}, name:"{% trans 'Month not logged on host' %}"}
]
}
]
......
......@@ -9,6 +9,7 @@ from django.conf import settings
from users.models import User
from orgs.mixins import OrgModelMixin
from common.models import common_settings
from .backends.command.models import AbstractSessionCommand
......@@ -62,6 +63,10 @@ class Terminal(models.Model):
configs[k] = getattr(settings, k)
configs.update(self.get_common_storage())
configs.update(self.get_replay_storage())
configs.update({
'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME or
settings.DEFAULT_SECURITY_MAX_IDLE_TIME,
})
return configs
def create_app_user(self):
......
......@@ -10,7 +10,7 @@ from django.urls import reverse_lazy, reverse
from common.mixins import JSONResponseMixin
from ..models import Terminal
from ..forms import TerminalForm
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
__all__ = [
......@@ -20,7 +20,7 @@ __all__ = [
]
class TerminalListView(AdminUserRequiredMixin, ListView):
class TerminalListView(SuperUserRequiredMixin, ListView):
model = Terminal
template_name = 'terminal/terminal_list.html'
form_class = TerminalForm
......@@ -35,7 +35,7 @@ class TerminalListView(AdminUserRequiredMixin, ListView):
return context
class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
class TerminalUpdateView(SuperUserRequiredMixin, UpdateView):
model = Terminal
form_class = TerminalForm
template_name = 'terminal/terminal_update.html'
......@@ -47,7 +47,7 @@ class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
return context
class TerminalDetailView(LoginRequiredMixin, DetailView):
class TerminalDetailView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
model = Terminal
template_name = 'terminal/terminal_detail.html'
context_object_name = 'terminal'
......@@ -61,13 +61,13 @@ class TerminalDetailView(LoginRequiredMixin, DetailView):
return context
class TerminalDeleteView(AdminUserRequiredMixin, DeleteView):
class TerminalDeleteView(SuperUserRequiredMixin, DeleteView):
model = Terminal
template_name = 'delete_confirm.html'
success_url = reverse_lazy('terminal:terminal-list')
class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
class TerminalAcceptView(SuperUserRequiredMixin, JSONResponseMixin, UpdateView):
model = Terminal
form_class = TerminalForm
template_name = 'terminal/terminal_modal_accept.html'
......@@ -92,7 +92,7 @@ class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
return self.render_json_response(data)
class TerminalConnectView(LoginRequiredMixin, DetailView):
class TerminalConnectView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
template_name = 'flash_message_standalone.html'
model = Terminal
......@@ -118,6 +118,6 @@ class TerminalConnectView(LoginRequiredMixin, DetailView):
return super(TerminalConnectView, self).get_context_data(**kwargs)
class WebTerminalView(LoginRequiredMixin, View):
class WebTerminalView(LoginRequiredMixin, SuperUserRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect('/luna/?' + request.GET.urlencode())
# -*- coding: utf-8 -*-
#
from .user import *
from .auth import *
from .group import *
# ~*~ coding: utf-8 ~*~
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
......@@ -6,233 +7,36 @@ from django.urls import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import generics
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_bulk import BulkModelViewSet
from .serializers import UserSerializer, UserGroupSerializer, \
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from .tasks import write_login_log_async
from .models import User, UserGroup, LoginLog
from .utils import check_user_valid, generate_token, get_login_ip, \
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
from .hands import Asset, SystemUser
from orgs.utils import current_org
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
from .hands import Asset, SystemUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from common.utils import get_logger, get_request_ip
from ..serializers import UserSerializer
from ..tasks import write_login_log_async
from ..models import User, LoginLog
from ..utils import check_user_valid, generate_token, \
check_otp_code, increase_login_failed_count, is_block_login, clean_failed_count
from common.permissions import IsOrgAdminOrAppUser
from ..hands import Asset, SystemUser
logger = get_logger(__name__)
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = User.objects.exclude(role="App")
serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id')
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users().values_list('id', flat=True)
queryset = queryset.filter(id__in=org_users)
return queryset
def get_permissions(self):
if self.action == "retrieve":
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
class ChangeUserPasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
queryset = User.objects.all()
serializer_class = ChangeUserPasswordSerializer
def perform_update(self, serializer):
user = self.get_object()
user.password_raw = serializer.validated_data["password"]
user.save()
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserUpdateGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserResetPasswordApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
# Note: we are not updating the user object here.
# We just do the reset-password stuff.
from .utils import send_reset_password_mail
user = self.get_object()
user.password_raw = str(uuid.uuid4())
user.save()
send_reset_password_mail(user)
class UserResetPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
from .utils import send_reset_ssh_key_mail
user = self.get_object()
user.is_public_key_valid = False
user.save()
send_reset_ssh_key_mail(user)
class UserUpdatePKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserPKUpdateSerializer
permission_classes = (IsCurrentUserOrReadOnly,)
def perform_update(self, serializer):
user = self.get_object()
user.public_key = serializer.validated_data['_public_key']
user.save()
class UserUnblockPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
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,)
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer
permission_classes = (IsOrgAdmin,)
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = generate_token(request, user)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserProfile(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = UserSerializer
def get_object(self):
return self.request.user
class UserOtpAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
return Response({'msg': 'MFA认证失败'}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
token = generate_token(request, user)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_login_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
logger = get_logger(__name__)
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(username, ip)
key_block = self.key_prefix_block.format(username)
if is_block_login(key_limit):
ip = ip or get_request_ip(request)
if is_block_login(username, ip):
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
return Response({'msg': msg}, status=401)
user, msg = self.check_user_valid(request)
......@@ -244,8 +48,7 @@ class UserAuthApi(APIView):
'status': False
}
self.write_login_log(request, data)
set_user_login_failed_count_to_cache(key_limit, key_block)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
......@@ -256,6 +59,8 @@ class UserAuthApi(APIView):
'status': True
}
self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = generate_token(request, user)
return Response(
{
......@@ -269,7 +74,8 @@ class UserAuthApi(APIView):
return Response(
{
'code': 101,
'msg': '请携带seed值,进行MFA二次认证',
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-users:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
......@@ -294,7 +100,7 @@ class UserAuthApi(APIView):
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_login_ip(request)
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
......@@ -345,3 +151,84 @@ class UserConnectionTokenApi(APIView):
if self.request.query_params.get('user-only', None):
self.permission_classes = (AllowAny,)
return super().get_permissions()
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = generate_token(request, user)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserOtpAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
return Response({'msg': _('MFA certification failed')}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
token = generate_token(request, user)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
# -*- coding: utf-8 -*-
#
from rest_framework import generics
from rest_framework_bulk import BulkModelViewSet
from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer
from ..models import UserGroup
from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin
__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer
permission_classes = (IsOrgAdmin,)
# ~*~ coding: utf-8 ~*~
import uuid
from django.core.cache import cache
from django.contrib.auth import logout
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_bulk import BulkModelViewSet
from ..serializers import UserSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from ..models import User
from orgs.utils import current_org
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
logger = get_logger(__name__)
__all__ = [
'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi',
'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi',
'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi',
]
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = User.objects.exclude(role="App")
serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id')
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users()
queryset = queryset.filter(id__in=org_users)
return queryset
def get_permissions(self):
if self.action == "retrieve":
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
queryset = User.objects.all()
serializer_class = ChangeUserPasswordSerializer
def perform_update(self, serializer):
user = self.get_object()
user.password_raw = serializer.validated_data["password"]
user.save()
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserUpdateGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserResetPasswordApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
# Note: we are not updating the user object here.
# We just do the reset-password stuff.
from ..utils import send_reset_password_mail
user = self.get_object()
user.password_raw = str(uuid.uuid4())
user.save()
send_reset_password_mail(user)
class UserResetPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
from ..utils import send_reset_ssh_key_mail
user = self.get_object()
user.is_public_key_valid = False
user.save()
send_reset_ssh_key_mail(user)
class UserUpdatePKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserPKUpdateSerializer
permission_classes = (IsCurrentUserOrReadOnly,)
def perform_update(self, serializer):
user = self.get_object()
user.public_key = serializer.validated_data['_public_key']
user.save()
class UserUnblockPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
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 UserProfileApi(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = UserSerializer
def get_object(self):
return self.request.user
class UserResetOTPApi(generics.RetrieveAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
user = self.get_object() if kwargs.get('pk') else request.user
if user == request.user:
msg = _("Could not reset self otp, use profile reset instead")
return Response({"msg": msg}, status=401)
if user.otp_enabled and user.otp_secret_key:
user.otp_secret_key = ''
user.save()
logout(request)
return Response({"msg": "success"})
......@@ -308,7 +308,7 @@ def user_limit_to():
class UserGroupForm(OrgModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.exclude(role=User.ROLE_APP),
queryset=User.objects.all(),
label=_("User"),
widget=forms.SelectMultiple(
attrs={
......@@ -349,12 +349,5 @@ class UserGroupForm(OrgModelForm):
}
class OrgUserField(forms.ModelMultipleChoiceField):
def get_limit_choices_to(self):
return {"orgs"}
class FileForm(forms.Form):
file = forms.FileField()
......@@ -14,7 +14,7 @@ from django.utils import timezone
from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default
from common.models import Setting
from common.models import common_settings
from orgs.mixins import OrgManager
from orgs.utils import current_org
......@@ -112,6 +112,10 @@ class User(AbstractUser):
def password_raw(self, password_raw_):
self.set_password(password_raw_)
def set_password(self, raw_password):
self._set_password = True
return super().set_password(raw_password)
@property
def otp_secret_key(self):
return signer.unsign(self._otp_secret_key)
......@@ -278,8 +282,7 @@ class User(AbstractUser):
@property
def otp_force_enabled(self):
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
if mfa_setting and mfa_setting.cleaned_value:
if common_settings.SECURITY_MFA_AUTH:
return True
return self.otp_level == 2
......
......@@ -11,6 +11,7 @@
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
</head>
<body>
......@@ -23,9 +24,9 @@
<a href="{% url 'index' %}">Jumpserver</a>
</div>
<div>
<a href="{% url 'index' %}">首页</a>
<a href="{% url 'index' %}">{% trans 'Home page' %}</a>
<b></b>
<a href="http://docs.jumpserver.org/zh/docs/">文档</a>
<a href="http://docs.jumpserver.org/zh/docs/">{% trans 'Docs' %}</a>
<b></b>
<a href="https://www.github.com/jumpserver/">GitHub</a>
</div>
......@@ -33,39 +34,14 @@
<!--内容-->
<article>
<div class="clearfix">
<ul class="change-color">
<li>
<div>
<i class="iconfont icon-step active"></i>
<span></span>
</div>
<div class="back">验证身份</div>
</li>
<li>
<div>
<i class="iconfont icon-step2"></i>
<span></span>
</div>
<div class="back">安装应用</div>
</li>
<li>
<div>
<i class="iconfont icon-step1"></i>
<span></span>
</div>
<div class="back">绑定MFA</div>
</li>
<li>
<div>
<i class="iconfont icon-duigou"></i>
</div>
<div>完成</div>
</li>
</ul>
</div>
<div class="" style="text-align: center; margin-bottom: 50px">
<h2>
{% block small_title %}
{% endblock %}
</h2>
</div>
<div >
<div class="verify">安全令牌验证&nbsp;&nbsp;账户&nbsp;<span>{{ user.username }}</span>&nbsp;&nbsp;请按照以下步骤完成绑定操作</div>
<div class="verify">{% trans 'Security token validation' %}&nbsp;&nbsp;{% trans 'Account' %}&nbsp;<span>{{ user.username }}</span>&nbsp;&nbsp;{% trans 'Follow these steps to complete the binding operation' %}</div>
<div class="line"></div>
{% block content %}
......
......@@ -8,10 +8,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<title>忘记密码</title>
<title>{% trans 'Forgot password' %}</title>
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>
......
......@@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>
.captcha {
......@@ -22,18 +23,19 @@
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
</p>
<p>
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
</p>
<p>
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
</p>
<p>
改变世界,从一点点开始。
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
......
......@@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<style>
......@@ -23,18 +24,18 @@
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
</p>
<p>
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
</p>
<p>
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
</p>
<p>
改变世界,从一点点开始。
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
......@@ -47,11 +48,11 @@
<div class="m-t">
<div class="form-group">
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
<div class="text-center">
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
</div>
<p style="margin: 30px auto">&nbsp;请打开手机Google Authenticator应用,输入6位动态码</p>
<p style="margin: 30px auto">&nbsp;{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
</div>
<form class="m-t" role="form" method="post" action="">
......
......@@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
......@@ -21,23 +22,22 @@
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源跳板机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理
{% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %}
</p>
<p>
我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的追求
{% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %}
</p>
<p>
专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的追求和动力
{% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %}
</p>
<p>
<small>永远年轻,永远热泪盈眶 stay foolish stay hungry</small>
<small>{% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %}</small>
</p>
</div>
......
......@@ -5,9 +5,9 @@
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
<div class="tagBtnList">
<a class="label label-primary" id="change_all" value="1">全选</a>
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
{% for field in form %}
{% if field.name != 'users' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
......
......@@ -149,7 +149,6 @@
</div>
</div>
</span></td>
</tr>
<tr>
<td>{% trans 'Force enabled MFA' %}:</td>
......@@ -166,6 +165,14 @@
</div>
</span></td>
</tr>
<tr>
<td>{% trans 'Reset MFA' %}:</td>
<td>
<span class="pull-right">
<button type="button" class="btn btn-primary btn-xs" id="btn-reset-mfa" style="width: 54px">{% trans 'Reset' %}</button>
</span>
</td>
</tr>
<tr>
<td>{% trans 'Send reset password mail' %}:</td>
<td>
......@@ -370,6 +377,7 @@ $(document).ready(function() {
text: "{% trans "This will reset the user password and send a reset mail"%}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......@@ -395,6 +403,7 @@ $(document).ready(function() {
text: "{% trans 'This will reset the user public key and send a reset mail' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......@@ -462,12 +471,19 @@ $(document).ready(function() {
text: "{% trans "After unlocking the user, the user can log in normally."%}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
}, function() {
doReset();
});
}).on('click', '#btn-reset-mfa', function () {
APIUpdateAttr({
url: "{% url 'api-users:user-reset-otp' pk=user_object.id %}",
method: "GET",
success_message: "{% trans 'Reset user MFA success' %}"
})
})
</script>
{% endblock %}
......@@ -82,6 +82,7 @@ $(document).ready(function() {
text: "{% trans 'This will delete the selected groups !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......
......@@ -196,6 +196,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
......
......@@ -2,11 +2,15 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Authenticate' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
<p style="margin: 20px auto;">请在手机中打开Google Authenticator应用,输入6位动态码</p>
<p style="margin: 20px auto;">{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
</div>
<form class="" role="form" method="post" action="">
......@@ -22,14 +26,11 @@
<button type="submit" class="next">{% trans 'Next' %}</button>
</form>
<script>
$(function(){
$('.change-color li').eq(2).remove();
$('.change-color li:eq(1) div').eq(1).html('解绑MFA')
})
$('.change-color li:eq(1) div').eq(1).html("{% trans 'Unbind' %}")
})
</script>
{% endblock %}
......
......@@ -2,10 +2,14 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Bind' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码</strong></p>
<p style="margin:20px auto;"><strong style="color: #000000">{% trans 'Use the mobile Google Authenticator application to scan the following qr code for a 6-bit verification code' %}</strong></p>
<div id="qr_code"></div>
......
......@@ -2,21 +2,25 @@
{% load i18n %}
{% load static %}
{% block small_title %}
{% trans 'Install' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">请在手机端下载并安装 Google Authenticator 应用</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'Download and install the Google Authenticator application on your phone' %}</strong></p>
<div>
<img src="{% static 'img/authenticator_android.png' %}" width="128" height="128" alt="">
<p>Android手机下载</p>
<p>{% trans 'Android downloads' %}</p>
</div>
<div>
<img src="{% static 'img/authenticator_iphone.png' %}" width="128" height="128" alt="">
<p>iPhone手机下载</p>
<p>{% trans 'iPhone downloads' %}</p>
</div>
<p style="margin: 20px auto;"></p>
<p style="margin: 20px auto;"><strong style="color: #000000">安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'After installation, click the next step to enter the binding page (if installed, go to the next step directly).' %}</strong></p>
</div>
<a href="{% url 'users:user-otp-enable-bind' %}" class="next">{% trans 'Next' %}</a>
......
......@@ -2,6 +2,10 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Authenticate' %}
{% endblock %}
{% block content %}
<form class="" role="form" method="post" action="">
{% csrf_token %}
......
......@@ -8,6 +8,7 @@
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>
......
......@@ -153,7 +153,7 @@
</td>
</tr>
<tr class="no-borders-tr">
<td>{% trans 'Update MFA settings' %}:</td>
<td>{% trans 'Set MFA' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
......@@ -173,6 +173,16 @@
</span>
</td>
</tr>
{% if request.user.otp_enabled and request.user.otp_secret_key %}
<tr>
<td>{% trans 'Update MFA' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-otp-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
{% endif %}
<tr>
<td>{% trans 'Update SSH public key' %}:</td>
<td>
......
......@@ -18,10 +18,12 @@ urlpatterns = [
# path(r'', api.UserListView.as_view()),
path('token/', api.UserToken.as_view(), name='user-token'),
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('profile/', api.UserProfile.as_view(), name='user-profile'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('users/<uuid:pk>/password/', api.ChangeUserPasswordApi.as_view(), name='change-user-password'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
path('users/<uuid:pk>/pubkey/update/', api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
......
......@@ -26,6 +26,7 @@ urlpatterns = [
path('profile/otp/enable/install-app/', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'),
path('profile/otp/enable/bind/', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
path('profile/otp/disable/authentication/', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'),
path('profile/otp/update/', views.UserOtpUpdateView.as_view(), name='user-otp-update'),
path('profile/otp/settings-success/', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
# User view
......@@ -48,5 +49,6 @@ urlpatterns = [
path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
# Login log
# Abandon
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
]
......@@ -19,7 +19,7 @@ from django.core.cache import cache
from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none
from common.models import Setting
from common.models import common_settings, Setting
from common.forms import SecuritySettingForm
from .models import User, LoginLog
......@@ -190,16 +190,6 @@ def validate_ip(ip):
return False
def get_login_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get('REMOTE_ADDR', '')
return login_ip
def write_login_log(*args, **kwargs):
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
......@@ -225,7 +215,12 @@ def get_ip_city(ip, timeout=10):
try:
data = r.json()
if not isinstance(data, int) and data['code'] == 0:
city = data['data']['country'] + ' ' + data['data']['city']
country = data['data']['country']
_city = data['data']['city']
if country == 'XX':
city = _city
else:
city = ' '.join([country, _city])
except ValueError:
pass
return city
......@@ -272,6 +267,8 @@ def generate_otp_uri(request, issuer="Jumpserver"):
def check_otp_code(otp_secret_key, otp_code):
if not otp_secret_key or not otp_code:
return False
totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code)
......@@ -310,8 +307,8 @@ def check_password_rules(password):
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE'
number_field_name = 'SECURITY_PASSWORD_NUMBER'
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR'
min_length_setting = Setting.objects.filter(name=min_field_name).first()
min_length = min_length_setting.value if min_length_setting else settings.DEFAULT_PASSWORD_MIN_LENGTH
min_length = getattr(common_settings, min_field_name) or \
settings.DEFAULT_PASSWORD_MIN_LENGTH
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
......@@ -333,37 +330,40 @@ def check_password_rules(password):
return bool(match_obj)
def set_user_login_failed_count_to_cache(key_limit, key_block):
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
# def increase_login_failed_count(key_limit, key_block):
def increase_login_failed_count(username, ip):
key_limit = key_prefix_limit.format(username, ip)
count = cache.get(key_limit)
count = count + 1 if count else 1
setting_limit_time = Setting.objects.filter(
name='SECURITY_LOGIN_LIMIT_TIME'
).first()
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
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
cache.set(key_limit, count, int(limit_time)*60)
if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60)
cache.set(key_limit, count, int(limit_time)*60)
def clean_failed_count(username, ip):
key_limit = key_prefix_limit.format(username, ip)
key_block = key_prefix_block.format(username)
cache.delete(key_limit)
cache.delete(key_block)
def is_block_login(key_limit):
count = cache.get(key_limit)
def is_block_login(username, ip):
key_limit = key_prefix_limit.format(username, ip)
key_block = key_prefix_block.format(username)
count = cache.get(key_limit, 0)
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
limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT or \
settings.DEFAULT_LOGIN_LIMIT_COUNT
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60)
if count and count >= limit_count:
return True
......
......@@ -11,6 +11,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from common.utils import get_logger
from common.const import create_success_msg, update_success_msg
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from ..models import User, UserGroup
from .. import forms
......@@ -55,13 +56,10 @@ class UserGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVie
success_message = update_success_msg
def get_context_data(self, **kwargs):
users = User.objects.all()
group_users = [user.id for user in self.object.users.all()]
context = {
'app': _('Users'),
'action': _('Update user group'),
'users': users,
'group_users': group_users
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -73,7 +71,7 @@ class UserGroupDetailView(AdminUserRequiredMixin, DetailView):
template_name = 'users/user_group_detail.html'
def get_context_data(self, **kwargs):
users = User.objects.exclude(id__in=self.object.users.all()).exclude(role=User.ROLE_APP)
users = current_org.get_org_users().exclude(id__in=self.object.users.all())
context = {
'app': _('Users'),
'action': _('User group detail'),
......
......@@ -8,7 +8,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
......@@ -21,15 +20,12 @@ from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView
from django.conf import settings
from common.utils import get_object_or_none
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from common.utils import get_object_or_none, get_request_ip
from ..models import User, LoginLog
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
from ..utils import send_reset_password_mail, check_otp_code, \
redirect_user_first_login_or_index, get_user_or_tmp_user, \
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
is_block_login, set_user_login_failed_count_to_cache
is_block_login, increase_login_failed_count, clean_failed_count
from ..tasks import write_login_log_async
from .. import forms
......@@ -51,8 +47,6 @@ class UserLoginView(FormView):
form_class_captcha = forms.UserLoginCaptchaForm
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:
......@@ -64,12 +58,10 @@ class UserLoginView(FormView):
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_login_ip(request)
ip = get_request_ip(request)
username = self.request.POST.get('username')
key_limit = self.key_prefix_limit.format(username, ip)
if is_block_login(key_limit):
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
......@@ -77,6 +69,10 @@ class UserLoginView(FormView):
return HttpResponse(_("Please enable cookies and try again."))
set_tmp_user_to_cache(self.request, form.get_user())
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
......@@ -91,10 +87,8 @@ class UserLoginView(FormView):
self.write_login_log(data)
# limit user login failed count
ip = get_login_ip(self.request)
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)
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
......@@ -104,7 +98,7 @@ class UserLoginView(FormView):
return super().form_invalid(form)
def get_form_class(self):
ip = get_login_ip(self.request)
ip = get_request_ip(self.request)
if cache.get(self.key_prefix_captcha.format(ip)):
return self.form_class_captcha
else:
......@@ -139,7 +133,7 @@ class UserLoginView(FormView):
return super().get_context_data(**kwargs)
def write_login_log(self, data):
login_ip = get_login_ip(self.request)
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
......@@ -178,14 +172,14 @@ class UserLoginOtpView(FormView):
'status': False
}
self.write_login_log(data)
form.add_error('otp_code', _('MFA code invalid'))
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def write_login_log(self, data):
login_ip = get_login_ip(self.request)
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
......@@ -361,46 +355,6 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
return form
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'users/login_log_list.html'
model = LoginLog
paginate_by = settings.DISPLAY_PER_PAGE
user = keyword = ""
date_to = date_from = None
@staticmethod
def get_org_users():
users = current_org.get_org_users().values_list('username', flat=True)
return users
def get_queryset(self):
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '')
queryset = queryset.filter(
datetime__gt=self.date_from, datetime__lt=self.date_to
)
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Login log list'),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'keyword': self.keyword,
'user_list': self.get_org_users(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
\ No newline at end of file
class LoginLogListView(ListView):
def get(self, request, *args, **kwargs):
return redirect(reverse('audits:login-log-list'))
......@@ -33,8 +33,9 @@ from django.contrib.auth import logout as auth_logout
from common.const import create_success_msg, update_success_msg
from common.mixins import JSONResponseMixin
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, common_settings
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from .. import forms
from ..models import User, UserGroup
from ..utils import generate_otp_uri, check_otp_code, \
......@@ -52,7 +53,7 @@ __all__ = [
'UserPublicKeyGenerateView',
'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView',
'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
'UserOtpDisableAuthenticationView',
'UserOtpDisableAuthenticationView', 'UserOtpUpdateView'
]
logger = get_logger(__name__)
......@@ -197,6 +198,12 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users().values_list('id', flat=True)
queryset = queryset.filter(id__in=org_users)
return queryset
@method_decorator(csrf_exempt, name='dispatch')
class UserExportView(View):
......@@ -355,10 +362,10 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs):
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
mfa_setting = common_settings.SECURITY_MFA_AUTH
context = {
'action': _('Profile'),
'mfa_setting': mfa_setting.cleaned_value if mfa_setting else False,
'mfa_setting': mfa_setting if mfa_setting is not None else False,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -514,7 +521,7 @@ class UserOtpEnableBindView(TemplateView, FormView):
return super().form_valid(form)
else:
form.add_error("otp_code", _("MFA code invalid"))
form.add_error("otp_code", _("MFA code invalid, or ntp sync server time"))
return self.form_invalid(form)
def save_otp(self, otp_secret_key):
......@@ -539,10 +546,14 @@ class UserOtpDisableAuthenticationView(FormView):
user.save()
return super().form_valid(form)
else:
form.add_error('otp_code', _('MFA code invalid'))
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
class UserOtpUpdateView(UserOtpDisableAuthenticationView):
success_url = reverse_lazy('users:user-otp-enable-bind')
class UserOtpSettingsSuccessView(TemplateView):
template_name = 'flash_message_standalone.html'
......
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