Merge branch 'dev' of github.com:jumpserver/jumpserver into dev

This commit is contained in:
liuzheng712 2018-03-07 17:18:06 +08:00
commit d1a2fe145d
No known key found for this signature in database
GPG Key ID: 76B55D91E7BB2978
74 changed files with 1822 additions and 383 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ celerybeat.pid
django.db django.db
celerybeat-schedule.db celerybeat-schedule.db
data/static data/static
_build/

View File

@ -12,7 +12,7 @@ __all__ = ['Node']
class Node(models.Model): class Node(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, unique=True, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
child_mark = models.IntegerField(default=0) child_mark = models.IntegerField(default=0)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)

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

View File

@ -91,7 +91,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None: if task_name is None:
task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.hostname for asset in assets] hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hostname_list, tasks=tasks, pattern='all', task_name, hosts=hostname_list, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
@ -120,7 +120,10 @@ def update_assets_hardware_info_period():
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
task_name = _("Update assets hardware info period") task_name = _("Update assets hardware info period")
hostname_list = [asset.hostname for asset in Asset.objects.all()] hostname_list = [
asset.hostname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike()
]
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
# Only create, schedule by celery beat # Only create, schedule by celery beat
@ -165,7 +168,8 @@ def test_admin_user_connectability_util(admin_user, task_name):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
assets = admin_user.get_related_assets() assets = admin_user.get_related_assets()
hosts = [asset.hostname for asset in assets] hosts = [asset.hostname for asset in assets
if asset.is_active and asset.is_unixlike()]
if not hosts: if not hosts:
return return
tasks = const.TEST_ADMIN_USER_CONN_TASKS tasks = const.TEST_ADMIN_USER_CONN_TASKS
@ -257,7 +261,7 @@ def test_system_user_connectability_util(system_user, task_name):
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
assets = system_user.assets assets = system_user.assets
hosts = [asset.hostname for asset in assets] hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts: if not hosts:
logger.info("No hosts, passed") logger.info("No hosts, passed")
@ -346,7 +350,7 @@ def push_system_user_util(system_users, assets, task_name):
logger.info("Not tasks, passed") logger.info("Not tasks, passed")
return {} return {}
hosts = [asset.hostname for asset in assets] hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts: if not hosts:
logger.info("Not hosts, passed") logger.info("Not hosts, passed")
return {} return {}

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

@ -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,7 +230,9 @@ function editTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
current_node.name = current_node.value; if (current_node.value) {
current_node.name = current_node.value;
}
zTree.editName(current_node); zTree.editName(current_node);
} }
@ -313,38 +321,36 @@ function beforeDrag() {
return true return true
} }
function beforeDrop() { function beforeDrop(treeId, treeNodes, targetNode, moveType) {
return true 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 onDrag(event, treeId, treeNodes) {
} }
function onDrop(event, treeId, treeNodes, targetNode, moveType) { function onDrop(event, treeId, treeNodes, targetNode, moveType) {
console.log("DROP");
console.log(event);
console.log(treeNodes);
console.log(targetNode);
console.log(moveType);
var treeNodesNames = [];
var treeNodesIds = []; var treeNodesIds = [];
$.each(treeNodes, function (index, value) { $.each(treeNodes, function (index, value) {
treeNodesNames.push(value.value);
treeNodesIds.push(value.id); treeNodesIds.push(value.id);
}); });
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.id); var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.id);
var body = {nodes: treeNodesIds}; var body = {nodes: treeNodesIds};
if (confirm(msg)){ APIUpdateAttr({
APIUpdateAttr({ url: the_url,
url: the_url, method: "PUT",
method: "PUT", body: JSON.stringify(body)
body: JSON.stringify(body) })
})
}
} }
function initTree() { function initTree() {

View File

@ -178,7 +178,7 @@
</tr> </tr>
<tr> <tr>
<td colspan="2" class="no-borders"> <td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-cluster">{% trans 'Confirm' %}</button> <button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td> </td>
</tr> </tr>
</form> </form>
@ -187,7 +187,7 @@
<tr> <tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.name }}</b></td> <td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.name }}</b></td>
<td> <td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-cluster" type="button"><i class="fa fa-minus"></i></button> <button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -204,27 +204,27 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
function updateSystemUserCluster(clusters) { function updateSystemUserCluster(nodes) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = { var body = {
cluster: Object.assign([], clusters) nodes: Object.assign([], nodes)
}; };
var success = function(data) { var success = function(data) {
// remove all the selected groups from select > option and rendered ul element; // remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty(); $('.select2-selection__rendered').empty();
$('#cluster_selected').val(''); $('#cluster_selected').val('');
$.map(jumpserver.cluster_selected, function(cluster_name, index) { $.map(jumpserver.nodes_selected, function(cluster_name, index) {
$('#opt_' + index).remove(); $('#opt_' + index).remove();
// change tr html of user groups. // change tr html of user groups.
$('.cluster_edit tbody').append( $('.cluster_edit tbody').append(
'<tr>' + '<tr>' +
'<td><b class="bdg_node" data-gid="' + index + '">' + cluster_name + '</b></td>' + '<td><b class="bdg_node" data-gid="' + index + '">' + cluster_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-cluster" type="button"><i class="fa fa-minus"></i></button></td>' + '<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>' '</tr>'
) )
}); });
// clear jumpserver.groups_selected // clear jumpserver.groups_selected
jumpserver.cluster_selected = {}; jumpserver.nodes_selected = {};
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
@ -232,16 +232,16 @@ function updateSystemUserCluster(clusters) {
success: success success: success
}); });
} }
jumpserver.cluster_selected = {}; jumpserver.nodes_selected = {};
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2() $('.select2').select2()
.on('select2:select', function(evt) { .on('select2:select', function(evt) {
var data = evt.params.data; var data = evt.params.data;
jumpserver.cluster_selected[data.id] = data.text; jumpserver.nodes_selected[data.id] = data.text;
}) })
.on('select2:unselect', function(evt) { .on('select2:unselect', function(evt) {
var data = evt.params.data; var data = evt.params.data;
delete jumpserver.cluster_selected[data.id]; delete jumpserver.nodes_selected[data.id];
}); });
}) })
.on('click', '#btn-auto-push', function () { .on('click', '#btn-auto-push', function () {
@ -255,26 +255,26 @@ $(document).ready(function () {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
}) })
.on('click', '#btn-add-to-cluster', function() { .on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.cluster_selected).length === 0) { if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false; return false;
} }
var clusters = $('.bdg_node').map(function() { var nodes = $('.bdg_node').map(function() {
return $(this).data('gid'); return $(this).data('gid');
}).get(); }).get();
$.map(jumpserver.cluster_selected, function(value, index) { $.map(jumpserver.nodes_selected, function(value, index) {
clusters.push(index); nodes.push(index);
}); });
updateSystemUserCluster(clusters); updateSystemUserCluster(nodes);
}) })
.on('click', '.btn-remove-from-cluster', function() { .on('click', '.btn-remove-from-node', function() {
var $this = $(this); var $this = $(this);
var $tr = $this.closest('tr'); var $tr = $this.closest('tr');
var $badge = $tr.find('.bdg_node'); var $badge = $tr.find('.bdg_node');
var gid = $badge.data('gid'); var gid = $badge.data('gid');
var cluster_name = $badge.html() || $badge.text(); var node_name = $badge.html() || $badge.text();
$('#groups_selected').append( $('#groups_selected').append(
'<option value="' + gid + '" id="opt_' + gid + '">' + cluster_name + '</option>' '<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
); );
$tr.remove(); $tr.remove();
var clusters = $('.bdg_node').map(function () { var clusters = $('.bdg_node').map(function () {

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)

View File

@ -97,6 +97,9 @@ class LDAPTestingAPI(APIView):
class DjangoSettingsAPI(APIView): class DjangoSettingsAPI(APIView):
def get(self, request): def get(self, request):
if not settings.DEBUG:
return Response('Only debug mode support')
configs = {} configs = {}
for i in dir(settings): for i in dir(settings):
if i.isupper(): if i.isupper():

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,7 @@
from django.views.generic import TemplateView import datetime
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,15 +48,22 @@ 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']
return month_str return month_str
def get_month_login_metrics(self): def get_month_login_metrics(self):
return [self.session_month.filter(date_start__date=d).count() data = []
for d in self.session_month_dates] time_min = datetime.datetime.min.time()
time_max = datetime.datetime.max.time()
for d in self.session_month_dates:
ds = datetime.datetime.combine(d, time_min).replace(tzinfo=timezone.get_current_timezone())
de = datetime.datetime.combine(d, time_max).replace(tzinfo=timezone.get_current_timezone())
data.append(self.session_month.filter(date_start__range=(ds, de)).count())
return data
def get_month_active_user_metrics(self): def get_month_active_user_metrics(self):
if self.session_month_dates_archive: if self.session_month_dates_archive:
@ -119,10 +129,18 @@ class IndexView(LoginRequiredMixin, TemplateView):
self.session_week = Session.objects.filter(date_start__gt=week_ago) self.session_week = Session.objects.filter(date_start__gt=week_ago)
self.session_month = Session.objects.filter(date_start__gt=month_ago) self.session_month = Session.objects.filter(date_start__gt=month_ago)
self.session_month_dates = self.session_month.dates('date_start', 'day') self.session_month_dates = self.session_month.dates('date_start', 'day')
self.session_month_dates_archive = [
self.session_month.filter(date_start__date=d) self.session_month_dates_archive = []
for d in self.session_month_dates time_min = datetime.datetime.min.time()
] time_max = datetime.datetime.max.time()
for d in self.session_month_dates:
ds = datetime.datetime.combine(d, time_min).replace(
tzinfo=timezone.get_current_timezone())
de = datetime.datetime.combine(d, time_max).replace(
tzinfo=timezone.get_current_timezone())
self.session_month_dates_archive.append(
self.session_month.filter(date_start__range=(ds, de)))
context = { context = {
'assets_count': self.get_asset_count(), 'assets_count': self.get_asset_count(),
@ -149,3 +167,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

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

@ -186,7 +186,7 @@ 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"]
}); });
zNodes = data; zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes); $.fn.zTree.init($("#assetTree"), setting, zNodes);

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

@ -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="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
<span class="m-r-sm text-muted welcome-message">{% trans '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/" target="_blank">
<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

@ -42,6 +42,11 @@
<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>

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

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

@ -5,14 +5,15 @@ import logging
import os import os
import uuid import uuid
from rest_framework import viewsets, serializers
from rest_framework.views import APIView, Response
from rest_framework.permissions import AllowAny
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone from django.utils import timezone
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from rest_framework import viewsets, serializers
from rest_framework.views import APIView, Response
from rest_framework.permissions import AllowAny
from rest_framework_bulk import BulkModelViewSet
from common.utils import get_object_or_none from common.utils import get_object_or_none
from .models import Terminal, Status, Session, Task from .models import Terminal, Status, Session, Task
@ -179,12 +180,29 @@ class SessionViewSet(viewsets.ModelViewSet):
return self.queryset return self.queryset
class TaskViewSet(viewsets.ModelViewSet): class TaskViewSet(BulkModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
class KillSessionAPI(APIView):
permission_classes = (IsSuperUserOrAppUser,)
model = Task
def post(self, request, *args, **kwargs):
validated_session = []
for session_id in request.data:
session = get_object_or_none(Session, id=session_id)
if session and not session.is_finished:
validated_session.append(session_id)
self.model.objects.create(
name="kill_session", args=session.id,
terminal=session.terminal,
)
return Response({"ok": validated_session})
class CommandViewSet(viewsets.ViewSet): class CommandViewSet(viewsets.ViewSet):
"""接受app发送来的command log, 格式如下 """接受app发送来的command log, 格式如下
{ {

View File

@ -129,7 +129,7 @@ class Session(models.Model):
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_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"), db_index=True)
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)
class Meta: class Meta:

View File

@ -3,7 +3,11 @@
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from common.utils import get_object_or_none
from .models import Terminal, Status, Session, Task from .models import Terminal, Status, Session, Task
from .backends import get_multi_command_store from .backends import get_multi_command_store
@ -47,6 +51,7 @@ class SessionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Session model = Session
list_serializer_class = BulkListSerializer
fields = '__all__' fields = '__all__'
def get_command_amount(self, obj): def get_command_amount(self, obj):
@ -60,11 +65,12 @@ class StatusSerializer(serializers.ModelSerializer):
model = Status model = Status
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta: class Meta:
fields = '__all__' fields = '__all__'
model = Task model = Task
list_serializer_class = BulkListSerializer
class ReplaySerializer(serializers.Serializer): class ReplaySerializer(serializers.Serializer):

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from common.celery import register_as_period_task, after_app_ready_start, \ from common.celery import register_as_period_task, after_app_ready_start, \
after_app_shutdown_clean after_app_shutdown_clean
from .models import Status from .models import Status, Session
CACHE_REFRESH_INTERVAL = 10 CACHE_REFRESH_INTERVAL = 10
@ -20,11 +20,17 @@ RUNNING = False
@after_app_ready_start @after_app_ready_start
@after_app_shutdown_clean @after_app_shutdown_clean
def delete_terminal_status_period(): def delete_terminal_status_period():
yesterday = timezone.now() - datetime.timedelta(days=1) yesterday = timezone.now() - datetime.timedelta(days=3)
Status.objects.filter(date_created__lt=yesterday).delete() 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

@ -109,6 +109,21 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block content_bottom_left %}
<div id="actions" {% if type != "online" %} style="display: none" {% endif %}>
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="terminate">{% 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>
@ -119,7 +134,7 @@
}, 1000) }, 1000)
} }
var success_message = '{% trans "Terminate task send, waiting ..." %}'; var success_message = '{% trans "Terminate task send, waiting ..." %}';
var the_url = "{% url 'api-terminal:tasks-list' %}"; var the_url = "{% url 'api-terminal:kill-session' %}";
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: 'POST', method: 'POST',
@ -150,14 +165,31 @@
}).on('click', '.btn-term', function () { }).on('click', '.btn-term', function () {
var $this = $(this); var $this = $(this);
var session_id = $this.attr('value'); var session_id = $this.attr('value');
var terminal_id = $this.attr('terminal'); var data = [
var data = { session_id
name: "kill_session", ];
args: session_id,
terminal: terminal_id
};
terminateSession(data) terminateSession(data)
}) }).on('click', '#btn_bulk_update', function () {
var action = $('#slct_bulk_update').val();
var id_list = [];
$(".cbx-term:checked").each(function (index, data) {
id_list.push($(data).attr("value"))
});
if (id_list.length === 0) {
return false;
}
function doTerminate() {
terminateSession(id_list)
}
switch(action) {
case 'terminate':
doTerminate();
break;
default:
break;
}
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -22,6 +22,7 @@ urlpatterns = [
url(r'^v1/sessions/(?P<pk>[0-9a-zA-Z\-]{36})/replay/$', url(r'^v1/sessions/(?P<pk>[0-9a-zA-Z\-]{36})/replay/$',
api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
name='session-replay'), name='session-replay'),
url(r'^v1/tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'),
url(r'^v1/terminal/(?P<terminal>[a-zA-Z0-9\-]{36})/access-key', api.TerminalTokenApi.as_view(), name='terminal-access-key'), url(r'^v1/terminal/(?P<terminal>[a-zA-Z0-9\-]{36})/access-key', api.TerminalTokenApi.as_view(), name='terminal-access-key'),
url(r'^v1/terminal/config', api.TerminalConfig.as_view(), name='terminal-config'), url(r'^v1/terminal/config', api.TerminalConfig.as_view(), name='terminal-config'),
] ]

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

@ -74,6 +74,7 @@ class SessionOnlineListView(SessionListView):
context = { context = {
'app': _('Terminal'), 'app': _('Terminal'),
'action': _('Session online list'), 'action': _('Session online list'),
'type': 'online',
'now': timezone.now(), 'now': timezone.now(),
} }
kwargs.update(context) kwargs.update(context)
@ -97,7 +98,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

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

@ -251,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

@ -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>
符合4A规范的专业运维审计系统拥有跳板机的所有功能认证授权审计文件上传 全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
</p> </p>
<p> <p>
极致的用户使用体验:拥有时尚外观是区别与以往版本和其他软件的铭牌,高雅的气质让你爱不释手; 使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
</p> </p>
<p> <p>
混合云环境下的堡垒机怎么能容忍传统堡垒机的繁琐步骤Jumpserver让你极致省力; 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发访问限制。
</p> </p>
<p> <p>
<small>改变世界,从一点点开始。</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>

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

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

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Jumpserver
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_static/img/logo-text.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

2
docs/admin_asset.rst Normal file
View File

@ -0,0 +1,2 @@
资产管理模块
=============

10
docs/admin_guide.rst Normal file
View File

@ -0,0 +1,10 @@
管理文档
=========
这里介绍管理员功能。
.. toctree::
:maxdepth: 1
admin_user
admin_asset

51
docs/admin_user.rst Normal file
View File

@ -0,0 +1,51 @@
用户管理
========
这里介绍用户管理模块的功能。
点击页面左侧“用户列表”菜单下的“用户列表,进入用户列表页面。
.. contents:: Topics
.. _create_user:
创建用户
````````
点击页面左上角“创建用户”按钮,进入创建用户页面,填写账户,角色安全,个人等信息,点击“提交”按钮,用户创建完成。
.. _update_user:
更新用户
````````
点击页面右边的“更新”按钮,进入编辑用户页面,编辑用户信息,点击“提交”按钮,更新用户完成。
.. _delete_user:
删除用户
````````
点击页面右边的“删除”按钮,弹出是否删除确认框,点击“确定”按钮,删除用户完成。
.. _export_user:
导出用户
````````
选中用户,点击右上角的“导出”按钮,导出用户完成。
.. _inport_user:
导入用户
````````
点击右上角的“导入”按钮弹出导入对话框选择要导入的CSV格式文件点击“确认”按钮导入用户完成。
.. _batch_operation:
批量操作
````````
选中用户,选择页面左下角的批量操作选项,点击”提交“按钮,批量操作完成。

166
docs/api_style_guide.rst Normal file
View File

@ -0,0 +1,166 @@
REST API规范约定
----------------
这里仅考虑REST API的基本情况。参考
`RESTful API 设计指南`_
`github api文档`_
协议
~~~~
API与用户的通信协议总是使用HTTPs协议。
域名
~~~~
这版api相对简单, 没有前后端分离, 没有独立app, 所以放在主域名下
::
https://example.org/api/
版本
~~~~
将API的版本号放入URL中, 由于一个项目多个app所以Jumpserver使用以下风格,
将版本号放到app后面
::
https://example.com/api/:app:/:version:/:resource:
https://example.com/api/assets/v1.0/assets [GET, POST]
https://example.com/api/assets/v1.0/assets/1 [GET, PUT, DELETE]
路径
~~~~
路径又称“终点”endpoint表示API的具体网址。
在RESTful架构中每个网址代表一种资源resource所以网址中不能有动词只能有名词而且所用的名词往往与数据库的表格名对应。一般来说数据库中的表都是同种记录的“集合”collection所以API中的名词也应该使用复数。
举例来说 cmdb中的assets列表, idc列表
::
https://example.com/api/:app:/:version:/:resource:
https://example.com/api/assets/v1.0/assets [GET, POST]
https://example.com/api/assets/v1.0/assets/1 [GET, PUT, DELETE]
https://example.com/api/assets/v1.0/idcs [GET, POST]
一般性的增删查改(CRUD)API完全使用HTTP
method加上url提供的语义url中的可变部分比如上面提到的
一般用来传递该API操作的核心实体对象的唯一ID如果有更多的参数需要提供GET方法请使用url
parameter
(例如:“?client_id=xxxxx&app_id=xxxxxx”)PUT/POST/DELETE方法请使用请求体传递参数。
HTTP Method
~~~~~~~~~~~
对于资源的具体操作类型由HTTP动词表示。
常用的HTTP动词有下面五个括号里是对应的SQL命令
- GETSELECT从服务器取出资源一项或多项
- POSTCREATE在服务器新建一个资源。
- PUTUPDATE在服务器更新资源客户端提供改变后的完整资源, 幂等
- PATCHUPDATE在服务器更新资源客户端提供改变的属性
- DELETEDELETE从服务器删除资源。
.. _RESTful API 设计指南: http://www.ruanyifeng.com/blog/2014/05/restful_api.html
.. _github api文档: https://developer.github.com/v3/
过滤信息
~~~~~~~~
常见参数约定
::
?keyword=localhost 模糊搜索
?limit=10指定返回记录的数量
?offset=10指定返回记录的开始位置。
?page=2&per_page=100指定第几页以及每页的记录数。
?sort=name&order=asc指定返回结果按照哪个属性排序以及排序顺序。
?asset_id=1指定筛选条件
状态码
~~~~~~
服务器向用户返回的状态码和提示信息常见的有以下一些方括号中是该状态码对应的HTTP动词
- 200 OK -
[GET]服务器成功返回用户请求的数据该操作是幂等的Idempotent
- 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
- 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
- 204 NO CONTENT - [DELETE]:用户删除数据成功。
- 400 INVALID REQUEST -
[POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
- 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
- 403 Forbidden - [*]
表示用户得到授权与401错误相对但是访问是被禁止的。
- 404 NOT FOUND -
[*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
- 406 Not Acceptable -
[GET]用户请求的格式不可得比如用户请求JSON格式但是只有XML格式
- 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH]
当创建一个对象时,发生一个验证错误。
- 500 INTERNAL SERVER ERROR -
[*]:服务器发生错误,用户将无法判断发出的请求是否成功。
错误处理
~~~~~~~~
如果状态码是4xx就应该向用户返回出错信息。一般来说返回的信息中将error作为键名出错信息作为键值即可。
::
{
error: "Invalid API key"
}
返回结果
~~~~~~~~
针对不同操作,服务器向用户返回的结果应该符合以下规范。
::
GET /collection返回资源对象的列表数组
GET /collection/resource返回单个资源对象
POST /collection返回新生成的资源对象
PUT /collection/resource返回完整的资源对象
PATCH /collection/resource返回完整的资源对象
DELETE /collection/resource返回一个空文档
Hypermedia API
~~~~~~~~~~~~~~
RESTful
API最好做到Hypermedia即返回结果中提供链接连向其他API方法使得用户不查文档也知道下一步应该做什么。
比如当用户向api.example.com的根目录发出请求会得到这样一个文档。
::
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代码表示文档中有一个link属性用户读取这个属性就知道下一步该调用什么API了。
rel表示这个API与当前网址的关系collection关系并给出该collection的网址
href表示API的路径title表示API的标题type表示返回类型。 Hypermedia
API的设计被称为HATEOAS。 Github的API就是这种设计.
其它
~~~~
1API的身份认证应该使用OAuth 2.0框架。
2服务器返回的数据格式应该尽量使用JSON

168
docs/conf.py Normal file
View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/stable/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import sphinx_rtd_theme
# -- Project information -----------------------------------------------------
project = 'jumpserver'
copyright = '北京堆栈科技有限公司 © 2014-2018'
author = 'Jumpserver team'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.5.0'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'zh_CN'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
html_show_sourcelink = False
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'logo_only': True,
'display_version': False
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Jumpserver 文档'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'jumpserver.tex', 'jumpserver Documentation',
'Jumpserver team', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'jumpserver', 'jumpserver Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'jumpserver', 'jumpserver Documentation',
author, 'jumpserver', 'One line description of project.',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------
html_logo = '_static/img/logo-text.png'

33
docs/contact.rst Normal file
View File

@ -0,0 +1,33 @@
联系方式
+++++++++++++++++++++++++
QQ群
~~~~~~~~
群1: 390139816
群2: 399218702
群3: 552054376
Github
~~~~~~~~
https://github.com/jumpserver/jumpserver.git
官网
~~~~~~~~
http://www.jumpserver.org
Demo
~~~~~~~~
http://demo.jumpserver.org:8080
邮件
~~~~~~~~
ibuler#fit2cloud.com (#替换为@)

13
docs/contributor.rst Normal file
View File

@ -0,0 +1,13 @@
贡献者
++++++++++++++++++++++++
感谢一下朋友为Jumpserver做出的贡献世界因你们而不同排名不分先后
- **小彧 <李磊>** Django资深开发者为用户模块贡献了很多代码
- **sofia <周小侠>** 资深前端工程师, 前端代码贡献者
- **liuz <刘正> 全栈工程师** 编写了Web terminal大部分代码
- **jiaxiangkong <陈尚委>** Jumpserver测试运营
- **halcyon <王墉>** DevOps 资深开发者, 0.3.2 核心开发者之一
- **yumaojun03 <喻茂峻>** DevOps 资深开发者擅长Python, Go以及PAAS平台开发
- **kelianchun <柯连春>** DevOps 资产开发者fix了很多bug

12
docs/development.rst Normal file
View File

@ -0,0 +1,12 @@
开发文档
======================================
.. toctree::
:maxdepth: 1
:caption: 开发文档
api_style_guide
python_style_guide
project_structure

2
docs/faq.rst Normal file
View File

@ -0,0 +1,2 @@
FAQ
+++++++++++++++++++++

29
docs/index.rst Normal file
View File

@ -0,0 +1,29 @@
.. jumpserver documentation master file, created by
sphinx-quickstart on Mon Feb 26 23:28:27 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Jumpserver 文档
======================================
.. toctree::
:maxdepth: 2
:caption: 文档:
intro
installation
admin_guide
user_guide
development
contributor
contact
faq
索引
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

9
docs/installation.rst Normal file
View File

@ -0,0 +1,9 @@
安装文档
++++++++++++++++++++++++
.. toctree::
:maxdepth: 1
quickstart
step_by_step
upgrade

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

@ -0,0 +1,51 @@
项目骨架
--------
说明如下:
::
.
├── config-example.py // 配置文件样例
├── docs // 所有doc文件放到该目录
│ └── README.md
├── LICENSE
├── README.md
├── install // 安装说明
├── logs // 日志目录
├── apps // 管理后台目录也是各app所在目录
│ └── assets // app目录
│ │ ├── admin.py
│ │ ├── apps.py // 新版本django app设置文件
│ │ ├── api.py // api文件
│ │ ├── __init__.py // 对外暴露的接口放到该文件中方便别的app引用
│ │ ├── migrations // models Migrations版本控制目录
│ │ │ └── __init__.py
│ │ ├── models.py // 数据模型目录
│ │ ├── static // app下静态资源目录,如果需要
│ │ │ └── assets // 多一层目录,防止资源重名
│ │ │ └── some_image.png
│ │ ├── templates // app下模板目录
│ │ │ └── assets // 多一层目录,防止资源重名
│ │ │ └── asset_list.html
│ │ ├── templatetags // 模板标签目录
│ │ ├── tests.py // 测试用例文件
│ │ ├── urls.py // urlconf文件
│ │ ├── utils.py // 将views和api可复用的代码放在这里, api和views只是请求和返回不同
│ │ └── views.py // views文件
│ ├── common
│ │ ├── templatetags // 通用template tag
│ │ ├── utils.py // 通用的函数方法
│ │ └── views.py
│ ├── fixtures // 初始化数据目录
│ │ ├── init.json // 初始化项目数据库
│ │ └── fake.json // 生成大量测试数据
│ ├── jumpserver // 项目设置目录
│ │ ├── __init__.py
│ │ ├── settings.py // 项目设置文件
│ │ ├── urls.py // 项目入口urlconf
│ │ └── wsgi.py
│ ├── manage.py
│ ├── static // 项目静态资源目录
│ ├── i18n // 项目多语言目录
│ └── templates // 项目模板目录

216
docs/python_style_guide.rst Normal file
View File

@ -0,0 +1,216 @@
Jumpserver 项目规范Draft
============================
语言框架
--------
1. Python 3.6.1 (当前最新)
2. Django 1.11 (当前最新)
3. Flask 0.12 Luna (当前最新)
4. Paramiko 2.12 Coco (当前最新)
Django规范
----------
1. 尽量使用Class Base View编程更少代码
2. 使用Django Form
3. 每个url独立命名不要硬编码同理static也是
4. 数据库表名手动指定,不要使用默认
5. 代码优雅简洁
6. 注释明确优美
7. 测试案例尽可能完整
8. 尽可能利用Django造好的轮子
代码风格
--------
Python方面大致的风格我们采用pocoo的\ `Style
Guidance`_\ ,但是有些细节部分会尽量放开 参考国内翻译
基本的代码布局
~~~~~~~~~~~~~~
缩进
^^^^
1. Python严格采用4个空格的缩进任何python代码都都必须遵守此规定。
2. web部分代码(HTML, CSS,
JavaScript)Node.js采用2空格缩进同样不使用tab (:raw-latex:`\t`)。
之所以与Python不同是因为js中有大量回调式的写法2空格可以显著降低视觉上的负担。
最大行长度
^^^^^^^^^^
按PEP8规范Python一般限制最大79个字符,
但是Django的命名,url等通常比较长,
而且21世纪都是宽屏了,所以我们限制最大120字符
**补充说明HTML代码不受此规范约束。**
长语句缩进
^^^^^^^^^^
编写长语句时,可以使用换行符()换行。在这种情况下,下一行应该与上一行的最后
一个“.”句点或“=”对齐或者是缩进4个空格符
::
this_is_a_very_long(function_call, 'with many parameters') \
.that_returns_an_object_with_an_attribute
MyModel.query.filter(MyModel.scalar > 120) \
.order_by(MyModel.name.desc()) \
.limit(10)
如果你使用括号“()”或花括号“{}”为长语句换行,那么下一行应与括号或花括号对齐:
::
this_is_a_very_long(function_call, 'with many parameters',
23, 42, 'and even more')
对于元素众多的列表或元组,在第一个“[”或“(”之后马上换行:
::
items = [
'this is the first', 'set of items', 'with more items',
'to come in this line', 'like this'
]
.. _Style Guidance: http://www.pocoo.org/internal/styleguide/
空行
^^^^
顶层函数与类之间空两行,此外都只空一行。不要在代码中使用太多的空行来区分不同的逻辑模块。
::
def hello(name):
print 'Hello %s!' % name
def goodbye(name):
print 'See you %s.' % name
class MyClass(object):
"""This is a simple docstring."""
def __init__(self, name):
self.name = name
def get_annoying_name(self):
return self.name.upper() + '!!!!111'
语句和表达式
~~~~~~~~~~~~
一般空格规则
^^^^^^^^^^^^
1. 单目运算符与运算对象之间不空格(例如,-~等),即使单目运算符位于括号内部也一样。
2. 双目运算符与运算对象之间要空格。
::
exp = -1.05
value = (item_value / item_count) * offset / exp
value = my_list[index]
value = my_dict['key']
比较
^^^^
1. 任意类型之间的比较,使用“==”和“!=”。
2. 与单例singletons进行比较时使用is和is not。
3. 永远不要与True或False进行比较例如不要这样写foo ==
False而应该这样写not foo
否定成员关系检查
^^^^^^^^^^^^^^^^
使用foo not in bar而不是not foo in bar。
命名约定
~~~~~~~~
1. 类名称采用骆驼拼写法CamelCase首字母缩略词保持大写不变HTTPWriter而不是HttpWriter
2. 变量名小写_以及_下划线lowercase_with_underscores
3. 方法与函数名小写_以及_下划线lowercase_with_underscores
4. 常量大写_以及_下划线UPPERCASE_WITH_UNDERSCORES
5. 预编译的正则表达式name_re。
6. 受保护的元素以一个下划线为前缀。双下划线前缀只有定义混入类mixin
classes时才使用。
7. 如果使用关键词keywords作为类名称应在名称后添加后置下划线trailing
underscore
允许与内建变量重名,不要在变量名后添加下划线进行区分。如果函数需要访问重名的内建变量,请将内建变量重新绑定为其他名称。
8. 命名要有寓意, 不使用拼音,不使用无意义简单字母命名 (循环中计数例外 for
i in)
9. 命名缩写要谨慎, 尽量是大家认可的缩写
函数和方法的参数:
^^^^^^^^^^^^^^^^^^
1. 类方法cls为第一个参数。
2. 实例方法self为第一个参数。
3. property函数中使用匿名函数lambdas匿名函数的第一个参数可以用x替代
例如display_name = property(lambda x: x.real_name or x.username)。
文档注释(Docstring即各方法类的说明文档注释)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
所有文档字符串均以reStructuredText格式编写方便Sphinx处理。文档字符串的行数不同布局也不一样。
如果只有一行,代表字符串结束的三个引号与代表字符串开始的三个引号在同一行。
如果为多行,文档字符串中的文本紧接着代表字符串开始的三个引号编写,代表字符串结束的三个引号则自己独立成一行。
(有能力尽可能用英文, 否则请中文优雅注释)
::
def foo():
"""This is a simple docstring."""
def bar():
"""This is a longer docstring with so much information in there
that it spans three lines. In this case, the closing triple quote
is on its own line.
"""
文档字符串应分成简短摘要(尽量一行)和详细介绍。如果必要的话,摘要与详细介绍之间空一行。
模块头部
~~~~~~~~
模块文件的头部包含有utf-8编码声明如果模块中使用了非ASCII编码的字符建议进行声明以及标准的文档字符串。
::
# -*- coding: utf-8 -*-
"""
package.module
~~~~~~~~~~~~~~
A brief description goes here.
:copyright: (c) YEAR by AUTHOR.
:license: LICENSE_NAME, see LICENSE_FILE for more details.
"""
注释(comment)
~~~~~~~~~~~~~
注释的规范与文档字符串编写规范类似。二者均以reStructuredText格式编写。
如果使用注释来编写类属性的文档,请在#符号后添加一个冒号“:”。
(有能力尽可能用英文, 否则请中文优雅注释)
::
class User(object):
#: the name of the user as unicode string
name = Column(String)
#: the sha1 hash of the password + inline salt
pw_hash = Column(String)

48
docs/quickstart.rst Normal file
View File

@ -0,0 +1,48 @@
快速安装
==========================
Jumpserver 封装了一个All in one Docker可以快速启动。该镜像集成了所有需要的组件可以使用外置db和redis
Tips: 不建议在生产中使用
Docker 安装见: `Docker官方安装文档 <https://docs.docker.com/install/>`_
快速启动
```````````````
使用root命令行输入::
$ docker run -p 8080:80 -p 2222:2222 jumpserver/jumpserver:0.5.0-beta2
访问
```````````````
浏览器访问: http://localhost:8080
ssh访问: ssh -p 2222 localhost
额外环境变量
```````````````
- DB_ENGINE = mysql
- DB_HOST = mysql_host
- DB_PORT = 3306
- DB_USER = xxx
- DB_PASSWORD = xxxx
- DB_NAME = jumpserver
- REDIS_HOST = ''
- REDIS_PORT = ''
- REDIS_PASSWORD = ''
::
docker run -p 8080:80 -p 2222:2222 -e DB_ENGINE=mysql -e DB_HOST=192.168.1.1 -e DB_PORT=3306 -e DB_USER=root -e DB_PASSWORD=xxx -e DB_NAME=jumpserver jumpserver/jumpserver:0.5.0-beta2
仓库地址
```````````````
https://github.com/jumpserver/Dockerfile

294
docs/step_by_step.rst Normal file
View File

@ -0,0 +1,294 @@
一步一步安装
--------------------------
环境
~~~~
- 系统: CentOS 7
- IP: 192.168.244.144
- 关闭 selinux和防火墙
::
# CentOS 7
$ setenforce 0 # 可以设置配置文件永久关闭
$ systemctl stop iptables.service
$ systemctl stop firewalld.service
# CentOS6
$ setenforce 0
$ service iptables stop
一. 准备Python3和Python虚拟环境
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**1.1 安装依赖包**
::
$ yum -y install wget sqlite-devel xz gcc automake zlib-devel openssl-devel epel-release
**1.2 编译安装**
::
$ wget https://www.python.org/ftp/python/3.6.1/Python-3.6.1.tar.xz
$ tar xvf Python-3.6.1.tar.xz && cd Python-3.6.1
$ ./configure && make && make install
**1.3 建立python虚拟环境**
因为CentOS
6/7自带的是Python2而Yum等工具依赖原来的Python为了不扰乱原来的环境我们来使用Python虚拟环境
::
$ cd /opt
$ python3 -m venv py3
$ source /opt/py3/bin/activate
# 看到下面的提示符代表成功以后运行jumpserver都要先运行以上source命令以下所有命令均在该虚拟环境中运行
(py3) [root@localhost py3]#
二. 安装Jumpserver 0.5.0
~~~~~~~~~~~~~~~~~~~~~~~~
**2.1 下载或clone项目**
项目提交较多git clone时较大你可以选择去github项目页面直接下载
zip包我的网速好我直接clone了
::
$ cd /opt/
$ git clone --depth=1 https://github.com/jumpserver/jumpserver.git && cd jumpserver && git checkout dev
**2.2 安装依赖rpm包**
::
$ cd /opt/jumpserver/requirements
$ yum -y install $(cat rpm_requirements.txt) # 如果没有任何报错请继续
**2.3 安装python库依赖**
::
$ pip install -r requirements.txt # 不要指定-i参数因为镜像上可能没有最新的包如果没有任何报错请继续
**2.4 安装Redis, jumpserver使用redis做cache和celery broker**
::
$ yum -y install redis
$ service redis start
**2.5 安装MySQL**
本教程使用mysql作为数据库如果不使用mysql可以跳过相关mysql安装和配置
::
# centos7
$ yum -y install mariadb mariadb-devel mariadb-server # centos7下安装的是mariadb
$ service mariadb start
# centos6
$ yum -y install mysql mysql-devel mysql-server
$ service mysqld start
**2.6 创建数据库 jumpserver并授权**
::
$ mysql
> create database jumpserver default charset 'utf8';
> grant all on jumpserver.* to 'jumpserver'@'127.0.0.1' identified by 'somepassword';
**2.7 修改jumpserver配置文件**
::
$ cd /opt/jumpserver
$ cp config_example.py config.py
$ vi config.py # 我们计划修改 DevelopmentConfig中的配置因为默认jumpserver是使用该配置它继承自Config
**注意: 配置文件是python格式不要用tab而要用空格** **注意:
配置文件是python格式不要用tab而要用空格** **注意:
配置文件是python格式不要用tab而要用空格**
::
class DevelopmentConfig(Config):
DEBUG = True
DB_ENGINE = 'mysql'
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_USER = 'jumpserver'
DB_PASSWORD = 'somepassword'
DB_NAME = 'jumpserver'
...
config = DevelopmentConfig() # 确保使用的是刚才设置的配置文件
**2.8 生成数据库表结构和初始化数据**
::
$ cd /opt/jumpserver/utils
$ bash make_migrations.sh
**2.9 运行Jumpserver**
::
$ cd /opt/jumpserver
$ python run_server.py all
运行不报错,请浏览器访问 http://192.168.244.144:8080/
(这里只是jumpserver, 没有web terminal,所以访问web terminal会报错)
账号:admin 密码: admin
三. 安装 SSH Server和Web Socket Server: Coco
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**3.1 下载clone项目**
新开一个终端,连接测试机,别忘了 source /opt/py3/bin/activate
::
$ cd /opt
$ git clone https://github.com/jumpserver/coco.git && cd coco && git checkout dev
**3.2 安装依赖**
::
$ cd /opt/coco/requirements $ yum -y install $(cat rpm_requirements.txt) $ pip install requirements.txt
**3.2 安装依赖**
::
$ cd /opt/coco/requirements
$ yum -y install $(cat rpm_requirements.txt)
$ pip install -r requirements.txt
**3.3 查看配置文件并运行**
::
$ cd /opt/coco
$ cp conf_example.py conf.py
$ python run_server.py
这时需要去
jumpserver管理后台-终端-终端(http://192.168.244.144:8080/terminal/terminal/)接受coco的注册
::
Coco version 0.4.0, more see https://www.jumpserver.org
Starting ssh server at 0.0.0.0:2222
Quit the server with CONTROL-C.
**3.4 测试连接**
::
$ ssh -p2222 admin@192.168.244.144
密码: admin
如果是用在windows下Xshell terminal登录语法如下
$ssh admin@192.168.244.144 2222
密码: admin
如果能登陆代表部署成功
四. 安装 Web Terminal 前端: Luna
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Luna已改为纯前端需要nginx来运行访问
下载 release包直接解压不需要编译
访问 https://github.com/jumpserver/luna/releases下载对应release包
4.1 解压luna
::
$ pwd
/opt/
$ tar xvf luna.tar.gz
$ ls /opt/luna
...
五. 安装Windows支持组件
~~~~~~~~~~~~~~~~~~~~~~~
使用docker启动 guacamole
.. code:: shell
docker run \
-p 8080:8080 \
-e JUMPSERVER_SERVER=http://<jumpserver>:8080 \
jumpserver/guacamole
这里所需要注意的是guacamole暴露出来的端口是8080若与jumpserver部署在同一主机上自定义一下。
修改JUMPSERVER_SERVER的配置填上jumpserver的内网地址
六. 配置 nginx 整合各组件
~~~~~~~~~~~~~~~~~~~~~~~~~
6.1 安装nginx 根据喜好选择安装方式和版本
6.2 配置文件
::
server {
listen 80;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /luna/ {
try_files $uri / /index.html;
alias /opt/luna/;
}
location /media/ {
add_header Content-Encoding gzip;
root /opt/jumpserver/data/;
}
location /static/ {
root /opt/jumpserver/data/;
}
location /socket.io/ {
proxy_pass http://localhost:5000/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /guacamole/ {
proxy_pass http://<guacamole>:8080/;
}
location / {
proxy_pass http://localhost:8080;
}
}
6.3 运行 nginx
6.4 访问 http://192.168.244.144

18
docs/upgrade.rst Normal file
View File

@ -0,0 +1,18 @@
升级
----
1. 升级 jumpserver
::
$ git pull && pip install -r requirements/requirements.txt && cd utils && sh make_migrations.sh
2. 升级 coco
::
$ git pull && cd requirements && pip install -r requirements.txt # 不要指定 -i参数
3. 升级 luna
重新下载release包

27
docs/user_asset.rst Normal file
View File

@ -0,0 +1,27 @@
个人资产
=========
这里介绍用户个人资产相关的功能。
.. contents:: Topics
.. _view_personal_assets:
查看个人资产
````````````
登录个人用户,默认展示个人资产列表。点击主机名,查看资产的详细信息。
.. _host_login:
主机登录
`````````
点解页面左侧的"Web终端"进入主机登录页然后点击页面右侧的主机IP地址连接主机页面右侧会展示当前连接的终端信息。
.. _host_logout:
主机登出
`````````
在主机登录页面,选择左上角的“服务器”按钮,出现两个选项,一个“断开链接“按钮,断开当前连接的主机;另一个”断开所有链接“,断开当前所有连接的主机。

10
docs/user_guide.rst Normal file
View File

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

34
docs/user_info.rst Normal file
View File

@ -0,0 +1,34 @@
个人信息
=========
这里介绍个人信息相关的功能。
.. contents:: Topics
.. _view_personal_info:
查看个人信息
````````````
点击页面左侧的“个人信息”查看用户的个人信息、SSH密钥。
.. _modify_personal_info:
修改个人信息
````````````
在个人信息页,点击页面右上角的“设置”按钮,进入个人信息修改页面,填写个人信息,点击“提交”按钮,完成个人信息修改。
.. _update_password:
更新密码
`````````
在个人信息页,点击页面右上角的“重置密码“按钮,进入密码更新页面,填写原来密码、新密码等信息,点击“提交”按钮,完成密码更新。
.. _update_ssh_key:
密钥更新
`````````
在个人信息页点击页面左上角的“重置SSH密钥“按钮进入密钥更新页面填写SSH公钥点击“提交”按钮完成密钥更新。

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