Merge pull request #3591 from jumpserver/dev

Dev
pull/3671/head
BaiJiangJie 2020-01-03 17:03:37 +08:00 committed by GitHub
commit a27fb18a17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
362 changed files with 13367 additions and 8363 deletions

View File

@ -1 +1,2 @@
from .remote_app import * from .remote_app import *
from .database_app import *

View File

@ -0,0 +1,20 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from .. import models
from .. import serializers
from ..hands import IsOrgAdminOrAppUser
__all__ = [
'DatabaseAppViewSet',
]
class DatabaseAppViewSet(OrgBulkModelViewSet):
model = models.DatabaseApp
filter_fields = ('name',)
search_fields = filter_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.DatabaseAppSerializer

View File

@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
# RemoteApp # RemoteApp
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor' REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
REMOTE_APP_TYPE_CHROME = 'chrome' REMOTE_APP_TYPE_CHROME = 'chrome'
@ -12,29 +13,6 @@ REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client' REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
REMOTE_APP_TYPE_CUSTOM = 'custom' REMOTE_APP_TYPE_CUSTOM = 'custom'
REMOTE_APP_TYPE_CHOICES = (
(
_('Browser'),
(
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
)
),
(
_('Database tools'),
(
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
)
),
(
_('Virtualization tools'),
(
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
)
),
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
)
# Fields attribute write_only default => False # Fields attribute write_only default => False
REMOTE_APP_TYPE_CHROME_FIELDS = [ REMOTE_APP_TYPE_CHROME_FIELDS = [
@ -60,9 +38,26 @@ REMOTE_APP_TYPE_CUSTOM_FIELDS = [
{'name': 'custom_password', 'write_only': True} {'name': 'custom_password', 'write_only': True}
] ]
REMOTE_APP_TYPE_MAP_FIELDS = { REMOTE_APP_TYPE_FIELDS_MAP = {
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS, REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS,
REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS, REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS,
REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS, REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS,
REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
} }
REMOTE_APP_TYPE_CHOICES = (
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
)
# DatabaseApp
DATABASE_APP_TYPE_MYSQL = 'mysql'
DATABASE_APP_TYPE_CHOICES = (
(DATABASE_APP_TYPE_MYSQL, 'MySQL'),
)

View File

@ -1 +1,2 @@
from .remote_app import * from .remote_app import *
from .database_app import *

View File

@ -0,0 +1,26 @@
# coding: utf-8
#
from django import forms
from django.utils.translation import ugettext_lazy as _
from .. import models
__all__ = ['DatabaseAppMySQLForm']
class BaseDatabaseAppForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['type'].widget.attrs['disabled'] = True
class Meta:
model = models.DatabaseApp
fields = [
'name', 'type', 'host', 'port', 'database', 'comment'
]
class DatabaseAppMySQLForm(BaseDatabaseAppForm):
pass

View File

@ -5,18 +5,52 @@ from django.utils.translation import ugettext as _
from django import forms from django import forms
from orgs.mixins.forms import OrgModelForm from orgs.mixins.forms import OrgModelForm
from assets.models import SystemUser
from ..models import RemoteApp from ..models import RemoteApp
from .. import const
__all__ = [ __all__ = [
'RemoteAppCreateUpdateForm', 'RemoteAppChromeForm', 'RemoteAppMySQLWorkbenchForm',
'RemoteAppVMwareForm', 'RemoteAppCustomForm'
] ]
class RemoteAppTypeChromeForm(forms.ModelForm): class BaseRemoteAppForm(OrgModelForm):
default_initial_data = {}
def __init__(self, *args, **kwargs):
# 过滤RDP资产和系统用户
super().__init__(*args, **kwargs)
field_asset = self.fields['asset']
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
self.fields['type'].widget.attrs['disabled'] = True
self.fields.move_to_end('comment')
self.initial_default()
def initial_default(self):
for name, value in self.default_initial_data.items():
field = self.fields.get(name)
if not field:
continue
field.initial = value
class Meta:
model = RemoteApp
fields = [
'name', 'asset', 'type', 'path', 'comment'
]
widgets = {
'asset': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Asset')
}),
}
class RemoteAppChromeForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
}
chrome_target = forms.CharField( chrome_target = forms.CharField(
max_length=128, label=_('Target URL'), required=False max_length=128, label=_('Target URL'), required=False
) )
@ -29,7 +63,12 @@ class RemoteAppTypeChromeForm(forms.ModelForm):
) )
class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm): class RemoteAppMySQLWorkbenchForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files\MySQL\MySQL Workbench 8.0 CE'
r'\MySQLWorkbench.exe'
}
mysql_workbench_ip = forms.CharField( mysql_workbench_ip = forms.CharField(
max_length=128, label=_('Database IP'), required=False max_length=128, label=_('Database IP'), required=False
) )
@ -45,7 +84,12 @@ class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm):
) )
class RemoteAppTypeVMwareForm(forms.ModelForm): class RemoteAppVMwareForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files (x86)\VMware\Infrastructure'
r'\Virtual Infrastructure Client\Launcher\VpxClient.exe'
}
vmware_target = forms.CharField( vmware_target = forms.CharField(
max_length=128, label=_('Target address'), required=False max_length=128, label=_('Target address'), required=False
) )
@ -58,7 +102,8 @@ class RemoteAppTypeVMwareForm(forms.ModelForm):
) )
class RemoteAppTypeCustomForm(forms.ModelForm): class RemoteAppCustomForm(BaseRemoteAppForm):
custom_cmdline = forms.CharField( custom_cmdline = forms.CharField(
max_length=128, label=_('Operating parameter'), required=False max_length=128, label=_('Operating parameter'), required=False
) )
@ -73,51 +118,3 @@ class RemoteAppTypeCustomForm(forms.ModelForm):
max_length=128, label=_('Login password'), required=False max_length=128, label=_('Login password'), required=False
) )
class RemoteAppTypeForms(
RemoteAppTypeChromeForm,
RemoteAppTypeMySQLWorkbenchForm,
RemoteAppTypeVMwareForm,
RemoteAppTypeCustomForm
):
pass
class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm):
def __init__(self, *args, **kwargs):
# 过滤RDP资产和系统用户
super().__init__(*args, **kwargs)
field_asset = self.fields['asset']
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
class Meta:
model = RemoteApp
fields = [
'name', 'asset', 'type', 'path', 'comment'
]
widgets = {
'asset': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Asset')
}),
}
def _clean_params(self):
app_type = self.data.get('type')
fields = const.REMOTE_APP_TYPE_MAP_FIELDS.get(app_type, [])
params = {}
for field in fields:
name = field['name']
value = self.cleaned_data[name]
params.update({name: value})
return params
def _save_params(self, instance):
params = self._clean_params()
instance.params = params
instance.save()
return instance
def save(self, commit=True):
instance = super().save(commit=commit)
instance = self._save_params(instance)
return instance

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.11 on 2019-12-10 08:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0002_remove_remoteapp_system_user'),
]
operations = [
migrations.AlterField(
model_name='remoteapp',
name='type',
field=models.CharField(choices=[('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type'),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.1.11 on 2019-12-18 09:05
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('applications', '0003_auto_20191210_1659'),
]
operations = [
migrations.CreateModel(
name='DatabaseApp',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=128, verbose_name='Type')),
('host', models.CharField(db_index=True, max_length=128, verbose_name='Host')),
('port', models.IntegerField(default=3306, verbose_name='Port')),
('database', models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='Database')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
],
options={
'verbose_name': 'DatabaseApp',
'ordering': ('name',),
},
),
migrations.AlterUniqueTogether(
name='databaseapp',
unique_together={('org_id', 'name')},
),
]

View File

@ -1 +1,2 @@
from .remote_app import * from .remote_app import *
from .database_app import *

View File

@ -0,0 +1,42 @@
# coding: utf-8
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from .. import const
__all__ = ['DatabaseApp']
class DatabaseApp(CommonModelMixin, OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
type = models.CharField(
default=const.DATABASE_APP_TYPE_MYSQL,
choices=const.DATABASE_APP_TYPE_CHOICES,
max_length=128, verbose_name=_('Type')
)
host = models.CharField(
max_length=128, verbose_name=_('Host'), db_index=True
)
port = models.IntegerField(default=3306, verbose_name=_('Port'))
database = models.CharField(
max_length=128, blank=True, null=True, verbose_name=_('Database'),
db_index=True
)
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name'), ]
verbose_name = _("DatabaseApp")
ordering = ('name', )

View File

@ -62,7 +62,7 @@ class RemoteApp(OrgModelMixin):
_parameters.append(self.type) _parameters.append(self.type)
path = '\"%s\"' % self.path path = '\"%s\"' % self.path
_parameters.append(path) _parameters.append(path)
for field in const.REMOTE_APP_TYPE_MAP_FIELDS[self.type]: for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]:
value = self.params.get(field['name']) value = self.params.get(field['name'])
if value is None: if value is None:
continue continue

View File

@ -1 +1,2 @@
from .remote_app import * from .remote_app import *
from .database_app import *

View File

@ -0,0 +1,26 @@
# coding: utf-8
#
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
from .. import models
__all__ = [
'DatabaseAppSerializer',
]
class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = models.DatabaseApp
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'type', 'get_type_display', 'host', 'port',
'database', 'comment', 'created_by', 'date_created', 'date_updated',
]
read_only_fields = [
'created_by', 'date_created', 'date_updated'
'get_type_display',
]

View File

@ -1,10 +1,11 @@
# coding: utf-8 # coding: utf-8
# #
import copy
from rest_framework import serializers from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.fields.serializer import CustomMetaDictField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import const from .. import const
@ -16,72 +17,54 @@ __all__ = [
] ]
class RemoteAppParamsDictField(serializers.DictField): class RemoteAppParamsDictField(CustomMetaDictField):
""" type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
RemoteApp field => params default_type = const.REMOTE_APP_TYPE_CHROME
""" convert_key_remove_type_prefix = False
@staticmethod convert_key_to_upper = False
def filter_attribute(attribute, instance):
"""
过滤掉params字段值中write_only特性的key-value值
For example, the chrome_password field is not returned when serializing
{
'chrome_target': 'http://www.jumpserver.org/',
'chrome_username': 'admin',
'chrome_password': 'admin',
}
"""
for field in const.REMOTE_APP_TYPE_MAP_FIELDS[instance.type]:
if field.get('write_only', False):
attribute.pop(field['name'], None)
return attribute
def get_attribute(self, instance):
"""
序列化时调用
"""
attribute = super().get_attribute(instance)
attribute = self.filter_attribute(attribute, instance)
return attribute
@staticmethod
def filter_value(dictionary, value):
"""
过滤掉不属于当前app_type所包含的key-value值
"""
app_type = dictionary.get('type', const.REMOTE_APP_TYPE_CHROME)
fields = const.REMOTE_APP_TYPE_MAP_FIELDS[app_type]
fields_names = [field['name'] for field in fields]
no_need_keys = [k for k in value.keys() if k not in fields_names]
for k in no_need_keys:
value.pop(k)
return value
def get_value(self, dictionary):
"""
反序列化时调用
"""
value = super().get_value(dictionary)
value = self.filter_value(dictionary, value)
return value
class RemoteAppSerializer(BulkOrgResourceModelSerializer): class RemoteAppSerializer(BulkOrgResourceModelSerializer):
params = RemoteAppParamsDictField() params = RemoteAppParamsDictField()
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
class Meta: class Meta:
model = RemoteApp model = RemoteApp
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ fields = [
'id', 'name', 'asset', 'type', 'path', 'params', 'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display',
'comment', 'created_by', 'date_created', 'asset_info', 'path', 'params', 'date_created', 'created_by', 'comment',
'get_type_display',
] ]
read_only_fields = [ read_only_fields = [
'created_by', 'date_created', 'asset_info', 'created_by', 'date_created', 'asset_info',
'get_type_display' 'get_type_display'
] ]
def process_params(self, instance, validated_data):
new_params = copy.deepcopy(validated_data.get('params', {}))
tp = validated_data.get('type', '')
if tp != instance.type:
return new_params
old_params = instance.params
fields = self.type_fields_map.get(instance.type, [])
for field in fields:
if not field.get('write_only', False):
continue
field_name = field['name']
new_value = new_params.get(field_name, '')
old_value = old_params.get(field_name, '')
field_value = new_value if new_value else old_value
new_params[field_name] = field_value
return new_params
def update(self, instance, validated_data):
params = self.process_params(instance, validated_data)
validated_data['params'] = params
return super().update(instance, validated_data)
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
parameter_remote_app = serializers.SerializerMethodField() parameter_remote_app = serializers.SerializerMethodField()

View File

@ -0,0 +1,55 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="DatabaseAppForm" method="post" class="form-horizontal">
{% bootstrap_form form 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 %}
{% block custom_foot_js %}
<script type="text/javascript">
var app_type_id = '#' + '{{ form.type.id_for_label }}';
function getFormDataType(){
return $(app_type_id+ " option:selected").val();
}
function getFormData(form){
var data = form.serializeObject();
data['type'] = getFormDataType();
return data
}
$(document).ready(function () {
})
.on("submit", "form", function (evt) {
evt.preventDefault();
var the_url = '{% url "api-applications:database-app-list" %}';
var redirect_to = '{% url "applications:database-app-list" %}';
var method = "POST";
{% if api_action == "update" %}
the_url = '{% url "api-applications:database-app-detail" object.id %}';
method = "PUT";
{% endif %}
var form = $("form");
var data = getFormData(form);
var props = {
url: the_url,
data: data,
method: method,
form: form,
redirect_to: redirect_to
};
formSubmit(props);
});
</script>
{% endblock %}

View File

@ -0,0 +1,103 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% 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 'applications:database-app-detail' pk=database_app.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'applications:database-app-update' pk=database_app.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-delete-application">
<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>{{ database_app.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>{{ database_app.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Type' %}:</td>
<td><b>{{ database_app.get_type_display }}</b></td>
</tr>
<tr>
<td>{% trans 'Host' %}:</td>
<td><b>{{ database_app.host }}</b></td>
</tr>
<tr>
<td>{% trans 'Port' %}:</td>
<td><b>{{ database_app.port }}</b></td>
</tr>
<tr>
<td>{% trans 'Database' %}:</td>
<td><b>{{ database_app.database }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ database_app.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ database_app.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ database_app.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
})
.on('click', '.btn-delete-application', function () {
var $this = $(this);
var name = "{{ database_app.name }}";
var rid = "{{ database_app.id }}";
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
var redirect_url = "{% url 'applications:database-app-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
</script>
{% endblock %}

View File

@ -0,0 +1,89 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block help_message %}
{% endblock %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="btn-group uc pull-left m-r-5">
<button class="btn btn-sm btn-primary">
{% trans "Create DatabaseApp" %}
</button>
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle"><span class="caret"></span></button>
<ul class="dropdown-menu">
{% for key, value in type_choices %}
<li><a class="" href="{% url 'applications:database-app-create' %}?type={{ key }}">{{ value }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-bordered table-hover " id="database_app_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 'Type' %}</th>
<th class="text-center">{% trans 'Host' %}</th>
<th class="text-center">{% trans 'Port' %}</th>
<th class="text-center">{% trans 'Database' %}</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: $('#database_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'applications:database-app-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
$(td).html(rowData.get_type_display)
}},
{targets: 7, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "applications:database-app-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-rid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-applications:database-app-list" %}',
columns: [
{data: "id"},
{data: "name" },
{data: "type"},
{data: "host"},
{data: "port"},
{data: "database"},
{data: "comment"},
{data: "id", orderable: false, width: "120px"}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#database_app_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var rid = $this.data('rid');
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@ -4,51 +4,8 @@
{% load i18n %} {% load i18n %}
{% block form %} {% block form %}
<form id="appForm" method="post" class="form-horizontal"> <form id="RemoteAppForm" method="post" class="form-horizontal">
{% if form.non_field_errors %} {% bootstrap_form form layout="horizontal" %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.asset layout="horizontal" %}
{% bootstrap_field form.type layout="horizontal" %}
{% bootstrap_field form.path layout="horizontal" %}
<div class="hr-line-dashed"></div>
{# chrome #}
<div class="chrome-fields">
{% bootstrap_field form.chrome_target layout="horizontal" %}
{% bootstrap_field form.chrome_username layout="horizontal" %}
{% bootstrap_field form.chrome_password layout="horizontal" %}
</div>
{# mysql workbench #}
<div class="mysql_workbench-fields">
{% bootstrap_field form.mysql_workbench_ip layout="horizontal" %}
{% bootstrap_field form.mysql_workbench_name layout="horizontal" %}
{% bootstrap_field form.mysql_workbench_username layout="horizontal" %}
{% bootstrap_field form.mysql_workbench_password layout="horizontal" %}
</div>
{# vmware #}
<div class="vmware_client-fields">
{% bootstrap_field form.vmware_target layout="horizontal" %}
{% bootstrap_field form.vmware_username layout="horizontal" %}
{% bootstrap_field form.vmware_password layout="horizontal" %}
</div>
{# custom #}
<div class="custom-fields">
{% bootstrap_field form.custom_cmdline layout="horizontal" %}
{% bootstrap_field form.custom_target layout="horizontal" %}
{% bootstrap_field form.custom_username layout="horizontal" %}
{% bootstrap_field form.custom_password layout="horizontal" %}
</div>
{% bootstrap_field form.comment layout="horizontal" %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-4 col-sm-offset-2"> <div class="col-sm-4 col-sm-offset-2">
@ -57,93 +14,49 @@
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button> <button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div> </div>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script type="text/javascript"> <script type="text/javascript">
var app_type_id = '#' + '{{ form.type.id_for_label }}'; var app_type_id = '#' + '{{ form.type.id_for_label }}';
var app_path_id = '#' + '{{ form.path.id_for_label }}';
var all_type_fields = [ function getFormDataType(){
'.chrome-fields',
'.mysql_workbench-fields',
'.vmware_client-fields',
'.custom-fields'
];
var app_type_map_default_fields_value = {
'chrome': {
'app_path': 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
},
'mysql_workbench': {
'app_path': 'C:\\Program Files\\MySQL\\MySQL Workbench 8.0 CE\\MySQLWorkbench.exe'
},
'vmware_client': {
'app_path': 'C:\\Program Files (x86)\\VMware\\Infrastructure\\Virtual Infrastructure Client\\Launcher\\VpxClient.exe'
},
'custom': {'app_path': ''}
};
function getAppType(){
return $(app_type_id+ " option:selected").val(); return $(app_type_id+ " option:selected").val();
} }
function initialDefaultValue(){ function constructFormDataParams(data){
var app_type = getAppType();
var app_path = $(app_path_id).val();
if(app_path){
app_type_map_default_fields_value[app_type]['app_path'] = app_path
}
}
function setDefaultValue(){
// 设置类型相关字段的默认值
var app_type = getAppType();
var app_path = app_type_map_default_fields_value[app_type]['app_path'];
$(app_path_id).val(app_path)
}
function hiddenFields(){
var app_type = getAppType();
$.each(all_type_fields, function(index, value){
$(value).addClass('hidden')
});
$('.' + app_type + '-fields').removeClass('hidden');
}
function constructParams(data) {
var typeList = ['chrome', 'mysql_workbench', 'vmware_client', 'custom'];
var params = {}; var params = {};
$.each(typeList, function(index, value){ var type =data.type;
if (data.type === value){ for (var k in data){
for (var k in data){ if (k.startsWith(type)){
if (k.startsWith(value)){ params[k] = data[k];
params[k] = data[k] delete data[k]
}
}
} }
}); }
return params; return params
} }
function getFormData(form){
var data = form.serializeObject();
data['type'] = getFormDataType();
data['params'] = constructFormDataParams(data);
return data
}
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2({ $('.select2').select2({
closeOnSelect: true closeOnSelect: true
}); });
initialDefaultValue(); }).on("submit", "form", function (evt) {
hiddenFields();
setDefaultValue();
})
.on('change', app_type_id, function(){
hiddenFields();
setDefaultValue();
})
.on("submit", "form", function (evt) {
evt.preventDefault(); evt.preventDefault();
var the_url = '{% url "api-applications:remote-app-list" %}'; var the_url = '{% url "api-applications:remote-app-list" %}';
var redirect_to = '{% url "applications:remote-app-list" %}'; var redirect_to = '{% url "applications:remote-app-list" %}';
var method = "POST"; var method = "POST";
{% if type == "update" %} {% if api_action == "update" %}
the_url = '{% url "api-applications:remote-app-detail" object.id %}'; the_url = '{% url "api-applications:remote-app-detail" object.id %}';
method = "PUT"; method = "PUT";
{% endif %} {% endif %}
var form = $("form"); var form = $("form");
var data = form.serializeObject(); var data = getFormData(form);
data["params"] = constructParams(data);
var props = { var props = {
url: the_url, url: the_url,
data: data, data: data,

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -6,8 +6,16 @@
{% endblock %} {% endblock %}
{% block table_search %}{% endblock %} {% block table_search %}{% endblock %}
{% block table_container %} {% block table_container %}
<div class="uc pull-left m-r-5"> <div class="btn-group uc pull-left m-r-5">
<a href="{% url 'applications:remote-app-create' %}" class="btn btn-sm btn-primary"> {% trans "Create RemoteApp" %} </a> <button class="btn btn-sm btn-primary">
{% trans "Create RemoteApp" %}
</button>
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle"><span class="caret"></span></button>
<ul class="dropdown-menu">
{% for key, value in type_choices %}
<li><a class="" href="{% url 'applications:remote-app-create' %}?type={{ key }}">{{ value }}</a></li>
{% endfor %}
</ul>
</div> </div>
<table class="table table-striped table-bordered table-hover " id="remote_app_list_table" > <table class="table table-striped table-bordered table-hover " id="remote_app_list_table" >
<thead> <thead>
@ -61,7 +69,7 @@ function initTable() {
{data: "get_type_display", orderable: false}, {data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false}, {data: "asset_info", orderable: false},
{data: "comment"}, {data: "comment"},
{data: "id", orderable: false} {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };

View File

@ -0,0 +1,83 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block custom_head_css_js %}
<script src="{% static 'js/jquery.form.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="database_app_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 'Type' %}</th>
<th class="text-center">{% trans 'Host' %}</th>
<th class="text-center">{% trans 'Database' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var inited = false;
var database_app_table, url;
function initTable() {
if (inited){
return
} else {
inited = true;
}
url = '{% url "api-perms:my-database-apps" %}';
var options = {
ele: $('#database_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData);
$(td).html(name)
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
var type = htmlEscape(rowData.get_type_display);
$(td).html(type);
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var host = htmlEscape(cellData);
$(td).html(host);
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var database = htmlEscape(cellData);
$(td).html(database);
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var conn_btn = '<a href="{% url "luna-view" %}?type=database_app&login_to=' + cellData +'" class="btn btn-xs btn-primary" target="_blank">{% trans "Connect" %}</a>';
$(td).html(conn_btn)
}}
],
ajax_url: url,
columns: [
{data: "id"},
{data: "name"},
{data: "type"},
{data: "host"},
{data: "database"},
{data: "comment", orderable: false},
{data: "id", orderable: false}
]
};
database_app_table = jumpserver.initServerSideDataTable(options);
return database_app_table
}
$(document).ready(function(){
initTable();
})
</script>
{% endblock %}

View File

@ -11,10 +11,12 @@ app_name = 'applications'
router = BulkRouter() router = BulkRouter()
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
urlpatterns = [ urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
] ]
old_version_urlpatterns = [ old_version_urlpatterns = [
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api) re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
] ]

View File

@ -11,6 +11,13 @@ urlpatterns = [
path('remote-app/<uuid:pk>/update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'), path('remote-app/<uuid:pk>/update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'),
path('remote-app/<uuid:pk>/', views.RemoteAppDetailView.as_view(), name='remote-app-detail'), path('remote-app/<uuid:pk>/', views.RemoteAppDetailView.as_view(), name='remote-app-detail'),
# User RemoteApp view # User RemoteApp view
path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list') path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list'),
path('database-app/', views.DatabaseAppListView.as_view(), name='database-app-list'),
path('database-app/create/', views.DatabaseAppCreateView.as_view(), name='database-app-create'),
path('database-app/<uuid:pk>/update/', views.DatabaseAppUpdateView.as_view(), name='database-app-update'),
path('database-app/<uuid:pk>/', views.DatabaseAppDetailView.as_view(), name='database-app-detail'),
# User DatabaseApp view
path('user-database-app/', views.UserDatabaseAppListView.as_view(), name='user-database-app-list'),
] ]

View File

@ -1 +1,2 @@
from .remote_app import * from .remote_app import *
from .database_app import *

View File

@ -0,0 +1,115 @@
# coding: utf-8
#
from django.http import Http404
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, UpdateView
from django.utils.translation import ugettext_lazy as _
from django.views.generic.detail import DetailView
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
from .. import models, const, forms
__all__ = [
'DatabaseAppListView', 'DatabaseAppCreateView', 'DatabaseAppUpdateView',
'DatabaseAppDetailView', 'UserDatabaseAppListView',
]
class DatabaseAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/database_app_list.html'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _("Application"),
'action': _('DatabaseApp list'),
'type_choices': const.DATABASE_APP_TYPE_CHOICES
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class BaseDatabaseAppCreateUpdateView:
template_name = 'applications/database_app_create_update.html'
model = models.DatabaseApp
permission_classes = [IsOrgAdmin]
default_type = const.DATABASE_APP_TYPE_MYSQL
form_class = forms.DatabaseAppMySQLForm
form_class_choices = {
const.DATABASE_APP_TYPE_MYSQL: forms.DatabaseAppMySQLForm,
}
def get_initial(self):
return {'type': self.get_type()}
def get_type(self):
return self.default_type
def get_form_class(self):
tp = self.get_type()
form_class = self.form_class_choices.get(tp)
if not form_class:
raise Http404()
return form_class
class DatabaseAppCreateView(BaseDatabaseAppCreateUpdateView, CreateView):
def get_type(self):
tp = self.request.GET.get("type")
if tp:
return tp.lower()
return super().get_type()
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Create DatabaseApp'),
'api_action': 'create'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DatabaseAppUpdateView(BaseDatabaseAppCreateUpdateView, UpdateView):
def get_type(self):
return self.object.type
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Create DatabaseApp'),
'api_action': 'update'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DatabaseAppDetailView(PermissionsMixin, DetailView):
template_name = 'applications/database_app_detail.html'
model = models.DatabaseApp
context_object_name = 'database_app'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('DatabaseApp detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserDatabaseAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/user_database_app_list.html'
permission_classes = [IsValidUser]
def get_context_data(self, **kwargs):
context = {
'action': _('My DatabaseApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -1,19 +1,16 @@
# coding: utf-8 # coding: utf-8
# #
from django.http import Http404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
from common.const import create_success_msg, update_success_msg
from ..models import RemoteApp from ..models import RemoteApp
from .. import forms from .. import forms, const
__all__ = [ __all__ = [
@ -30,53 +27,79 @@ class RemoteAppListView(PermissionsMixin, TemplateView):
context = { context = {
'app': _('Applications'), 'app': _('Applications'),
'action': _('RemoteApp list'), 'action': _('RemoteApp list'),
'type_choices': const.REMOTE_APP_TYPE_CHOICES,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): class BaseRemoteAppCreateUpdateView:
template_name = 'applications/remote_app_create_update.html' template_name = 'applications/remote_app_create_update.html'
model = RemoteApp model = RemoteApp
form_class = forms.RemoteAppCreateUpdateForm
success_url = reverse_lazy('applications:remote-app-list')
permission_classes = [IsOrgAdmin] permission_classes = [IsOrgAdmin]
default_type = const.REMOTE_APP_TYPE_CHROME
form_class = forms.RemoteAppChromeForm
form_class_choices = {
const.REMOTE_APP_TYPE_CHROME: forms.RemoteAppChromeForm,
const.REMOTE_APP_TYPE_MYSQL_WORKBENCH: forms.RemoteAppMySQLWorkbenchForm,
const.REMOTE_APP_TYPE_VMWARE_CLIENT: forms.RemoteAppVMwareForm,
const.REMOTE_APP_TYPE_CUSTOM: forms.RemoteAppCustomForm
}
def get_initial(self):
return {'type': self.get_type()}
def get_type(self):
return self.default_type
def get_form_class(self):
tp = self.get_type()
form_class = self.form_class_choices.get(tp)
if not form_class:
raise Http404()
return form_class
class RemoteAppCreateView(BaseRemoteAppCreateUpdateView,
PermissionsMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Applications'), 'app': _('Applications'),
'action': _('Create RemoteApp'), 'action': _('Create RemoteApp'),
'type': 'create' 'api_action': 'create'
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_success_message(self, cleaned_data): def get_type(self):
return create_success_msg % ({'name': cleaned_data['name']}) tp = self.request.GET.get("type")
if tp:
return tp.lower()
return super().get_type()
class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): class RemoteAppUpdateView(BaseRemoteAppCreateUpdateView,
template_name = 'applications/remote_app_create_update.html' PermissionsMixin, UpdateView):
model = RemoteApp
form_class = forms.RemoteAppCreateUpdateForm
success_url = reverse_lazy('applications:remote-app-list')
permission_classes = [IsOrgAdmin]
def get_initial(self): def get_initial(self):
return {k: v for k, v in self.object.params.items()} initial_data = super().get_initial()
params = {k: v for k, v in self.object.params.items()}
initial_data.update(params)
return initial_data
def get_type(self):
return self.object.type
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Applications'), 'app': _('Applications'),
'action': _('Update RemoteApp'), 'action': _('Update RemoteApp'),
'type': 'update' 'api_action': 'update'
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_success_message(self, cleaned_data):
return update_success_msg % ({'name': cleaned_data['name']})
class RemoteAppDetailView(PermissionsMixin, DetailView): class RemoteAppDetailView(PermissionsMixin, DetailView):
template_name = 'applications/remote_app_detail.html' template_name = 'applications/remote_app_detail.html'

View File

@ -2,6 +2,7 @@ from .admin_user import *
from .asset import * from .asset import *
from .label import * from .label import *
from .system_user import * from .system_user import *
from .system_user_relation import *
from .node import * from .node import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *

View File

@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from django.db import transaction from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.response import Response from rest_framework.response import Response
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
@ -44,6 +45,11 @@ class AdminUserViewSet(OrgBulkModelViewSet):
serializer_class = serializers.AdminUserSerializer serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
return queryset
class AdminUserAuthApi(generics.UpdateAPIView): class AdminUserAuthApi(generics.UpdateAPIView):
model = AdminUser model = AdminUser

View File

@ -4,24 +4,27 @@
import random import random
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from ..models import Asset, Node from ..models import Asset, Node, Platform
from .. import serializers from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \ from ..tasks import (
test_asset_connectivity_manual update_asset_hardware_info_manual, test_asset_connectivity_manual
)
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetViewSet', 'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi', 'AssetGatewayApi', 'AssetPlatformViewSet',
] ]
@ -53,6 +56,34 @@ class AssetViewSet(OrgBulkModelViewSet):
self.set_assets_node(assets) self.set_assets_node(assets)
class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.PlatformSerializer
def get_object(self):
asset_pk = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_pk)
return asset.platform
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']
search_fields = ['name']
def check_object_permissions(self, request, obj):
if request.method.lower() in ['delete', 'put', 'patch'] and \
obj.internal:
self.permission_denied(
request, message={"detail": "Internal platform"}
)
return super().check_object_permissions(request, obj)
class AssetRefreshHardwareApi(generics.RetrieveAPIView): class AssetRefreshHardwareApi(generics.RetrieveAPIView):
""" """
Refresh asset hardware info Refresh asset hardware info

View File

@ -114,7 +114,7 @@ class AssetUserExportViewSet(AssetUserViewSet):
permission_classes = [IsOrgAdminOrAppUser] permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self): def get_permissions(self):
if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions() return super().get_permissions()
@ -124,7 +124,7 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView):
permission_classes = [IsOrgAdminOrAppUser] permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self): def get_permissions(self):
if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions() return super().get_permissions()

View File

@ -177,7 +177,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
if not include_assets: if not include_assets:
return queryset return queryset
assets = self.instance.get_assets().only( assets = self.instance.get_assets().only(
"id", "hostname", "ip", 'platform', "os", "id", "hostname", "ip", "os",
"org_id", "protocols", "org_id", "protocols",
) )
for asset in assets: for asset in assets:

View File

@ -14,8 +14,8 @@
# limitations under the License. # limitations under the License.
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.conf import settings
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Count
from common.serializers import CeleryTaskSerializer from common.serializers import CeleryTaskSerializer
from common.utils import get_logger from common.utils import get_logger
@ -50,6 +50,11 @@ class SystemUserViewSet(OrgBulkModelViewSet):
serializer_class = serializers.SystemUserSerializer serializer_class = serializers.SystemUserSerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
return queryset
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
""" """

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
#
from django.db.models import F, Value
from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
from .. import models, serializers
__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet']
class RelationMixin(OrgBulkModelViewSet):
def get_queryset(self):
queryset = self.model.objects.all()
org_id = current_org.org_id()
if org_id is not None:
queryset = queryset.filter(systemuser__org_id=org_id)
queryset = queryset.annotate(systemuser_display=Concat(
F('systemuser__name'), Value('('), F('systemuser__username'),
Value(')')
))
return queryset
class SystemUserAssetRelationViewSet(RelationMixin):
serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'asset', 'systemuser',
]
search_fields = [
"id", "asset__hostname", "asset__ip",
"systemuser__name", "systemuser__username"
]
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(
asset_display=Concat(
F('asset__hostname'), Value('('),
F('asset__ip'), Value(')')
)
)
return queryset
class SystemUserNodeRelationViewSet(RelationMixin):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'node', 'systemuser',
]
search_fields = [
"node__value", "systemuser__name", "systemuser_username"
]
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset \
.annotate(node_key=F('node__key'))
return queryset

View File

@ -43,7 +43,7 @@ class AssetUserQuerySet(list):
else: else:
in_kwargs[k] = v in_kwargs[k] = v
for k in in_kwargs: for k in in_kwargs:
kwargs.pop(k) kwargs.pop(k, None)
if len(in_kwargs) == 0: if len(in_kwargs) == 0:
return self return self
@ -56,7 +56,7 @@ class AssetUserQuerySet(list):
v = [str(i) for i in v] v = [str(i) for i in v]
if isinstance(attr, uuid.UUID): if isinstance(attr, uuid.UUID):
attr = str(attr) attr = str(attr)
if v in attr: if attr in v:
matched = True matched = True
if matched: if matched:
queryset.append(i) queryset.append(i)
@ -68,11 +68,12 @@ class AssetUserQuerySet(list):
real = [] real = []
for k, v in kwargs.items(): for k, v in kwargs.items():
wanted.append(v) wanted.append(v)
value = getattr(obj, k) value = getattr(obj, k, None)
if isinstance(value, uuid.UUID): if isinstance(value, uuid.UUID):
value = str(value) value = str(value)
real.append(value) real.append(value)
return wanted == real return wanted == real
kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1}
if len(kwargs) > 0: if len(kwargs) > 0:
queryset = AssetUserQuerySet([i for i in self if filter_it(i)]) queryset = AssetUserQuerySet([i for i in self if filter_it(i)])
else: else:

View File

@ -5,3 +5,4 @@ from .label import *
from .user import * from .user import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .platform import *

View File

@ -6,13 +6,13 @@ from django.utils.translation import gettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from orgs.mixins.forms import OrgModelForm from orgs.mixins.forms import OrgModelForm
from ..models import Asset, Node from ..models import Asset
from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', 'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
] ]
@ -27,17 +27,27 @@ class ProtocolForm(forms.Form):
) )
class AssetCreateForm(OrgModelForm): class AssetCreateUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.data: self.set_platform_to_name()
return self.set_fields_queryset()
def set_fields_queryset(self):
nodes_field = self.fields['nodes'] nodes_field = self.fields['nodes']
nodes_choices = []
if self.instance: if self.instance:
nodes_field.choices = [(n.id, n.full_value) for n in nodes_choices = [
self.instance.nodes.all()] (n.id, n.full_value) for n in
else: self.instance.nodes.all()
nodes_field.choices = [] ]
nodes_field.choices = nodes_choices
def set_platform_to_name(self):
platform_field = self.fields['platform']
platform_field.to_field_name = 'name'
if self.instance:
self.initial['platform'] = self.instance.platform.name
def add_nodes_initial(self, node): def add_nodes_initial(self, node):
nodes_field = self.fields['nodes'] nodes_field = self.fields['nodes']
@ -49,7 +59,7 @@ class AssetCreateForm(OrgModelForm):
fields = [ fields = [
'hostname', 'ip', 'public_ip', 'protocols', 'comment', 'hostname', 'ip', 'public_ip', 'protocols', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform', 'nodes', 'is_active', 'admin_user', 'labels', 'platform',
'domain', 'domain', 'number',
] ]
widgets = { widgets = {
'nodes': forms.SelectMultiple(attrs={ 'nodes': forms.SelectMultiple(attrs={
@ -64,52 +74,8 @@ class AssetCreateForm(OrgModelForm):
'domain': forms.Select(attrs={ 'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain') 'class': 'select2', 'data-placeholder': _('Domain')
}), }),
} 'platform': forms.Select(attrs={
labels = { 'class': 'select2', 'data-placeholder': _('Platform')
'nodes': _("Node"),
}
help_texts = {
'hostname': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT,
'admin_user': _(
'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': _("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")
}
class AssetUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.data:
return
nodes_field = self.fields['nodes']
if self.instance:
nodes_field.choices = ((n.id, n.full_value) for n in
self.instance.nodes.all())
else:
nodes_field.choices = []
class Meta:
model = Asset
fields = [
'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels',
'domain',
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
'class': 'nodes-select2', 'data-placeholder': _('Node')
}),
'admin_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Admin user')
}),
'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Label')
}),
'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain')
}), }),
} }
labels = { labels = {

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from django import forms
from django.utils.translation import ugettext_lazy as _
from ..models import Platform
__all__ = ['PlatformForm', 'PlatformMetaForm']
class PlatformMetaForm(forms.Form):
SECURITY_CHOICES = (
('rdp', "RDP"),
('nla', "NLA"),
('tls', 'TLS'),
('any', "Any"),
)
CONSOLE_CHOICES = (
(True, _('Yes')),
(False, _('No')),
)
security = forms.ChoiceField(
choices=SECURITY_CHOICES, initial='any', label=_("RDP security"),
required=False,
)
console = forms.ChoiceField(
choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"),
required=False,
)
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = [
'name', 'base', 'comment',
]
labels = {
'base': _("Base platform")
}

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.7 on 2019-12-06 07:26
import common.fields.model
from django.db import migrations, models
def create_internal_platform(apps, schema_editor):
model = apps.get_model("assets", "Platform")
db_alias = schema_editor.connection.alias
type_platforms = (
('Linux', 'Linux', None),
('Unix', 'Unix', None),
('MacOS', 'MacOS', None),
('BSD', 'BSD', None),
('Windows', 'Windows', None),
('Windows2016', 'Windows', {'security': 'tls'}),
('Other', 'Other', None),
)
for name, base, meta in type_platforms:
model.objects.using(db_alias).create(
name=name, base=base, internal=True, meta=meta
)
class Migration(migrations.Migration):
dependencies = [
('assets', '0043_auto_20191114_1111'),
]
operations = [
migrations.CreateModel(
name='Platform',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('internal', models.BooleanField(default=False, verbose_name='Internal')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
],
options={
'verbose_name': 'Platform'
}
),
migrations.RunPython(create_internal_platform)
]

View File

@ -0,0 +1,47 @@
# Generated by Django 2.2.7 on 2019-12-06 08:07
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
def migrate_platform_to_asset_type(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
platform_model = apps.get_model("assets", "Platform")
db_alias = schema_editor.connection.alias
platforms = platform_model.objects.using(db_alias).all()
platforms_map = {p.name: p for p in platforms}
for name, p in platforms_map.items():
asset_model.objects.using(db_alias)\
.filter(_platform=name)\
.update(platform=p)
class Migration(migrations.Migration):
dependencies = [
('assets', '0044_platform'),
]
operations = [
migrations.RenameField(
model_name='asset',
old_name='platform',
new_name='_platform',
),
migrations.AddField(
model_name='asset',
name='platform',
field=models.ForeignKey(
default=assets.models.asset.Platform.default,
on_delete=django.db.models.deletion.PROTECT,
related_name='assets', to='assets.Platform',
verbose_name='Platform'),
),
migrations.RunPython(migrate_platform_to_asset_type),
migrations.RemoveField(
model_name='asset',
name='_platform',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.11 on 2019-12-18 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0045_auto_20191206_1607'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@ -11,10 +11,12 @@ from collections import OrderedDict
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 .utils import Connectivity from common.fields.model import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from .utils import Connectivity
__all__ = ['Asset', 'ProtocolsMixin'] __all__ = ['Asset', 'ProtocolsMixin', 'Platform']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,6 +39,13 @@ def default_node():
return None return None
class AssetManager(OrgManager):
def get_queryset(self):
return super().get_queryset().annotate(
platform_base=models.F('platform__base')
)
class AssetQuerySet(models.QuerySet): class AssetQuerySet(models.QuerySet):
def active(self): def active(self):
return self.filter(is_active=True) return self.filter(is_active=True)
@ -119,6 +128,47 @@ class NodesRelationMixin:
return nodes return nodes
class Platform(models.Model):
CHARSET_CHOICES = (
('utf8', 'UTF-8'),
('gbk', 'GBK'),
)
BASE_CHOICES = (
('Linux', 'Linux'),
('Unix', 'Unix'),
('MacOS', 'MacOS'),
('BSD', 'BSD'),
('Windows', 'Windows'),
('Other', 'Other'),
)
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base"))
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta"))
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
@classmethod
def default(cls):
linux, created = cls.objects.get_or_create(
defaults={'name': 'Linux'}, name='Linux'
)
return linux.id
def is_windows(self):
return self.base.lower() in ('windows',)
def is_unixlike(self):
return self.base.lower() in ("linux", "unix", "macos", "bsd")
def __str__(self):
return self.name
class Meta:
verbose_name = _("Platform")
# ordering = ('name',)
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
# Important # Important
PLATFORM_CHOICES = ( PLATFORM_CHOICES = (
@ -138,9 +188,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
choices=ProtocolsMixin.PROTOCOL_CHOICES, choices=ProtocolsMixin.PROTOCOL_CHOICES,
verbose_name=_('Protocol')) verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
@ -175,7 +224,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
objects = OrgManager.from_queryset(AssetQuerySet)() objects = AssetManager.from_queryset(AssetQuerySet)()
_connectivity = None _connectivity = None
def __str__(self): def __str__(self):
@ -190,20 +239,18 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
return False, warning return False, warning
return True, warning return True, warning
@lazyproperty
def platform_base(self):
return self.platform.base
def is_windows(self): def is_windows(self):
if self.platform in ("Windows", "Windows2016"): return self.platform.is_windows()
return True
else:
return False
def is_unixlike(self): def is_unixlike(self):
if self.platform not in ("Windows", "Windows2016", "Other"): return self.platform.is_unixlike()
return True
else:
return False
def is_support_ansible(self): def is_support_ansible(self):
return self.has_protocol('ssh') and self.platform not in ("Other",) return self.has_protocol('ssh') and self.platform_base not in ("Other",)
@property @property
def cpu_info(self): def cpu_info(self):
@ -264,9 +311,9 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def as_tree_node(self, parent_node): def as_tree_node(self, parent_node):
from common.tree import TreeNode from common.tree import TreeNode
icon_skin = 'file' icon_skin = 'file'
if self.platform.lower() == 'windows': if self.platform_base.lower() == 'windows':
icon_skin = 'windows' icon_skin = 'windows'
elif self.platform.lower() == 'linux': elif self.platform_base.lower() == 'linux':
icon_skin = 'linux' icon_skin = 'linux'
data = { data = {
'id': str(self.id), 'id': str(self.id),
@ -283,7 +330,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
'hostname': self.hostname, 'hostname': self.hostname,
'ip': self.ip, 'ip': self.ip,
'protocols': self.protocols_as_list, 'protocols': self.protocols_as_list,
'platform': self.platform, 'platform': self.platform_base,
} }
} }
} }

View File

@ -11,14 +11,13 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from common.utils import ( from common.utils import (
get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
) )
from common.validators import alphanumeric from common.validators import alphanumeric
from common import fields from common import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .utils import private_key_validator, Connectivity from .utils import private_key_validator, Connectivity
signer = get_signer()
logger = get_logger(__file__) logger = get_logger(__file__)
@ -41,6 +40,7 @@ class AssetUser(OrgModelMixin):
ASSET_USER_CACHE_TIME = 3600 * 24 ASSET_USER_CACHE_TIME = 3600 * 24
_prefer = "system_user" _prefer = "system_user"
_assets_amount = None
@property @property
def private_key_obj(self): def private_key_obj(self):
@ -143,6 +143,8 @@ class AssetUser(OrgModelMixin):
@property @property
def assets_amount(self): def assets_amount(self):
if self._assets_amount is not None:
return self._assets_amount
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cached = cache.get(cache_key) cached = cache.get(cache_key)
if not cached: if not cached:

View File

@ -10,14 +10,13 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import get_signer from common.utils import signer
from .base import AssetUser from .base import AssetUser
from .asset import Asset from .asset import Asset
__all__ = ['AdminUser', 'SystemUser'] __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
signer = get_signer()
class AdminUser(AssetUser): class AdminUser(AssetUser):
@ -93,11 +92,13 @@ class SystemUser(AssetUser):
PROTOCOL_RDP = 'rdp' PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc' PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'), (PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'), (PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'), (PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'),
) )
LOGIN_AUTO = 'auto' LOGIN_AUTO = 'auto'
@ -134,6 +135,18 @@ class SystemUser(AssetUser):
else: else:
return False return False
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_MYSQL]
@property
def is_need_test_asset_connective(self):
return self.protocol not in [self.PROTOCOL_MYSQL]
@property
def can_perm_to_asset(self):
return self.protocol not in [self.PROTOCOL_MYSQL]
@property @property
def cmd_filter_rules(self): def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule from .cmd_filter import CommandFilterRule

View File

@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import Asset, Node, Label from ..models import Asset, Node, Label, Platform
from ..const import ( from ..const import (
GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN,
GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG
@ -16,7 +16,8 @@ from .base import ConnectivitySerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'AssetSerializer', 'AssetSimpleSerializer',
'ProtocolsField', 'ProtocolsField', 'PlatformSerializer',
'AssetDetailSerializer',
] ]
@ -65,6 +66,9 @@ class ProtocolsField(serializers.ListField):
class AssetSerializer(BulkOrgResourceModelSerializer): class AssetSerializer(BulkOrgResourceModelSerializer):
platform = serializers.SlugRelatedField(
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
)
protocols = ProtocolsField(label=_('Protocols'), required=False) protocols = ProtocolsField(label=_('Protocols'), required=False)
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
@ -111,7 +115,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
Prefetch('nodes', queryset=Node.objects.all().only('id')), Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')), Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain') ).select_related('admin_user', 'domain', 'platform')
return queryset return queryset
def compatible_with_old_protocol(self, validated_data): def compatible_with_old_protocol(self, validated_data):
@ -139,6 +143,21 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True)
class Meta:
model = Platform
fields = [
'id', 'name', 'base', 'charset',
'internal', 'meta', 'comment'
]
class AssetDetailSerializer(AssetSerializer):
platform = PlatformSerializer(read_only=True)
class AssetSimpleSerializer(serializers.ModelSerializer): class AssetSimpleSerializer(serializers.ModelSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))

View File

@ -4,8 +4,10 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Node
from ..models import SystemUser from ..models import SystemUser
from ..const import ( from ..const import (
GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN,
@ -13,6 +15,12 @@ from ..const import (
) )
from .base import AuthSerializer, AuthSerializerMixin from .base import AuthSerializer, AuthSerializerMixin
__all__ = [
'SystemUserSerializer', 'SystemUserAuthSerializer',
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer',
]
class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
""" """
@ -95,8 +103,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate(self, attrs): def validate(self, attrs):
username = attrs.get("username", "manual") username = attrs.get("username", "manual")
auto_gen_key = attrs.pop("auto_generate_key", False)
protocol = attrs.get("protocol") protocol = attrs.get("protocol")
auto_gen_key = attrs.get("auto_generate_key", False)
if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]:
return attrs
if auto_gen_key: if auto_gen_key:
password = SystemUser.gen_password() password = SystemUser.gen_password()
attrs["password"] = password attrs["password"] = password
@ -111,7 +123,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
public_key = ssh_pubkey_gen(private_key, password=password, public_key = ssh_pubkey_gen(private_key, password=password,
username=username) username=username)
attrs["public_key"] = public_key attrs["public_key"] = public_key
attrs.pop("auto_generate_key", None)
return attrs return attrs
@classmethod @classmethod
@ -143,4 +154,43 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'username') fields = ('id', 'name', 'username')
class RelationMixin(BulkSerializerMixin, serializers.Serializer):
systemuser_display = serializers.ReadOnlyField()
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(['systemuser', "systemuser_display"])
return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = SystemUser.assets.through
fields = [
'id', "asset", "asset_display",
]
class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
node_display = serializers.SerializerMethodField()
class Meta(RelationMixin.Meta):
model = SystemUser.nodes.through
fields = [
'id', 'node', "node_display",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tree = Node.tree()
def get_node_display(self, obj):
if hasattr(obj, 'node_key'):
return self.tree.get_node_full_tag(obj.node_key)
else:
return obj.node.full_value

View File

@ -1,6 +0,0 @@
{% extends '_import_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Import admin user" %}{% endblock %}
{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %}

View File

@ -1,4 +0,0 @@
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update admin user" %}{% endblock %}

View File

@ -1,6 +0,0 @@
{% extends '_import_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Import assets" %}{% endblock %}
{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %}

View File

@ -25,7 +25,7 @@
<div class="wrapper wrapper-content"> <div class="wrapper wrapper-content">
<div class="row"> <div class="row">
<div class="col-lg-3" id="split-left" style="padding-left: 3px;overflow: auto;max-height: 500px"> <div class="col-sm-3" id="split-left" style="padding-left: 3px;overflow: auto;max-height: 500px">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px"> <div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager "> <div class="file-manager ">
@ -37,7 +37,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-9 animated fadeInRight" id="split-right"> <div class="col-sm-9 animated fadeInRight" id="split-right">
<div class="mail-box-header"> <div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%"> <table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%">
<thead> <thead>
@ -190,6 +190,16 @@ function setAssetModalOptions(options) {
assetModalOption = options; assetModalOption = options;
} }
function initAssetTreeModel(selector) {
$(selector).parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
e.preventDefault();
e.stopPropagation();
$("#asset_list_modal").modal();
}
})
}
$(document).ready(function(){ $(document).ready(function(){

View File

@ -1,4 +0,0 @@
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update assets" %}{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
<style>
.modal-body {
background-color: white !important;
}
</style>
{% block modal_id %}node_detail_modal{% endblock %}
{% block modal_title %}{% trans "Node detail" %}{% endblock %}
{% block modal_body %}
<form class="form-horizontal" action="" style="padding-top: 20px">
<div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'ID' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_node_detail_id_view"></p>
</div>
<div class="col-sm-2" style="padding-left: 2px">
<a class="btn btn-white btn-sm btn-node-detail-copy-id"><i class="fa fa-copy"></i></a>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Name' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_node_detail_name_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Full name' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_node_detail_full_name_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Key' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_node_detail_key_view"></p>
</div>
</div>
</div>
</form>
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
function initClipboard() {
var clipboard = new Clipboard('.btn-node-detail-copy-id', {
text: function (trigger) {
return $("#id_node_detail_id_view").html()
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
$(document).ready(function () {
initClipboard();
})
</script>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@ -216,8 +216,9 @@ function OnRightClick(event, treeId, treeNode) {
function showRMenu(type, x, y) { function showRMenu(type, x, y) {
var offset = $("#tree-node-id").offset(); var offset = $("#tree-node-id").offset();
var scrollTop = document.querySelector('.treebox').scrollTop;
x -= offset.left; x -= offset.left;
y -= offset.top; y -= offset.top + scrollTop;
x += document.body.scrollLeft; x += document.body.scrollLeft;
y += document.body.scrollTop + document.documentElement.scrollTop; y += document.body.scrollTop + document.documentElement.scrollTop;
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});

View File

@ -2,10 +2,6 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
@ -99,7 +95,7 @@ function autoLoginModeProtocol() {
// 协议+自动登录模式字段控制 // 协议+自动登录模式字段控制
$('#auth_title_id').removeClass('hidden'); $('#auth_title_id').removeClass('hidden');
var protocol = $(protocol_id + " option:selected").text(); var protocol = $(protocol_id + " option:selected").text();
if (protocol === 'rdp') { if (['rdp'].indexOf(protocol) !== -1) {
authFieldsDisplay(); authFieldsDisplay();
$(auto_generate_key).closest('.form-group').removeClass('hidden'); $(auto_generate_key).closest('.form-group').removeClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden');
@ -109,7 +105,7 @@ function autoLoginModeProtocol() {
$(sudo_id).closest('.form-group').addClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden');
} }
else if (protocol === 'vnc') { else if (['vnc', 'mysql'].indexOf(protocol) !== -1) {
$('.auth-fields').removeClass('hidden'); $('.auth-fields').removeClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden');
@ -145,7 +141,7 @@ function manualLoginModeProtocol() {
// 协议+手动登录模式字段控制 // 协议+手动登录模式字段控制
$('#auth_title_id').addClass('hidden'); $('#auth_title_id').addClass('hidden');
var protocol = $(protocol_id + " option:selected").text(); var protocol = $(protocol_id + " option:selected").text();
if (protocol === 'rdp') { if (['rdp'].indexOf(protocol) !== -1) {
$('.auth-fields').addClass('hidden'); $('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden');
@ -155,7 +151,7 @@ function manualLoginModeProtocol() {
$(sudo_id).closest('.form-group').addClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden');
} }
else if (protocol === 'vnc') { else if (['vnc', 'mysql'].indexOf(protocol) !== -1) {
$('.auth-fields').addClass('hidden'); $('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden');

View File

@ -1,6 +0,0 @@
{% extends '_import_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Import system user" %}{% endblock %}
{% block import_modal_download_template_url %}{% url "api-assets:system-user-list" %}{% endblock %}

View File

@ -1,4 +0,0 @@
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update system user" %}{% endblock %}

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -2,10 +2,6 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -5,28 +5,7 @@
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} {% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<div class="" style="float: right"> {% include '_csv_import_export.html' %}
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
@ -42,9 +21,6 @@
<th class="text-center">{% trans 'Name' %}</th> <th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Username' %}</th> <th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Asset' %}</th> <th class="text-center">{% trans 'Asset' %}</th>
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
{# <th class="text-center">{% trans 'Unreachable' %}</th>#}
{# <th class="text-center">{% trans 'Ratio' %}</th>#}
<th class="text-center">{% trans 'Comment' %}</th> <th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
@ -52,8 +28,6 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
{% include 'assets/_admin_user_import_modal.html' %}
{% include 'assets/_admin_user_update_modal.html' %}
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %} {% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
@ -77,17 +51,16 @@ function initTable() {
columns: [ columns: [
{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false}, {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false},
{#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#} {#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#}
{data: "comment"}, {data: "id", orderable: false, width: "100px"} {data: "comment"}, {data: "id", orderable: false, width: "120px"}
] ]
}; };
admin_user_table = jumpserver.initServerSideDataTable(options); return jumpserver.initServerSideDataTable(options);
return admin_user_table
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); admin_user_table = initTable();
initCsvImportExport(admin_user_table, "{% trans "Admin user" %}")
}) })
.on('click', '.btn_admin_user_delete', function () { .on('click', '.btn_admin_user_delete', function () {
var $this = $(this); var $this = $(this);
var $data_table = $("#admin_user_list_table").DataTable(); var $data_table = $("#admin_user_list_table").DataTable();
@ -100,69 +73,5 @@ $(document).ready(function(){
}, 3000); }, 3000);
}) })
.on('click', '.btn_export', function(){
var admin_users = admin_user_table.selected;
var data = {
'resources': admin_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:admin-user-list' %}",
format: "csv",
params: {
search: search
}
};
APIExportData(props);
}).on('click', '#btn_import_confirm',function () {
var url = "{% url 'api-assets:admin-user-list' %}";
var file = document.getElementById('id_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var data_table = $('#admin_user_list_table').DataTable();
APIImportData({
url: url,
method: "POST",
body: file,
data_table: data_table
});
})
.on('click', '#download_update_template', function () {
var admin_users = admin_user_table.selected;
var data = {
'resources': admin_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:admin-user-list' %}?format=csv&template=update",
format: 'csv',
params: {
search: search
}
};
APIExportData(props);
})
.on('click', '#btn_update_confirm', function () {
var file = document.getElementById('update_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var url = "{% url 'api-assets:admin-user-list' %}";
var data_table = $('#admin_user_list_table').DataTable();
APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -32,13 +32,7 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
$("#id_assets").parent().find(".select2-selection").on('click', function (e) { initAssetTreeModel("#id_assets");
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
e.preventDefault();
e.stopPropagation();
$("#asset_list_modal").modal();
}
})
}).on('click', '.field-tag', function() { }).on('click', '.field-tag', function() {
changeField(this); changeField(this);
}).on('click', '#change_all', function () { }).on('click', '#change_all', function () {

View File

@ -3,9 +3,7 @@
{% load i18n %} {% load i18n %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<link href='{% static "css/plugins/sweetalert/sweetalert.css" %}' rel="stylesheet"> <link href='{% static "css/plugins/sweetalert/sweetalert.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
<script src='{% static "js/plugins/sweetalert/sweetalert.min.js" %}'></script> <script src='{% static "js/plugins/sweetalert/sweetalert.min.js" %}'></script>
{% endblock %} {% endblock %}

View File

@ -1,132 +1,54 @@
{% extends 'base.html' %} {% extends '_base_asset_tree_list.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block help_message %} {% block help_message %}
{# <div class="alert alert-info help-message">#}
{# <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>#}
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %} {% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
{# </div>#}
{% endblock %} {% endblock %}
{% block custom_head_css_js %} {% block table_container %}
{# <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">#} <div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div>
{# <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>#} {% include '_csv_import_export.html' %}
<script src="{% static 'js/jquery.form.min.js' %}"></script> <div class="btn-group" style="float: right">
<style type="text/css"> <button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
div#rMenu { <ul class="dropdown-menu labels">
position:absolute; {% for label in labels %}
visibility:hidden; <li><a style="font-weight: bolder">{{ label.name }}#{{ label.value }}</a></li>
text-align: left; {% endfor %}
{#top: 100%;#} </ul>
top: 0; </div>
left: 0; <table class="table table-striped table-bordered table-hover " id="asset_list_table" style="width: 100%">
z-index: 1000; <thead>
{#float: left;#} <tr>
padding: 0 0; <th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
margin: 2px 0 0; <th class="text-center">{% trans 'Hostname' %}</th>
list-style: none; <th class="text-center">{% trans 'IP' %}</th>
background-clip: padding-box; <th class="text-center">{% trans 'Hardware' %}</th>
} <th class="text-center">{% trans 'Reachable' %}</th>
.dataTables_wrapper .dataTables_processing { <th class="text-center">{% trans 'Action' %}</th>
opacity: .9; </tr>
border: none; </thead>
} <tbody>
div#rMenu li{ </tbody>
margin: 1px 0; </table>
cursor: pointer; <div id="actions" class="hide">
list-style: none outside none; <div class="input-group">
} <select class="form-control m-b" style="width: auto" id="slct_bulk_update">
.dropdown a:hover { <option value="delete">{% trans 'Delete selected' %}</option>
background-color: #f1f1f1 <option value="update">{% trans 'Update selected' %}</option>
} <option value="remove">{% trans 'Remove from this node' %}</option>
</style> <option value="deactive">{% trans 'Deactive selected' %}</option>
<option value="active">{% trans 'Active selected' %}</option>
{% endblock %} </select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
{% block content %} <button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
<div class="wrapper wrapper-content"> {% trans 'Submit' %}
<div class="row"> </button>
<div class="col-lg-3" id="split-left" style="padding-left: 3px;padding-right: 0">
{% include 'assets/_node_tree.html' %}
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle" style="z-index: 9999">
<div class="btn btn-sm btn-primary tree-toggle-btn">
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div>
</div>
<div class="mail-box-header">
<div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div>
<div class="" style="float: right">
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="btn-group" style="float: right">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
<ul class="dropdown-menu labels">
{% for label in labels %}
<li><a style="font-weight: bolder">{{ label.name }}#{{ label.value }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-bordered table-hover " id="asset_list_table" style="width: 100%">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Hardware' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="actions" class="hide">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="delete">{% trans 'Delete selected' %}</option>
<option value="update">{% trans 'Update selected' %}</option>
<option value="remove">{% trans 'Remove from this node' %}</option>
<option value="deactive">{% trans 'Deactive selected' %}</option>
<option value="active">{% trans 'Active 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>
</div> </div>
</div> </div>
</div> </div>
</div>
{% include 'assets/_asset_update_modal.html' %}
{% include 'assets/_asset_import_modal.html' %}
{% include 'assets/_asset_list_modal.html' %} {% include 'assets/_asset_list_modal.html' %}
{% include 'assets/_node_detail_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
@ -177,7 +99,7 @@ function initTable() {
data: "connectivity", data: "connectivity",
orderable: false, orderable: false,
width: '60px' width: '60px'
}, {data: "id", orderable: false, width: "100px"} }, {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
@ -198,26 +120,12 @@ function initTree() {
<li class="divider"></li> <li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li> <li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li> <li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
<li class="divider"></li>
<li id="menu_node_detail" class="btn-node-detail" tabindex="-1"><a><i class="fa fa-info-circle"></i> {% trans 'Node detail' %}</a></li>
` `
}) })
} }
function toggle() {
if (show === 0) {
$("#split-left").hide(500, function () {
$("#split-right").attr("class", "col-lg-12");
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
show = 1;
});
} else {
$("#split-right").attr("class", "col-lg-9");
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
$("#split-left").show(500);
show = 0;
}
}
function onNodeSelected(event, treeNode) { function onNodeSelected(event, treeNode) {
current_node = treeNode; current_node = treeNode;
current_node_id = treeNode.meta.node.id; current_node_id = treeNode.meta.node.id;
@ -258,7 +166,8 @@ function onAssetModalConfirmAddAssetToNode(table) {
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); asset_table = initTable();
initCsvImportExport(asset_table, "{% trans "Asset" %}");
initTree(); initTree();
if(getCookie('show_current_asset') === '1'){ if(getCookie('show_current_asset') === '1'){
@ -279,81 +188,6 @@ $(document).ready(function(){
$("#asset_list_table_filter input").val(val); $("#asset_list_table_filter input").val(val);
asset_table.search(val).draw(); asset_table.search(val).draw();
}) })
.on('click', '.btn_export', function () {
var assets = asset_table.selected;
var data = {
'resources': assets
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:asset-list' %}",
format: 'csv',
params: {
search: search,
node_id: current_node_id || '',
show_current_asset: getCookie('show_current_asset')
}
};
APIExportData(props);
})
.on('click', '#btn_import_confirm', function () {
var file = document.getElementById('id_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var url = "{% url 'api-assets:asset-list' %}";
if (current_node_id){
url = setUrlParam(url, 'node_id', current_node_id);
}
var data_table = $('#asset_list_table').DataTable();
APIImportData({
url: url,
method: "POST",
body: file,
data_table: data_table
});
})
.on('click', '#download_update_template', function () {
var assets = asset_table.selected;
var data = {
'resources': assets
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:asset-list' %}?format=csv&template=update",
format: 'csv',
params: {
search: search,
node_id: current_node_id || ''
}
};
APIExportData(props);
})
.on('click', '#btn_update_confirm', function () {
var file = document.getElementById('update_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var url = "{% url 'api-assets:asset-list' %}";
if (current_node_id){
url = setUrlParam(url, 'node_id', current_node_id);
}
var data_table = $('#asset_list_table').DataTable();
APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
})
.on('click', '.btn-create-asset', function () { .on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}"; var url = "{% url 'assets:asset-create' %}";
if (current_node_id) { if (current_node_id) {
@ -382,9 +216,6 @@ $(document).ready(function(){
var data = { var data = {
'resources': id_list 'resources': id_list
}; };
function refreshPage() {
setTimeout( function () {window.location.reload();}, 300);
}
function reloadTable() { function reloadTable() {
asset_table.ajax.reload(); asset_table.ajax.reload();
@ -550,6 +381,30 @@ $(document).ready(function(){
flash_message: false flash_message: false
}); });
}).on('click', '#menu_node_detail', function(e) {
e.preventDefault();
var the_url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", current_node_id);
function drawingNodeDetailModal(data){
$('#id_node_detail_id_view').html(data['id']);
$('#id_node_detail_name_view').html(data['name']);
$('#id_node_detail_full_name_view').html(data['full_value']);
$('#id_node_detail_key_view').html(data['key']);
$('#node_detail_modal').modal();
}
function error(data) {
alert(data)
}
function success(data) {
drawingNodeDetailModal(data)
}
requestApi({
url: the_url,
error: error,
method: 'GET',
success: success,
flash_message: false
});
}) })
</script> </script>

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -62,7 +62,7 @@ function initTable() {
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "rules", orderable: false}, {data: "id"}, {data: "name" }, {data: "rules", orderable: false},
{data: "system_users", orderable: false}, {data: "comment"}, {data: "system_users", orderable: false}, {data: "comment"},
{data: "id", orderable: false, width: "100px"} {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };

View File

@ -2,10 +2,6 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -25,9 +25,7 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2().off("select2:open"); $('.select2').select2().off("select2:open");
}).on('click', '.select2-selection__rendered', function (e) { initAssetTreeModel('#id_assets');
e.preventDefault();
$("#asset_list_modal").modal();
}) })
.on("submit", "form", function (evt) { .on("submit", "form", function (evt) {
evt.preventDefault(); evt.preventDefault();

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">

View File

@ -55,7 +55,7 @@ function initTable() {
ajax_url: '{% url "api-assets:domain-list" %}', ajax_url: '{% url "api-assets:domain-list" %}',
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "asset_count", orderable: false }, {data: "id"}, {data: "name" }, {data: "asset_count", orderable: false },
{data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "100px"} {data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };

View File

@ -2,10 +2,6 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">

View File

@ -29,9 +29,7 @@ $(document).ready(function () {
$('.select2').select2({ $('.select2').select2({
closeOnSelect: false closeOnSelect: false
}) })
}).on('click', '.select2-selection__rendered', function (e) { initAssetTreeModel("#id_assets");
e.preventDefault();
$("#asset_list_modal").modal();
}) })
.on("submit", "form", function (evt) { .on("submit", "form", function (evt) {
evt.preventDefault(); evt.preventDefault();

View File

@ -45,7 +45,7 @@ function initTable() {
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "value" }, {data: "id"}, {data: "name" }, {data: "value" },
{data: "asset_count", orderable: false}, {data: "asset_count", orderable: false},
{data: "id", orderable: false, width: "100px"} {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };

View File

@ -0,0 +1,79 @@
{% extends '_base_create_update.html' %} {% load static %} {% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="platformForm" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.base layout="horizontal" %}
<div class="meta-config">
<div hidden class="windows-config">
{% bootstrap_field meta_form.security layout="horizontal" %}
{% bootstrap_field meta_form.console layout="horizontal" %}
</div>
</div>
{% 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 %}
{% block custom_foot_js %}
<script type="text/javascript">
function toggleWindowConfig() {
var baseRef = $("#id_base");
var windowConfigRef = $(".windows-config");
var windowConfigInputRef = windowConfigRef.find(".form-control");
if (baseRef.val().toLowerCase() === 'windows') {
windowConfigInputRef.attr('disabled', false);
windowConfigRef.show();
} else {
windowConfigInputRef.attr('disabled', true);
windowConfigRef.hide();
}
}
$(document).ready(function () {
toggleWindowConfig()
})
.on("change", "#id_base", function () {
toggleWindowConfig()
})
.on("submit", "form", function (evt) {
evt.preventDefault();
var form = $("form");
var data = form.serializeObject();
var method = "POST";
var theUrl = '{% url "api-assets:platform-list" %}';
var redirectTo = '{% url "assets:platform-list" %}';
{% if type == "update" %}
theUrl = '{% url 'api-assets:platform-detail' pk=object.id %}';
method = "PUT";
{% endif %}
var metaData = $(".meta-config .form-control").serializeObject();
objectAttrsIsBool(metaData, ['console']);
var metaKeys = Object.keys(metaData);
metaKeys.forEach(function (k, v) {
delete data[k]
});
data.meta = metaData;
console.log(data);
var props = {
url: theUrl,
data: data,
method: method,
form: form,
redirect_to: redirectTo
};
formSubmit(props);
})
</script>
{% endblock %}

View File

@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% 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:platform-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:platform-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</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 'Base platform' %}:</td>
<td><b>{{ object.base }}</b></td>
</tr>
<tr>
<td>{% trans 'Charset' %}:</td>
<td><b>{{ object.charset }}</b></td>
</tr>
<tr>
<td>{% trans 'Meta' %}:</td>
<td><b>{{ object.meta }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
{% endblock %}

View File

@ -0,0 +1,75 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url "assets:platform-create" %}" class="btn btn-sm btn-primary"> {% trans "Create platform" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="platform_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 'Base platform' %}</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>
var platformTable = 0;
function initTable() {
var options = {
ele: $('#platform_list_table'),
columnDefs: [
{targets: 1, render: function (cellData, tp, rowData, meta) {
cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "assets:platform-detail" pk=999 %}">' + cellData + '</a>';
return detailBtn.replace('999', rowData.id);
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var updateBtn = '<a href="{% url "assets:platform-update" pk=9999 %}" class="btn btn-xs m-l-xs btn-info" disabled>{% trans "Update" %}</a>'.replace('9999', cellData);
var delBtn = '<a class="btn btn-xs btn-danger m-l-xs btn-object-delete" data-uid="{{ DEFAULT_PK }}" disabled>{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
if (!rowData.internal) {
updateBtn = updateBtn.replace('disabled', '');
delBtn = delBtn.replace('disabled', '');
}
$(td).html(updateBtn + delBtn)
}}
],
ajax_url: '{% url "api-assets:platform-list" %}',
columns: [
{data: "id"}, {data: "name"}, {data: "base" },
{data: "comment"}, {data: "id", orderable: false, width: "120px"}
]
};
platformTable = jumpserver.initServerSideDataTable(options);
return platformTable
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-object-delete', function () {
var $this = $(this);
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var theUrl = '{% url "api-assets:platform-detail" pk=0 %}'.replace('0', uid);
objectDelete($this, name, theUrl);
setTimeout( function () {
platformTable.ajax.reload();
}, 3000);
})
</script>
{% endblock %}

View File

@ -4,9 +4,13 @@
{% load i18n %} {% load i18n %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet"> <style>
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script> .table.node_edit {
margin-bottom: 0;
}
</style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">
@ -98,15 +102,8 @@
</td> </td>
</tr> </tr>
</form> </form>
<table class="table" id="node_list_table">
{% for node in system_user.nodes.all|sort %} </table>
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.full_value }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -120,91 +117,115 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var assetsRelationUrl = "{% url 'api-assets:system-users-assets-relation-list' %}";
var nodesRelationUrl = "{% url 'api-assets:system-users-nodes-relation-list' %}";
function updateSystemUserNode(nodes) { function getRelationUrl(type) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}"; var theUrl = "";
var body = { switch (type) {
nodes: Object.assign([], nodes) case "asset":
}; theUrl = assetsRelationUrl;
var success = function(data) { break;
// remove all the selected groups from select > option and rendered ul element; case "node":
$('.select2-selection__rendered').empty(); theUrl = nodesRelationUrl;
$('#node_selected').val(''); break;
$.map(jumpserver.nodes_selected, function(node_name, index) { }
$('#opt_' + index).remove(); return theUrl;
// change tr html of user groups. }
$('.node_edit tbody').append(
'<tr>' + function addObjects(objectsId, type) {
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' + if (!objectsId || objectsId.length === 0) {
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' + return
'</tr>' }
) var theUrl = getRelationUrl(type);
}); var body = [];
// clear jumpserver.nodes_selected objectsId.forEach(function (v) {
jumpserver.nodes_selected = {}; var data = {systemuser: "{{ object.id }}"};
}; data[type] = v;
body.push(data)
});
requestApi({ requestApi({
url: the_url, url: theUrl,
body: JSON.stringify(body), body: JSON.stringify(body),
method: "POST",
success: reloadPage
});
}
function removeObject(objectId, type, success) {
if (!objectId) {
return
}
var theUrl = getRelationUrl(type);
theUrl = setUrlParam(theUrl, 'systemuser', "{{ object.id }}");
theUrl = setUrlParam(theUrl, type, objectId);
if (!success) {
success = reloadPage
}
requestApi({
url: theUrl,
method: "DELETE",
success: success success: success
}); });
} }
jumpserver.nodes_selected = {};
function initNodeTable() {
var theUrl = setUrlParam(nodesRelationUrl, "systemuser", "{{ object.id }}");
var options = {
ele: $('#node_list_table'),
toggle: true,
columnDefs: [
{targets: 1, createdCell: function (td, cellData) {
var removeBtn = '<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" data-uid="UID" type="button"><i class="fa fa-minus"></i></button>'
.replace('UID', cellData);
$(td).html(removeBtn);
}}
],
ajax_url: theUrl,
dom: "trp",
hideDefaultDefs: true,
columns: [
{data: "node_display", orderable: false},
{data: "node", orderable: false},
],
};
table = jumpserver.initServerSideDataTable(options);
return table
}
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2() $('.select2').select2();
nodesSelect2Init(".nodes-select2") nodesSelect2Init(".nodes-select2");
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
assetUserListUrl = setUrlParam(assetUserListUrl, "system_user_id", "{{ system_user.id }}"); assetUserListUrl = setUrlParam(assetUserListUrl, "system_user_id", "{{ system_user.id }}");
needPush = true; needPush = true;
initAssetUserTable(); initAssetUserTable();
initNodeTable();
}) })
.on('click', '.btn-remove-from-node', function() { .on('click', '.btn-remove-from-node', function() {
var $this = $(this); var $this = $(this);
var $tr = $this.closest('tr'); var nodeId = $(this).data('uid');
var $badge = $tr.find('.bdg_node'); var success = function() {
var gid = $badge.data('gid'); var $tr = $this.closest('tr');
var node_name = $badge.html() || $badge.text(); $tr.remove();
$('#groups_selected').append( };
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>' removeObject(nodeId, "node", success)
);
$tr.remove();
var nodes = $('.bdg_node').map(function () {
return $(this).data('gid');
}).get();
updateSystemUserNode(nodes);
}) })
.on('click', '#btn-add-to-node', function() { .on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) { var nodes = $("#node_selected").val();
return false; addObjects(nodes, "node");
}
var nodes = $('.bdg_node').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
updateSystemUserNode(nodes);
}) })
.on('click', '.btn-push', function () { .on('click', '.btn-push', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}"; var theUrl = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
var success = function (data) { var success = function (data) {
var task_id = data.task; var taskId = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(taskId);
}; };
requestApi({ requestApi({
url: the_url, url: theUrl,
error: error, error: error,
method: 'GET', method: 'GET',
success: success success: success
@ -213,39 +234,37 @@ $(document).ready(function () {
.on('click', '.btn-push-auth', function () { .on('click', '.btn-push-auth', function () {
var $this = $(this); var $this = $(this);
var asset_id = $this.data('asset'); var asset_id = $this.data('asset');
var the_url = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}"; var theUrl = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id); theUrl = theUrl.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) { var success = function (data) {
var task_id = data.task; var taskId = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(taskId);
}; };
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
requestApi({ requestApi({
url: the_url, url: theUrl,
method: 'GET', method: 'GET',
success: success, success: success,
error: error error: error
}) })
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}"; var theUrl = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
var success = function (data) { var success = function (data) {
var task_id = data.task; var taskId = data.task;
showCeleryTaskLog(task_id); showCeleryTaskLog(taskId);
}; };
requestApi({ requestApi({
url: the_url, url: theUrl,
error: error, error: error,
method: 'GET', method: 'GET',
success: success success: success
}); });
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -2,11 +2,6 @@
{% load static %} {% load static %}
{% load i18n %} {% 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 %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">
@ -17,11 +12,13 @@
<li class="active"> <li class="active">
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a> <a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li> </li>
{% if system_user.can_perm_to_asset %}
<li> <li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center"> <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %} <i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
</a> </a>
</li> </li>
{% endif %}
<li class="pull-right"> <li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a> <a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li> </li>
@ -144,6 +141,7 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if system_user.is_need_test_asset_connective %}
<tr> <tr>
<td width="50%">{% trans 'Test assets connective' %}:</td> <td width="50%">{% trans 'Test assets connective' %}:</td>
<td> <td>
@ -152,13 +150,14 @@
</span> </span>
</td> </td>
</tr> </tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% if system_user.protocol != 'rdp' %} {% if system_user.is_need_cmd_filter %}
<div class="col-sm-4" style="padding-left: 0; padding-right: 0"> <div class="col-sm-4" style="padding-left: 0; padding-right: 0">
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading"> <div class="panel-heading">

View File

@ -8,28 +8,7 @@
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<div class="" style="float: right"> {% include '_csv_import_export.html' %}
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
@ -57,8 +36,6 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
{% include 'assets/_system_user_import_modal.html' %}
{% include 'assets/_system_user_update_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
@ -85,18 +62,17 @@ function initTable() {
columns: [ columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"},
{data: "login_mode"}, {data: "assets_amount", orderable: false }, {data: "login_mode"}, {data: "assets_amount", orderable: false },
{data: "comment" }, {data: "id", orderable: false, width: "100px"} {data: "comment" }, {data: "id", orderable: false, width: "120px"}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
system_user_table = jumpserver.initServerSideDataTable(options); return jumpserver.initServerSideDataTable(options);
return system_user_table
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); system_user_table = initTable();
initCsvImportExport(system_user_table, "{% trans 'System user' %}")
}) })
.on('click', '.btn_admin_user_delete', function () { .on('click', '.btn_admin_user_delete', function () {
var $this = $(this); var $this = $(this);
var $data_table = $('#cluster_list_table').DataTable(); var $data_table = $('#cluster_list_table').DataTable();
@ -108,125 +84,6 @@ $(document).ready(function(){
$data_table.ajax.reload(); $data_table.ajax.reload();
}, 3000); }, 3000);
}) })
.on('click', '#btn_bulk_update', function () {
var action = $('#slct_bulk_update').val();
var $data_table = $('#system_user_list_table').DataTable();
var id_list = [];
var plain_id_list = [];
$data_table.rows({selected: true}).every(function(){
id_list.push({id: this.data().id});
plain_id_list.push(this.data().id);
});
if (id_list === []) {
return false;
}
var the_url = "{% url 'api-assets:system-user-list' %}";
function doDelete() {
swal({
title: "{% trans 'Are you sure?' %}",
text: "{% trans 'This will delete the selected System Users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
}, function() {
var success = function() {
var msg = "{% trans 'System Users Deleted.' %}";
swal("{% trans 'System Users Delete' %}", msg, "success");
$('#system_user_list_table').DataTable().ajax.reload();
};
var fail = function() {
var msg = "{% trans 'System Users Deleting failed.' %}";
swal("{% trans 'System Users Delete' %}", msg, "error");
};
var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list);
requestApi({url: url_delete, method: 'DELETE', success: success, error: fail});
$data_table.ajax.reload();
jumpserver.checked = false;
});
}
function doUpdate() {
{# TODO: bulk update the System Users #}
}
switch (action) {
case 'delete':
doDelete();
break;
case 'update':
doUpdate();
break;
default:
break;
}
})
.on('click', '.btn_export', function () {
var system_users = system_user_table.selected;
var data = {
'resources': system_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:system-user-list' %}",
format: "csv",
params:{
search:search
}
};
APIExportData(props);
})
.on('click', '#btn_import_confirm', function () {
var url = "{% url 'api-assets:system-user-list' %}";
var file = document.getElementById('id_file').files[0];
if(!file){
toastr.error("{% trans 'Please select file' %}");
return
}
var data_table = $('#system_user_list_table').DataTable();
APIImportData({
url: url,
method: "POST",
body: file,
data_table: data_table
});
})
.on('click', '#download_update_template', function () {
var system_users = system_user_table.selected;
var data = {
'resources': system_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:system-user-list' %}?format=csv&template=update",
format: "csv",
params:{
search:search
}
};
APIExportData(props);
})
.on('click', '#btn_update_confirm', function () {
var file = document.getElementById('update_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var url = "{% url 'api-assets:system-user-list' %}";
var data_table = $('#system_user_list_table').DataTable();
APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -12,6 +12,7 @@ app_name = 'assets'
router = BulkRouter() router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset') router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user')
router.register(r'labels', api.LabelViewSet, 'label') router.register(r'labels', api.LabelViewSet, 'label')
@ -23,6 +24,8 @@ router.register(r'asset-users', api.AssetUserViewSet, 'asset-user')
router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info')
router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user')
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
@ -35,6 +38,8 @@ urlpatterns = [
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/', path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'), api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('assets/<uuid:pk>/platform/',
api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'),
path('asset-users/auth-info/', path('asset-users/auth-info/',
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'), api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),

View File

@ -16,6 +16,11 @@ urlpatterns = [
# Asset user view # Asset user view
path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'), path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'),
path('platform/', views.PlatformListView.as_view(), name='platform-list'),
path('platform/create/', views.PlatformCreateView.as_view(), name='platform-create'),
path('platform/<int:pk>/', views.PlatformDetailView.as_view(), name='platform-detail'),
path('platform/<int:pk>/update/', views.PlatformUpdateView.as_view(), name='platform-update'),
# User asset view # User asset view
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'), path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),

View File

@ -1,5 +1,6 @@
# coding:utf-8 # coding:utf-8
from .asset import * from .asset import *
from .platform import *
from .system_user import * from .system_user import *
from .admin_user import * from .admin_user import *
from .label import * from .label import *

View File

@ -74,7 +74,7 @@ class UserAssetListView(PermissionsMixin, TemplateView):
class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): class AssetCreateView(PermissionsMixin, FormMixin, TemplateView):
model = Asset model = Asset
form_class = forms.AssetCreateForm form_class = forms.AssetCreateUpdateForm
template_name = 'assets/asset_create.html' template_name = 'assets/asset_create.html'
success_url = reverse_lazy('assets:asset-list') success_url = reverse_lazy('assets:asset-list')
permission_classes = [IsOrgAdmin] permission_classes = [IsOrgAdmin]
@ -110,7 +110,7 @@ class AssetCreateView(PermissionsMixin, FormMixin, TemplateView):
class AssetUpdateView(PermissionsMixin, UpdateView): class AssetUpdateView(PermissionsMixin, UpdateView):
model = Asset model = Asset
form_class = forms.AssetUpdateForm form_class = forms.AssetCreateUpdateForm
template_name = 'assets/asset_update.html' template_name = 'assets/asset_update.html'
success_url = reverse_lazy('assets:asset-list') success_url = reverse_lazy('assets:asset-list')
permission_classes = [IsOrgAdmin] permission_classes = [IsOrgAdmin]

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.views.generic import TemplateView, CreateView, \ from django.views.generic import (
UpdateView, DeleteView, DetailView TemplateView, CreateView, UpdateView, DeleteView, DetailView
)
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from django.views import generic
from django.utils.translation import ugettext as _
from common.permissions import PermissionsMixin, IsSuperUser
from ..models import Platform
from ..forms import PlatformForm, PlatformMetaForm
__all__ = [
'PlatformListView', 'PlatformUpdateView', 'PlatformCreateView',
'PlatformDetailView',
]
class PlatformListView(PermissionsMixin, generic.TemplateView):
template_name = 'assets/platform_list.html'
permission_classes = (IsSuperUser,)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'app': _('Assets'),
'action': _("Platform list"),
})
return context
class PlatformCreateView(PermissionsMixin, generic.CreateView):
form_class = PlatformForm
permission_classes = (IsSuperUser,)
template_name = 'assets/platform_create_update.html'
model = Platform
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
meta_form = PlatformMetaForm()
context.update({
'app': _('Assets'),
'action': _("Create platform"),
'meta_form': meta_form,
})
return context
class PlatformUpdateView(generic.UpdateView):
form_class = PlatformForm
permission_classes = (IsSuperUser,)
model = Platform
template_name = 'assets/platform_create_update.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
meta_form = PlatformMetaForm(initial=self.object.meta)
context.update({
'app': _('Assets'),
'action': _("Update platform"),
'type': 'update',
'meta_form': meta_form,
})
return context
class PlatformDetailView(generic.DetailView):
permission_classes = (IsSuperUser,)
model = Platform
template_name = 'assets/platform_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'app': _('Assets'),
'action': _("Platform detail"),
})
return context

View File

@ -11,4 +11,4 @@ class FTPLogViewSet(OrgModelViewSet):
model = FTPLog model = FTPLog
serializer_class = FTPLogSerializer serializer_class = FTPLogSerializer
permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
http_method_names = ['get', 'post', 'head', 'options']

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.7 on 2019-12-02 02:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0006_auto_20190726_1753'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='remote_addr',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'),
),
migrations.AlterField(
model_name='operatelog',
name='remote_addr',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'),
),
migrations.AlterField(
model_name='passwordchangelog',
name='remote_addr',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'),
),
]

View File

@ -16,7 +16,7 @@ __all__ = [
class FTPLog(OrgModelMixin): class FTPLog(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User')) user = models.CharField(max_length=128, verbose_name=_('User'))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
asset = models.CharField(max_length=1024, verbose_name=_("Asset")) asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
system_user = models.CharField(max_length=128, verbose_name=_("System user")) system_user = models.CharField(max_length=128, verbose_name=_("System user"))
operate = models.CharField(max_length=16, verbose_name=_("Operate")) operate = models.CharField(max_length=16, verbose_name=_("Operate"))
@ -39,7 +39,7 @@ class OperateLog(OrgModelMixin):
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action"))
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource")) resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
@ -50,7 +50,7 @@ class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User')) user = models.CharField(max_length=128, verbose_name=_('User'))
change_by = models.CharField(max_length=128, verbose_name=_("Change by")) change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):

View File

@ -11,14 +11,15 @@ from rest_framework.request import Request
from jumpserver.utils import current_request from jumpserver.utils import current_request
from common.utils import get_request_ip, get_logger, get_syslogger from common.utils import get_request_ip, get_logger, get_syslogger
from users.models import User from users.models import User
from users.signals import post_user_change_password
from authentication.signals import post_auth_failed, post_auth_success from authentication.signals import post_auth_failed, post_auth_success
from terminal.models import Session, Command from terminal.models import Session, Command
from terminal.backends.command.serializers import SessionCommandSerializer from common.utils.encode import model_to_json
from . import models, serializers from .utils import write_login_log
from .tasks import write_login_log_async from . import models
logger = get_logger(__name__) logger = get_logger(__name__)
sys_logger = get_syslogger("audits") sys_logger = get_syslogger(__name__)
json_render = JSONRenderer() json_render = JSONRenderer()
@ -26,6 +27,8 @@ MODELS_NEED_RECORD = (
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser', 'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter', 'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask', 'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
'Platform', 'ChangeAuthPlan', 'GatherUserTask',
'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission',
) )
@ -50,8 +53,11 @@ def create_operate_log(action, sender, resource):
logger.error("Create operate log error: {}".format(e)) logger.error("Create operate log error: {}".format(e))
@receiver(post_save, dispatch_uid="my_unique_identifier") @receiver(post_save)
def on_object_created_or_update(sender, instance=None, created=False, **kwargs): def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
if instance._meta.object_name == 'User' and \
update_fields and 'last_login' in update_fields:
return
if created: if created:
action = models.OperateLog.ACTION_CREATE action = models.OperateLog.ACTION_CREATE
else: else:
@ -59,47 +65,46 @@ def on_object_created_or_update(sender, instance=None, created=False, **kwargs):
create_operate_log(action, sender, instance) create_operate_log(action, sender, instance)
@receiver(post_delete, dispatch_uid="my_unique_identifier") @receiver(post_delete)
def on_object_delete(sender, instance=None, **kwargs): def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance)
@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier") @receiver(post_user_change_password, sender=User)
def on_user_change_password(sender, instance=None, **kwargs): def on_user_change_password(sender, user=None, **kwargs):
if hasattr(instance, '_set_password'): if not current_request:
if not current_request or not current_request.user.is_authenticated: remote_addr = '127.0.0.1'
return change_by = 'System'
with transaction.atomic(): else:
models.PasswordChangeLog.objects.create( remote_addr = get_request_ip(current_request)
user=instance, change_by=current_request.user, if not current_request.user.is_authenticated:
remote_addr=get_request_ip(current_request), change_by = str(user)
) else:
change_by = str(current_request.user)
with transaction.atomic():
models.PasswordChangeLog.objects.create(
user=str(user), change_by=change_by,
remote_addr=remote_addr,
)
def on_audits_log_create(sender, instance=None, **kwargs): def on_audits_log_create(sender, instance=None, **kwargs):
if sender == models.UserLoginLog: if sender == models.UserLoginLog:
category = "login_log" category = "login_log"
serializer = serializers.LoginLogSerializer
elif sender == models.FTPLog: elif sender == models.FTPLog:
serializer = serializers.FTPLogSerializer
category = "ftp_log" category = "ftp_log"
elif sender == models.OperateLog: elif sender == models.OperateLog:
category = "operation_log" category = "operation_log"
serializer = serializers.OperateLogSerializer
elif sender == models.PasswordChangeLog: elif sender == models.PasswordChangeLog:
category = "password_change_log" category = "password_change_log"
serializer = serializers.PasswordChangeLogSerializer
elif sender == Session: elif sender == Session:
category = "host_session_log" category = "host_session_log"
serializer = serializers.SessionAuditSerializer
elif sender == Command: elif sender == Command:
category = "session_command_log" category = "session_command_log"
serializer = SessionCommandSerializer
else: else:
return return
s = serializer(instance=instance) data = model_to_json(instance, indent=None)
data = json_render.render(s.data).decode(errors='ignore')
msg = "{} - {}".format(category, data) msg = "{} - {}".format(category, data)
sys_logger.info(msg) sys_logger.info(msg)
@ -129,7 +134,7 @@ def on_user_auth_success(sender, user, request, **kwargs):
logger.debug('User login success: {}'.format(user.username)) logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request) data = generate_data(user.username, request)
data.update({'mfa': int(user.mfa_enabled), 'status': True}) data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log_async.delay(**data) write_login_log(**data)
@receiver(post_auth_failed) @receiver(post_auth_failed)
@ -137,4 +142,4 @@ def on_user_auth_failed(sender, username, request, reason, **kwargs):
logger.debug('User login failed: {}'.format(username)) logger.debug('User login failed: {}'.format(username))
data = generate_data(username, request) data = generate_data(username, request)
data.update({'reason': reason, 'status': False}) data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data) write_login_log(**data)

View File

@ -6,8 +6,7 @@ from django.conf import settings
from celery import shared_task from celery import shared_task
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog from .models import UserLoginLog, OperateLog
from .utils import write_login_log
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)
@ -22,6 +21,13 @@ def clean_login_log_period():
UserLoginLog.objects.filter(datetime__lt=expired_day).delete() UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)
@shared_task @shared_task
def write_login_log_async(*args, **kwargs): def clean_operation_log_period():
write_login_log(*args, **kwargs) now = timezone.now()
try:
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 90
expired_day = now - datetime.timedelta(days=days)
OperateLog.objects.filter(datetime__lt=expired_day).delete()

View File

@ -5,8 +5,6 @@
{% load common_tags %} {% load common_tags %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style> <style>
#search_btn { #search_btn {
margin-bottom: 0; margin-bottom: 0;
@ -14,6 +12,9 @@
.form-control { .form-control {
height: 30px; height: 30px;
} }
.select2-selection__rendered span.select2-selection, .select2-container .select2-selection--single {
height: 30px !important;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@ -102,47 +102,47 @@
{% 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>
$(document).ready(function() { $(document).ready(function() {
jumpserver.initStaticTable('#login_log_table'); jumpserver.initStaticTable('#login_log_table');
$('#date .input-daterange').datepicker({ $('#date .input-daterange').datepicker({
format: "yyyy-mm-dd", format: "yyyy-mm-dd",
todayBtn: "linked", todayBtn: "linked",
keyboardNavigation: false, keyboardNavigation: false,
forceParse: false, forceParse: false,
calendarWeeks: true, calendarWeeks: true,
autoclose: true autoclose: true
}); });
$('.select2').select2({ $('.select2').select2({
dropdownAutoWidth: true, dropdownAutoWidth: true,
width: 'auto' width: 'auto'
}); });
})
.on('click', '.btn_export', function () {
var date_from = $('#id_date_from').val();
var date_to = $('#id_date_to').val();
var user = $('.select2 option:selected').val();
var keyword = $('#search').val();
$.ajax({
url: "{% url "audits:login-log-export" %}",
method: 'POST',
data: JSON.stringify({
'date_from':date_from,
'date_to':date_to,
'user':user,
'keyword':keyword
}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
}
}) })
.on('click', '.btn_export', function () { })
var date_from = $('#id_date_from').val(); </script>
var date_to = $('#id_date_to').val();
var user = $('.select2 option:selected').val();
var keyword = $('#search').val();
$.ajax({
url: "{% url "audits:login-log-export" %}",
method: 'POST',
data: JSON.stringify({
'date_from':date_from,
'date_to':date_to,
'user':user,
'keyword':keyword
}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
}
})
})
</script>
{% endblock %} {% endblock %}

View File

@ -5,8 +5,6 @@
{% load common_tags %} {% load common_tags %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style> <style>
#search_btn { #search_btn {
margin-bottom: 0; margin-bottom: 0;
@ -69,7 +67,7 @@
{% endblock %} {% endblock %}
{% block table_head %} {% block table_head %}
<th class="text-center">{% trans 'User' %}</th> <th class="text-center">{% trans 'Handlers' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
<th class="text-center">{% trans 'Resource Type' %}</th> <th class="text-center">{% trans 'Resource Type' %}</th>
<th class="text-center">{% trans 'Resource' %}</th> <th class="text-center">{% trans 'Resource' %}</th>

View File

@ -5,8 +5,6 @@
{% load common_tags %} {% load common_tags %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style> <style>
#search_btn { #search_btn {
margin-bottom: 0; margin-bottom: 0;

View File

@ -109,7 +109,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
class AccessTokenAuthentication(authentication.BaseAuthentication): class AccessTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Bearer' keyword = 'Bearer'
expiration = settings.TOKEN_EXPIRATION or 3600 # expiration = settings.TOKEN_EXPIRATION or 3600
model = get_user_model() model = get_user_model()
def authenticate(self, request): def authenticate(self, request):

View File

@ -1,10 +1,11 @@
# coding:utf-8 # coding:utf-8
# #
import warnings
import ldap import ldap
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend from django_auth_ldap.backend import _LDAPUser, LDAPBackend, LDAPSettings
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
from users.utils import construct_user_email from users.utils import construct_user_email
@ -17,7 +18,6 @@ class LDAPAuthorizationBackend(LDAPBackend):
""" """
Override this class to override _LDAPUser to LDAPUser Override this class to override _LDAPUser to LDAPUser
""" """
@staticmethod @staticmethod
def user_can_authenticate(user): def user_can_authenticate(user):
""" """
@ -27,18 +27,26 @@ class LDAPAuthorizationBackend(LDAPBackend):
is_valid = getattr(user, 'is_valid', None) is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None return is_valid or is_valid is None
def authenticate(self, request=None, username=None, password=None, **kwargs): def pre_check(self, username):
if not settings.AUTH_LDAP:
return False
logger.info('Authentication LDAP backend') logger.info('Authentication LDAP backend')
if not username: if not username:
logger.info('Authenticate failed: username is None') logger.info('Authenticate failed: username is None')
return None return False
if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS: if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS:
user_model = self.get_user_model() user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists() exist = user_model.objects.filter(username=username).exists()
if not exist: if not exist:
msg = 'Authentication failed: user ({}) is not in the user list' msg = 'Authentication failed: user ({}) is not in the user list'
logger.info(msg.format(username)) logger.info(msg.format(username))
return None return False
return True
def authenticate(self, request=None, username=None, password=None, **kwargs):
match = self.pre_check(username)
if not match:
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request) ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password) user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user)) logger.info('Authenticate user: {}'.format(user))

View File

@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model
from keycloak.realm import KeycloakRealm from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID from keycloak.keycloak_openid import KeycloakOpenID
from .signals import post_create_openid_user from .signals import post_create_or_update_openid_user
from .decorator import ssl_verification from .decorator import ssl_verification
OIDT_ACCESS_TOKEN = 'oidt_access_token' OIDT_ACCESS_TOKEN = 'oidt_access_token'
@ -155,7 +155,7 @@ class Client(object):
""" """
userinfo = self.get_userinfo(token=token_response['access_token']) userinfo = self.get_userinfo(token=token_response['access_token'])
with transaction.atomic(): with transaction.atomic():
user, _ = get_user_model().objects.update_or_create( user, created = get_user_model().objects.update_or_create(
username=userinfo.get('preferred_username', ''), username=userinfo.get('preferred_username', ''),
defaults={ defaults={
'email': userinfo.get('email', ''), 'email': userinfo.get('email', ''),
@ -169,7 +169,9 @@ class Client(object):
refresh_token=token_response['refresh_token'], refresh_token=token_response['refresh_token'],
) )
if user: if user:
post_create_openid_user.send(sender=user.__class__, user=user) post_create_or_update_openid_user.send(
sender=user.__class__, user=user, created=created
)
return oidt_profile return oidt_profile

View File

@ -1,5 +1,5 @@
from django.dispatch import Signal from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',)) post_create_or_update_openid_user = Signal(providing_args=('user',))
post_openid_login_success = Signal(providing_args=('user', 'request')) post_openid_login_success = Signal(providing_args=('user', 'request'))

View File

@ -25,7 +25,8 @@ __all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
class OpenIDLoginView(RedirectView): class OpenIDLoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL) redirect_uri = settings.BASE_SITE_URL + \
str(settings.AUTH_OPENID_LOGIN_COMPLETE_URL)
nonce = Nonce( nonce = Nonce(
redirect_uri=redirect_uri, redirect_uri=redirect_uri,
next_path=self.request.GET.get('next') next_path=self.request.GET.get('next')

View File

@ -118,9 +118,9 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError): class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login' error = 'block_login'
msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
def __init__(self, username, ip): def __init__(self, username, ip):
self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip) super().__init__(username=username, ip=ip)

Some files were not shown because too many files have changed in this diff Show More