pull/1053/head
fit2cloud-fengyi 7 years ago
commit 8e09151a02

@ -19,7 +19,7 @@ from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from common.utils import get_logger, get_object_or_none
from ..hands import IsSuperUser
from ..models import Node
from .. import serializers
@ -29,6 +29,7 @@ logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi',
'NodeAddChildrenApi',
]
@ -75,6 +76,24 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
return Response(response, status=200)
class NodeAddChildrenApi(generics.UpdateAPIView):
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
def put(self, request, *args, **kwargs):
instance = self.get_object()
nodes_id = request.data.get("nodes")
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
for node in children:
if not node:
continue
node.parent = instance
node.save()
return Response("OK")
class NodeAddAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all()

@ -15,7 +15,7 @@ class AssetCreateForm(forms.ModelForm):
model = Asset
fields = [
'hostname', 'ip', 'public_ip', 'port', 'comment',
'nodes', 'is_active', 'admin_user', 'labels',
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
]
widgets = {
@ -44,7 +44,7 @@ class AssetUpdateForm(forms.ModelForm):
class Meta:
model = Asset
fields = [
'hostname', 'ip', 'port', 'nodes', 'is_active',
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels',
]
widgets = {

@ -38,6 +38,14 @@ def default_node():
class Asset(models.Model):
# Important
PLATFORM_CHOICES = (
('Linux', 'Linux'),
('Unix', 'Unix'),
('MacOS', 'MacOS'),
('BSD', 'BSD'),
('Windows', 'Windows'),
('Other', 'Other'),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname'))
@ -64,7 +72,7 @@ class Asset(models.Model):
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
platform = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Platform'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
@ -87,6 +95,12 @@ class Asset(models.Model):
return True, ''
return False, warning
def is_unixlike(self):
if self.platform not in ("Windows", "Other"):
return True
else:
return False
@property
def hardware_info(self):
if self.cpu_count:
@ -99,6 +113,8 @@ class Asset(models.Model):
@property
def is_connective(self):
if not self.is_unixlike():
return True
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
if val == 1:
return True

@ -61,6 +61,9 @@ class Node(models.Model):
assets = Asset.objects.filter(nodes__id=self.id)
return assets
def get_active_assets(self):
return self.get_assets().filter(is_active=True)
def get_all_assets(self):
from .asset import Asset
if self.is_root():
@ -70,6 +73,9 @@ class Node(models.Model):
assets = Asset.objects.filter(nodes__in=nodes)
return assets
def get_all_active_assets(self):
return self.get_all_assets().filter(is_active=True)
def is_root(self):
return self.key == '0'
@ -88,6 +94,10 @@ class Node(models.Model):
else:
return parent
@parent.setter
def parent(self, parent):
self.key = parent.get_next_child_key()
@property
def ancestor(self):
if self.parent == self.__class__.root():

@ -26,14 +26,14 @@ signer = get_signer()
class AssetUser(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
username = models.CharField(max_length=16, verbose_name=_('Username'))
username = models.CharField(max_length=128, verbose_name=_('Username'))
_password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
_private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ])
_public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=32, null=True, verbose_name=_('Created by'))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
@property
def password(self):
@ -175,15 +175,12 @@ class AdminUser(AssetUser):
return info
def get_related_assets(self):
assets = []
for cluster in self.cluster_set.all():
assets.extend(cluster.assets.all())
assets.extend(self.asset_set.all())
return list(set(assets))
assets = self.asset_set.all()
return assets
@property
def assets_amount(self):
return len(self.get_related_assets())
return self.get_related_assets().count()
class Meta:
ordering = ['name']

@ -65,4 +65,8 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class Meta:
model = Node
fields = ['assets']
fields = ['assets']
class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField()

@ -13,7 +13,7 @@
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>{% trans 'Create system user' %}</h5>
<h5>{{ action }}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
@ -81,6 +81,14 @@
{% block custom_foot_js %}
<script>
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
var password_id = '#' + '{{ form.password.id_for_label }}';
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
var shell_id = '#' + '{{ form.shell.id_for_label }}';
var need_change_field = [auto_generate_key, private_key_id, sudo_id, shell_id] ;
function authFieldsDisplay() {
if ($(auto_generate_key).prop('checked')) {
$('.auth-fields').addClass('hidden');
@ -88,9 +96,23 @@
$('.auth-fields').removeClass('hidden');
}
}
function protocolChange() {
if ($(protocol_id).attr('value') === 'rdp') {
$.each(need_change_field, function (index, value) {
$(value).addClass('hidden')
});
$(password_id).removeClass('hidden')
} else {
$.each(need_change_field, function (index, value) {
$(value).removeClass('hidden')
});
}
}
$(document).ready(function () {
$('.select2').select2();
authFieldsDisplay();
protocolChange();
$(auto_generate_key).change(function () {
authFieldsDisplay();
});

@ -13,7 +13,7 @@
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>{% trans 'Create admin user' %}</h5>
<h5>{{ action }}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>

@ -17,6 +17,7 @@
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
<div class="hr-line-dashed"></div>

@ -2,6 +2,12 @@
{% load static %}
{% load i18n %}
{% block help_message %}
<div class="alert alert-info help-message">
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
</div>
{% endblock %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
@ -224,6 +230,9 @@ function editTreeNode() {
if (!current_node){
return
}
if (current_node.value) {
current_node.name = current_node.value;
}
zTree.editName(current_node);
}
@ -308,6 +317,42 @@ function selectQueryNode() {
}
}
function beforeDrag() {
return true
}
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = [];
$.each(treeNodes, function (index, value) {
treeNodesNames.push(value.value);
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
if (confirm(msg)){
return true
} else {
return false
}
}
function onDrag(event, treeId, treeNodes) {
}
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.id);
var body = {nodes: treeNodesIds};
APIUpdateAttr({
url: the_url,
method: "PUT",
body: JSON.stringify(body)
})
}
function initTree() {
var setting = {
view: {
@ -319,11 +364,24 @@ function initTree() {
enable: true
}
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onRightClick: OnRightClick,
beforeClick: beforeClick,
onRename: onRename,
onSelected: onSelected
onSelected: onSelected,
beforeDrag: beforeDrag,
onDrag: onDrag,
beforeDrop: beforeDrop,
onDrop: onDrop
}
};
@ -334,7 +392,8 @@ function initTree() {
{#if (value["key"] === "0") {#}
value["open"] = true;
{# }#}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')'
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
@ -415,7 +474,7 @@ $(document).ready(function(){
current_node = nodes[0];
url += "?node_id=" + current_node.id;
}
window.open(url);
window.open(url, '_self');
})
.on('click', '.btn_asset_delete', function () {
var $this = $(this);

@ -22,6 +22,7 @@
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
<div class="hr-line-dashed"></div>

@ -44,6 +44,7 @@ urlpatterns = [
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
]

@ -58,8 +58,7 @@ class UserAssetListView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Asset list'),
'action': _('My assets'),
'system_users': SystemUser.objects.all(),
}
kwargs.update(context)
@ -248,6 +247,7 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
f = form.cleaned_data['file']
det_result = chardet.detect(f.read())
f.seek(0) # reset file seek index
file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
csv_file = StringIO(file_data)
reader = csv.reader(csv_file)

@ -68,10 +68,10 @@ class BaseForm(forms.Form):
class BasicSettingForm(BaseForm):
SITE_URL = forms.URLField(
label=_("Current SITE URL"),
help_text="http://jumpserver.abc.com:8080"
help_text="eg: http://jumpserver.abc.com:8080"
)
USER_GUIDE_URL = forms.URLField(
label=_("User Guide URL"),
label=_("User Guide URL"), required=False,
help_text=_("User first login update profile done redirect to it")
)
EMAIL_SUBJECT_PREFIX = forms.CharField(
@ -135,7 +135,7 @@ class LDAPSettingForm(BaseForm):
AUTH_LDAP_START_TLS = forms.BooleanField(
label=_("Use SSL"), initial=False, required=False
)
AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False)
AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False, required=False)
class TerminalSettingForm(BaseForm):

@ -99,9 +99,8 @@ class DatetimeSearchMixin:
if date_from_s:
date_from = timezone.datetime.strptime(date_from_s, self.date_format)
self.date_from = date_from.replace(
tzinfo=timezone.get_current_timezone()
)
tz = timezone.get_current_timezone()
self.date_from = tz.localize(date_from)
else:
self.date_from = timezone.now() - timezone.timedelta(7)

@ -73,17 +73,20 @@ def to_html(s):
@register.filter
def time_util_with_seconds(date_from, date_to):
if date_from and date_to:
delta = date_to - date_from
seconds = delta.seconds
if seconds < 60:
return '{} s'.format(seconds)
elif seconds < 60*60:
return '{} m'.format(seconds//60)
else:
return '{} h'.format(seconds//3600)
else:
if not date_from:
return ''
if not date_to:
return ''
date_to = timezone.now()
delta = date_to - date_from
seconds = delta.seconds
if seconds < 60:
return '{} s'.format(seconds)
elif seconds < 60*60:
return '{} m'.format(seconds//60)
else:
return '{} h'.format(seconds//3600)
@register.filter

File diff suppressed because it is too large Load Diff

@ -397,6 +397,6 @@ BOOTSTRAP3 = {
}
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25
DEFAULT_EXPIRED_YEARS = 70
USER_GUIDE_URL = ""

@ -4,16 +4,16 @@ from __future__ import unicode_literals
from django.conf.urls import url, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.static import serve as static_serve
from rest_framework.schemas import get_schema_view
from rest_framework_swagger.renderers import SwaggerUIRenderer, OpenAPIRenderer
from .views import IndexView
from .views import IndexView, LunaView
schema_view = get_schema_view(title='Users API', renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer])
urlpatterns = [
url(r'^$', IndexView.as_view(), name='index'),
url(r'^luna/$', LunaView.as_view(), name='luna-error'),
url(r'^users/', include('users.urls.views_urls', namespace='users')),
url(r'^assets/', include('assets.urls.views_urls', namespace='assets')),
url(r'^perms/', include('perms.urls.views_urls', namespace='perms')),

@ -1,4 +1,5 @@
from django.views.generic import TemplateView
from django.http import HttpResponse
from django.views.generic import TemplateView, View
from django.utils import timezone
from django.db.models import Count
from django.contrib.auth.mixins import LoginRequiredMixin
@ -45,7 +46,8 @@ class IndexView(LoginRequiredMixin, TemplateView):
return self.session_week.values('user').distinct().count()
def get_week_login_asset_count(self):
return self.session_week.values('asset').distinct().count()
return self.session_week.count()
# return self.session_week.values('asset').distinct().count()
def get_month_day_metrics(self):
month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0']
@ -149,3 +151,12 @@ class IndexView(LoginRequiredMixin, TemplateView):
kwargs.update(context)
return super(IndexView, self).get_context_data(**kwargs)
class LunaView(View):
def get(self, request):
msg = """
Luna是单独部署的一个程序你需要部署lunacoco配置nginx做url分发,
如果你看到了这个页面证明你访问的不是nginx监听的端口祝你好运
"""
return HttpResponse(msg)

@ -54,7 +54,11 @@ class UserGrantedAssetsApi(ListAPIView):
user = self.request.user
for k, v in NodePermissionUtil.get_user_assets(user).items():
k.system_users_granted = v
if k.is_unixlike():
system_users_granted = [s for s in v if s.protocol == 'ssh']
else:
system_users_granted = [s for s in v if s.protocol == 'rdp']
k.system_users_granted = system_users_granted
queryset.append(k)
return queryset
@ -118,9 +122,16 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
user = get_object_or_404(User, id=user_id)
nodes = NodePermissionUtil.get_user_nodes_with_assets(user)
assets = {}
for k, v in NodePermissionUtil.get_user_assets(user).items():
if k.is_unixlike():
system_users_granted = [s for s in v if s.protocol == 'ssh']
else:
system_users_granted = [s for s in v if s.protocol == 'rdp']
assets[k] = system_users_granted
for node, v in nodes.items():
for asset in v['assets']:
asset.system_users_granted = v['system_users']
asset.system_users_granted = assets[asset]
node.assets_granted = v['assets']
queryset.append(node)
return queryset

@ -12,7 +12,7 @@ class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = NodePermission
fields = [
'node', 'user_group', 'system_user',
'id', 'node', 'user_group', 'system_user',
'is_active', 'date_expired'
]

@ -14,7 +14,7 @@
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>{% trans 'Create asset permission ' %}</h5>
<h5>{{ action }}</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>

@ -215,16 +215,6 @@ $(document).ready(function(){
initTable();
initTree();
})
.on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node=" + current_node.id;
}
window.open(url);
})
.on('click', '.btn-del', function () {
var $this = $(this);
var uid = $this.data('uid');
@ -241,7 +231,7 @@ $(document).ready(function(){
current_node = nodes[0];
url += "?node_id=" + current_node.id;
}
window.open(url);
window.open(url, '_self');
})
</script>

@ -56,7 +56,7 @@ class NodePermissionUtil:
nodes_with_assets = dict()
for node, system_users in nodes.items():
nodes_with_assets[node] = {
'assets': node.get_assets(),
'assets': node.get_active_assets(),
'system_users': system_users
}
return nodes_with_assets
@ -87,7 +87,7 @@ class NodePermissionUtil:
nodes_with_assets = dict()
for node, system_users in nodes.items():
nodes_with_assets[node] = {
'assets': node.get_assets(),
'assets': node.get_active_assets(),
'system_users': system_users
}
return nodes_with_assets

@ -427,3 +427,9 @@ div.dataTables_wrapper div.dataTables_filter {
text-align: center;
padding: 5px 0;
}
.profile-dropdown li a {
font-size: 12px !important;
}

@ -3299,7 +3299,7 @@ body.tour-open .animated {
border-bottom: 1px solid #e7eaec;
}
body {
font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-family: "open sans", "Helvetica Neue", "微软雅黑", Helvetica, Arial, sans-serif;
background-color: #2f4050;
font-size: 13px;
color: #676a6c;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

@ -0,0 +1 @@
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018

@ -14,8 +14,13 @@
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
{# </li>#}
<li class="dropdown">
<a class="dropdown-toggle count-info" data-toggle="dropdown" href="#">
<span class="m-r-sm text-muted welcome-message">{% trans 'Help' %}</span>
<a class="dropdown-toggle count-info" data-toggle="dropdown" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1">
<span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>
</a>
</li>
<li class="dropdown">
<a class="count-info" href="http://jumpserver.readthedocs.io/">
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
</a>
</li>
<li class="dropdown">
@ -28,9 +33,8 @@
</span>
</span>
</a>
<ul class="dropdown-menu animated fadeInRight m-t-xs">
<li><a href="{% url 'users:user-profile' %}"><i class="fa fa-cogs"> </i><span> {% trans 'Profile' %}</span></a></li>
<li class="divider"></li>
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
<li><a href="{% url 'users:user-profile' %}"><i class="fa fa-cogs"> </i><span> {% trans 'Profile' %}</span></a></li>
{% if request.user.is_superuser %}
{% if request.COOKIES.IN_ADMIN_PAGE == 'No' %}
<li><a id="switch_admin"><i class="fa fa-exchange"></i><span> {% trans 'Admin page' %}</span></a></li>
@ -57,11 +61,11 @@
<li>
<a href="">{% trans 'Dashboard' %}</a>
</li>
<li>
{% if app %}
<li>
<a>{{ app }}</a>
{% endif %}
</li>
{% endif %}
{% if action %}
<li class="active">
<strong>{{ action }}</strong>

@ -1,12 +1,13 @@
{% load i18n %}
<li id="index">
<a href="{% url 'index' %}">
<i class="fa fa-dashboard" style="font-size: 13px"></i> <span class="nav-label">{% trans 'Dashboard' %}</span><span class="label label-info pull-right"></span>
<i class="fa fa-dashboard" style="width: 14px"></i> <span class="nav-label">{% trans 'Dashboard' %}</span><span
class="label label-info pull-right"></span>
</a>
</li>
<li id="users">
<a href="#">
<i class="fa fa-group" style="font-size: 13px"></i> <span class="nav-label">{% trans 'Users' %}</span><span class="fa arrow"></span>
<i class="fa fa-group" style="width: 14px"></i> <span class="nav-label">{% trans 'Users' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
@ -16,7 +17,7 @@
</li>
<li id="assets">
<a>
<i class="fa fa-inbox"></i> <span class="nav-label">{% trans 'Assets' %}</span><span class="fa arrow"></span>
<i class="fa fa-inbox" style="width: 14px"></i> <span class="nav-label">{% trans 'Assets' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset list' %}</a></li>
@ -26,7 +27,7 @@
</ul>
</li>
<li id="perms">
<a href="#"><i class="fa fa-edit"></i> <span class="nav-label">{% trans 'Perms' %}</span><span class="fa arrow"></span></a>
<a href="#"><i class="fa fa-edit" style="width: 14px"></i> <span class="nav-label">{% trans 'Perms' %}</span><span class="fa arrow"></span></a>
<ul class="nav nav-second-level">
<li id="asset-permission">
<a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a>
@ -35,18 +36,23 @@
</li>
<li id="terminal">
<a>
<i class="fa fa-rocket"></i> <span class="nav-label">{% trans 'Sessions' %}</span><span class="fa arrow"></span>
<i class="fa fa-rocket" style="width: 14px"></i> <span class="nav-label">{% trans 'Sessions' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="session-online"><a href="{% url 'terminal:session-online-list' %}">{% trans 'Session online' %}</a></li>
<li id="session-offline"><a href="{% url 'terminal:session-offline-list' %}">{% trans 'Session offline' %}</a></li>
<li id="command"><a href="{% url 'terminal:command-list' %}">{% trans 'Commands' %}</a></li>
<li>
<a href="{% url 'terminal:web-terminal' %}" target="_blank">
<span class="nav-label">{% trans 'Web terminal' %}</span>
</a>
</li>
<li id="terminal"><a href="{% url 'terminal:terminal-list' %}">{% trans 'Terminal' %}</a></li>
</ul>
</li>
<li id="ops">
<a>
<i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
<i class="fa fa-coffee" style="width: 14px"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>

@ -1,16 +1,16 @@
{% load i18n %}
<li id="assets">
<a href="{% url 'assets:user-asset-list' %}">
<i class="fa fa-files-o"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span>
<i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span>
</a>
</li>
<li id="users">
<a href="{% url 'users:user-profile' %}">
<i class="fa fa-user" ></i> <span class="nav-label">{% trans 'Profile' %}</span><span class="label label-info pull-right"></span>
<i class="fa fa-user" style="width: 14px"></i> <span class="nav-label">{% trans 'Profile' %}</span><span class="label label-info pull-right"></span>
</a>
</li>
<li >
<a href="{% url 'terminal:web-terminal' %}" target="_blank"><i class="fa fa-window-maximize"></i>
<a href="{% url 'terminal:web-terminal' %}" target="_blank"><i class="fa fa-window-maximize" style="width: 14px"></i>
<span class="nav-label">{% trans 'Web terminal' %}</span>
</a>
</li>

@ -3,7 +3,7 @@
<li class="nav-header">
<div class="dropdown profile-element">
<div href="http://www.jumpserver.org" target="_blank">
<img alt="image" height="55" src="/static/img/logo-text.png" style="margin-left: 10px"/>
<img alt="logo" height="55" width="185" src="/static/img/logo-text.png" style="margin-left: 20px"/>
</div>
</div>
<div class="clearfix"></div>

@ -49,7 +49,7 @@
<hr/>
<div class="row">
<div class="col-md-6">
Copyright Jumpserver.org
{% include '_copyright.html' %}
</div>
<div class="col-md-6 text-right">
<small>2014-2018</small>

@ -11,7 +11,7 @@
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1>
<small>All user</small>
<small>All users</small>
</div>
</div>
</div>
@ -23,7 +23,7 @@
</div>
<div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1>
<small>All host</small>
<small>All hosts</small>
</div>
</div>
</div>
@ -36,7 +36,7 @@
</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>
<small>Online user</small>
<small>Online users</small>
</div>
</div>
</div>
@ -57,7 +57,7 @@
<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>
<small>过去一周共有<span class="text-info">{{ user_visit_count_weekly }}</span>位用户登录<span class="text-success">{{ asset_visit_count_weekly }}</span>资产.</small>
<ul class="list-group clear-list m-t">
{% for data in user_visit_count_top_five %}
<li class="list-group-item fist-item">

@ -25,18 +25,22 @@ def get_all_replay_storage():
class TerminalForm(forms.ModelForm):
command_storage = forms.ChoiceField(choices=get_all_command_storage(),
label=_("Command storage"))
replay_storage = forms.ChoiceField(choices=get_all_replay_storage(),
label=_("Replay storage"))
command_storage = forms.ChoiceField(
choices=get_all_command_storage(),
label=_("Command storage")
)
replay_storage = forms.ChoiceField(
choices=get_all_replay_storage(),
label=_("Replay storage")
)
class Meta:
model = Terminal
fields = ['name', 'remote_addr', 'ssh_port', 'http_port', 'comment', 'command_storage', 'replay_storage']
fields = [
'name', 'remote_addr', 'ssh_port', 'http_port', 'comment',
'command_storage', 'replay_storage',
]
help_texts = {
'ssh_port': _("Coco ssh listen port"),
'http_port': _("Coco http/ws listen port"),
}
widgets = {
'name': forms.TextInput(attrs={'readonly': 'readonly'})
}

@ -4,6 +4,7 @@ import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.conf import settings
from users.models import User
@ -127,6 +128,7 @@ class Session(models.Model):
has_replay = models.BooleanField(default=False, verbose_name=_("Replay"))
has_command = models.BooleanField(default=False, verbose_name=_("Command"))
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE)
date_last_active = models.DateTimeField(verbose_name=_("Date last active"), default=timezone.now)
date_start = models.DateTimeField(verbose_name=_("Date start"))
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)

@ -1,20 +1,36 @@
# -*- coding: utf-8 -*-
#
import datetime
from celery import shared_task
from django.utils import timezone
from common.celery import register_as_period_task, after_app_ready_start, \
after_app_shutdown_clean
from .models import Status, Session
CACHE_REFRESH_INTERVAL = 10
RUNNING = False
# Todo: 定期清理上报history
@shared_task
def clean_terminal_history():
pass
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def delete_terminal_status_period():
yesterday = timezone.now() - datetime.timedelta(days=3)
Status.objects.filter(date_created__lt=yesterday).delete()
@shared_task
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def clean_orphan_session():
active_sessions = Session.objects.filter(is_finished=False)
for session in active_sessions:
if not session.terminal.is_active:
session.is_finished = True
session.save()

@ -75,6 +75,7 @@
<th class="text-center">{% trans 'Terminal' %}</th>
<th class="text-center">{% trans 'Command' %}</th>
<th class="text-center">{% trans 'Date start' %}</th>
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
<th class="text-center">{% trans 'Duration' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
{% endblock %}
@ -94,6 +95,7 @@
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
<td class="text-center">{{ session.date_start }}</td>
{# <td class="text-center">{{ session.date_last_active }}</td>#}
<td class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td>
<td>
{% if session.is_finished %}
@ -107,6 +109,21 @@
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
<div id="actions" >
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="delete">{% trans 'Terminate selected' %}</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
{% trans 'Submit' %}
</button>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>

@ -6,7 +6,7 @@ from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _
from common.mixins import DatetimeSearchMixin
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
from ..models import Command
from .. import utils
from ..backends import get_multi_command_store
@ -15,7 +15,7 @@ __all__ = ['CommandListView']
common_storage = get_multi_command_store()
class CommandListView(DatetimeSearchMixin, ListView):
class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView):
model = Command
template_name = "terminal/command_list.html"
context_object_name = 'command_list'

@ -97,7 +97,7 @@ class SessionOfflineListView(SessionListView):
return super().get_context_data(**kwargs)
class SessionDetailView(SingleObjectMixin, ListView):
class SessionDetailView(SingleObjectMixin, AdminUserRequiredMixin, ListView):
template_name = 'terminal/session_detail.html'
model = Session
object = None

@ -145,7 +145,8 @@ class UserAuthApi(APIView):
if not login_ip:
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for:
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get("REMOTE_ADDR")

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

@ -16,7 +16,8 @@ class AccessKey(models.Model):
default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, verbose_name='User', on_delete=models.CASCADE, related_name='access_key')
user = models.ForeignKey(User, verbose_name='User',
on_delete=models.CASCADE, related_name='access_key')
def get_id(self):
return str(self.id)

@ -22,6 +22,7 @@ class UserGroup(NoDeleteModelMixin):
class Meta:
ordering = ['name']
verbose_name = _("User group")
@classmethod
def initial(cls):

@ -151,6 +151,10 @@ class User(AbstractUser):
def save(self, *args, **kwargs):
if not self.name:
self.name = self.username
if self.username == 'admin':
self.role = 'Admin'
self.is_active = True
super().save(*args, **kwargs)
@property
@ -247,6 +251,7 @@ class User(AbstractUser):
class Meta:
ordering = ['username']
verbose_name = _("User")
#: Use this method initial user
@classmethod

@ -1,21 +1,5 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import Signal
from django.dispatch import Signal, receiver
from django.db.models.signals import post_save
from common.utils import get_logger
from .models import User
logger = get_logger(__file__)
@receiver(post_save, sender=User)
def on_user_created(sender, instance=None, created=False, **kwargs):
if created:
logger.debug("Receive user `{}` create signal".format(instance.name))
from .utils import send_user_created_mail
logger.info(" - Sending welcome mail ...".format(instance.name))
if instance.email:
send_user_created_mail(instance)
post_user_create = Signal(providing_args=('user',))

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import post_save
from common.utils import get_logger
from .models import User
logger = get_logger(__file__)
@receiver(post_save, sender=User)
def on_user_created(sender, instance=None, created=False, **kwargs):
if created:
logger.debug("Receive user `{}` create signal".format(instance.name))
from .utils import send_user_created_mail
logger.info(" - Sending welcome mail ...".format(instance.name))
if instance.email:
send_user_created_mail(instance)

@ -51,11 +51,8 @@
</div>
<hr/>
<div class="row">
<div class="col-md-6">
Copyright Jumpserver.org
</div>
<div class="col-md-6 text-right">
<small>© 2014-2018</small>
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>

@ -22,24 +22,27 @@
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源跳板</h2>
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒</h2>
<p>
Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理
全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
</p>
<p>
我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的追求
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
</p>
<p>
专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的追求和动力
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发访问限制。
</p>
<p>
<small>永远年轻,永远热泪盈眶 stay foolish stay hungry</small>
改变世界,从一点点开始。
</p>
</div>
<div class="col-md-6">
<div class="ibox-content">
<div><img src="{% static 'img/logo.png' %}" width="82" height="82"> <span class="font-bold text-center" style="font-size: 32px; font-family: inherit">{% trans 'Login' %}</span></div>
<div>
<img src="{% static 'img/logo.png' %}" width="60" height="60">
<span class="font-bold text-center" style="font-size: 24px; font-family: inherit; margin-left: 20px">{% trans 'Login' %}</span>
</div>
<form class="m-t" role="form" method="post" action="">
{% csrf_token %}
{% if form.errors %}
@ -60,12 +63,16 @@
</div>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Login' %}</button>
{% if demo_mode %}
<p class="text-muted font-bold" style="color: red">
Demo账号: admin 密码: admin
</p>
{% endif %}
<a href="{% url 'users:forgot-password' %}">
<small>{% trans 'Forgot password' %}?</small>
</a>
<p class="text-muted text-center">
</p>
</form>
<p class="m-t">
</p>
@ -74,11 +81,8 @@
</div>
<hr/>
<div class="row">
<div class="col-md-6">
Copyright 北京堆栈科技有限公司
</div>
<div class="col-md-6 text-right">
<small>© 2014-2018</small>
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>

@ -70,11 +70,8 @@
</div>
<hr/>
<div class="row">
<div class="col-md-6">
Copyright Jumpserver.org
</div>
<div class="col-md-6 text-right">
<small>© 2014-2018</small>
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>

@ -92,8 +92,8 @@ class UserGroupGrantedAssetView(AdminUserRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = {
'app': 'User',
'action': 'User group granted asset',
'app': _('Users'),
'action': _('User group granted asset'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

@ -1,6 +1,7 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
import os
from django import forms
from django.shortcuts import render
from django.contrib.auth import login as auth_login, logout as auth_logout
@ -56,6 +57,7 @@ class UserLoginView(FormView):
return HttpResponse(_("Please enable cookies and try again."))
auth_login(self.request, form.get_user())
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
@ -75,6 +77,13 @@ class UserLoginView(FormView):
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, reverse('index')))
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView):
@ -237,7 +246,7 @@ class LoginLogListView(DatetimeSearchMixin, ListView):
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = self.queryset.filter(
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)

@ -6,6 +6,7 @@ import json
import uuid
import csv
import codecs
import chardet
from io import StringIO
from django.contrib import messages
@ -20,6 +21,7 @@ from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.views import View
from django.views.generic.base import TemplateView
from django.db import transaction
from django.views.generic.edit import (
CreateView, UpdateView, FormMixin, FormView
)
@ -33,7 +35,7 @@ from common.utils import get_logger, get_object_or_none, is_uuid
from .. import forms
from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin
from ..signals import on_user_created
from ..signals import post_user_create
__all__ = [
@ -212,8 +214,10 @@ class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
# todo: need be patch, method to long
def form_valid(self, form):
file = form.cleaned_data['file']
data = file.read().decode('utf-8').strip(codecs.BOM_UTF8.decode('utf-8'))
f = form.cleaned_data['file']
det_result = chardet.detect(f.read())
f.seek(0) # reset file seek index
data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
csv_file = StringIO(data)
reader = csv.reader(csv_file)
csv_data = [row for row in reader]
@ -252,15 +256,15 @@ class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
else:
continue
user_dict[k] = v
user = get_object_or_none(User, id=id_) if is_uuid(id_) else None
user = get_object_or_none(User, id=id_) if id_ and is_uuid(id_) else None
if not user:
try:
groups = user_dict.pop('groups')
user = User.objects.create(**user_dict)
user.groups.set(groups)
created.append(user_dict['username'])
on_user_created.send(self.__class__, user=user)
with transaction.atomic():
groups = user_dict.pop('groups')
user = User.objects.create(**user_dict)
user.groups.set(groups)
created.append(user_dict['username'])
post_user_create.send(self.__class__, user=user)
except Exception as e:
failed.append('%s: %s' % (user_dict['username'], str(e)))
else:
@ -309,7 +313,6 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Profile'),
}
kwargs.update(context)

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@ -4,7 +4,7 @@
contain the root `toctree` directive.
Jumpserver 文档
====================
======================================
目录:

@ -0,0 +1,51 @@
简介
============
Jumpserver是混合云下更好用的堡垒机, 分布式架构设计无限扩展轻松对接混合云资产支持使用云存储AWS S3, ES等存储录像、命令
Jumpserver颠覆传统堡垒机, 无主机和并发数量限制支持水平扩容FIT2CLOUD提供完备的商业服务支持用户无后顾之忧
Jumpserver拥有极致的用户体验, 极致UI体验容器化的部署方式部署过程方便快捷可持续升级
组件说明
++++++++++++++++++++++++
Jumpserver
```````````
现指Jumpserver管理后台是核心组件(Core), 使用 Django Class Based View 风格开发支持Restful API。
`Github <https://github.com/jumpserver/jumpserver.git>`_
Coco
````````
实现了SSH Server 和 Web Terminal Server的组件提供ssh和websocket接口, 使用 Paramiko 和 Flask 开发。
`Github <https://github.com/jumpserver/coco.git>`__
Luna
````````
现在是Web Terminal前端计划前端页面都由该项目提供Jumpserver只提供API不再负责后台渲染html等。
`Github <https://github.com/jumpserver/luna.git>`__
Guacamole
```````````
Apache 跳板机项目Jumpserver使用其组件实现RDP功能Jumpserver并没有修改其代码而是添加了额外的插件支持Jumpserver调用
Jumpserver-python-sdk
```````````````````````
Jumpserver API Python SDKCoco目前使用该SDK与Jumpserver API交互
`Github <https://github.com/jumpserver/jumpserver-python-sdk.git>`__
组件架构图
++++++++++++++++++++++++
.. image:: _static/img/structure.png
:alt: 组件架构图

@ -1,7 +1,7 @@
用户使用文档
===============
=============
这部分给您介绍Jumpserver的用户使用方法。
这部分给您介绍Jumpserver的用户管理模块的使用方法。
.. toctree::
:maxdepth: 1

@ -56,8 +56,8 @@ uritemplate==3.0.0
urllib3==1.22
vine==1.1.4
gunicorn==19.7.1
https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat
#django_celery_beat==1.1.0
#https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat
django_celery_beat==1.1.1
ephem==3.7.6.0
python-gssapi==0.6.4
jms-es-sdk

Loading…
Cancel
Save