Commit 8e09151a authored by fit2cloud-fengyi's avatar fit2cloud-fengyi

Merge branch 'docs' of https://github.com/jumpserver/jumpserver into docs

parents 18841d6c 3fe5dade
...@@ -19,7 +19,7 @@ from rest_framework.response import Response ...@@ -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__) ...@@ -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): ...@@ -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()
......
...@@ -15,7 +15,7 @@ class AssetCreateForm(forms.ModelForm): ...@@ -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): ...@@ -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 = {
......
...@@ -38,6 +38,14 @@ def default_node(): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -61,6 +61,9 @@ class Node(models.Model): ...@@ -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): ...@@ -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): ...@@ -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():
......
...@@ -26,14 +26,14 @@ signer = get_signer() ...@@ -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): ...@@ -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']
......
...@@ -65,4 +65,8 @@ class NodeAssetsSerializer(serializers.ModelSerializer): ...@@ -65,4 +65,8 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Node model = Node
fields = ['assets'] fields = ['assets']
\ No newline at end of file
class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField()
...@@ -13,7 +13,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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();
}); });
......
...@@ -13,7 +13,7 @@ ...@@ -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>
......
...@@ -17,6 +17,7 @@ ...@@ -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>
......
...@@ -2,6 +2,12 @@ ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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(){ ...@@ -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);
......
...@@ -22,6 +22,7 @@ ...@@ -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>
......
...@@ -44,6 +44,7 @@ urlpatterns = [ ...@@ -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'),
] ]
......
...@@ -58,8 +58,7 @@ class UserAssetListView(LoginRequiredMixin, TemplateView): ...@@ -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): ...@@ -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)
......
...@@ -68,10 +68,10 @@ class BaseForm(forms.Form): ...@@ -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): ...@@ -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):
......
...@@ -99,9 +99,8 @@ class DatetimeSearchMixin: ...@@ -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)
......
...@@ -73,17 +73,20 @@ def to_html(s): ...@@ -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 return ''
seconds = delta.seconds if not date_to:
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 ''
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
......
This diff is collapsed.
...@@ -397,6 +397,6 @@ BOOTSTRAP3 = { ...@@ -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 = ""
...@@ -4,16 +4,16 @@ from __future__ import unicode_literals ...@@ -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')),
......
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): ...@@ -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): ...@@ -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是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发,
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运
"""
return HttpResponse(msg)
\ No newline at end of file
...@@ -54,7 +54,11 @@ class UserGrantedAssetsApi(ListAPIView): ...@@ -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): ...@@ -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
......
...@@ -12,7 +12,7 @@ class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer): ...@@ -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'
] ]
......
...@@ -14,7 +14,7 @@ ...@@ -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>
......
...@@ -215,16 +215,6 @@ $(document).ready(function(){ ...@@ -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(){ ...@@ -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>
......
...@@ -56,7 +56,7 @@ class NodePermissionUtil: ...@@ -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: ...@@ -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
......
...@@ -427,3 +427,9 @@ div.dataTables_wrapper div.dataTables_filter { ...@@ -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;
}
...@@ -3299,7 +3299,7 @@ body.tour-open .animated { ...@@ -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;
......
apps/static/img/logo-text.png

12.4 KB | W: | H:

apps/static/img/logo-text.png

17.7 KB | W: | H:

apps/static/img/logo-text.png
apps/static/img/logo-text.png
apps/static/img/logo-text.png
apps/static/img/logo-text.png
  • 2-up
  • Swipe
  • Onion skin
<strong>Copyright</strong> 北京堆栈科技有限公司 &copy; 2014-2018
\ No newline at end of file
...@@ -14,8 +14,13 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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>
......
{% 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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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>
......
{% 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>
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -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>
......
...@@ -49,7 +49,7 @@ ...@@ -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>
......
...@@ -11,7 +11,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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">
......
...@@ -25,18 +25,22 @@ def get_all_replay_storage(): ...@@ -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'})
}
...@@ -4,6 +4,7 @@ import uuid ...@@ -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): ...@@ -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)
......
# -*- 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()
...@@ -75,6 +75,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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>
......
...@@ -6,7 +6,7 @@ from django.conf import settings ...@@ -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'] ...@@ -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'
......
...@@ -97,7 +97,7 @@ class SessionOfflineListView(SessionListView): ...@@ -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
......
...@@ -145,7 +145,8 @@ class UserAuthApi(APIView): ...@@ -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")
......
...@@ -6,3 +6,6 @@ from django.apps import AppConfig ...@@ -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()
...@@ -16,7 +16,8 @@ class AccessKey(models.Model): ...@@ -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)
......
...@@ -22,6 +22,7 @@ class UserGroup(NoDeleteModelMixin): ...@@ -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):
......
...@@ -151,6 +151,10 @@ class User(AbstractUser): ...@@ -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): ...@@ -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
......
# -*- 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 post_user_create = Signal(providing_args=('user',))
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)
# -*- 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)
\ No newline at end of file
...@@ -51,11 +51,8 @@ ...@@ -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>
......
...@@ -22,24 +22,27 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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>
......
...@@ -70,11 +70,8 @@ ...@@ -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>
......
...@@ -92,8 +92,8 @@ class UserGroupGrantedAssetView(AdminUserRequiredMixin, DetailView): ...@@ -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)
# ~*~ 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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -6,6 +6,7 @@ import json ...@@ -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 _ ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
contain the root `toctree` directive. contain the root `toctree` directive.
Jumpserver 文档 Jumpserver 文档
==================== ======================================
目录: 目录:
......
简介
============
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 SDK,Coco目前使用该SDK与Jumpserver API交互
`Github <https://github.com/jumpserver/jumpserver-python-sdk.git>`__
组件架构图
++++++++++++++++++++++++
.. image:: _static/img/structure.png
:alt: 组件架构图
用户使用文档 用户使用文档
=============== =============
这部分给您介绍Jumpserver的用户使用方法。 这部分给您介绍Jumpserver的用户管理模块的使用方法。
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
......
...@@ -56,8 +56,8 @@ uritemplate==3.0.0 ...@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment