Unverified Commit 0665644f authored by 老广's avatar 老广 Committed by GitHub
Browse files

Merge pull request #1905 from jumpserver/dev

支持命令过滤
parents 1341983f 7bafa546
Showing with 470 additions and 18 deletions
+470 -18
......@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
__version__ = "1.4.2"
__version__ = "1.4.3"
......@@ -4,3 +4,4 @@ from .label import *
from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *
# -*- coding: utf-8 -*-
#
from rest_framework_bulk import BulkModelViewSet
from django.shortcuts import get_object_or_404
from ..hands import IsOrgAdmin
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(BulkModelViewSet):
permission_classes = (IsOrgAdmin,)
queryset = CommandFilter.objects.all()
serializer_class = serializers.CommandFilterSerializer
class CommandFilterRuleViewSet(BulkModelViewSet):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
def get_queryset(self):
fpk = self.kwargs.get('filter_pk')
if not fpk:
return CommandFilterRule.objects.none()
group = get_object_or_404(CommandFilter, pk=fpk)
return group.rules.all().order_by('priority')
......@@ -33,7 +33,8 @@ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
]
......@@ -126,4 +127,17 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset)
return Response({"task": task.id})
\ No newline at end of file
return Response({"task": task.id})
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get_serializer_class(self):
from ..serializers import CommandFilterRuleSerializer
return CommandFilterRuleSerializer
def get_queryset(self):
pk = self.kwargs.get('pk', None)
system_user = get_object_or_404(SystemUser, pk=pk)
return system_user.cmd_filter_rules
......@@ -4,3 +4,4 @@ from .asset import *
from .label import *
from .user import *
from .domain import *
from .cmd_filter import *
......@@ -48,7 +48,7 @@ class AssetCreateForm(OrgModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
),
# 'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
'domain': _("If your have some network not connect with each other, you can set domain")
}
......@@ -88,7 +88,7 @@ class AssetUpdateForm(OrgModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
),
# 'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
'domain': _("If your have some network not connect with each other, you can set domain")
}
......
# -*- coding: utf-8 -*-
#
from django import forms
from orgs.mixins import OrgModelForm
from ..models import CommandFilter, CommandFilterRule
__all__ = ['CommandFilterForm', 'CommandFilterRuleForm']
class CommandFilterForm(OrgModelForm):
class Meta:
model = CommandFilter
fields = ['name', 'comment']
class CommandFilterRuleForm(OrgModelForm):
class Meta:
model = CommandFilterRule
fields = [
'filter', 'type', 'content', 'priority', 'action', 'comment'
]
widgets = {
'content': forms.Textarea(attrs={
'placeholder': 'eg:\r\nreboot\r\nrm -rf'
}),
}
......@@ -3,8 +3,9 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from ..models import AdminUser, SystemUser
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
from orgs.mixins import OrgModelForm
from ..models import AdminUser, SystemUser
logger = get_logger(__file__)
__all__ = [
......@@ -85,7 +86,7 @@ class AdminUserForm(PasswordAndKeyAuthForm):
}
class SystemUserForm(PasswordAndKeyAuthForm):
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
# Admin user assets define, let user select, save it in form not in view
auto_generate_key = forms.BooleanField(initial=True, required=False)
......@@ -136,11 +137,14 @@ class SystemUserForm(PasswordAndKeyAuthForm):
fields = [
'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key_file', 'auto_push', 'sudo',
'comment', 'shell', 'priority', 'login_mode',
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
'cmd_filters': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Command filter')
}),
}
help_texts = {
'name': '* required',
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from .user import AdminUser, SystemUser
from .user import *
from .label import Label
from .cluster import *
from .group import *
from .domain import *
from .node import *
from .asset import *
from .cmd_filter import *
from .utils import *
......@@ -15,7 +15,7 @@ from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin,OrgManager
from orgs.mixins import OrgModelMixin, OrgManager
__all__ = ['Asset']
logger = logging.getLogger(__name__)
......
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
__all__ = [
'CommandFilter', 'CommandFilterRule'
]
class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
def __str__(self):
return self.name
class CommandFilterRule(OrgModelMixin):
TYPE_REGEX = 'regex'
TYPE_COMMAND = 'command'
TYPE_CHOICES = (
(TYPE_REGEX, _('Regex')),
(TYPE_COMMAND, _('Command')),
)
ACTION_DENY, ACTION_ALLOW = range(2)
ACTION_CHOICES = (
(ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(max_length=1024, verbose_name=_("Content"), help_text=_("One line one command"))
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
class Meta:
ordering = ('priority', 'action')
def __str__(self):
return '{} % {}'.format(self.type, self.content)
......@@ -38,12 +38,10 @@ class Node(OrgModelMixin):
return True
self_key = [int(k) for k in self.key.split(':')]
other_key = [int(k) for k in other.key.split(':')]
if len(self_key) < len(other_key):
return True
elif len(self_key) > len(other_key):
return False
else:
return self_key[-1] < other_key[-1]
return self_key.__lt__(other_key)
def __lt__(self, other):
return not self.__gt__(other)
@property
def name(self):
......
......@@ -3,7 +3,6 @@
#
import logging
import uuid
from django.core.cache import cache
from django.db import models
......@@ -118,6 +117,7 @@ class SystemUser(AssetUser):
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}"
......@@ -163,6 +163,14 @@ class SystemUser(AssetUser):
def expire_cache(self):
cache.delete(self.cache_key.format(self.id))
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).order_by('priority').distinct()
return rules
@classmethod
def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid))
......
......@@ -7,3 +7,4 @@ from .label import *
from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.fields import ChoiceDisplayField
from ..models import CommandFilter, CommandFilterRule, SystemUser
class CommandFilterSerializer(serializers.ModelSerializer):
rules = serializers.PrimaryKeyRelatedField(queryset=CommandFilterRule.objects.all(), many=True)
system_users = serializers.PrimaryKeyRelatedField(queryset=SystemUser.objects.all(), many=True)
class Meta:
model = CommandFilter
fields = '__all__'
class CommandFilterRuleSerializer(serializers.ModelSerializer):
serializer_choice_field = ChoiceDisplayField
class Meta:
model = CommandFilterRule
fields = '__all__'
......@@ -62,6 +62,10 @@
</div>
</div>
{% endblock %}
<div id="command-filter-block">
<h3>{% trans 'Command filter' %}</h3>
{% bootstrap_field form.cmd_filters layout="horizontal" %}
</div>
<h3>{% trans 'Other' %}</h3>
{% bootstrap_field form.sudo layout="horizontal" %}
{% bootstrap_field form.shell layout="horizontal" %}
......@@ -101,6 +105,7 @@ var need_change_field_login_mode = [
function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') {
$('.auth-fields').removeClass('hidden');
$('#command-filter-block').addClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden')
});
......
......@@ -90,7 +90,7 @@
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.value }}</option>
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
......
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="groupForm" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.comment layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li class="active">
<a href="{% url 'assets:cmd-filter-detail' pk=object.id %}" class="text-center">
<i class="fa fa-laptop"></i> {% trans 'Detail' %}
</a>
</li>
<li>
<li>
<a href="{% url 'assets:cmd-filter-rule-list' pk=object.id %}" class="text-center">
<i class="fa fa-laptop"></i> {% trans 'Rules' %}
</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:cmd-filter-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-del">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ object.name }}</b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ object.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ object.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Date updated' %}:</td>
<td><b>{{ object.date_updated }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ object.created_by }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0; padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'System users' %}
</div>
<div class="panel-body">
<table class="table group_edit" id="table-clusters">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Binding to system user' %}" id="system_users_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for system_user in system_users_remain %}
<option value="{{ system_user.id }}" id="opt_{{ system_user.id }}">{{ system_user }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-primary btn-sm" id="btn-binding-system-users">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for system_user in object.system_users.all %}
<tr>
<td><b class="bdg-system-users" data-gid={{ system_user.id }}>{{ system_user }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-unbound-system-user" data-gid={{ system_user.id }} type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
function updateCMDFilterSystemUsers(system_users) {
var the_url = "{% url 'api-assets:cmd-filter-detail' pk=object.id %}";
var body = {
system_users: Object.assign([], system_users)
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
method: 'PATCH',
success: success
});
}
$(document).ready(function () {
$(".select2").select2();
}).on('click', '#btn-binding-system-users', function () {
var origin_system_users = $.map($(".bdg-system-users"), function (s) {
return $(s).data('gid')
});
var new_selected_system_users_id = $.map($("#system_users_selected").select2('data'), function (s) {
return s.id;
});
var system_users = origin_system_users.concat(new_selected_system_users_id);
updateCMDFilterSystemUsers(system_users)
}).on('click', '.btn-unbound-system-user', function () {
var unbound_system_user = $(this).data('gid');
var origin_system_users = $.map($(".bdg-system-users"), function (s) {
return $(s).data('gid')
});
var system_users = $.grep(origin_system_users, function (n, i) {
return n !== unbound_system_user
});
updateCMDFilterSystemUsers(system_users)
})
</script>
{% endblock %}
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block help_message %}
<div class="alert alert-info help-message">
{% trans 'System user bound some command filter, each command filter has some rules,'%}
{% trans 'When user login asset with this system user, then run a command,' %}
{% trans 'The command will be filter by rules, higher priority(lower number) rule run first,' %}
{% trans 'When a rule matched, if rule action is allow, then allow command execute,' %}
{% trans 'else if action is deny, then command with be deny,' %}
{% trans 'else match next rule, if none matched, allowed' %}
</div>
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:cmd-filter-create' %}" class="btn btn-sm btn-primary"> {% trans "Create command filter" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="cmd_filter_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Rules' %}</th>
<th class="text-center">{% trans 'System users' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#cmd_filter_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url 'assets:cmd-filter-detail' pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
var filters_list_btn = '<a href="{% url "assets:cmd-filter-rule-list" pk=DEFAULT_PK %}">' + cellData.length + '</a>';
filters_list_btn = filters_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
$(td).html(filters_list_btn);
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var system_users_list_btn = '<a href="{% url "assets:cmd-filter-detail" pk=DEFAULT_PK %}">' + cellData.length + '</a>';
system_users_list_btn = system_users_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
$(td).html(system_users_list_btn);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:cmd-filter-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:cmd-filter-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "rules" },
{data: "system_users" }, {data: "comment"}, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#cmd_filter_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:cmd-filter-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}
Supports Markdown
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