* [Update] 初始化操作日志

* [Feature] 完成操作日志记录

* [Update] 修改mfa失败提示

* [Update] 修改增加created by内容

* [Update] 增加改密日志

* [Update] 登录日志迁移到日志审计中

* [Update] change block user logic, if login success, clean block limit

*  [Update] 更新中/英文翻译(ALL) (#1662)

* Revert "授权页面分页问题"

* 增加命令导出 (#1566)

* [Update] gunicorn不使用eventlet

* [Update] 添加eventlet

* 替换淘宝IP查询接口

* [Feature] 添加命令记录下载功能 (#1559)

* [Feature] 添加命令记录下载功能

* [Update] 文案修改,导出记录、提交,取消全部命令导出

* [Update] 命令导出,修复时间问题

* [Update] paramiko => 2.4.1

* [Update] 修改settings

* [Update] 修改权限判断

* Dev (#1646)

* [Update] 添加org

* [Update] 修改url

* [Update] 完成基本框架

* [Update] 修改一些逻辑

* [Update] 修改用户view

* [Update] 修改资产

* [Update] 修改asset api

* [Update] 修改协议小问题

* [Update] stash it

* [Update] 修改约束

* [Update] 修改外键为org_id

* [Update] 删掉Premiddleware

* [Update] 修改Node

* [Update] 修改get_current_org 为 proxy对象 current_org

* [Bugfix] 解决Node.root() 死循环,移动AdminRequired到permission中 (#1571)

* [Update] 修改permission (#1574)

* Tmp org (#1579)

* [Update] 添加org api, 升级到django 2.0

* [Update] fix some bug

* [Update] 修改一些bug

* [Update] 添加授权规则org (#1580)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* Tmp org (#1583)

* [Update] 修改一些内容

* [Update] 修改datatable 支持process

* [Bugfix] 修复asset queryset 没有valid方法的bug

* [Update] 在线/历史/命令model添加org;修复命令记录保存org失败bug (#1584)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* [Update] 在线/历史/命令model添加org

* [Bugfix] 修复命令记录,保存org不成功bug

* [Update] Org功能修改

* [Bugfix] 修复merge带来的问题

* [Update] org admin显示资产详情右侧选项卡;修复资产授权添加用户,会显示其他org用户的bug (#1594)

* [Bugfix] 修复资产授权添加用户,显示其他org的用户bug

* [Update] org admin 显示资产详情右侧选项卡

* Tmp org (#1596)

* [Update] 修改index view

* [Update] 修改nav

* [Update] 修改profile

* [Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug

* [Update] 修改get_all_assets

* [Bugfix] 修复节点前面有个空目录

* [Bugfix] 修复merge引起的bug

* [Update] Add init

* [Update] Node get_all_assets 过滤游离资产,条件nodes_key=None -> nodes=None

* [Update] 恢复原来的api地址

* [Update] 修改api

* [Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug

* [Bugfix] Fix perm name unique

* [Bugfix] 修复校验失败api

* [Update] Merge with org

* [Merge] 修改一下bug

* [Update] 暂时修改一些url

* [Update] 修改url 为django 2.0 path

* [Update] 优化datatable 和显示组织优化

* [Update] 升级url

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode(… (#1613)

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode() method的bug

* [Bugfix] (task任务系统)修复资产连接性测试、硬件刷新和系统用户连接性测试失败等bug

* [Bugfix] 修复一些bug

* [Bugfix] 修复一些bug

*  [Update] 更新org下普通用户的资产详情 (#1619)

* [Update] 更新org下普通用户查看资产详情,只显示数据

* [Update] 优化org下普通用户查看资产详情前端代码

* [Update] 创建/更新用户的role选项;密码强度提示信息中英文; (#1623)

* [Update] 修改 超级管理员/组织管理员 在 创建/更新 用户时role的选项 问题

* [Update] 用户密码强度提示信息支持中英文

* [Update] 修改token返回

* [Update] Asset返回org name

* [Update] 修改支持xpack

* [Update] 修改url

* [Bugfix] 修复不登录就能查看资产的bug

* [Update] 用户修改

* [Bugfix] ...

* [Bugfix] 修复跳转错误的问题

*  [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; (#1644)

* [Update] 更新xpack下orgs的翻译信息

* [Update] 更新model Label,继承OrgModelMixin;

* [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug;

* [Bugfix] 修复小bug

* [Update] 优化一些api

* [Update] 优化用户资产页面

* [Update] 更新 xpack/orgs 删除功能:限制在当前org下删除当前org (#1645)

* [Update] 修改版本号

* [Update] 添加功能: 语言切换(中/英);修改 header_bar <商业支持、文档>显示方式

* [Update] 中/英切换文案修改;修改django_language key 从 settings 中获取

* [Update] 修改Dashboard页面文案,支持英文

* [Update] 更新中/英文翻译(ALL)

* [Update] 解决翻译文件冲突

* [Update] 系统用户支持单独隋松

* [Update] 重置用户MFA

* [Update] 设置session空闲时间

* [Update] 加密setting配置

* [Update] 修改单独推送和测试资产可连接性

*  [Update] 添加功能:用户个人详情页添加 更改MFA操作 (#1748)

* [Update] 添加功能:用户个人详情页添加 更改MFA操作

* [Update] 删除print

* [Bugfix] 添加部分views的权限控制;从组织移除用户,同时从授权规则和用户组中移除此用户。 (#1746)

* [Bugfix] 修复上传command log 为空

* [Update] 修复执行任务的bug

* [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups

* [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制

* [Update] 修改日志滚动

* [Bugfix] 修复组织权限控制的bug (#1763)

* [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups

* [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制
pull/1770/head
老广 2018-09-03 11:24:25 +08:00 committed by GitHub
parent dc918c031c
commit fe45d839fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 2731 additions and 1165 deletions

View File

@ -55,7 +55,7 @@ class NodeViewSet(viewsets.ModelViewSet):
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You cant update the root node name")},
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
@ -218,7 +218,8 @@ class RefreshNodeHardwareInfoApi(APIView):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
task_name = _("更新节点资产硬件信息: {}".format(node.name))
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
task_name = _("Update node asset hardware information: {}").format(node.name)
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
@ -231,6 +232,7 @@ class TestNodeConnectiveApi(APIView):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
task_name = _("测试节点下资产是否可连接: {}".format(node.name))
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name)
return Response({"task": task.id})

View File

@ -13,22 +13,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser
from ..models import SystemUser, Asset
from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset
logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi'
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi',
]
@ -82,3 +87,43 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def get_queryset(self):
system_user = self.get_object()
return system_user.assets.all()
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = push_system_user_a_asset_manual.delay(system_user, asset)
return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset)
return Response({"task": task.id})

View File

@ -52,7 +52,8 @@ class Cluster(models.Model):
contact=forgery_py.name.full_name(),
phone=forgery_py.address.phone(),
address=forgery_py.address.city() + forgery_py.address.street_address(),
operator=choice(['北京联通', '北京电信', 'BGP全网通']),
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:

View File

@ -20,6 +20,9 @@ class Domain(OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
def __str__(self):
return self.name
@ -53,3 +56,4 @@ class Gateway(AssetUser):
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")

View File

@ -24,6 +24,9 @@ class Node(OrgModelMixin):
is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
class Meta:
verbose_name = _("Node")
def __str__(self):
return self.full_value

View File

@ -93,8 +93,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
"""
from ops.utils import update_or_create_ansible_task
if task_name is None:
# task_name = _("Update some assets hardware info")
task_name = _("更新资产硬件信息")
task_name = _("Update some assets hardware info")
# task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hostname_list:
@ -113,8 +113,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task
def update_asset_hardware_info_manual(asset):
# task_name = _("Update asset hardware info")
task_name = _("更新资产硬件信息")
task_name = _("Update asset hardware info")
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util([asset], task_name=task_name)
@ -132,8 +132,8 @@ def update_assets_hardware_info_period():
return
from ops.utils import update_or_create_ansible_task
# task_name = _("Update assets hardware info period")
task_name = _("定期更新资产硬件信息")
task_name = _("Update assets hardware info period")
# task_name = _("定期更新资产硬件信息")
hostname_list = [
asset.fullname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike()
@ -210,15 +210,15 @@ def test_admin_user_connectability_period():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
# task_name = _("Test admin user connectability period: {}".format(admin_user.name))
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_admin_user_connectability_manual(admin_user):
# task_name = _("Test admin user connectability: {}").format(admin_user.name)
task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
task_name = _("Test admin user connectability: {}").format(admin_user.name)
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name)
@ -227,8 +227,8 @@ def test_asset_connectability_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
# task_name = _("Test assets connectability")
task_name = _("测试资产可连接性")
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts:
logger.info("No hosts, passed")
@ -272,15 +272,16 @@ def set_system_user_connectablity_info(result, **kwargs):
@shared_task
def test_system_user_connectability_util(system_user, task_name):
def test_system_user_connectability_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
:param system_user:
:param assets:
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
assets = system_user.get_assets()
# assets = system_user.get_assets()
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts:
@ -299,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name):
@shared_task
def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user)
return test_system_user_connectability_util(system_user, task_name)
assets = system_user.get_assets()
return test_system_user_connectability_util(system_user, assets, task_name)
@shared_task
def test_system_user_connectability_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format(
system_user, asset
)
return test_system_user_connectability_util(system_user, [asset], task_name)
@shared_task
@ -313,8 +323,8 @@ def test_system_user_connectability_period():
system_users = SystemUser.objects.all()
for system_user in system_users:
# task_name = _("Test system user connectability period: {}".format(system_user))
task_name = _("定期测试系统用户可连接性: {}".format(system_user))
task_name = _("Test system user connectability period: {}".format(system_user))
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name)
@ -393,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name):
@shared_task
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets()
task_name = "推送系统用户到入资产: {}".format(system_user.name)
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name)
@shared_task
def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset.fullname
)
return push_system_user_util([system_user], [asset], task_name=task_name)
@shared_task
def push_system_user_to_assets(system_user, assets):
task_name = _("推送系统用户到入资产: {}").format(system_user.name)
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util.delay([system_user], assets, task_name)

View File

@ -5,8 +5,11 @@
{% block help_message %}
<div class="alert alert-info help-message">
管理用户是资产被控服务器上的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
Windows或其它硬件可以随意设置一个
{# 管理用户是资产被控服务器上的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
{# Windows或其它硬件可以随意设置一个#}
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
{% trans 'You can set any one for Windows or other hardware.' %}
</div>
{% endblock %}

View File

@ -5,9 +5,9 @@
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
<div class="tagBtnList">
<a class="label label-primary" id="change_all" value="1">全选</a>
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
{% for field in form %}
{% if field.name != 'assets' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>

View File

@ -4,7 +4,8 @@
{% block help_message %}
<div class="alert alert-info help-message">
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
</div>
{% endblock %}
@ -627,6 +628,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected assets !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false

View File

@ -120,8 +120,8 @@ $(document).ready(function(){
APIUpdateAttr({
url: the_url,
method: "GET",
success_message: "可连接",
fail_message: "连接失败"
success_message: "{% trans 'Can be connected' %}",
fail_message: "{% trans 'The connection fails' %}"
})
});
</script>

View File

@ -4,8 +4,11 @@
{% block help_message %}
<div class="alert alert-info help-message">
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>
JMS => 网域网关 => 目标资产
{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
{# JMS => 网域网关 => 目标资产#}
{% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
<br>
{% trans 'JMS => Domain gateway => Target assets' %}
</div>
{% endblock %}

View File

@ -50,6 +50,7 @@
<th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
@ -67,14 +68,6 @@
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
<tr>
<td width="50%">{% trans 'Test assets connective' %}:</td>
<td>
<span style="float: right">
@ -82,6 +75,52 @@
</span>
</td>
</tr>
{% if system_user.auto_push %}
<tr>
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@ -96,7 +135,7 @@
{% block custom_foot_js %}
<script>
function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe}};
var unreachable = {{ system_user.unreachable_assets|safe }};
var options = {
ele: $('#system_user_list'),
buttons: [],
@ -112,27 +151,64 @@ function initAssetsTable() {
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var push_btn = '';
{% if system_user.auto_push %}
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{% endif %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
$(td).html(push_btn + test_btn);
}}
],
ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }, {data: "id"}],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
function updateSystemUserNode(nodes) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = {
nodes: Object.assign([], nodes)
};
var success = function(data) {
// remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty();
$('#node_selected').val('');
$.map(jumpserver.nodes_selected, function(node_name, index) {
$('#opt_' + index).remove();
// change tr html of user groups.
$('.node_edit tbody').append(
'<tr>' +
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>'
)
});
// clear jumpserver.nodes_selected
jumpserver.nodes_selected = {};
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
jumpserver.nodes_selected = {};
$(document).ready(function () {
$('.select2').select2()
.on("select2:select", function (evt) {
var data = evt.params.data;
jumpserver.assets_selected[data.id] = data.text;
jumpserver.asset_groups_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.assets_selected[data.id];
delete jumpserver.asset_groups_selected[data.id];
});
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
initAssetsTable();
})
.on('click', '.btn-push', function () {
@ -140,11 +216,16 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
success: success
});
})
.on('click', '.btn-test-connective', function () {
@ -152,15 +233,85 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, seen left assets status" %}"
success: success
});
})
.on('click', '.btn-remove-from-node', function() {
var $this = $(this);
var $tr = $this.closest('tr');
var $badge = $tr.find('.bdg_node');
var gid = $badge.data('gid');
var node_name = $badge.html() || $badge.text();
$('#groups_selected').append(
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
);
$tr.remove();
var nodes = $('.bdg_node').map(function () {
return $(this).data('gid');
}).get();
updateSystemUserNode(nodes);
})
.on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false;
}
var nodes = $('.bdg_node').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
updateSystemUserNode(nodes);
})
.on('click', '.btn-push-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
.on('click', '.btn-test-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-test-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
</script>
{% endblock %}

View File

@ -17,11 +17,11 @@
<li class="active">
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
{# <li>#}
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#}
{# <i class="fa fa-bar-chart-o"></i> {% trans 'Attached assets' %}#}
{# </a>#}
{# </li>#}
<li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
@ -152,63 +152,46 @@
</span>
</td>
</tr>
{# <tr>#}
{# <td width="50%">{% trans 'Clear auth' %}:</td>#}
{# <td>#}
{# <span style="float: right">#}
{# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
{# </span>#}
{# </td>#}
{# </tr>#}
{# <tr>#}
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
{# <td>#}
{# <span style="float: right">#}
{# <button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>#}
{# </span>#}
{# </td>#}
{# </tr>#}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# <div class="panel panel-info">#}
{# <div class="panel-heading">#}
{# <i class="fa fa-info-circle"></i> {% trans 'Nodes' %}#}
{# </div>#}
{# <div class="panel-body">#}
{# <table class="table node_edit" id="add-asset2group">#}
{# <tbody>#}
{# <form>#}
{# <tr>#}
{# <td colspan="2" class="no-borders">#}
{# <select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">#}
{# {% for node in nodes_remain %}#}
{# <option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>#}
{# {% endfor %}#}
{# </select>#}
{# </td>#}
{# </tr>#}
{# <tr>#}
{# <td colspan="2" class="no-borders">#}
{# <button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>#}
{# </td>#}
{# </tr>#}
{# </form>#}
{##}
{# {% for node in system_user.nodes.all %}#}
{# <tr>#}
{# <td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>#}
{# <td>#}
{# <button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>#}
{# </td>#}
{# </tr>#}
{# {% endfor %}#}
{# </tbody>#}
{# </table>#}
{# </div>#}
{# </div>#}
</div>
</div>
</div>
@ -338,13 +321,13 @@ $(document).ready(function () {
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
var name = '{{ system_user.name }}';
swal({
title: '你确定清除该系统用户的认证信息吗 ?',
title: "{% trans 'Are you sure to remove authentication information for the system user ?' %}",
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: true
}, function () {
APIUpdateAttr({

View File

@ -3,10 +3,13 @@
{% block help_message %}
<div class="alert alert-info help-message">
系统用户是 Jumpserver跳转登录资产时使用的用户可以理解为登录资产用户如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
目前还不支持Windows的自动推送
{# 系统用户是 Jumpserver跳转登录资产时使用的用户可以理解为登录资产用户如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);#}
{# 简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。#}
{# 系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。#}
{# 目前还不支持Windows的自动推送#}
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%}
{% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %}
</div>
{% endblock %}
@ -135,6 +138,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected System Users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false

View File

@ -19,6 +19,8 @@ urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',
@ -31,10 +33,16 @@ urlpatterns = [
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),

View File

@ -94,6 +94,7 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
context = {
'app': _('assets'),
'action': _('System user asset'),
'nodes_remain': Node.objects.exclude(systemuser=self.object)
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -3,3 +3,6 @@ from django.apps import AppConfig
class AuditsConfig(AppConfig):
name = 'audits'
def ready(self):
from . import signals_handler

4
apps/audits/hands.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from users.models import LoginLog

View File

@ -4,6 +4,11 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
]
class FTPLog(OrgModelMixin):
@ -16,3 +21,40 @@ class FTPLog(OrgModelMixin):
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True)
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
ACTION_CHOICES = (
(ACTION_CREATE, _("Create")),
(ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete"))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action"))
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog):
class Meta:
proxy = True

View File

@ -0,0 +1,59 @@
# -*- 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),
)

View File

@ -66,7 +66,6 @@
{% endblock %}
{% block table_head %}
<th class="text-center"></th>
{# <th class="text-center">{% trans 'ID' %}</th>#}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
@ -82,7 +81,6 @@
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}

View File

@ -0,0 +1,119 @@
{% 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 %}

View File

@ -0,0 +1,96 @@
{% 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 %}

View File

@ -9,5 +9,8 @@ __all__ = ["urlpatterns"]
app_name = "audits"
urlpatterns = [
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
]

View File

@ -1,11 +1,26 @@
from django.conf import settings
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from django.db.models import Q
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from .models import FTPLog
from orgs.utils import current_org
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
def get_resource_type_list():
from users.models import User, UserGroup
from assets.models import Asset, Node, AdminUser, SystemUser, Domain, Gateway
from orgs.models import Organization
from perms.models import AssetPermission
models = [
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
Gateway, Organization, AssetPermission
]
return [model._meta.verbose_name for model in models]
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
@ -53,3 +68,125 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = OperateLog
template_name = 'audits/operate_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = action = resource_type = ''
date_from = date_to = None
actions_dict = dict(OperateLog.ACTION_CHOICES)
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
self.action = self.request.GET.get('action')
self.resource_type = self.request.GET.get('resource_type')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if self.action:
filter_kwargs['action'] = self.action
if self.resource_type:
filter_kwargs['resource_type'] = self.resource_type
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'actions': self.actions_dict,
'resource_type_list': get_resource_type_list(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'action': self.action,
'resource_type': self.resource_type,
"app": _("Audits"),
"action": _("Operate log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = PasswordChangeLog
template_name = 'audits/password_change_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = ''
date_from = date_to = None
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
"app": _("Audits"),
"action": _("Password change log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'audits/login_log_list.html'
model = UserLoginLog
paginate_by = settings.DISPLAY_PER_PAGE
user = keyword = ""
date_to = date_from = None
@staticmethod
def get_org_users():
users = current_org.get_org_users().values_list('username', flat=True)
return users
def get_queryset(self):
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '')
queryset = queryset.filter(
datetime__gt=self.date_from, datetime__lt=self.date_to
)
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Login log'),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'keyword': self.keyword,
'user_list': self.get_org_users(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -74,3 +74,10 @@ class EncryptCharField(EncryptMixin, models.CharField):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)
class FormEncryptMixin:
pass
class FormEncryptCharField(FormEncryptMixin, forms.CharField):
pass

View File

@ -4,66 +4,71 @@ import json
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape
from django.db import transaction
from django.conf import settings
from .models import Setting
from .fields import DictField
from .models import Setting, common_settings
from .fields import DictField, FormEncryptCharField, FormEncryptMixin
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 ""
# 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):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
db_settings = Setting.objects.all()
# db_settings = Setting.objects.all()
for name, field in self.fields.items():
db_value = getattr(db_settings, name).value
db_value = getattr(common_settings, name)
django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is False or db_value:
field.initial = to_form_value(db_value)
field.initial = db_value
elif django_value is False or django_value:
field.initial = to_form_value(to_model_value(django_value))
field.initial = django_value
def save(self, category="default"):
if not self.is_bound:
raise ValueError("Form is not bound")
db_settings = Setting.objects.all()
if self.is_valid():
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == to_form_value(getattr(db_settings, name).value):
continue
defaults = {
'name': name,
'category': category,
'value': to_model_value(value)
}
Setting.objects.update_or_create(defaults=defaults, name=name)
else:
# db_settings = Setting.objects.all()
if not self.is_valid():
raise ValueError(self.errors)
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == getattr(common_settings, name):
continue
encrypted = True if isinstance(field, FormEncryptMixin) else False
try:
setting = Setting.objects.get(name=name)
except Setting.DoesNotExist:
setting = Setting()
setting.name = name
setting.category = category
setting.encrypted = encrypted
setting.cleaned_value = value
setting.save()
return setting
class BasicSettingForm(BaseForm):
SITE_URL = forms.URLField(
@ -88,7 +93,7 @@ class EmailSettingForm(BaseForm):
EMAIL_HOST_USER = forms.CharField(
max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
)
EMAIL_HOST_PASSWORD = forms.CharField(
EMAIL_HOST_PASSWORD = FormEncryptCharField(
max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
required=False, help_text=_("Some provider use token except password")
)
@ -109,7 +114,7 @@ class LDAPSettingForm(BaseForm):
AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
)
AUTH_LDAP_BIND_PASSWORD = forms.CharField(
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='',
widget=forms.PasswordInput, required=False
)
@ -194,6 +199,14 @@ class SecuritySettingForm(BaseForm):
"number of times, no login is allowed during this time interval."
)
)
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False,
label=_("Connection max idle time"),
help_text=_(
'If idle time more than it, disconnect connection(only ssh now) '
'Unit: minute'
),
)
# min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"),
@ -223,9 +236,10 @@ class SecuritySettingForm(BaseForm):
'and resets must contain numeric characters')
)
# special char
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
initial=False, required=False,
label=_("Must contain special characters"),
help_text=_('After opening, the user password changes '
'and resets must contain special characters')
)

View File

@ -7,6 +7,10 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer
signer = get_signer()
class SettingQuerySet(models.QuerySet):
def __getattr__(self, item):
@ -26,6 +30,7 @@ class Setting(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
value = models.TextField(verbose_name=_("Value"))
category = models.CharField(max_length=128, default="default")
encrypted = models.BooleanField(default=False)
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
comment = models.TextField(verbose_name=_("Comment"))
@ -34,10 +39,21 @@ class Setting(models.Model):
def __str__(self):
return self.name
def __getattr__(self, item):
instances = self.__class__.objects.filter(name=item)
if len(instances) == 1:
return instances[0].cleaned_value
else:
return None
@property
def cleaned_value(self):
try:
return json.loads(self.value)
value = self.value
if self.encrypted:
value = signer.unsign(value)
value = json.loads(value)
return value
except json.JSONDecodeError:
return None
@ -45,6 +61,8 @@ class Setting(models.Model):
def cleaned_value(self, item):
try:
v = json.dumps(item)
if self.encrypted:
v = signer.sign(v)
self.value = v
except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e)))
@ -59,11 +77,7 @@ class Setting(models.Model):
pass
def refresh_setting(self):
try:
value = json.loads(self.value)
except json.JSONDecodeError:
return
setattr(settings, self.name, value)
setattr(settings, self.name, self.cleaned_value)
if self.name == "AUTH_LDAP":
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
@ -81,3 +95,5 @@ class Setting(models.Model):
class Meta:
db_table = "settings"
common_settings = Setting()

View File

@ -92,3 +92,9 @@ class AdminUserRequiredMixin(UserPassesTestMixin):
return redirect('orgs:switch-a-org')
return HttpResponseForbidden()
return super().dispatch(request, *args, **kwargs)
class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser:
return True

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_save
from django.conf import settings
from django.db.utils import ProgrammingError, OperationalError
from jumpserver.utils import current_request
from .models import Setting
from .utils import get_logger
from .signals import django_ready, ldap_auth_enable
@ -42,3 +43,9 @@ def ldap_auth_on_changed(sender, enabled=True, **kwargs):
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
@receiver(pre_save, dispatch_uid="my_unique_identifier")
def on_create_set_created_by(sender, instance=None, **kwargs):
if hasattr(instance, 'created_by') and not instance.created_by:
if current_request and current_request.user.is_authenticated:
instance.created_by = current_request.user.name

View File

@ -41,7 +41,7 @@
<h3>{% trans "User login settings" %}</h3>
{% for field in form %}
{% if forloop.counter == 4 %}
{% if forloop.counter == 5 %}
<div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3>
{% endif %}

View File

@ -378,6 +378,15 @@ def get_signer():
return signer
def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get('REMOTE_ADDR', '')
return login_ip
class TeeObj:
origin_stdout = sys.stdout

View File

@ -6,11 +6,11 @@ from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable
class BasicSettingView(AdminUserRequiredMixin, TemplateView):
class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form_class = BasicSettingForm
template_name = "common/basic_setting.html"
@ -36,7 +36,7 @@ class BasicSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class EmailSettingView(AdminUserRequiredMixin, TemplateView):
class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form_class = EmailSettingForm
template_name = "common/email_setting.html"
@ -62,7 +62,7 @@ class EmailSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form_class = LDAPSettingForm
template_name = "common/ldap_setting.html"
@ -90,7 +90,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form_class = TerminalSettingForm
template_name = "common/terminal_setting.html"
@ -120,7 +120,7 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
return render(request, self.template_name, context)
class SecuritySettingView(AdminUserRequiredMixin, TemplateView):
class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm
template_name = "common/security_setting.html"

Binary file not shown.

View File

@ -6,6 +6,8 @@ import pytz
from django.utils import timezone
from django.shortcuts import HttpResponse
from .utils import set_current_request
class TimezoneMiddleware:
def __init__(self, get_response):
@ -45,3 +47,13 @@ class DemoMiddleware:
else:
response = self.get_response(request)
return response
class RequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
set_current_request(request)
response = self.get_response(request)
return response

View File

@ -95,6 +95,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware',
'orgs.middleware.OrgMiddleware',
]
@ -127,7 +128,6 @@ TEMPLATES = [
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'jumpserver.context_processor.jumpserver_processor',
'django.template.context_processors.i18n',
'django.template.context_processors.debug',
'django.template.context_processors.request',
@ -136,6 +136,7 @@ TEMPLATES = [
'django.template.context_processors.static',
'django.template.context_processors.request',
'django.template.context_processors.media',
'jumpserver.context_processor.jumpserver_processor',
'orgs.context_processor.org_processor',
*get_xpack_context_processor(),
],
@ -214,7 +215,10 @@ LOGGING = {
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'class': 'logging.handlers.TimedRotatingFileHandler',
'when': "D",
'interval': 1,
"backupCount": 7,
'formatter': 'main',
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
},
@ -270,7 +274,8 @@ LOGGING = {
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en'
# LANGUAGE_CODE = 'en'
LANGUAGE_CODE = 'zh'
TIME_ZONE = 'Asia/Shanghai'
@ -281,7 +286,9 @@ USE_L10N = True
USE_TZ = True
# I18N translation
LOCALE_PATHS = [os.path.join(BASE_DIR, 'i18n'), ]
LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale'),
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
@ -441,7 +448,8 @@ TERMINAL_REPLAY_STORAGE = {
DEFAULT_PASSWORD_MIN_LENGTH = 6
DEFAULT_LOGIN_LIMIT_COUNT = 7
DEFAULT_LOGIN_LIMIT_TIME = 30
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = {
@ -465,4 +473,4 @@ SWAGGER_SETTINGS = {
'type': 'basic'
}
},
}
}

View File

@ -6,6 +6,8 @@ import os
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
@ -14,7 +16,7 @@ from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from .views import IndexView, LunaView
from .views import IndexView, LunaView, I18NView
schema_view = get_schema_view(
openapi.Info(
@ -78,10 +80,14 @@ app_view_patterns = [
if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
js_i18n_patterns = i18n_patterns(
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
)
urlpatterns = [
path('', IndexView.as_view(), name='index'),
path('luna/', LunaView.as_view(), name='luna-error'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api),
@ -93,6 +99,7 @@ urlpatterns = [
urlpatterns += app_view_patterns
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += js_i18n_patterns
if settings.DEBUG:
urlpatterns += [

29
apps/jumpserver/utils.py Normal file
View File

@ -0,0 +1,29 @@
# -*- 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'))

View File

@ -1,8 +1,10 @@
import datetime
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings
from django.views.generic import TemplateView, View
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin
@ -85,7 +87,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
return self.session_month.values('user').distinct().count()
def get_month_inactive_user_total(self):
return User.objects.all().count() - self.get_month_active_user_total()
return current_org.get_org_users().count() - self.get_month_active_user_total()
def get_month_active_asset_total(self):
return self.session_month.values('asset').distinct().count()
@ -95,7 +97,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
@staticmethod
def get_user_disabled_total():
return User.objects.filter(is_active=False).count()
return current_org.get_org_users().filter(is_active=False).count()
@staticmethod
def get_asset_disabled_total():
@ -173,11 +175,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
class LunaView(View):
def get(self, request):
msg = """
Luna是单独部署的一个程序你需要部署lunacoco配置nginx做url分发,
如果你看到了这个页面证明你访问的不是nginx监听的端口祝你好运
"""
msg = _("<div>Luna is a separately deployed program, you need to deploy Luna, coco, configure nginx for url distribution,</div> "
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
return HttpResponse(msg)
class I18NView(View):
def get(self, request, lang):
referer_url = request.META.get('HTTP_REFERER', '/')
response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
return response

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,107 @@
# 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 "没有记录"

View File

@ -207,6 +207,7 @@ class AdHoc(models.Model):
return {}
def run(self, record=True):
set_to_root_org()
if record:
return self._run_and_record()
else:

View File

@ -6,10 +6,10 @@ from django.views.generic import ListView, DetailView, TemplateView
from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
paginate_by = settings.DISPLAY_PER_PAGE
model = Task
ordering = ('-date_created',)
@ -43,7 +43,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
return super().get_context_data(**kwargs)
class TaskDetailView(AdminUserRequiredMixin, DetailView):
class TaskDetailView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_detail.html'
@ -56,7 +56,7 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_adhoc.html'
@ -69,7 +69,7 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
model = Task
template_name = 'ops/task_history.html'
@ -82,7 +82,7 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
model = AdHoc
template_name = 'ops/adhoc_detail.html'
@ -95,7 +95,7 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
model = AdHoc
template_name = 'ops/adhoc_history.html'
@ -108,7 +108,7 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
model = AdHocRunHistory
template_name = 'ops/adhoc_history_detail.html'
@ -121,6 +121,6 @@ class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
class CeleryTaskLogView(SuperUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask

View File

@ -20,6 +20,9 @@ class Organization(models.Model):
ROOT_ID_NAME = 'ROOT'
DEFAULT_ID_NAME = 'DEFAULT'
class Meta:
verbose_name = _("Organization")
def __str__(self):
return self.name
@ -63,15 +66,18 @@ class Organization(models.Model):
org = cls.default() if default else None
return org
def get_org_users(self):
def get_org_users(self, include_app=False):
from users.models import User
if self.is_default():
users = User.objects.filter(orgs__isnull=True)
elif not self.is_real():
users = User.objects.all()
elif self.is_root():
users = User.objects.all()
else:
users = self.users.all()
users = users.exclude(role=User.ROLE_APP)
if not include_app:
users = users.exclude(role=User.ROLE_APP)
return users
def get_org_admins(self):

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Organization
from .hands import set_current_org, current_org, Node
from perms.models import AssetPermission
from users.models import UserGroup
@receiver(post_save, sender=Organization)
@ -21,3 +24,20 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
if instance and not created:
instance.expire_cache()
@receiver(m2m_changed, sender=Organization.users.through)
def on_org_user_changed(sender, instance=None, **kwargs):
if isinstance(instance, Organization):
old_org = current_org
set_current_org(instance)
if kwargs['action'] == 'pre_remove':
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
for user in users:
perms = AssetPermission.objects.filter(users=user)
user_groups = UserGroup.objects.filter(users=user)
for perm in perms:
perm.users.remove(user)
for user_group in user_groups:
user_group.users.remove(user)
set_current_org(old_org)

View File

@ -40,5 +40,3 @@ def get_current_org():
current_org = LocalProxy(partial(_find, 'current_org'))
current_user = LocalProxy(partial(_find, 'current_user'))
current_request = LocalProxy(partial(_find, 'current_request'))

View File

@ -6,23 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelForm
from orgs.utils import current_org
from .hands import User
from .models import AssetPermission
class AssetPermissionForm(OrgModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.exclude(role=User.ROLE_APP),
label=_("User"),
widget=forms.SelectMultiple(
attrs={
'class': 'select2',
'data-placeholder': _('Select users')
}
),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'initial' not in kwargs:

View File

@ -42,6 +42,7 @@ class AssetPermission(OrgModelMixin):
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Asset permission")
def __str__(self):
return self.name

View File

@ -154,8 +154,8 @@ function activeNav() {
function APIUpdateAttr(props) {
// props = {url: .., body: , success: , error: , method: ,}
props = props || {};
var success_message = props.success_message || '更新成功!';
var fail_message = props.fail_message || '更新时发生未知错误.';
var success_message = props.success_message || gettext('Update is successful!');
var fail_message = props.fail_message || gettext('An unknown error occurred while updating..');
var flash_message = props.flash_message || true;
if (props.flash_message === false){
flash_message = false;
@ -199,25 +199,25 @@ function objectDelete(obj, name, url, redirectTo) {
};
var fail = function() {
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
swal(gettext('Error'), "[ "+name+" ]" + gettext("Being used by the asset, please unbind the asset first."), "error");
};
APIUpdateAttr({
url: url,
body: JSON.stringify(body),
method: 'DELETE',
success_message: "删除成功",
success_message: gettext("Delete the success"),
success: success,
error: fail
});
}
swal({
title: '你确定删除吗 ?',
title: gettext('Are you sure about deleting it?'),
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: gettext('Confirm'),
closeOnConfirm: true,
}, function () {
doDelete()
@ -236,29 +236,29 @@ function orgDelete(obj, name, url, redirectTo){
};
var fail = function(responseText, status) {
if (status === 400){
swal("错误", "[ " + name + " ] 组织中包含未删除信息,请删除后重试", "error");
swal(gettext("Error"), "[ " + name + " ] " + gettext("The organization contains undeleted information. Please try again after deleting"), "error");
}
else if (status === 405){
swal("错误", "请勿在组织 [ "+ name + " ] 下执行此操作,切换到其他组织后重试", "error");
swal(gettext("Error"), " [ "+ name + " ] " + gettext("Do not perform this operation under this organization. Try again after switching to another organization"), "error");
}
};
APIUpdateAttr({
url: url,
body: JSON.stringify(body),
method: 'DELETE',
success_message: "删除成功",
success_message: gettext("Delete the success"),
success: success,
error: fail
});
}
swal({
title: "请确保组织内的以下信息已删除",
text: "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则",
title: gettext("Please ensure that the following information in the organization has been deleted"),
text: gettext("User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission"),
type: "warning",
showCancelButton: true,
cancelButtonText: '取消',
cancelButtonText: gettext('Cancel'),
confirmButtonColor: "#ed5565",
confirmButtonText: '确认',
confirmButtonText: gettext('Confirm'),
closeOnConfirm: true
}, function () {
doDelete();
@ -292,20 +292,20 @@ var jumpserver = {};
jumpserver.checked = false;
jumpserver.selected = {};
jumpserver.language = {
processing: "加载中",
search: "搜索",
processing: gettext('Loading ...'),
search: gettext('Search'),
select: {
rows: {
_: "选中 %d 项",
_: gettext("Selected item %d"),
0: ""
}
},
lengthMenu: "每页 _MENU_",
info: "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项",
lengthMenu: gettext("Per page _MENU_"),
info: gettext('Displays the results of items _START_ to _END_; A total of _TOTAL_ entries'),
infoFiltered: "",
infoEmpty: "",
zeroRecords: "没有匹配项",
emptyTable: "没有记录",
zeroRecords: gettext("No match"),
emptyTable: gettext('No record'),
paginate: {
first: "«",
previous: "",

View File

@ -1 +1,2 @@
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018
{% load i18n %}
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2018

View File

@ -6,6 +6,7 @@
<!-- Custom and plugin javascript -->
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
<script src="{% static "js/inspinia.js" %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script>
activeNav();

View File

@ -1,9 +1,10 @@
{% load i18n %}
<div class="footer fixed">
<div class="pull-right">
Version <strong>1.4.0-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div>
<div>
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2018
</div>
</div>

View File

@ -13,16 +13,68 @@
{# <li>#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
{# </li>#}
{# <li class="dropdown">#}
{# <a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>#}
{# </a>#}
{# </li>#}
{# <li class="dropdown">#}
{# <a class="count-info" href="http://docs.jumpserver.org/" target="_blank">#}
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>#}
{# </a>#}
{# </li>#}
<li class="dropdown">
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-handshake-o"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Help' %} <b class="caret"></b></span>
</a>
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
<li>
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
<i class="fa fa-file-text"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
</a>
</li>
<li>
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
<i class="fa fa-suitcase"></i>
<span class="m-r-sm text-muted welcome-message">{% trans 'Commercial support' %}</span>
</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-globe"></i>
{% ifequal request.COOKIES.django_language 'en' %}
<span class="m-r-sm text-muted welcome-message">English<b class="caret"></b></span>
{% else %}
<span class="m-r-sm text-muted welcome-message">中文<b class="caret"></b></span>
{% endifequal %}
</a>
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
<li>
<a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}">
<i class="fa fa-flag"></i>
<span> 中文</span>
</a>
</li>
<li>
<a id="switch_en" href="{% url 'i18n-switch' lang='en' %}">
<i class="fa fa-flag-checkered"></i>
<span> English</span>
</a>
</li>
</ul>
</li>
<li class="dropdown">
{% if request.user.is_authenticated %}
<a data-toggle="dropdown" class="dropdown-toggle" href="#">

View File

@ -12,7 +12,6 @@
<ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
<li id="user-group"><a href="{% url 'users:user-group-list' %}">{% trans 'User group' %}</a></li>
<li id="login-log"><a href="{% url 'users:login-log-list' %}">{% trans 'Login logs' %}</a></li>
</ul>
</li>
<li id="assets">
@ -68,7 +67,10 @@
<i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="login-log"><a href="{% url 'audits:login-log-list' %}">{% trans 'Login log' %}</a></li>
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
</ul>
</li>
{#<li id="">#}

View File

@ -1,8 +1,9 @@
{% load i18n %}
{% load common_tags %}
{% if is_paginated %}
<div class="col-sm-4">
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite">
显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项
{# 显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项#}
</div>
</div>
<div class="col-sm-4">
@ -10,7 +11,7 @@
<ul class="pagination" style="margin-top: 0; float: right">
{% if page_obj.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
<a data-page="next" href="?page={{ page_obj.previous_page_number}}"></a>
<a data-page="next" class="page" href="?page={{ page_obj.previous_page_number}}"></a>
</li>
{% endif %}
@ -26,7 +27,7 @@
{% if page_obj.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
<a data-page="next" href="?page={{ page_obj.next_page_number }}"></a>
<a data-page="next" class="page" href="?page={{ page_obj.next_page_number }}"></a>
</li>
{% endif %}
</ul>
@ -40,7 +41,7 @@
var old_href = $(this).attr('href').replace('?', '');
var searchArray = searchStr.split('&');
if (searchStr == '') {
if (searchStr === '') {
searchStr = '?page=1'
}
@ -53,6 +54,13 @@
$(this).attr('href', searchArray.join('&'));
}
})
$('#editable_info').html(
"{% trans 'Displays the results of items _START_ to _END_; A total of _TOTAL_ entries' %}"
.replace('_START_', {{ page_obj.start_index }})
.replace('_END_', {{ page_obj.end_index }})
.replace('_TOTAL_', {{ paginator.count }})
)
});
</script>

View File

@ -1,3 +1,4 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
@ -10,6 +11,7 @@
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>
@ -40,7 +42,7 @@
{% endif %}
<div class="row">
<div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">返回</a>
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">{% trans 'Return' %}</a>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<div class="wrapper wrapper-content">
@ -7,7 +8,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-success pull-right">Users</span>
<h5>用户总数</h5>
<h5>{% trans 'Total users' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1>
@ -19,7 +20,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-info pull-right">Hosts</span>
<h5>主机总数</h5>
<h5>{% trans 'Total hosts' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1>
@ -32,7 +33,7 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-primary pull-right">Online</span>
<h5>在线用户</h5>
<h5>{% trans 'Online users' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_users"></span>{{ online_user_count }}</a></h1>
@ -45,7 +46,8 @@
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label label-danger pull-right">Connected</span>
<h5>在线会话</h5>
<h5>{% trans 'Online sessions' %}</h5>
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_hosts"></span>{{ online_asset_count }}</a></h1>
@ -56,13 +58,13 @@
</div>
<div class="row">
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
<h2>活跃用户TOP5</h2>
<small>过去一周共有<span class="text-info">{{ user_visit_count_weekly }}</span>位用户登录<span class="text-success">{{ asset_visit_count_weekly }}</span>次资产.</small>
<h2>{% trans ' Top 5 Active user' %}</h2>
<small>{% trans 'In the past week, a total of ' %}<span class="text-info">{{ user_visit_count_weekly }}</span>{% trans ' users have logged in ' %}<span class="text-success">{{ asset_visit_count_weekly }}</span>{% trans ' times asset.' %}</small>
<ul class="list-group clear-list m-t">
{% for data in user_visit_count_top_five %}
<li class="list-group-item fist-item">
<span class="pull-right">
{{ data.total }}次/周
{{ data.total }}{% trans ' times/week' %}
</span>
<span class="label ">{{ forloop.counter }}</span> {{ data.user }}
</li>
@ -73,21 +75,20 @@
<div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px">
<div class="statistic-box">
<h4>
活跃用户资产占比
{% trans 'Active user asset ratio' %}
</h4>
<p>
以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比
{% trans 'The following graphs describe the percentage of active users per month and assets per user host per month, respectively.' %}
</p>
<div class="row text-center">
<div class="col-sm-6">
<div id="activeUser" style="width: 140px; height: 140px;">
</div>
<h5>用户</h5>
<h5>{% trans 'User' %}</h5>
</div>
<div class="col-sm-6">
<div id="activeAsset" style="width: 140px; height: 140px;"></div>
<h5>主机</h5>
<h5>{% trans 'Host' %}</h5>
</div>
</div>
<div class="m-t">
@ -102,7 +103,7 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>一周Top10资产</h5>
<h5>{% trans 'Top 10 assets in a week' %}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
@ -117,8 +118,8 @@
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-inbox"></i> 一周Top10资产 </h3>
<small><i class="fa fa-map-marker"></i> 登录次数及最近一次登录记录. </small>
<h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small>
</div>
<div class="ibox-content inspinia-timeline">
{% if week_asset_hot_ten %}
@ -129,18 +130,18 @@
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.asset }}">{{ data.asset }}</strong>
<br/>
<small class="text-navy">{{ data.total }}</small>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">最近一次登录用户</p>
<p class="m-b-xs">{% trans 'The last time a user logged in' %}</p>
<p>{{ data.last.user }}</p>
<p>{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
@ -148,14 +149,14 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>最近十次登录</h5>
<h5>{% trans 'Last 10 login' %}</h5>
<div class="ibox-tools">
<span class="label label-info-light">10 Messages</span>
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-paper-plane-o"></i> 登录记录 </h3>
<small><i class="fa fa-map-marker"></i> 最近十次登录记录. </small>
<h3><i class="fa fa-paper-plane-o"></i> {% trans 'Login record' %}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'Last 10 login records.' %}</small>
</div>
<div class="ibox-content">
<div>
@ -168,18 +169,18 @@
</a>
<div class="media-body ">
{% ifequal login.is_finished 0 %}
<small class="pull-right text-navy">{{ login.date_start|timesince }} </small>
<small class="pull-right text-navy">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% else %}
<small class="pull-right">{{ login.date_start|timesince }} </small>
<small class="pull-right">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
{% endifequal %}
<strong>{{ login.user }}</strong> 登录了{{ login.asset }} <br>
<strong>{{ login.user }}</strong> {% trans 'Login in ' %}{{ login.asset }} <br>
<small class="text-muted">{{ login.date_start }}</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
@ -190,7 +191,7 @@
<div class="col-sm-4">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>一周Top10用户</h5>
<h5>{% trans 'Top 10 users in a week' %}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
@ -205,8 +206,8 @@
</div>
</div>
<div class="ibox-content ibox-heading">
<h3><i class="fa fa-user"></i> 一周Top10用户 </h3>
<small><i class="fa fa-map-marker"></i> 用户登录次数及最近一次登录记录. </small>
<h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3>
<small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small>
</div>
<div class="ibox-content inspinia-timeline">
{% if week_user_hot_ten %}
@ -217,18 +218,18 @@
<i class="fa fa-info-circle"></i>
<strong data-toggle="tooltip" title="{{ data.user }}">{{ data.user }}</strong>
<br/>
<small class="text-navy">{{ data.total }}</small>
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
</div>
<div class="col-xs-7 content no-top-border">
<p class="m-b-xs">最近一次登录主机</p>
<p class="m-b-xs">{% trans 'The last time logged on to the host' %}</p>
<p>{{ data.last.asset }}</p>
<p>{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">(暂无)</p>
<p class="text-center">{% trans '(No)' %}</p>
{% endif %}
</div>
</div>
@ -264,8 +265,8 @@ $(document).ready(function(){
var top10Chart = ec.init(document.getElementById('top10'));
var option = {
title : {
text: '月数据总览',
subtext: '一个月内历史汇总',
text: "{% trans 'Monthly data overview' %}",
subtext: "{% trans 'History summary in one month' %}",
x: 'center'
},
tooltip : {
@ -273,7 +274,7 @@ $(document).ready(function(){
},
backgroundColor: '#fff',
legend: {
data:['登陆次数', '活跃用户','活跃资产'],
data:["{% trans 'Login count' %}", "{% trans 'Active users' %}", "{% trans 'Active assets' %}"],
y: 'bottom'
},
toolbox: {
@ -297,21 +298,21 @@ $(document).ready(function(){
],
series : [
{
name:'登陆次数',
name:"{% trans 'Login count' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_total_visit_count|safe}}
},
{
name:'活跃用户',
name:"{% trans 'Active users' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: {{ month_user|safe }}
},
{
name:'活跃资产',
name:"{% trans 'Active assets' %}",
type:'line',
smooth:true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
@ -338,7 +339,7 @@ $(document).ready(function(){
show: false,
orient : 'vertical',
x : 'left',
data:['月活跃用户','禁用用户','月未登陆用户']
data:["{% trans 'Monthly active users' %}", "{% trans 'Disable user' %}", "{% trans 'Month not logged in user' %}"]
},
toolbox: {
show : false,
@ -364,7 +365,7 @@ $(document).ready(function(){
calculable : true,
series : [
{
name:'访问来源',
name:"{% trans 'Access to the source' %}",
type:'pie',
radius : ['50%', '70%'],
itemStyle : {
@ -388,9 +389,9 @@ $(document).ready(function(){
}
},
data:[
{value:{{ month_user_active }}, name:'月活跃用户'},
{value:{{ month_user_disabled }}, name:'禁用用户'},
{value:{{ month_user_inactive }}, name:'月未登陆用户'}
{value:{{ month_user_active }}, name:"{% trans 'Monthly active users' %}"},
{value:{{ month_user_disabled }}, name:"{% trans 'Disable user' %}"},
{value:{{ month_user_inactive }}, name:"{% trans 'Month not logged in user' %}"}
]
}
]
@ -414,7 +415,7 @@ $(document).ready(function(){
show: false,
orient : 'vertical',
x : 'left',
data:['月被登陆主机','禁用主机','月未登陆主机']
data:["{% trans 'Month is logged into the host' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"]
},
toolbox: {
show : false,
@ -440,7 +441,7 @@ $(document).ready(function(){
calculable : true,
series : [
{
name:'访问来源',
name:"{% trans 'Access to the source' %}",
type:'pie',
radius : ['50%', '70%'],
itemStyle : {
@ -464,9 +465,9 @@ $(document).ready(function(){
}
},
data:[
{value:{{ month_asset_active }}, name:'月被登陆主机'},
{value:{{ month_asset_disabled }}, name:'禁用主机'},
{value:{{ month_asset_inactive }}, name:'月未登陆主机'}
{value:{{ month_asset_active }}, name:"{% trans 'Month is logged into the host' %}"},
{value:{{ month_asset_disabled }}, name:"{% trans 'Disable host' %}"},
{value:{{ month_asset_inactive }}, name:"{% trans 'Month not logged on host' %}"}
]
}
]

View File

@ -9,6 +9,7 @@ from django.conf import settings
from users.models import User
from orgs.mixins import OrgModelMixin
from common.models import common_settings
from .backends.command.models import AbstractSessionCommand
@ -62,6 +63,10 @@ class Terminal(models.Model):
configs[k] = getattr(settings, k)
configs.update(self.get_common_storage())
configs.update(self.get_replay_storage())
configs.update({
'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME or
settings.DEFAULT_SECURITY_MAX_IDLE_TIME,
})
return configs
def create_app_user(self):

View File

@ -10,7 +10,7 @@ from django.urls import reverse_lazy, reverse
from common.mixins import JSONResponseMixin
from ..models import Terminal
from ..forms import TerminalForm
from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin
__all__ = [
@ -20,7 +20,7 @@ __all__ = [
]
class TerminalListView(AdminUserRequiredMixin, ListView):
class TerminalListView(SuperUserRequiredMixin, ListView):
model = Terminal
template_name = 'terminal/terminal_list.html'
form_class = TerminalForm
@ -35,7 +35,7 @@ class TerminalListView(AdminUserRequiredMixin, ListView):
return context
class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
class TerminalUpdateView(SuperUserRequiredMixin, UpdateView):
model = Terminal
form_class = TerminalForm
template_name = 'terminal/terminal_update.html'
@ -47,7 +47,7 @@ class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
return context
class TerminalDetailView(LoginRequiredMixin, DetailView):
class TerminalDetailView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
model = Terminal
template_name = 'terminal/terminal_detail.html'
context_object_name = 'terminal'
@ -61,13 +61,13 @@ class TerminalDetailView(LoginRequiredMixin, DetailView):
return context
class TerminalDeleteView(AdminUserRequiredMixin, DeleteView):
class TerminalDeleteView(SuperUserRequiredMixin, DeleteView):
model = Terminal
template_name = 'delete_confirm.html'
success_url = reverse_lazy('terminal:terminal-list')
class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
class TerminalAcceptView(SuperUserRequiredMixin, JSONResponseMixin, UpdateView):
model = Terminal
form_class = TerminalForm
template_name = 'terminal/terminal_modal_accept.html'
@ -92,7 +92,7 @@ class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
return self.render_json_response(data)
class TerminalConnectView(LoginRequiredMixin, DetailView):
class TerminalConnectView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
template_name = 'flash_message_standalone.html'
model = Terminal
@ -118,6 +118,6 @@ class TerminalConnectView(LoginRequiredMixin, DetailView):
return super(TerminalConnectView, self).get_context_data(**kwargs)
class WebTerminalView(LoginRequiredMixin, View):
class WebTerminalView(LoginRequiredMixin, SuperUserRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect('/luna/?' + request.GET.urlencode())

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
#
from .user import *
from .auth import *
from .group import *

View File

@ -1,4 +1,5 @@
# ~*~ coding: utf-8 ~*~
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
@ -6,233 +7,36 @@ from django.urls import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import generics
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_bulk import BulkModelViewSet
from .serializers import UserSerializer, UserGroupSerializer, \
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from .tasks import write_login_log_async
from .models import User, UserGroup, LoginLog
from .utils import check_user_valid, generate_token, get_login_ip, \
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
from .hands import Asset, SystemUser
from orgs.utils import current_org
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
from .hands import Asset, SystemUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from common.utils import get_logger, get_request_ip
from ..serializers import UserSerializer
from ..tasks import write_login_log_async
from ..models import User, LoginLog
from ..utils import check_user_valid, generate_token, \
check_otp_code, increase_login_failed_count, is_block_login, clean_failed_count
from common.permissions import IsOrgAdminOrAppUser
from ..hands import Asset, SystemUser
logger = get_logger(__name__)
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = User.objects.exclude(role="App")
serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id')
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users().values_list('id', flat=True)
queryset = queryset.filter(id__in=org_users)
return queryset
def get_permissions(self):
if self.action == "retrieve":
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
class ChangeUserPasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
queryset = User.objects.all()
serializer_class = ChangeUserPasswordSerializer
def perform_update(self, serializer):
user = self.get_object()
user.password_raw = serializer.validated_data["password"]
user.save()
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserUpdateGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserResetPasswordApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
# Note: we are not updating the user object here.
# We just do the reset-password stuff.
from .utils import send_reset_password_mail
user = self.get_object()
user.password_raw = str(uuid.uuid4())
user.save()
send_reset_password_mail(user)
class UserResetPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
from .utils import send_reset_ssh_key_mail
user = self.get_object()
user.is_public_key_valid = False
user.save()
send_reset_ssh_key_mail(user)
class UserUpdatePKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserPKUpdateSerializer
permission_classes = (IsCurrentUserOrReadOnly,)
def perform_update(self, serializer):
user = self.get_object()
user.public_key = serializer.validated_data['_public_key']
user.save()
class UserUnblockPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = UserSerializer
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def perform_update(self, serializer):
user = self.get_object()
username = user.username if user else ''
key_limit = self.key_prefix_limit.format(username, '*')
key_block = self.key_prefix_block.format(username)
cache.delete_pattern(key_limit)
cache.delete(key_block)
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer
permission_classes = (IsOrgAdmin,)
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = generate_token(request, user)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserProfile(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = UserSerializer
def get_object(self):
return self.request.user
class UserOtpAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
return Response({'msg': 'MFA认证失败'}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
token = generate_token(request, user)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_login_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
class UserAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def post(self, request):
# limit login
username = request.data.get('username')
ip = request.data.get('remote_addr', None)
ip = ip if ip else get_login_ip(request)
key_limit = self.key_prefix_limit.format(username, ip)
key_block = self.key_prefix_block.format(username)
if is_block_login(key_limit):
ip = ip or get_request_ip(request)
if is_block_login(username, ip):
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
return Response({'msg': msg}, status=401)
user, msg = self.check_user_valid(request)
@ -244,8 +48,7 @@ class UserAuthApi(APIView):
'status': False
}
self.write_login_log(request, data)
set_user_login_failed_count_to_cache(key_limit, key_block)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
@ -256,6 +59,8 @@ class UserAuthApi(APIView):
'status': True
}
self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = generate_token(request, user)
return Response(
{
@ -269,7 +74,8 @@ class UserAuthApi(APIView):
return Response(
{
'code': 101,
'msg': '请携带seed值,进行MFA二次认证',
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-users:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
@ -294,7 +100,7 @@ class UserAuthApi(APIView):
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_login_ip(request)
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
@ -345,3 +151,84 @@ class UserConnectionTokenApi(APIView):
if self.request.query_params.get('user-only', None):
self.permission_classes = (AllowAny,)
return super().get_permissions()
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = generate_token(request, user)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserOtpAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
return Response({'msg': _('MFA certification failed')}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
token = generate_token(request, user)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)

26
apps/users/api/group.py Normal file
View File

@ -0,0 +1,26 @@
# -*- 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,)

140
apps/users/api/user.py Normal file
View File

@ -0,0 +1,140 @@
# ~*~ coding: utf-8 ~*~
import uuid
from django.core.cache import cache
from django.contrib.auth import logout
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_bulk import BulkModelViewSet
from ..serializers import UserSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from ..models import User
from orgs.utils import current_org
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
logger = get_logger(__name__)
__all__ = [
'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi',
'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi',
'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi',
]
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = User.objects.exclude(role="App")
serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id')
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users()
queryset = queryset.filter(id__in=org_users)
return queryset
def get_permissions(self):
if self.action == "retrieve":
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
queryset = User.objects.all()
serializer_class = ChangeUserPasswordSerializer
def perform_update(self, serializer):
user = self.get_object()
user.password_raw = serializer.validated_data["password"]
user.save()
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserUpdateGroupSerializer
permission_classes = (IsOrgAdmin,)
class UserResetPasswordApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
# Note: we are not updating the user object here.
# We just do the reset-password stuff.
from ..utils import send_reset_password_mail
user = self.get_object()
user.password_raw = str(uuid.uuid4())
user.save()
send_reset_password_mail(user)
class UserResetPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def perform_update(self, serializer):
from ..utils import send_reset_ssh_key_mail
user = self.get_object()
user.is_public_key_valid = False
user.save()
send_reset_ssh_key_mail(user)
class UserUpdatePKApi(generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = UserPKUpdateSerializer
permission_classes = (IsCurrentUserOrReadOnly,)
def perform_update(self, serializer):
user = self.get_object()
user.public_key = serializer.validated_data['_public_key']
user.save()
class UserUnblockPKApi(generics.UpdateAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = UserSerializer
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def perform_update(self, serializer):
user = self.get_object()
username = user.username if user else ''
key_limit = self.key_prefix_limit.format(username, '*')
key_block = self.key_prefix_block.format(username)
cache.delete_pattern(key_limit)
cache.delete(key_block)
class UserProfileApi(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = UserSerializer
def get_object(self):
return self.request.user
class UserResetOTPApi(generics.RetrieveAPIView):
queryset = User.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
user = self.get_object() if kwargs.get('pk') else request.user
if user == request.user:
msg = _("Could not reset self otp, use profile reset instead")
return Response({"msg": msg}, status=401)
if user.otp_enabled and user.otp_secret_key:
user.otp_secret_key = ''
user.save()
logout(request)
return Response({"msg": "success"})

View File

@ -308,7 +308,7 @@ def user_limit_to():
class UserGroupForm(OrgModelForm):
users = forms.ModelMultipleChoiceField(
queryset=User.objects.exclude(role=User.ROLE_APP),
queryset=User.objects.all(),
label=_("User"),
widget=forms.SelectMultiple(
attrs={
@ -349,12 +349,5 @@ class UserGroupForm(OrgModelForm):
}
class OrgUserField(forms.ModelMultipleChoiceField):
def get_limit_choices_to(self):
return {"orgs"}
class FileForm(forms.Form):
file = forms.FileField()

View File

@ -14,7 +14,7 @@ from django.utils import timezone
from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default
from common.models import Setting
from common.models import common_settings
from orgs.mixins import OrgManager
from orgs.utils import current_org
@ -112,6 +112,10 @@ class User(AbstractUser):
def password_raw(self, password_raw_):
self.set_password(password_raw_)
def set_password(self, raw_password):
self._set_password = True
return super().set_password(raw_password)
@property
def otp_secret_key(self):
return signer.unsign(self._otp_secret_key)
@ -278,8 +282,7 @@ class User(AbstractUser):
@property
def otp_force_enabled(self):
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
if mfa_setting and mfa_setting.cleaned_value:
if common_settings.SECURITY_MFA_AUTH:
return True
return self.otp_level == 2

View File

@ -11,6 +11,7 @@
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
</head>
<body>
@ -23,9 +24,9 @@
<a href="{% url 'index' %}">Jumpserver</a>
</div>
<div>
<a href="{% url 'index' %}">首页</a>
<a href="{% url 'index' %}">{% trans 'Home page' %}</a>
<b></b>
<a href="http://docs.jumpserver.org/zh/docs/">文档</a>
<a href="http://docs.jumpserver.org/zh/docs/">{% trans 'Docs' %}</a>
<b></b>
<a href="https://www.github.com/jumpserver/">GitHub</a>
</div>
@ -33,39 +34,14 @@
<!--内容-->
<article>
<div class="clearfix">
<ul class="change-color">
<li>
<div>
<i class="iconfont icon-step active"></i>
<span></span>
</div>
<div class="back">验证身份</div>
</li>
<li>
<div>
<i class="iconfont icon-step2"></i>
<span></span>
</div>
<div class="back">安装应用</div>
</li>
<li>
<div>
<i class="iconfont icon-step1"></i>
<span></span>
</div>
<div class="back">绑定MFA</div>
</li>
<li>
<div>
<i class="iconfont icon-duigou"></i>
</div>
<div>完成</div>
</li>
</ul>
</div>
<div class="" style="text-align: center; margin-bottom: 50px">
<h2>
{% block small_title %}
{% endblock %}
</h2>
</div>
<div >
<div class="verify">安全令牌验证&nbsp;&nbsp;账户&nbsp;<span>{{ user.username }}</span>&nbsp;&nbsp;请按照以下步骤完成绑定操作</div>
<div class="verify">{% trans 'Security token validation' %}&nbsp;&nbsp;{% trans 'Account' %}&nbsp;<span>{{ user.username }}</span>&nbsp;&nbsp;{% trans 'Follow these steps to complete the binding operation' %}</div>
<div class="line"></div>
{% block content %}

View File

@ -8,10 +8,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<title>忘记密码</title>
<title>{% trans 'Forgot password' %}</title>
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>

View File

@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>
.captcha {
@ -22,18 +23,19 @@
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
</p>
<p>
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
</p>
<p>
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发访问限制。
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
</p>
<p>
改变世界,从一点点开始。
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>

View File

@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<style>
@ -23,18 +24,18 @@
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
</p>
<p>
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
</p>
<p>
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发访问限制。
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
</p>
<p>
改变世界,从一点点开始。
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
@ -47,11 +48,11 @@
<div class="m-t">
<div class="form-group">
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
<div class="text-center">
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
</div>
<p style="margin: 30px auto">&nbsp;请打开手机Google Authenticator应用输入6位动态码</p>
<p style="margin: 30px auto">&nbsp;{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
</div>
<form class="m-t" role="form" method="post" action="">

View File

@ -10,6 +10,7 @@
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
@ -21,23 +22,22 @@
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源跳板机</h2>
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
<p>
Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理
{% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %}
</p>
<p>
我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的追求
{% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %}
</p>
<p>
专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的追求和动力
{% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %}
</p>
<p>
<small>永远年轻,永远热泪盈眶 stay foolish stay hungry</small>
<small>{% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %}</small>
</p>
</div>

View File

@ -5,9 +5,9 @@
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
<div class="tagBtnList">
<a class="label label-primary" id="change_all" value="1">全选</a>
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
{% for field in form %}
{% if field.name != 'users' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>

View File

@ -149,7 +149,6 @@
</div>
</div>
</span></td>
</tr>
<tr>
<td>{% trans 'Force enabled MFA' %}:</td>
@ -166,6 +165,14 @@
</div>
</span></td>
</tr>
<tr>
<td>{% trans 'Reset MFA' %}:</td>
<td>
<span class="pull-right">
<button type="button" class="btn btn-primary btn-xs" id="btn-reset-mfa" style="width: 54px">{% trans 'Reset' %}</button>
</span>
</td>
</tr>
<tr>
<td>{% trans 'Send reset password mail' %}:</td>
<td>
@ -370,6 +377,7 @@ $(document).ready(function() {
text: "{% trans "This will reset the user password and send a reset mail"%}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
@ -395,6 +403,7 @@ $(document).ready(function() {
text: "{% trans 'This will reset the user public key and send a reset mail' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
@ -462,12 +471,19 @@ $(document).ready(function() {
text: "{% trans "After unlocking the user, the user can log in normally."%}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
}, function() {
doReset();
});
}).on('click', '#btn-reset-mfa', function () {
APIUpdateAttr({
url: "{% url 'api-users:user-reset-otp' pk=user_object.id %}",
method: "GET",
success_message: "{% trans 'Reset user MFA success' %}"
})
})
</script>
{% endblock %}

View File

@ -82,6 +82,7 @@ $(document).ready(function() {
text: "{% trans 'This will delete the selected groups !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false

View File

@ -196,6 +196,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false

View File

@ -2,11 +2,15 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Authenticate' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
<p style="margin: 20px auto;">请在手机中打开Google Authenticator应用输入6位动态码</p>
<p style="margin: 20px auto;">{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
</div>
<form class="" role="form" method="post" action="">
@ -22,14 +26,11 @@
<button type="submit" class="next">{% trans 'Next' %}</button>
</form>
<script>
$(function(){
$('.change-color li').eq(2).remove();
$('.change-color li:eq(1) div').eq(1).html('解绑MFA')
})
$('.change-color li:eq(1) div').eq(1).html("{% trans 'Unbind' %}")
})
</script>
{% endblock %}

View File

@ -2,10 +2,14 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Bind' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码获取6位验证码</strong></p>
<p style="margin:20px auto;"><strong style="color: #000000">{% trans 'Use the mobile Google Authenticator application to scan the following qr code for a 6-bit verification code' %}</strong></p>
<div id="qr_code"></div>

View File

@ -2,21 +2,25 @@
{% load i18n %}
{% load static %}
{% block small_title %}
{% trans 'Install' %}
{% endblock %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">请在手机端下载并安装 Google Authenticator 应用</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'Download and install the Google Authenticator application on your phone' %}</strong></p>
<div>
<img src="{% static 'img/authenticator_android.png' %}" width="128" height="128" alt="">
<p>Android手机下载</p>
<p>{% trans 'Android downloads' %}</p>
</div>
<div>
<img src="{% static 'img/authenticator_iphone.png' %}" width="128" height="128" alt="">
<p>iPhone手机下载</p>
<p>{% trans 'iPhone downloads' %}</p>
</div>
<p style="margin: 20px auto;"></p>
<p style="margin: 20px auto;"><strong style="color: #000000">安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)</strong></p>
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'After installation, click the next step to enter the binding page (if installed, go to the next step directly).' %}</strong></p>
</div>
<a href="{% url 'users:user-otp-enable-bind' %}" class="next">{% trans 'Next' %}</a>

View File

@ -2,6 +2,10 @@
{% load static %}
{% load i18n %}
{% block small_title %}
{% trans 'Authenticate' %}
{% endblock %}
{% block content %}
<form class="" role="form" method="post" action="">
{% csrf_token %}

View File

@ -8,6 +8,7 @@
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>

View File

@ -153,7 +153,7 @@
</td>
</tr>
<tr class="no-borders-tr">
<td>{% trans 'Update MFA settings' %}:</td>
<td>{% trans 'Set MFA' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
@ -173,6 +173,16 @@
</span>
</td>
</tr>
{% if request.user.otp_enabled and request.user.otp_secret_key %}
<tr>
<td>{% trans 'Update MFA' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-otp-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
{% endif %}
<tr>
<td>{% trans 'Update SSH public key' %}:</td>
<td>

View File

@ -18,10 +18,12 @@ urlpatterns = [
# path(r'', api.UserListView.as_view()),
path('token/', api.UserToken.as_view(), name='user-token'),
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('profile/', api.UserProfile.as_view(), name='user-profile'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('users/<uuid:pk>/password/', api.ChangeUserPasswordApi.as_view(), name='change-user-password'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
path('users/<uuid:pk>/pubkey/update/', api.UserUpdatePKApi.as_view(), name='user-public-key-update'),

View File

@ -26,6 +26,7 @@ urlpatterns = [
path('profile/otp/enable/install-app/', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'),
path('profile/otp/enable/bind/', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
path('profile/otp/disable/authentication/', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'),
path('profile/otp/update/', views.UserOtpUpdateView.as_view(), name='user-otp-update'),
path('profile/otp/settings-success/', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
# User view
@ -48,5 +49,6 @@ urlpatterns = [
path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
# Login log
# Abandon
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
]

View File

@ -19,7 +19,7 @@ from django.core.cache import cache
from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none
from common.models import Setting
from common.models import common_settings, Setting
from common.forms import SecuritySettingForm
from .models import User, LoginLog
@ -190,16 +190,6 @@ def validate_ip(ip):
return False
def get_login_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get('REMOTE_ADDR', '')
return login_ip
def write_login_log(*args, **kwargs):
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
@ -225,7 +215,12 @@ def get_ip_city(ip, timeout=10):
try:
data = r.json()
if not isinstance(data, int) and data['code'] == 0:
city = data['data']['country'] + ' ' + data['data']['city']
country = data['data']['country']
_city = data['data']['city']
if country == 'XX':
city = _city
else:
city = ' '.join([country, _city])
except ValueError:
pass
return city
@ -272,6 +267,8 @@ def generate_otp_uri(request, issuer="Jumpserver"):
def check_otp_code(otp_secret_key, otp_code):
if not otp_secret_key or not otp_code:
return False
totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code)
@ -310,8 +307,8 @@ def check_password_rules(password):
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE'
number_field_name = 'SECURITY_PASSWORD_NUMBER'
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR'
min_length_setting = Setting.objects.filter(name=min_field_name).first()
min_length = min_length_setting.value if min_length_setting else settings.DEFAULT_PASSWORD_MIN_LENGTH
min_length = getattr(common_settings, min_field_name) or \
settings.DEFAULT_PASSWORD_MIN_LENGTH
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
@ -333,37 +330,40 @@ def check_password_rules(password):
return bool(match_obj)
def set_user_login_failed_count_to_cache(key_limit, key_block):
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
# def increase_login_failed_count(key_limit, key_block):
def increase_login_failed_count(username, ip):
key_limit = key_prefix_limit.format(username, ip)
count = cache.get(key_limit)
count = count + 1 if count else 1
setting_limit_time = Setting.objects.filter(
name='SECURITY_LOGIN_LIMIT_TIME'
).first()
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
else settings.DEFAULT_LOGIN_LIMIT_TIME
setting_limit_count = Setting.objects.filter(
name='SECURITY_LOGIN_LIMIT_COUNT'
).first()
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
else settings.DEFAULT_LOGIN_LIMIT_COUNT
if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60)
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
cache.set(key_limit, count, int(limit_time)*60)
def is_block_login(key_limit):
count = cache.get(key_limit)
def clean_failed_count(username, ip):
key_limit = key_prefix_limit.format(username, ip)
key_block = key_prefix_block.format(username)
cache.delete(key_limit)
cache.delete(key_block)
setting_limit_count = Setting.objects.filter(
name='SECURITY_LOGIN_LIMIT_COUNT'
).first()
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
else settings.DEFAULT_LOGIN_LIMIT_COUNT
def is_block_login(username, ip):
key_limit = key_prefix_limit.format(username, ip)
key_block = key_prefix_block.format(username)
count = cache.get(key_limit, 0)
limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT or \
settings.DEFAULT_LOGIN_LIMIT_COUNT
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60)
if count and count >= limit_count:
return True

View File

@ -11,6 +11,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from common.utils import get_logger
from common.const import create_success_msg, update_success_msg
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from ..models import User, UserGroup
from .. import forms
@ -55,13 +56,10 @@ class UserGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVie
success_message = update_success_msg
def get_context_data(self, **kwargs):
users = User.objects.all()
group_users = [user.id for user in self.object.users.all()]
context = {
'app': _('Users'),
'action': _('Update user group'),
'users': users,
'group_users': group_users
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@ -73,7 +71,7 @@ class UserGroupDetailView(AdminUserRequiredMixin, DetailView):
template_name = 'users/user_group_detail.html'
def get_context_data(self, **kwargs):
users = User.objects.exclude(id__in=self.object.users.all()).exclude(role=User.ROLE_APP)
users = current_org.get_org_users().exclude(id__in=self.object.users.all())
context = {
'app': _('Users'),
'action': _('User group detail'),

View File

@ -8,7 +8,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
@ -21,15 +20,12 @@ from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView
from django.conf import settings
from common.utils import get_object_or_none
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from common.utils import get_object_or_none, get_request_ip
from ..models import User, LoginLog
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
from ..utils import send_reset_password_mail, check_otp_code, \
redirect_user_first_login_or_index, get_user_or_tmp_user, \
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
is_block_login, set_user_login_failed_count_to_cache
is_block_login, increase_login_failed_count, clean_failed_count
from ..tasks import write_login_log_async
from .. import forms
@ -51,8 +47,6 @@ class UserLoginView(FormView):
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
key_prefix_block = "_LOGIN_BLOCK_{}"
def get(self, request, *args, **kwargs):
if request.user.is_staff:
@ -64,12 +58,10 @@ class UserLoginView(FormView):
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_login_ip(request)
ip = get_request_ip(request)
username = self.request.POST.get('username')
key_limit = self.key_prefix_limit.format(username, ip)
if is_block_login(key_limit):
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
@ -77,6 +69,10 @@ class UserLoginView(FormView):
return HttpResponse(_("Please enable cookies and try again."))
set_tmp_user_to_cache(self.request, form.get_user())
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
@ -91,10 +87,8 @@ class UserLoginView(FormView):
self.write_login_log(data)
# limit user login failed count
ip = get_login_ip(self.request)
key_limit = self.key_prefix_limit.format(username, ip)
key_block = self.key_prefix_block.format(username)
set_user_login_failed_count_to_cache(key_limit, key_block)
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
@ -104,7 +98,7 @@ class UserLoginView(FormView):
return super().form_invalid(form)
def get_form_class(self):
ip = get_login_ip(self.request)
ip = get_request_ip(self.request)
if cache.get(self.key_prefix_captcha.format(ip)):
return self.form_class_captcha
else:
@ -139,7 +133,7 @@ class UserLoginView(FormView):
return super().get_context_data(**kwargs)
def write_login_log(self, data):
login_ip = get_login_ip(self.request)
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
@ -178,14 +172,14 @@ class UserLoginOtpView(FormView):
'status': False
}
self.write_login_log(data)
form.add_error('otp_code', _('MFA code invalid'))
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def write_login_log(self, data):
login_ip = get_login_ip(self.request)
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
@ -361,46 +355,6 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
return form
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'users/login_log_list.html'
model = LoginLog
paginate_by = settings.DISPLAY_PER_PAGE
user = keyword = ""
date_to = date_from = None
@staticmethod
def get_org_users():
users = current_org.get_org_users().values_list('username', flat=True)
return users
def get_queryset(self):
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '')
queryset = queryset.filter(
datetime__gt=self.date_from, datetime__lt=self.date_to
)
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Login log list'),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'keyword': self.keyword,
'user_list': self.get_org_users(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class LoginLogListView(ListView):
def get(self, request, *args, **kwargs):
return redirect(reverse('audits:login-log-list'))

View File

@ -33,8 +33,9 @@ from django.contrib.auth import logout as auth_logout
from common.const import create_success_msg, update_success_msg
from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.models import Setting
from common.models import Setting, common_settings
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from .. import forms
from ..models import User, UserGroup
from ..utils import generate_otp_uri, check_otp_code, \
@ -52,7 +53,7 @@ __all__ = [
'UserPublicKeyGenerateView',
'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView',
'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
'UserOtpDisableAuthenticationView',
'UserOtpDisableAuthenticationView', 'UserOtpUpdateView'
]
logger = get_logger(__name__)
@ -197,6 +198,12 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_queryset(self):
queryset = super().get_queryset()
org_users = current_org.get_org_users().values_list('id', flat=True)
queryset = queryset.filter(id__in=org_users)
return queryset
@method_decorator(csrf_exempt, name='dispatch')
class UserExportView(View):
@ -355,10 +362,10 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs):
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
mfa_setting = common_settings.SECURITY_MFA_AUTH
context = {
'action': _('Profile'),
'mfa_setting': mfa_setting.cleaned_value if mfa_setting else False,
'mfa_setting': mfa_setting if mfa_setting is not None else False,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@ -514,7 +521,7 @@ class UserOtpEnableBindView(TemplateView, FormView):
return super().form_valid(form)
else:
form.add_error("otp_code", _("MFA code invalid"))
form.add_error("otp_code", _("MFA code invalid, or ntp sync server time"))
return self.form_invalid(form)
def save_otp(self, otp_secret_key):
@ -539,10 +546,14 @@ class UserOtpDisableAuthenticationView(FormView):
user.save()
return super().form_valid(form)
else:
form.add_error('otp_code', _('MFA code invalid'))
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
class UserOtpUpdateView(UserOtpDisableAuthenticationView):
success_url = reverse_lazy('users:user-otp-enable-bind')
class UserOtpSettingsSuccessView(TemplateView):
template_name = 'flash_message_standalone.html'