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 @@ ...@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.4.0" __version__ = "1.4.1"
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
# #
import random import random
import time
from rest_framework import generics, permissions from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
...@@ -14,8 +13,8 @@ from django.db.models import Q ...@@ -14,8 +13,8 @@ from django.db.models import Q
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, SystemUser, AdminUser, Node from ..models import Asset, AdminUser, Node
from .. import serializers from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \ from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual test_asset_connectability_manual
...@@ -40,7 +39,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): ...@@ -40,7 +39,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
queryset = Asset.objects.all() queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (permissions.AllowAny,) permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self): def filter_node(self):
node_id = self.request.query_params.get("node_id") node_id = self.request.query_params.get("node_id")
......
...@@ -55,7 +55,7 @@ class NodeViewSet(viewsets.ModelViewSet): ...@@ -55,7 +55,7 @@ class NodeViewSet(viewsets.ModelViewSet):
post_value = request.data.get('value') post_value = request.data.get('value')
if node_value != post_value: if node_value != post_value:
return Response( return Response(
{"msg": _("You cant update the root node name")}, {"msg": _("You can't update the root node name")},
status=400 status=400
) )
return super().update(request, *args, **kwargs) return super().update(request, *args, **kwargs)
...@@ -218,7 +218,8 @@ class RefreshNodeHardwareInfoApi(APIView): ...@@ -218,7 +218,8 @@ class RefreshNodeHardwareInfoApi(APIView):
node_id = kwargs.get('pk') node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id) node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all() 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) task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return Response({"task": task.id}) return Response({"task": task.id})
...@@ -231,6 +232,7 @@ class TestNodeConnectiveApi(APIView): ...@@ -231,6 +232,7 @@ class TestNodeConnectiveApi(APIView):
node_id = kwargs.get('pk') node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id) node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all() 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) task = test_asset_connectability_util.delay(assets, task_name=task_name)
return Response({"task": task.id}) return Response({"task": task.id})
...@@ -13,22 +13,27 @@ ...@@ -13,22 +13,27 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \ 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__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi' 'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi',
] ]
...@@ -82,3 +87,43 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView): ...@@ -82,3 +87,43 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
system_user = self.get_object() system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user) task = test_system_user_connectability_manual.delay(system_user)
return Response({"task": task.id}) 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): ...@@ -98,15 +98,18 @@ class SystemUserForm(PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False) auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys() 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 system_user.auto_push = 0
auto_generate_key = False
system_user.save() system_user.save()
if auto_generate_key: if auto_generate_key:
logger.info('Auto generate key and set system user auth') logger.info('Auto generate key and set system user auth')
system_user.auto_gen_auth() system_user.auto_gen_auth()
else: 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 return system_user
......
...@@ -52,7 +52,8 @@ class Cluster(models.Model): ...@@ -52,7 +52,8 @@ class Cluster(models.Model):
contact=forgery_py.name.full_name(), contact=forgery_py.name.full_name(),
phone=forgery_py.address.phone(), phone=forgery_py.address.phone(),
address=forgery_py.address.city() + forgery_py.address.street_address(), 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(), comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake') created_by='Fake')
try: try:
......
...@@ -20,6 +20,9 @@ class Domain(OrgModelMixin): ...@@ -20,6 +20,9 @@ class Domain(OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, null=True, date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created')) verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -53,3 +56,4 @@ class Gateway(AssetUser): ...@@ -53,3 +56,4 @@ class Gateway(AssetUser):
class Meta: class Meta:
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
...@@ -24,6 +24,9 @@ class Node(OrgModelMixin): ...@@ -24,6 +24,9 @@ class Node(OrgModelMixin):
is_node = True is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}' _full_value_cache_key_prefix = '_NODE_VALUE_{}'
class Meta:
verbose_name = _("Node")
def __str__(self): def __str__(self):
return self.full_value return self.full_value
......
...@@ -93,8 +93,8 @@ def update_assets_hardware_info_util(assets, task_name=None): ...@@ -93,8 +93,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
# task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
task_name = _("更新资产硬件信息") # task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hostname_list: if not hostname_list:
...@@ -113,8 +113,8 @@ def update_assets_hardware_info_util(assets, task_name=None): ...@@ -113,8 +113,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task @shared_task
def update_asset_hardware_info_manual(asset): def update_asset_hardware_info_manual(asset):
# task_name = _("Update asset hardware info") task_name = _("Update asset hardware info")
task_name = _("更新资产硬件信息") # task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util([asset], task_name=task_name) return update_assets_hardware_info_util([asset], task_name=task_name)
...@@ -132,8 +132,8 @@ def update_assets_hardware_info_period(): ...@@ -132,8 +132,8 @@ def update_assets_hardware_info_period():
return return
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
# task_name = _("Update assets hardware info period") task_name = _("Update assets hardware info period")
task_name = _("定期更新资产硬件信息") # task_name = _("定期更新资产硬件信息")
hostname_list = [ hostname_list = [
asset.fullname for asset in Asset.objects.all() asset.fullname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike() if asset.is_active and asset.is_unixlike()
...@@ -210,15 +210,15 @@ def test_admin_user_connectability_period(): ...@@ -210,15 +210,15 @@ def test_admin_user_connectability_period():
admin_users = AdminUser.objects.all() admin_users = AdminUser.objects.all()
for admin_user in admin_users: for admin_user in admin_users:
# task_name = _("Test admin user connectability period: {}".format(admin_user.name)) task_name = _("Test admin user connectability period: {}".format(admin_user.name))
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name)) # task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
test_admin_user_connectability_util(admin_user, task_name) test_admin_user_connectability_util(admin_user, task_name)
@shared_task @shared_task
def test_admin_user_connectability_manual(admin_user): def test_admin_user_connectability_manual(admin_user):
# task_name = _("Test admin user connectability: {}").format(admin_user.name) task_name = _("Test admin user connectability: {}").format(admin_user.name)
task_name = _("测试管理行号可连接性: {}").format(admin_user.name) # task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name) return test_admin_user_connectability_util(admin_user, task_name)
...@@ -227,8 +227,8 @@ def test_asset_connectability_util(assets, task_name=None): ...@@ -227,8 +227,8 @@ def test_asset_connectability_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
# task_name = _("Test assets connectability") task_name = _("Test assets connectability")
task_name = _("测试资产可连接性") # task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts: if not hosts:
logger.info("No hosts, passed") logger.info("No hosts, passed")
...@@ -272,15 +272,16 @@ def set_system_user_connectablity_info(result, **kwargs): ...@@ -272,15 +272,16 @@ def set_system_user_connectablity_info(result, **kwargs):
@shared_task @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. Test system cant connect his assets or not.
:param system_user: :param system_user:
:param assets:
:param task_name: :param task_name:
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task 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()] hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts: if not hosts:
...@@ -299,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name): ...@@ -299,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name):
@shared_task @shared_task
def test_system_user_connectability_manual(system_user): def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(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 @shared_task
...@@ -313,8 +323,8 @@ def test_system_user_connectability_period(): ...@@ -313,8 +323,8 @@ def test_system_user_connectability_period():
system_users = SystemUser.objects.all() system_users = SystemUser.objects.all()
for system_user in system_users: for system_user in system_users:
# task_name = _("Test system user connectability period: {}".format(system_user)) task_name = _("Test system user connectability period: {}".format(system_user))
task_name = _("定期测试系统用户可连接性: {}".format(system_user)) # task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name) test_system_user_connectability_util(system_user, task_name)
...@@ -393,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name): ...@@ -393,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name):
@shared_task @shared_task
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets() 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) 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 @shared_task
def push_system_user_to_assets(system_user, assets): 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) return push_system_user_util.delay([system_user], assets, task_name)
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。 {# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
Windows或其它硬件可以随意设置一个 {# 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> </div>
{% endblock %} {% endblock %}
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
{% block form %} {% block form %}
<div class="ydxbd" id="formlists" style="display: block;"> <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"> <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 %} {% for field in form %}
{% if field.name != 'assets' %} {% if field.name != 'assets' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a> <a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info 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> </div>
{% endblock %} {% endblock %}
...@@ -627,6 +628,7 @@ $(document).ready(function(){ ...@@ -627,6 +628,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected assets !!!' %}", text: "{% trans 'This will delete the selected assets !!!' %}",
type: "warning", type: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55", confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}", confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false closeOnConfirm: false
......
...@@ -120,8 +120,8 @@ $(document).ready(function(){ ...@@ -120,8 +120,8 @@ $(document).ready(function(){
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: "GET", method: "GET",
success_message: "可连接", success_message: "{% trans 'Can be connected' %}",
fail_message: "连接失败" fail_message: "{% trans 'The connection fails' %}"
}) })
}); });
</script> </script>
......
...@@ -4,8 +4,11 @@ ...@@ -4,8 +4,11 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br> {# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
JMS => 网域网关 => 目标资产 {# 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> </div>
{% endblock %} {% endblock %}
......
...@@ -17,11 +17,11 @@ ...@@ -17,11 +17,11 @@
<li class="active"> <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> <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>
{# <li>#} <li>
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#} <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' %}#} <i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
{# </a>#} </a>
{# </li>#} </li>
<li class="pull-right"> <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> <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> </li>
...@@ -152,63 +152,46 @@ ...@@ -152,63 +152,46 @@
</span> </span>
</td> </td>
</tr> </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> </tbody>
</table> </table>
</div> </div>
</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> </div>
</div> </div>
...@@ -338,13 +321,13 @@ $(document).ready(function () { ...@@ -338,13 +321,13 @@ $(document).ready(function () {
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}'; var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
var name = '{{ system_user.name }}'; var name = '{{ system_user.name }}';
swal({ swal({
title: '你确定清除该系统用户的认证信息吗 ?', title: "{% trans 'Are you sure to remove authentication information for the system user ?' %}",
text: " [" + name + "] ", text: " [" + name + "] ",
type: "warning", type: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonText: '取消', cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#ed5565", confirmButtonColor: "#ed5565",
confirmButtonText: '确认', confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: true closeOnConfirm: true
}, function () { }, function () {
APIUpdateAttr({ APIUpdateAttr({
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`); {# 系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);#}
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。 {# 简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。#}
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。 {# 系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。#}
目前还不支持Windows的自动推送 {# 目前还不支持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> </div>
{% endblock %} {% endblock %}
...@@ -135,6 +138,7 @@ $(document).ready(function(){ ...@@ -135,6 +138,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected System Users !!!' %}", text: "{% trans 'This will delete the selected System Users !!!' %}",
type: "warning", type: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55", confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}", confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false closeOnConfirm: false
......
...@@ -19,6 +19,8 @@ urlpatterns = [ ...@@ -19,6 +19,8 @@ urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'), path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/', path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-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/', path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/', path('assets/<uuid:pk>/alive/',
...@@ -31,10 +33,16 @@ urlpatterns = [ ...@@ -31,10 +33,16 @@ urlpatterns = [
api.AdminUserAuthApi.as_view(), name='admin-user-auth'), api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/', path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('system-user/<uuid:pk>/push/', path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-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/', path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('nodes/<uuid:pk>/children/', path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'), api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
......
...@@ -94,6 +94,7 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView): ...@@ -94,6 +94,7 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
context = { context = {
'app': _('assets'), 'app': _('assets'),
'action': _('System user asset'), 'action': _('System user asset'),
'nodes_remain': Node.objects.exclude(systemuser=self.object)
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -3,3 +3,6 @@ from django.apps import AppConfig ...@@ -3,3 +3,6 @@ from django.apps import AppConfig
class AuditsConfig(AppConfig): class AuditsConfig(AppConfig):
name = 'audits' 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 ...@@ -4,6 +4,11 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
]
class FTPLog(OrgModelMixin): class FTPLog(OrgModelMixin):
...@@ -16,3 +21,40 @@ class FTPLog(OrgModelMixin): ...@@ -16,3 +21,40 @@ class FTPLog(OrgModelMixin):
filename = models.CharField(max_length=1024, verbose_name=_("Filename")) filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success")) is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True) 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 @@ ...@@ -66,7 +66,6 @@
{% endblock %} {% endblock %}
{% block table_head %} {% block table_head %}
<th class="text-center"></th>
{# <th class="text-center">{% trans 'ID' %}</th>#} {# <th class="text-center">{% trans 'ID' %}</th>#}
<th class="text-center">{% trans 'User' %}</th> <th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Asset' %}</th> <th class="text-center">{% trans 'Asset' %}</th>
...@@ -82,7 +81,6 @@ ...@@ -82,7 +81,6 @@
{% block table_body %} {% block table_body %}
{% for object in object_list %} {% for object in object_list %}
<tr class="gradeX"> <tr class="gradeX">
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
{# <td class="text-center">#} {# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#} {# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#} {# </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"] ...@@ -9,5 +9,8 @@ __all__ = ["urlpatterns"]
app_name = "audits" app_name = "audits"
urlpatterns = [ urlpatterns = [
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-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.conf import settings
from django.views.generic import ListView from django.views.generic import ListView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db.models import Q
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin 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): class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
...@@ -53,3 +68,125 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -53,3 +68,125 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) 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 ...@@ -13,7 +13,7 @@ from .utils import get_signer
signer = get_signer() signer = get_signer()
class DictField(forms.Field): class FormDictField(forms.Field):
widget = forms.Textarea widget = forms.Textarea
def to_python(self, value): def to_python(self, value):
...@@ -23,6 +23,7 @@ class DictField(forms.Field): ...@@ -23,6 +23,7 @@ class DictField(forms.Field):
# RadioSelect will provide. Because bool("True") == bool('1') == True, # RadioSelect will provide. Because bool("True") == bool('1') == True,
# we don't need to handle that explicitly. # we don't need to handle that explicitly.
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
value = value.replace("'", '"')
try: try:
value = json.loads(value) value = json.loads(value)
return value return value
...@@ -74,3 +75,14 @@ class EncryptCharField(EncryptMixin, models.CharField): ...@@ -74,3 +75,14 @@ class EncryptCharField(EncryptMixin, models.CharField):
kwargs['max_length'] = 2048 kwargs['max_length'] = 2048
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class FormEncryptMixin:
pass
class FormEncryptCharField(FormEncryptMixin, forms.CharField):
pass
class FormEncryptDictField(FormEncryptMixin, FormDictField):
pass
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import json import json
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape
from django.db import transaction from django.db import transaction
from django.conf import settings from django.conf import settings
from .models import Setting from .models import Setting, common_settings
from .fields import DictField from .fields import FormDictField, FormEncryptCharField, \
FormEncryptMixin, FormEncryptDictField
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 ""
class BaseForm(forms.Form): class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
db_settings = Setting.objects.all()
for name, field in self.fields.items(): 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 django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is False or db_value: 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: 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"): def save(self, category="default"):
if not self.is_bound: if not self.is_bound:
raise ValueError("Form is not bound") raise ValueError("Form is not bound")
db_settings = Setting.objects.all() # db_settings = Setting.objects.all()
if self.is_valid(): if not 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:
raise ValueError(self.errors) 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): class BasicSettingForm(BaseForm):
SITE_URL = forms.URLField( SITE_URL = forms.URLField(
...@@ -88,7 +79,7 @@ class EmailSettingForm(BaseForm): ...@@ -88,7 +79,7 @@ class EmailSettingForm(BaseForm):
EMAIL_HOST_USER = forms.CharField( EMAIL_HOST_USER = forms.CharField(
max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' 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, max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
required=False, help_text=_("Some provider use token except password") required=False, help_text=_("Some provider use token except password")
) )
...@@ -109,7 +100,7 @@ class LDAPSettingForm(BaseForm): ...@@ -109,7 +100,7 @@ class LDAPSettingForm(BaseForm):
AUTH_LDAP_BIND_DN = forms.CharField( AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
) )
AUTH_LDAP_BIND_PASSWORD = forms.CharField( AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='', label=_("Password"), initial='',
widget=forms.PasswordInput, required=False widget=forms.PasswordInput, required=False
) )
...@@ -121,15 +112,12 @@ class LDAPSettingForm(BaseForm): ...@@ -121,15 +112,12 @@ class LDAPSettingForm(BaseForm):
label=_("User search filter"), initial='(cn=%(user)s)', label=_("User search filter"), initial='(cn=%(user)s)',
help_text=_("Choice may be (cn|uid|sAMAccountName)=%(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"), label=_("User attr map"),
initial=json.dumps({
"username": "cn",
"name": "sn",
"email": "mail"
}),
help_text=_( 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_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
...@@ -156,13 +144,13 @@ class TerminalSettingForm(BaseForm): ...@@ -156,13 +144,13 @@ class TerminalSettingForm(BaseForm):
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth") initial=True, required=False, label=_("Public key auth")
) )
TERMINAL_COMMAND_STORAGE = DictField( TERMINAL_COMMAND_STORAGE = FormEncryptDictField(
label=_("Command storage"), help_text=_( label=_("Command storage"), help_text=_(
"Set terminal storage setting, `default` is the using as default," "Set terminal storage setting, `default` is the using as default,"
"You can set other storage and some terminal using" "You can set other storage and some terminal using"
) )
) )
TERMINAL_REPLAY_STORAGE = DictField( TERMINAL_REPLAY_STORAGE = FormEncryptDictField(
label=_("Replay storage"), help_text=_( label=_("Replay storage"), help_text=_(
"Set replay storage setting, `default` is the using as default," "Set replay storage setting, `default` is the using as default,"
"You can set other storage and some terminal using" "You can set other storage and some terminal using"
...@@ -194,6 +182,14 @@ class SecuritySettingForm(BaseForm): ...@@ -194,6 +182,14 @@ class SecuritySettingForm(BaseForm):
"number of times, no login is allowed during this time interval." "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 # min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"), initial=6, label=_("Password minimum length"),
...@@ -223,9 +219,10 @@ class SecuritySettingForm(BaseForm): ...@@ -223,9 +219,10 @@ class SecuritySettingForm(BaseForm):
'and resets must contain numeric characters') 'and resets must contain numeric characters')
) )
# special char # special char
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField( SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
initial=False, required=False, initial=False, required=False,
label=_("Must contain special characters"), label=_("Must contain special characters"),
help_text=_('After opening, the user password changes ' help_text=_('After opening, the user password changes '
'and resets must contain special characters') 'and resets must contain special characters')
) )
...@@ -7,6 +7,10 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -7,6 +7,10 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer
signer = get_signer()
class SettingQuerySet(models.QuerySet): class SettingQuerySet(models.QuerySet):
def __getattr__(self, item): def __getattr__(self, item):
...@@ -26,6 +30,7 @@ class Setting(models.Model): ...@@ -26,6 +30,7 @@ class Setting(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
value = models.TextField(verbose_name=_("Value")) value = models.TextField(verbose_name=_("Value"))
category = models.CharField(max_length=128, default="default") category = models.CharField(max_length=128, default="default")
encrypted = models.BooleanField(default=False)
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
comment = models.TextField(verbose_name=_("Comment")) comment = models.TextField(verbose_name=_("Comment"))
...@@ -34,10 +39,21 @@ class Setting(models.Model): ...@@ -34,10 +39,21 @@ class Setting(models.Model):
def __str__(self): def __str__(self):
return self.name 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 @property
def cleaned_value(self): def cleaned_value(self):
try: 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: except json.JSONDecodeError:
return None return None
...@@ -45,6 +61,8 @@ class Setting(models.Model): ...@@ -45,6 +61,8 @@ class Setting(models.Model):
def cleaned_value(self, item): def cleaned_value(self, item):
try: try:
v = json.dumps(item) v = json.dumps(item)
if self.encrypted:
v = signer.sign(v)
self.value = v self.value = v
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e))) raise ValueError("Json dump error: {}".format(str(e)))
...@@ -59,11 +77,7 @@ class Setting(models.Model): ...@@ -59,11 +77,7 @@ class Setting(models.Model):
pass pass
def refresh_setting(self): def refresh_setting(self):
try: setattr(settings, self.name, self.cleaned_value)
value = json.loads(self.value)
except json.JSONDecodeError:
return
setattr(settings, self.name, value)
if self.name == "AUTH_LDAP": if self.name == "AUTH_LDAP":
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
...@@ -81,3 +95,5 @@ class Setting(models.Model): ...@@ -81,3 +95,5 @@ class Setting(models.Model):
class Meta: class Meta:
db_table = "settings" db_table = "settings"
common_settings = Setting()
...@@ -92,3 +92,9 @@ class AdminUserRequiredMixin(UserPassesTestMixin): ...@@ -92,3 +92,9 @@ class AdminUserRequiredMixin(UserPassesTestMixin):
return redirect('orgs:switch-a-org') return redirect('orgs:switch-a-org')
return HttpResponseForbidden() return HttpResponseForbidden()
return super().dispatch(request, *args, **kwargs) 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 -*- # -*- coding: utf-8 -*-
# #
from django.dispatch import receiver 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.conf import settings
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from jumpserver.utils import current_request
from .models import Setting from .models import Setting
from .utils import get_logger from .utils import get_logger
from .signals import django_ready, ldap_auth_enable from .signals import django_ready, ldap_auth_enable
...@@ -42,3 +43,9 @@ def ldap_auth_on_changed(sender, enabled=True, **kwargs): ...@@ -42,3 +43,9 @@ def ldap_auth_on_changed(sender, enabled=True, **kwargs):
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) 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 @@ ...@@ -41,7 +41,7 @@
<h3>{% trans "User login settings" %}</h3> <h3>{% trans "User login settings" %}</h3>
{% for field in form %} {% for field in form %}
{% if forloop.counter == 4 %} {% if forloop.counter == 5 %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3> <h3>{% trans "Password check rule" %}</h3>
{% endif %} {% endif %}
......
...@@ -378,6 +378,15 @@ def get_signer(): ...@@ -378,6 +378,15 @@ def get_signer():
return 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: class TeeObj:
origin_stdout = sys.stdout origin_stdout = sys.stdout
......
...@@ -6,11 +6,11 @@ from django.conf import settings ...@@ -6,11 +6,11 @@ from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm TerminalSettingForm, SecuritySettingForm
from common.permissions import AdminUserRequiredMixin from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable from .signals import ldap_auth_enable
class BasicSettingView(AdminUserRequiredMixin, TemplateView): class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form_class = BasicSettingForm form_class = BasicSettingForm
template_name = "common/basic_setting.html" template_name = "common/basic_setting.html"
...@@ -36,7 +36,7 @@ class BasicSettingView(AdminUserRequiredMixin, TemplateView): ...@@ -36,7 +36,7 @@ class BasicSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class EmailSettingView(AdminUserRequiredMixin, TemplateView): class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form_class = EmailSettingForm form_class = EmailSettingForm
template_name = "common/email_setting.html" template_name = "common/email_setting.html"
...@@ -62,7 +62,7 @@ class EmailSettingView(AdminUserRequiredMixin, TemplateView): ...@@ -62,7 +62,7 @@ class EmailSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class LDAPSettingView(AdminUserRequiredMixin, TemplateView): class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form_class = LDAPSettingForm form_class = LDAPSettingForm
template_name = "common/ldap_setting.html" template_name = "common/ldap_setting.html"
...@@ -90,7 +90,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView): ...@@ -90,7 +90,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class TerminalSettingView(AdminUserRequiredMixin, TemplateView): class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form_class = TerminalSettingForm form_class = TerminalSettingForm
template_name = "common/terminal_setting.html" template_name = "common/terminal_setting.html"
...@@ -120,7 +120,7 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView): ...@@ -120,7 +120,7 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class SecuritySettingView(AdminUserRequiredMixin, TemplateView): class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm form_class = SecuritySettingForm
template_name = "common/security_setting.html" template_name = "common/security_setting.html"
......
...@@ -6,6 +6,8 @@ import pytz ...@@ -6,6 +6,8 @@ import pytz
from django.utils import timezone from django.utils import timezone
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse
from .utils import set_current_request
class TimezoneMiddleware: class TimezoneMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
...@@ -45,3 +47,13 @@ class DemoMiddleware: ...@@ -45,3 +47,13 @@ class DemoMiddleware:
else: else:
response = self.get_response(request) response = self.get_response(request)
return response 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 = [ ...@@ -95,6 +95,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware',
'orgs.middleware.OrgMiddleware', 'orgs.middleware.OrgMiddleware',
] ]
...@@ -127,7 +128,6 @@ TEMPLATES = [ ...@@ -127,7 +128,6 @@ TEMPLATES = [
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'jumpserver.context_processor.jumpserver_processor',
'django.template.context_processors.i18n', 'django.template.context_processors.i18n',
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
...@@ -136,6 +136,7 @@ TEMPLATES = [ ...@@ -136,6 +136,7 @@ TEMPLATES = [
'django.template.context_processors.static', 'django.template.context_processors.static',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.template.context_processors.media', 'django.template.context_processors.media',
'jumpserver.context_processor.jumpserver_processor',
'orgs.context_processor.org_processor', 'orgs.context_processor.org_processor',
*get_xpack_context_processor(), *get_xpack_context_processor(),
], ],
...@@ -214,7 +215,10 @@ LOGGING = { ...@@ -214,7 +215,10 @@ LOGGING = {
}, },
'file': { 'file': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.FileHandler', 'class': 'logging.handlers.TimedRotatingFileHandler',
'when': "D",
'interval': 1,
"backupCount": 7,
'formatter': 'main', 'formatter': 'main',
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log') 'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
}, },
...@@ -270,7 +274,8 @@ LOGGING = { ...@@ -270,7 +274,8 @@ LOGGING = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en' # LANGUAGE_CODE = 'en'
LANGUAGE_CODE = 'zh'
TIME_ZONE = 'Asia/Shanghai' TIME_ZONE = 'Asia/Shanghai'
...@@ -281,7 +286,9 @@ USE_L10N = True ...@@ -281,7 +286,9 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
# I18N translation # I18N translation
LOCALE_PATHS = [os.path.join(BASE_DIR, 'i18n'), ] LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale'),
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
...@@ -441,7 +448,8 @@ TERMINAL_REPLAY_STORAGE = { ...@@ -441,7 +448,8 @@ TERMINAL_REPLAY_STORAGE = {
DEFAULT_PASSWORD_MIN_LENGTH = 6 DEFAULT_PASSWORD_MIN_LENGTH = 6
DEFAULT_LOGIN_LIMIT_COUNT = 7 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 # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
...@@ -465,4 +473,4 @@ SWAGGER_SETTINGS = { ...@@ -465,4 +473,4 @@ SWAGGER_SETTINGS = {
'type': 'basic' 'type': 'basic'
} }
}, },
} }
\ No newline at end of file
...@@ -6,6 +6,8 @@ import os ...@@ -6,6 +6,8 @@ import os
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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 rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse from django.http import HttpResponse
...@@ -14,7 +16,7 @@ from rest_framework import permissions ...@@ -14,7 +16,7 @@ from rest_framework import permissions
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
from .views import IndexView, LunaView from .views import IndexView, LunaView, I18NView
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
...@@ -78,10 +80,14 @@ app_view_patterns = [ ...@@ -78,10 +80,14 @@ app_view_patterns = [
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) 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 = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), path('', IndexView.as_view(), name='index'),
path('luna/', LunaView.as_view(), name='luna-error'), 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('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')), path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api), path('api/v1/', redirect_format_api),
...@@ -93,6 +99,7 @@ urlpatterns = [ ...@@ -93,6 +99,7 @@ urlpatterns = [
urlpatterns += app_view_patterns urlpatterns += app_view_patterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += js_i18n_patterns
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ 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 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.views.generic import TemplateView, View
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
...@@ -85,7 +87,7 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -85,7 +87,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
return self.session_month.values('user').distinct().count() return self.session_month.values('user').distinct().count()
def get_month_inactive_user_total(self): 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): def get_month_active_asset_total(self):
return self.session_month.values('asset').distinct().count() return self.session_month.values('asset').distinct().count()
...@@ -95,7 +97,7 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -95,7 +97,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
@staticmethod @staticmethod
def get_user_disabled_total(): 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 @staticmethod
def get_asset_disabled_total(): def get_asset_disabled_total():
...@@ -173,11 +175,14 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -173,11 +175,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
class LunaView(View): class LunaView(View):
def get(self, request): def get(self, request):
msg = """ msg = _("<div>Luna is a separately deployed program, you need to deploy Luna, coco, configure nginx for url distribution,</div> "
Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发, "</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运
"""
return HttpResponse(msg) 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
# 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): ...@@ -207,6 +207,7 @@ class AdHoc(models.Model):
return {} return {}
def run(self, record=True): def run(self, record=True):
set_to_root_org()
if record: if record:
return self._run_and_record() return self._run_and_record()
else: else:
......
...@@ -6,10 +6,10 @@ from django.views.generic import ListView, DetailView, TemplateView ...@@ -6,10 +6,10 @@ from django.views.generic import ListView, DetailView, TemplateView
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask 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 paginate_by = settings.DISPLAY_PER_PAGE
model = Task model = Task
ordering = ('-date_created',) ordering = ('-date_created',)
...@@ -43,7 +43,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -43,7 +43,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskDetailView(AdminUserRequiredMixin, DetailView): class TaskDetailView(SuperUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_detail.html' template_name = 'ops/task_detail.html'
...@@ -56,7 +56,7 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView): ...@@ -56,7 +56,7 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskAdhocView(AdminUserRequiredMixin, DetailView): class TaskAdhocView(SuperUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_adhoc.html' template_name = 'ops/task_adhoc.html'
...@@ -69,7 +69,7 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView): ...@@ -69,7 +69,7 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskHistoryView(AdminUserRequiredMixin, DetailView): class TaskHistoryView(SuperUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_history.html' template_name = 'ops/task_history.html'
...@@ -82,7 +82,7 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView): ...@@ -82,7 +82,7 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocDetailView(AdminUserRequiredMixin, DetailView): class AdHocDetailView(SuperUserRequiredMixin, DetailView):
model = AdHoc model = AdHoc
template_name = 'ops/adhoc_detail.html' template_name = 'ops/adhoc_detail.html'
...@@ -95,7 +95,7 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView): ...@@ -95,7 +95,7 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocHistoryView(AdminUserRequiredMixin, DetailView): class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
model = AdHoc model = AdHoc
template_name = 'ops/adhoc_history.html' template_name = 'ops/adhoc_history.html'
...@@ -108,7 +108,7 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView): ...@@ -108,7 +108,7 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView): class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
model = AdHocRunHistory model = AdHocRunHistory
template_name = 'ops/adhoc_history_detail.html' template_name = 'ops/adhoc_history_detail.html'
...@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView): ...@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView): class CeleryTaskLogView(SuperUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html' template_name = 'ops/celery_task_log.html'
model = CeleryTask model = CeleryTask
...@@ -20,6 +20,9 @@ class Organization(models.Model): ...@@ -20,6 +20,9 @@ class Organization(models.Model):
ROOT_ID_NAME = 'ROOT' ROOT_ID_NAME = 'ROOT'
DEFAULT_ID_NAME = 'DEFAULT' DEFAULT_ID_NAME = 'DEFAULT'
class Meta:
verbose_name = _("Organization")
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -63,15 +66,18 @@ class Organization(models.Model): ...@@ -63,15 +66,18 @@ class Organization(models.Model):
org = cls.default() if default else None org = cls.default() if default else None
return org return org
def get_org_users(self): def get_org_users(self, include_app=False):
from users.models import User from users.models import User
if self.is_default(): if self.is_default():
users = User.objects.filter(orgs__isnull=True) users = User.objects.filter(orgs__isnull=True)
elif not self.is_real(): elif not self.is_real():
users = User.objects.all() users = User.objects.all()
elif self.is_root():
users = User.objects.all()
else: else:
users = self.users.all() users = self.users.all()
users = users.exclude(role=User.ROLE_APP) if not include_app:
users = users.exclude(role=User.ROLE_APP)
return users return users
def get_org_admins(self): def get_org_admins(self):
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from .models import Organization from .models import Organization
from .hands import set_current_org, current_org, Node from .hands import set_current_org, current_org, Node
from perms.models import AssetPermission
from users.models import UserGroup
@receiver(post_save, sender=Organization) @receiver(post_save, sender=Organization)
...@@ -21,3 +24,20 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs): ...@@ -21,3 +24,20 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
if instance and not created: if instance and not created:
instance.expire_cache() 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(): ...@@ -40,5 +40,3 @@ def get_current_org():
current_org = LocalProxy(partial(_find, '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 _ ...@@ -6,23 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelForm from orgs.mixins import OrgModelForm
from orgs.utils import current_org from orgs.utils import current_org
from .hands import User
from .models import AssetPermission from .models import AssetPermission
class AssetPermissionForm(OrgModelForm): 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'initial' not in kwargs: if 'initial' not in kwargs:
......
...@@ -42,6 +42,7 @@ class AssetPermission(OrgModelMixin): ...@@ -42,6 +42,7 @@ class AssetPermission(OrgModelMixin):
class Meta: class Meta:
unique_together = [('org_id', 'name')] unique_together = [('org_id', 'name')]
verbose_name = _("Asset permission")
def __str__(self): def __str__(self):
return self.name return self.name
......
...@@ -154,8 +154,8 @@ function activeNav() { ...@@ -154,8 +154,8 @@ function activeNav() {
function APIUpdateAttr(props) { function APIUpdateAttr(props) {
// props = {url: .., body: , success: , error: , method: ,} // props = {url: .., body: , success: , error: , method: ,}
props = props || {}; props = props || {};
var success_message = props.success_message || '更新成功!'; var success_message = props.success_message || gettext('Update is successful!');
var fail_message = props.fail_message || '更新时发生未知错误.'; var fail_message = props.fail_message || gettext('An unknown error occurred while updating..');
var flash_message = props.flash_message || true; var flash_message = props.flash_message || true;
if (props.flash_message === false){ if (props.flash_message === false){
flash_message = false; flash_message = false;
...@@ -199,25 +199,25 @@ function objectDelete(obj, name, url, redirectTo) { ...@@ -199,25 +199,25 @@ function objectDelete(obj, name, url, redirectTo) {
}; };
var fail = function() { var fail = function() {
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error"); // swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error"); swal(gettext('Error'), "[ "+name+" ]" + gettext("Being used by the asset, please unbind the asset first."), "error");
}; };
APIUpdateAttr({ APIUpdateAttr({
url: url, url: url,
body: JSON.stringify(body), body: JSON.stringify(body),
method: 'DELETE', method: 'DELETE',
success_message: "删除成功", success_message: gettext("Delete the success"),
success: success, success: success,
error: fail error: fail
}); });
} }
swal({ swal({
title: '你确定删除吗 ?', title: gettext('Are you sure about deleting it?'),
text: " [" + name + "] ", text: " [" + name + "] ",
type: "warning", type: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonText: '取消', cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565", confirmButtonColor: "#ed5565",
confirmButtonText: '确认', confirmButtonText: gettext('Confirm'),
closeOnConfirm: true, closeOnConfirm: true,
}, function () { }, function () {
doDelete() doDelete()
...@@ -236,29 +236,29 @@ function orgDelete(obj, name, url, redirectTo){ ...@@ -236,29 +236,29 @@ function orgDelete(obj, name, url, redirectTo){
}; };
var fail = function(responseText, status) { var fail = function(responseText, status) {
if (status === 400){ 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){ 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({ APIUpdateAttr({
url: url, url: url,
body: JSON.stringify(body), body: JSON.stringify(body),
method: 'DELETE', method: 'DELETE',
success_message: "删除成功", success_message: gettext("Delete the success"),
success: success, success: success,
error: fail error: fail
}); });
} }
swal({ swal({
title: "请确保组织内的以下信息已删除", title: gettext("Please ensure that the following information in the organization has been deleted"),
text: "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则", text: gettext("User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission"),
type: "warning", type: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonText: '取消', cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565", confirmButtonColor: "#ed5565",
confirmButtonText: '确认', confirmButtonText: gettext('Confirm'),
closeOnConfirm: true closeOnConfirm: true
}, function () { }, function () {
doDelete(); doDelete();
...@@ -292,20 +292,20 @@ var jumpserver = {}; ...@@ -292,20 +292,20 @@ var jumpserver = {};
jumpserver.checked = false; jumpserver.checked = false;
jumpserver.selected = {}; jumpserver.selected = {};
jumpserver.language = { jumpserver.language = {
processing: "加载中", processing: gettext('Loading ...'),
search: "搜索", search: gettext('Search'),
select: { select: {
rows: { rows: {
_: "选中 %d 项", _: gettext("Selected item %d"),
0: "" 0: ""
} }
}, },
lengthMenu: "每页 _MENU_", lengthMenu: gettext("Per page _MENU_"),
info: "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项", info: gettext('Displays the results of items _START_ to _END_; A total of _TOTAL_ entries'),
infoFiltered: "", infoFiltered: "",
infoEmpty: "", infoEmpty: "",
zeroRecords: "没有匹配项", zeroRecords: gettext("No match"),
emptyTable: "没有记录", emptyTable: gettext('No record'),
paginate: { paginate: {
first: "«", first: "«",
previous: "‹", previous: "‹",
......
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018 {% load i18n %}
\ No newline at end of file <strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2018
\ No newline at end of file
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<!-- Custom and plugin javascript --> <!-- Custom and plugin javascript -->
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script> <script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
<script src="{% static "js/inspinia.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 src="{% static "js/jumpserver.js" %}"></script>
<script> <script>
activeNav(); activeNav();
......
{% load i18n %}
<div class="footer fixed"> <div class="footer fixed">
<div class="pull-right"> <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">--> <!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div> </div>
<div> <div>
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018 <strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2018
</div> </div>
</div> </div>
...@@ -13,16 +13,68 @@ ...@@ -13,16 +13,68 @@
{# <li>#} {# <li>#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#} {# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
{# </li>#} {# </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"> <li class="dropdown">
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank"> <a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span> <i class="fa fa-handshake-o"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Help' %} <b class="caret"></b></span>
</a> </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>
<li class="dropdown"> <li class="dropdown">
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank"> <a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span> <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> </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>
<li class="dropdown"> <li class="dropdown">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a data-toggle="dropdown" class="dropdown-toggle" href="#"> <a data-toggle="dropdown" class="dropdown-toggle" href="#">
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
<ul class="nav nav-second-level active"> <ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li> <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="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> </ul>
</li> </li>
<li id="assets"> <li id="assets">
...@@ -68,7 +67,10 @@ ...@@ -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> <i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level"> <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="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> </ul>
</li> </li>
{#<li id="">#} {#<li id="">#}
......
{% load i18n %}
{% load common_tags %} {% load common_tags %}
{% if is_paginated %} {% if is_paginated %}
<div class="col-sm-4"> <div class="col-sm-4">
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite"> <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> </div>
<div class="col-sm-4"> <div class="col-sm-4">
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
<ul class="pagination" style="margin-top: 0; float: right"> <ul class="pagination" style="margin-top: 0; float: right">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="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> </li>
{% endif %} {% endif %}
...@@ -26,7 +27,7 @@ ...@@ -26,7 +27,7 @@
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="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> </li>
{% endif %} {% endif %}
</ul> </ul>
...@@ -40,7 +41,7 @@ ...@@ -40,7 +41,7 @@
var old_href = $(this).attr('href').replace('?', ''); var old_href = $(this).attr('href').replace('?', '');
var searchArray = searchStr.split('&'); var searchArray = searchStr.split('&');
if (searchStr == '') { if (searchStr === '') {
searchStr = '?page=1' searchStr = '?page=1'
} }
...@@ -53,6 +54,13 @@ ...@@ -53,6 +54,13 @@
$(this).attr('href', searchArray.join('&')); $(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> </script>
{% load i18n %}
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
...@@ -10,6 +11,7 @@ ...@@ -10,6 +11,7 @@
{% include '_head_css_js.html' %} {% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet"> <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/jumpserver.js" %}"></script>
</head> </head>
...@@ -40,7 +42,7 @@ ...@@ -40,7 +42,7 @@
{% endif %} {% endif %}
<div class="row"> <div class="row">
<div class="col-lg-3"> <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> </div>
</div> </div>
......
This diff is collapsed.
...@@ -9,6 +9,7 @@ from django.conf import settings ...@@ -9,6 +9,7 @@ from django.conf import settings
from users.models import User from users.models import User
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
from common.models import common_settings
from .backends.command.models import AbstractSessionCommand from .backends.command.models import AbstractSessionCommand
...@@ -62,6 +63,10 @@ class Terminal(models.Model): ...@@ -62,6 +63,10 @@ class Terminal(models.Model):
configs[k] = getattr(settings, k) configs[k] = getattr(settings, k)
configs.update(self.get_common_storage()) configs.update(self.get_common_storage())
configs.update(self.get_replay_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 return configs
def create_app_user(self): def create_app_user(self):
......
This diff is collapsed.
# -*- coding: utf-8 -*-
#
from .user import *
from .auth import *
from .group import *
# -*- 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,)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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