pull/1053/head
fit2cloud-fengyi 2018-03-06 18:30:42 +08:00
commit 8e09151a02
60 changed files with 686 additions and 392 deletions

View File

@ -19,7 +19,7 @@ from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _ 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 ..hands import IsSuperUser
from ..models import Node from ..models import Node
from .. import serializers from .. import serializers
@ -29,6 +29,7 @@ logger = get_logger(__file__)
__all__ = [ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeViewSet', 'NodeChildrenApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi',
'NodeAddChildrenApi',
] ]
@ -75,6 +76,24 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
return Response(response, status=200) 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): class NodeAddAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all() queryset = Node.objects.all()

View File

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

View File

@ -38,6 +38,14 @@ def default_node():
class Asset(models.Model): class Asset(models.Model):
# Important # Important
PLATFORM_CHOICES = (
('Linux', 'Linux'),
('Unix', 'Unix'),
('MacOS', 'MacOS'),
('BSD', 'BSD'),
('Windows', 'Windows'),
('Other', 'Other'),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) 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_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')) 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 = 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_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')) 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 True, ''
return False, warning return False, warning
def is_unixlike(self):
if self.platform not in ("Windows", "Other"):
return True
else:
return False
@property @property
def hardware_info(self): def hardware_info(self):
if self.cpu_count: if self.cpu_count:
@ -99,6 +113,8 @@ class Asset(models.Model):
@property @property
def is_connective(self): def is_connective(self):
if not self.is_unixlike():
return True
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname)) val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
if val == 1: if val == 1:
return True return True

View File

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

View File

@ -26,14 +26,14 @@ signer = get_signer()
class AssetUser(models.Model): class AssetUser(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) 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')) _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, ]) _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')) _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=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 @property
def password(self): def password(self):
@ -175,15 +175,12 @@ class AdminUser(AssetUser):
return info return info
def get_related_assets(self): def get_related_assets(self):
assets = [] assets = self.asset_set.all()
for cluster in self.cluster_set.all(): return assets
assets.extend(cluster.assets.all())
assets.extend(self.asset_set.all())
return list(set(assets))
@property @property
def assets_amount(self): def assets_amount(self):
return len(self.get_related_assets()) return self.get_related_assets().count()
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View File

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

View File

@ -13,7 +13,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-title"> <div class="ibox-title">
<h5>{% trans 'Create system user' %}</h5> <h5>{{ action }}</h5>
<div class="ibox-tools"> <div class="ibox-tools">
<a class="collapse-link"> <a class="collapse-link">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
@ -81,6 +81,14 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}'; 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() { function authFieldsDisplay() {
if ($(auto_generate_key).prop('checked')) { if ($(auto_generate_key).prop('checked')) {
$('.auth-fields').addClass('hidden'); $('.auth-fields').addClass('hidden');
@ -88,9 +96,23 @@
$('.auth-fields').removeClass('hidden'); $('.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 () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
authFieldsDisplay(); authFieldsDisplay();
protocolChange();
$(auto_generate_key).change(function () { $(auto_generate_key).change(function () {
authFieldsDisplay(); authFieldsDisplay();
}); });

View File

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

View File

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

View File

@ -2,6 +2,12 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block help_message %}
<div class="alert alert-info help-message">
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
</div>
{% endblock %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet"> <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> <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
@ -224,6 +230,9 @@ function editTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
if (current_node.value) {
current_node.name = current_node.value;
}
zTree.editName(current_node); 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() { function initTree() {
var setting = { var setting = {
view: { view: {
@ -319,11 +364,24 @@ function initTree() {
enable: true enable: true
} }
}, },
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: { callback: {
onRightClick: OnRightClick, onRightClick: OnRightClick,
beforeClick: beforeClick, beforeClick: beforeClick,
onRename: onRename, onRename: onRename,
onSelected: onSelected onSelected: onSelected,
beforeDrag: beforeDrag,
onDrag: onDrag,
beforeDrop: beforeDrop,
onDrop: onDrop
} }
}; };
@ -334,7 +392,8 @@ function initTree() {
{#if (value["key"] === "0") {#} {#if (value["key"] === "0") {#}
value["open"] = true; value["open"] = true;
{# }#} {# }#}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')' value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
}); });
zNodes = data; zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes); $.fn.zTree.init($("#assetTree"), setting, zNodes);
@ -415,7 +474,7 @@ $(document).ready(function(){
current_node = nodes[0]; current_node = nodes[0];
url += "?node_id=" + current_node.id; url += "?node_id=" + current_node.id;
} }
window.open(url); window.open(url, '_self');
}) })
.on('click', '.btn_asset_delete', function () { .on('click', '.btn_asset_delete', function () {
var $this = $(this); var $this = $(this);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,17 +73,20 @@ def to_html(s):
@register.filter @register.filter
def time_util_with_seconds(date_from, date_to): def time_util_with_seconds(date_from, date_to):
if date_from and date_to: if not date_from:
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:
return '' 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 @register.filter

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -397,6 +397,6 @@ BOOTSTRAP3 = {
} }
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 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 DEFAULT_EXPIRED_YEARS = 70
USER_GUIDE_URL = "" USER_GUIDE_URL = ""

View File

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

View File

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

View File

@ -54,7 +54,11 @@ class UserGrantedAssetsApi(ListAPIView):
user = self.request.user user = self.request.user
for k, v in NodePermissionUtil.get_user_assets(user).items(): 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) queryset.append(k)
return queryset return queryset
@ -118,9 +122,16 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
nodes = NodePermissionUtil.get_user_nodes_with_assets(user) 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 node, v in nodes.items():
for asset in v['assets']: for asset in v['assets']:
asset.system_users_granted = v['system_users'] asset.system_users_granted = assets[asset]
node.assets_granted = v['assets'] node.assets_granted = v['assets']
queryset.append(node) queryset.append(node)
return queryset return queryset

View File

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

View File

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

View File

@ -215,16 +215,6 @@ $(document).ready(function(){
initTable(); initTable();
initTree(); 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 () { .on('click', '.btn-del', function () {
var $this = $(this); var $this = $(this);
var uid = $this.data('uid'); var uid = $this.data('uid');
@ -241,7 +231,7 @@ $(document).ready(function(){
current_node = nodes[0]; current_node = nodes[0];
url += "?node_id=" + current_node.id; url += "?node_id=" + current_node.id;
} }
window.open(url); window.open(url, '_self');
}) })
</script> </script>

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

@ -14,8 +14,13 @@
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#} {# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
{# </li>#} {# </li>#}
<li class="dropdown"> <li class="dropdown">
<a class="dropdown-toggle count-info" data-toggle="dropdown" href="#"> <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 'Help' %}</span> <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> </a>
</li> </li>
<li class="dropdown"> <li class="dropdown">
@ -28,9 +33,8 @@
</span> </span>
</span> </span>
</a> </a>
<ul class="dropdown-menu animated fadeInRight m-t-xs"> <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> <li><a href="{% url 'users:user-profile' %}"><i class="fa fa-cogs"> </i><span> {% trans 'Profile' %}</span></a></li>
<li class="divider"></li>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
{% if request.COOKIES.IN_ADMIN_PAGE == 'No' %} {% 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> <li><a id="switch_admin"><i class="fa fa-exchange"></i><span> {% trans 'Admin page' %}</span></a></li>
@ -57,11 +61,11 @@
<li> <li>
<a href="">{% trans 'Dashboard' %}</a> <a href="">{% trans 'Dashboard' %}</a>
</li> </li>
<li>
{% if app %} {% if app %}
<li>
<a>{{ app }}</a> <a>{{ app }}</a>
{% endif %}
</li> </li>
{% endif %}
{% if action %} {% if action %}
<li class="active"> <li class="active">
<strong>{{ action }}</strong> <strong>{{ action }}</strong>

View File

@ -1,12 +1,13 @@
{% load i18n %} {% load i18n %}
<li id="index"> <li id="index">
<a href="{% url '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> </a>
</li> </li>
<li id="users"> <li id="users">
<a href="#"> <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> </a>
<ul class="nav nav-second-level active"> <ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li> <li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
@ -16,7 +17,7 @@
</li> </li>
<li id="assets"> <li id="assets">
<a> <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> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset list' %}</a></li> <li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset list' %}</a></li>
@ -26,7 +27,7 @@
</ul> </ul>
</li> </li>
<li id="perms"> <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"> <ul class="nav nav-second-level">
<li id="asset-permission"> <li id="asset-permission">
<a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a> <a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a>
@ -35,18 +36,23 @@
</li> </li>
<li id="terminal"> <li id="terminal">
<a> <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> </a>
<ul class="nav nav-second-level"> <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-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="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 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> <li id="terminal"><a href="{% url 'terminal:terminal-list' %}">{% trans 'Terminal' %}</a></li>
</ul> </ul>
</li> </li>
<li id="ops"> <li id="ops">
<a> <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> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li> <li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>

View File

@ -1,16 +1,16 @@
{% load i18n %} {% load i18n %}
<li id="assets"> <li id="assets">
<a href="{% url 'assets:user-asset-list' %}"> <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> </a>
</li> </li>
<li id="users"> <li id="users">
<a href="{% url 'users:user-profile' %}"> <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> </a>
</li> </li>
<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> <span class="nav-label">{% trans 'Web terminal' %}</span>
</a> </a>
</li> </li>

View File

@ -3,7 +3,7 @@
<li class="nav-header"> <li class="nav-header">
<div class="dropdown profile-element"> <div class="dropdown profile-element">
<div href="http://www.jumpserver.org" target="_blank"> <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> </div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

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

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1> <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> </div>
</div> </div>
@ -23,7 +23,7 @@
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1> <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> </div>
</div> </div>
@ -36,7 +36,7 @@
</div> </div>
<div class="ibox-content"> <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> <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> </div>
</div> </div>
@ -57,7 +57,7 @@
<div class="row"> <div class="row">
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px"> <div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
<h2>活跃用户TOP5</h2> <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"> <ul class="list-group clear-list m-t">
{% for data in user_visit_count_top_five %} {% for data in user_visit_count_top_five %}
<li class="list-group-item fist-item"> <li class="list-group-item fist-item">

View File

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

View File

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

View File

@ -1,20 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import datetime
from celery import shared_task 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 CACHE_REFRESH_INTERVAL = 10
RUNNING = False RUNNING = False
# Todo: 定期清理上报history
@shared_task @shared_task
def clean_terminal_history(): @register_as_period_task(interval=3600)
pass @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()

View File

@ -75,6 +75,7 @@
<th class="text-center">{% trans 'Terminal' %}</th> <th class="text-center">{% trans 'Terminal' %}</th>
<th class="text-center">{% trans 'Command' %}</th> <th class="text-center">{% trans 'Command' %}</th>
<th class="text-center">{% trans 'Date start' %}</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 'Duration' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
{% endblock %} {% endblock %}
@ -94,6 +95,7 @@
<td class="text-center">{{ session.id | get_session_command_amount }}</td> <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_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 class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td>
<td> <td>
{% if session.is_finished %} {% if session.is_finished %}
@ -107,6 +109,21 @@
{% endfor %} {% endfor %}
{% endblock %} {% 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 %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script> <script>

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,8 @@ class AccessKey(models.Model):
default=uuid.uuid4, editable=False) default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret', secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False) 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): def get_id(self):
return str(self.id) return str(self.id)

View File

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

View File

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

View File

@ -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) post_user_create = Signal(providing_args=('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)

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
docs/_static/img/structure.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

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

51
docs/intro.rst Normal file
View File

@ -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: 组件架构图

View File

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

View File

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