Merge pull request #2737 from jumpserver/dev

Dev
pull/2790/head^2
BaiJiangJie 2019-05-29 11:34:43 +08:00 committed by GitHub
commit d46f5858f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 6374 additions and 1590 deletions

View File

@ -6,7 +6,8 @@ RUN useradd jumpserver
COPY ./requirements /tmp/requirements COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm RUN yum -y install epel-release && \
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt) RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && \ RUN cd /tmp/requirements && pip install --upgrade pip setuptools && \
pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1 @@
from .remote_app import *

View File

@ -0,0 +1,31 @@
# coding: utf-8
#
from rest_framework import generics
from rest_framework.pagination import LimitOffsetPagination
from rest_framework_bulk import BulkModelViewSet
from ..hands import IsOrgAdmin, IsAppUser
from ..models import RemoteApp
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
__all__ = [
'RemoteAppViewSet', 'RemoteAppConnectionInfoApi',
]
class RemoteAppViewSet(BulkModelViewSet):
filter_fields = ('name',)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
queryset = RemoteApp.objects.all()
serializer_class = RemoteAppSerializer
pagination_class = LimitOffsetPagination
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
queryset = RemoteApp.objects.all()
permission_classes = (IsAppUser, )
serializer_class = RemoteAppConnectionInfoSerializer

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class ApplicationsConfig(AppConfig):
name = 'applications'

View File

@ -0,0 +1,68 @@
# coding: utf-8
#
from django.utils.translation import ugettext_lazy as _
# RemoteApp
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
REMOTE_APP_TYPE_CHROME = 'chrome'
REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
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
REMOTE_APP_TYPE_CHROME_FIELDS = [
{'name': 'chrome_target'},
{'name': 'chrome_username'},
{'name': 'chrome_password', 'write_only': True}
]
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
{'name': 'mysql_workbench_ip'},
{'name': 'mysql_workbench_name'},
{'name': 'mysql_workbench_username'},
{'name': 'mysql_workbench_password', 'write_only': True}
]
REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS = [
{'name': 'vmware_target'},
{'name': 'vmware_username'},
{'name': 'vmware_password', 'write_only': True}
]
REMOTE_APP_TYPE_CUSTOM_FIELDS = [
{'name': 'custom_cmdline'},
{'name': 'custom_target'},
{'name': 'custom_username'},
{'name': 'custom_password', 'write_only': True}
]
REMOTE_APP_TYPE_MAP_FIELDS = {
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_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_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
}

View File

@ -0,0 +1 @@
from .remote_app import *

View File

@ -0,0 +1,132 @@
# coding: utf-8
#
from django.utils.translation import ugettext as _
from django import forms
from orgs.mixins import OrgModelForm
from assets.models import Asset, SystemUser
from ..models import RemoteApp
from .. import const
__all__ = [
'RemoteAppCreateUpdateForm',
]
class RemoteAppTypeChromeForm(forms.ModelForm):
chrome_target = forms.CharField(
max_length=128, label=_('Target URL'), required=False
)
chrome_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
chrome_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Login password'), required=False
)
class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm):
mysql_workbench_ip = forms.CharField(
max_length=128, label=_('Database IP'), required=False
)
mysql_workbench_name = forms.CharField(
max_length=128, label=_('Database name'), required=False
)
mysql_workbench_username = forms.CharField(
max_length=128, label=_('Database username'), required=False
)
mysql_workbench_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Database password'), required=False
)
class RemoteAppTypeVMwareForm(forms.ModelForm):
vmware_target = forms.CharField(
max_length=128, label=_('Target address'), required=False
)
vmware_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
vmware_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Login password'), required=False
)
class RemoteAppTypeCustomForm(forms.ModelForm):
custom_cmdline = forms.CharField(
max_length=128, label=_('Operating parameter'), required=False
)
custom_target = forms.CharField(
max_length=128, label=_('Target address'), required=False
)
custom_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
custom_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
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.filter(
protocol=Asset.PROTOCOL_RDP
)
field_system_user = self.fields['system_user']
field_system_user.queryset = field_system_user.queryset.filter(
protocol=SystemUser.PROTOCOL_RDP
)
class Meta:
model = RemoteApp
fields = [
'name', 'asset', 'system_user', 'type', 'path', 'comment'
]
widgets = {
'asset': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Asset')
}),
'system_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('System user')
})
}
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,16 @@
"""
jumpserver.__app__.hands.py
~~~~~~~~~~~~~~~~~
This app depends other apps api, function .. should be import or write mack here.
Other module of this app shouldn't connect with other app.
:copyright: (c) 2014-2018 by Jumpserver Team.
:license: GPL v2, see LICENSE for more details.
"""
from common.permissions import AdminUserRequiredMixin
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from users.models import User, UserGroup

View File

@ -0,0 +1,42 @@
# Generated by Django 2.1.7 on 2019-05-20 11:04
import common.fields.model
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('assets', '0026_auto_20190325_2035'),
]
operations = [
migrations.CreateModel(
name='RemoteApp',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
('path', models.CharField(max_length=128, verbose_name='App path')),
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('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')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset', verbose_name='Asset')),
('system_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='System user')),
],
options={
'verbose_name': 'RemoteApp',
'ordering': ('name',),
},
),
migrations.AlterUniqueTogether(
name='remoteapp',
unique_together={('org_id', 'name')},
),
]

View File

View File

@ -0,0 +1 @@
from .remote_app import *

View File

@ -0,0 +1,89 @@
# coding: utf-8
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from common.fields.model import EncryptJsonDictTextField
from .. import const
__all__ = [
'RemoteApp',
]
class RemoteApp(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
asset = models.ForeignKey(
'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')
)
system_user = models.ForeignKey(
'assets.SystemUser', on_delete=models.CASCADE,
verbose_name=_('System user')
)
type = models.CharField(
default=const.REMOTE_APP_TYPE_CHROME,
choices=const.REMOTE_APP_TYPE_CHOICES,
max_length=128, verbose_name=_('App type')
)
path = models.CharField(
max_length=128, blank=False, null=False,
verbose_name=_('App path')
)
params = EncryptJsonDictTextField(
max_length=4096, default={}, blank=True, null=True,
verbose_name=_('Parameters')
)
created_by = models.CharField(
max_length=32, null=True, blank=True, verbose_name=_('Created by')
)
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')
)
class Meta:
verbose_name = _("RemoteApp")
unique_together = [('org_id', 'name')]
ordering = ('name', )
def __str__(self):
return self.name
@property
def parameters(self):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
_parameters = list()
_parameters.append(self.type)
path = '\"%s\"' % self.path
_parameters.append(path)
for field in const.REMOTE_APP_TYPE_MAP_FIELDS[self.type]:
value = self.params.get(field['name'])
if value is None:
continue
_parameters.append(value)
_parameters = ' '.join(_parameters)
return _parameters
@property
def asset_info(self):
return {
'id': self.asset.id,
'hostname': self.asset.hostname
}
@property
def system_user_info(self):
return {
'id': self.system_user.id,
'name': self.system_user.name
}

View File

@ -0,0 +1 @@
from .remote_app import *

View File

@ -0,0 +1,103 @@
# coding: utf-8
#
from rest_framework import serializers
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from .. import const
from ..models import RemoteApp
__all__ = [
'RemoteAppSerializer', 'RemoteAppConnectionInfoSerializer',
]
class RemoteAppParamsDictField(serializers.DictField):
"""
RemoteApp field => params
"""
@staticmethod
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(BulkSerializerMixin, serializers.ModelSerializer):
params = RemoteAppParamsDictField()
class Meta:
model = RemoteApp
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'asset', 'system_user', 'type', 'path', 'params',
'comment', 'created_by', 'date_created', 'asset_info',
'system_user_info', 'get_type_display',
]
read_only_fields = [
'created_by', 'date_created', 'asset_info',
'system_user_info', 'get_type_display'
]
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
parameter_remote_app = serializers.SerializerMethodField()
class Meta:
model = RemoteApp
fields = [
'id', 'name', 'asset', 'system_user', 'parameter_remote_app',
]
read_only_fields = ['parameter_remote_app']
@staticmethod
def get_parameter_remote_app(obj):
parameter = {
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
'working_directory': '',
'parameters': obj.parameters,
}
return parameter

View File

@ -0,0 +1,123 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="appForm" method="post" class="form-horizontal">
{% if form.non_field_errors %}
<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.system_user 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="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 }}';
var app_path_id = '#' + '{{ form.path.id_for_label }}';
var all_type_fields = [
'.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();
}
function initialDefaultValue(){
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');
}
$(document).ready(function () {
$('.select2').select2({
closeOnSelect: true
});
initialDefaultValue();
hiddenFields();
setDefaultValue();
})
.on('change', app_type_id, function(){
hiddenFields();
setDefaultValue();
});
</script>
{% endblock %}

View File

@ -0,0 +1,109 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li class="active">
<a href="{% url 'applications:remote-app-detail' pk=remote_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:remote-app-update' pk=remote_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>{{ remote_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>{{ remote_app.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Asset' %}:</td>
<td><b><a href="{% url 'assets:asset-detail' pk=remote_app.asset.id %}">{{ remote_app.asset.hostname }}</a></b></td>
</tr>
<tr>
<td>{% trans 'System user' %}:</td>
<td><b><a href="{% url 'assets:system-user-detail' pk=remote_app.system_user.id %}">{{ remote_app.system_user.name }}</a></b></td>
</tr>
<tr>
<td>{% trans 'App type' %}:</td>
<td><b>{{ remote_app.get_type_display }}</b></td>
</tr>
<tr>
<td>{% trans 'App path' %}:</td>
<td><b>{{ remote_app.path }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ remote_app.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ remote_app.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ remote_app.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
jumpserver.nodes_selected = {};
$(document).ready(function () {
})
.on('click', '.btn-delete-application', function () {
var $this = $(this);
var name = "{{ remote_app.name }}";
var rid = "{{ remote_app.id }}";
var the_url = '{% url "api-applications:remote-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
var redirect_url = "{% url 'applications:remote-app-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
</script>
{% endblock %}

View File

@ -0,0 +1,90 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block help_message %}
<div class="alert alert-info help-message">
{% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %}
<b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b>
</div>
{% endblock %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'applications:remote-app-create' %}" class="btn btn-sm btn-primary"> {% trans "Create RemoteApp" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="remote_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 'App type' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'System user' %}</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: $('#remote_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'applications:remote-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: 3, createdCell: function (td, cellData, rowData) {
var hostname = htmlEscape(cellData.hostname);
var detail_btn = '<a href="{% url 'assets:asset-detail' pk=DEFAULT_PK %}">' + hostname + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData.name);
var detail_btn = '<a href="{% url 'assets:system-user-detail' pk=DEFAULT_PK %}">' + name + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "applications:remote-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:remote-app-list" %}',
columns: [
{data: "id"},
{data: "name" },
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
{data: "system_user_info", orderable: false},
{data: "comment"},
{data: "id", orderable: false}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#remote_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:remote-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

@ -0,0 +1,79 @@
{% 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="remote_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 'App type' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'System user' %}</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 remote_app_table, url;
function initTable() {
if (inited){
return
} else {
inited = true;
}
url = '{% url "api-perms:my-remote-apps" %}';
var options = {
ele: $('#remote_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData);
$(td).html(name)
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var hostname = htmlEscape(cellData.hostname);
$(td).html(hostname);
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData.name);
$(td).html(name);
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var conn_btn = '<a href="{% url "luna-view" %}?login_to=' + cellData +'" class="btn btn-xs btn-primary">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(conn_btn)
}}
],
ajax_url: url,
columns: [
{data: "id"},
{data: "name"},
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
{data: "system_user_info", orderable: false},
{data: "comment", orderable: false},
{data: "id", orderable: false}
]
};
remote_app_table = jumpserver.initServerSideDataTable(options);
return remote_app_table
}
$(document).ready(function(){
initTable();
})
</script>
{% endblock %}

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,7 @@
# coding: utf-8
#
__all__ = [
]

View File

@ -0,0 +1,20 @@
# coding:utf-8
#
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'applications'
router = BulkRouter()
router.register(r'remote-app', api.RemoteAppViewSet, 'remote-app')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/',
api.RemoteAppConnectionInfoApi.as_view(),
name='remote-app-connection-info')
]
urlpatterns += router.urls

View File

@ -0,0 +1,16 @@
# coding:utf-8
from django.urls import path
from .. import views
app_name = 'applications'
urlpatterns = [
# RemoteApp
path('remote-app/', views.RemoteAppListView.as_view(), name='remote-app-list'),
path('remote-app/create/', views.RemoteAppCreateView.as_view(), name='remote-app-create'),
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'),
# User RemoteApp view
path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list')
]

View File

@ -0,0 +1 @@
from .remote_app import *

View File

@ -0,0 +1,99 @@
# coding: utf-8
#
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.detail import DetailView
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from ..models import RemoteApp
from .. import forms
__all__ = [
'RemoteAppListView', 'RemoteAppCreateView', 'RemoteAppUpdateView',
'RemoteAppDetailView', 'UserRemoteAppListView',
]
class RemoteAppListView(AdminUserRequiredMixin, TemplateView):
template_name = 'applications/remote_app_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('RemoteApp list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
template_name = 'applications/remote_app_create_update.html'
model = RemoteApp
form_class = forms.RemoteAppCreateUpdateForm
success_url = reverse_lazy('applications:remote-app-list')
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Create RemoteApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_success_message(self, cleaned_data):
return create_success_msg % ({'name': cleaned_data['name']})
class RemoteAppUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
template_name = 'applications/remote_app_create_update.html'
model = RemoteApp
form_class = forms.RemoteAppCreateUpdateForm
success_url = reverse_lazy('applications:remote-app-list')
def get_initial(self):
return {k: v for k, v in self.object.params.items()}
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Update RemoteApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_success_message(self, cleaned_data):
return update_success_msg % ({'name': cleaned_data['name']})
class RemoteAppDetailView(AdminUserRequiredMixin, DetailView):
template_name = 'applications/remote_app_detail.html'
model = RemoteApp
context_object_name = 'remote_app'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('RemoteApp detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserRemoteAppListView(LoginRequiredMixin, TemplateView):
template_name = 'applications/user_remote_app_list.html'
def get_context_data(self, **kwargs):
context = {
'action': _('My RemoteApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -20,7 +20,7 @@ from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger from common.utils import get_logger
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset from ..models import AdminUser, Asset
@ -36,7 +36,7 @@ __all__ = [
] ]
class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
""" """
Admin user api set, for add,delete,update,list,retrieve resource Admin user api set, for add,delete,update,list,retrieve resource
""" """

View File

@ -16,8 +16,9 @@ from django.urls import reverse_lazy
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q from django.db.models import Q
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from ..models import Asset, AdminUser, Node from ..models import Asset, AdminUser, Node
@ -35,7 +36,7 @@ __all__ = [
] ]
class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet):
""" """
API endpoint that allows Asset to be viewed or edited. API endpoint that allows Asset to be viewed or edited.
""" """
@ -47,6 +48,19 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
def set_assets_node(self, assets):
if not isinstance(assets, list):
assets = [assets]
node = Node.objects.get(value='Default')
node_id = self.request.query_params.get('node_id')
if node_id:
node = get_object_or_none(Node, pk=node_id)
node.assets.add(*assets)
def perform_create(self, serializer):
assets = serializer.save()
self.set_assets_node(assets)
def filter_node(self, queryset): def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id") node_id = self.request.query_params.get("node_id")
if not node_id: if not node_id:
@ -89,7 +103,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
return queryset return queryset
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView):
""" """
Asset bulk update api Asset bulk update api
""" """

View File

@ -39,6 +39,8 @@ __all__ = [
class NodeViewSet(viewsets.ModelViewSet): class NodeViewSet(viewsets.ModelViewSet):
filter_fields = ('value', 'key', )
search_fields = filter_fields
queryset = Node.objects.all() queryset = Node.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer

View File

@ -21,6 +21,7 @@ from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.mixins import IDInCacheFilterMixin
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \ from ..tasks import push_system_user_to_assets_manual, \
@ -38,7 +39,7 @@ __all__ = [
] ]
class SystemUserViewSet(BulkModelViewSet): class SystemUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
""" """
System user api set, for add,delete,update,list,retrieve resource System user api set, for add,delete,update,list,retrieve resource
""" """

View File

@ -11,7 +11,7 @@ class AuthBookBackend(BaseBackend):
@classmethod @classmethod
def filter(cls, username=None, asset=None, latest=True): def filter(cls, username=None, asset=None, latest=True):
queryset = AuthBook.objects.all() queryset = AuthBook.objects.all()
if username: if username is not None:
queryset = queryset.filter(username=username) queryset = queryset.filter(username=username)
if asset: if asset:
queryset = queryset.filter(asset=asset) queryset = queryset.filter(asset=asset)

View File

@ -27,7 +27,7 @@ class AdminUserBackend(BaseBackend):
instances = [] instances = []
assets = cls._get_assets(asset) assets = cls._get_assets(asset)
for asset in assets: for asset in assets:
if username and asset.admin_user.username != username: if username is not None and asset.admin_user.username != username:
continue continue
instance = construct_authbook_object(asset.admin_user, asset) instance = construct_authbook_object(asset.admin_user, asset)
instances.append(instance) instances.append(instance)

View File

@ -30,7 +30,7 @@ class SystemUserBackend(BaseBackend):
@classmethod @classmethod
def _filter_system_users_by_username(cls, system_users, username): def _filter_system_users_by_username(cls, system_users, username):
_system_users = cls._distinct_system_users_by_username(system_users) _system_users = cls._distinct_system_users_by_username(system_users)
if username: if username is not None:
_system_users = [su for su in _system_users if username == su.username] _system_users = [su for su in _system_users if username == su.username]
return _system_users return _system_users

View File

@ -1,6 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
UPDATE_ASSETS_HARDWARE_TASKS = [ UPDATE_ASSETS_HARDWARE_TASKS = [
{ {
'name': "setup", 'name': "setup",
@ -51,3 +54,4 @@ TASK_OPTIONS = {
CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}' CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}'

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-05-21 09:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0026_auto_20190325_2035'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='ip',
field=models.CharField(db_index=True, max_length=128, verbose_name='IP'),
),
]

View File

@ -8,3 +8,4 @@ from .asset import *
from .cmd_filter import * from .cmd_filter import *
from .utils import * from .utils import *
from .authbook import * from .authbook import *
from applications.models.remote_app import *

View File

@ -71,7 +71,7 @@ class Asset(OrgModelMixin):
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))

View File

@ -78,7 +78,7 @@ class AuthBook(AssetUser):
if host == self.asset.hostname: if host == self.asset.hostname:
_connectivity = self.UNREACHABLE _connectivity = self.UNREACHABLE
for host in value.get('contacted', {}).keys(): for host in value.get('contacted', []):
if host == self.asset.hostname: if host == self.asset.hostname:
_connectivity = self.REACHABLE _connectivity = self.REACHABLE

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
@ -15,14 +16,29 @@ class AdminUserSerializer(serializers.ModelSerializer):
""" """
管理用户 管理用户
""" """
assets_amount = serializers.SerializerMethodField() password = serializers.CharField(
unreachable_amount = serializers.SerializerMethodField() required=False, write_only=True, label=_('Password')
reachable_amount = serializers.SerializerMethodField() )
unreachable_amount = serializers.SerializerMethodField(label=_('Unreachable'))
assets_amount = serializers.SerializerMethodField(label=_('Asset'))
reachable_amount = serializers.SerializerMethodField(label=_('Reachable'))
class Meta: class Meta:
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
model = AdminUser model = AdminUser
fields = '__all__' fields = [
'id', 'org_id', 'name', 'username', 'assets_amount',
'reachable_amount', 'unreachable_amount', 'password', 'comment',
'date_created', 'date_updated', 'become', 'become_method',
'become_user', 'created_by',
]
extra_kwargs = {
'date_created': {'label': _('Date created')},
'date_updated': {'label': _('Date updated')},
'become': {'read_only': True}, 'become_method': {'read_only': True},
'become_user': {'read_only': True}, 'created_by': {'read_only': True}
}
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)

View File

@ -2,6 +2,9 @@
# #
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgResourceSerializerMixin
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import Asset from ..models import Asset
@ -13,15 +16,35 @@ __all__ = [
] ]
class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResourceSerializerMixin):
""" """
资产的数据结构 资产的数据结构
""" """
class Meta: class Meta:
model = Asset model = Asset
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' # validators = [] # 解决批量导入时unique_together字段校验失败
validators = [] fields = [
'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port',
'platform', 'is_active', 'public_ip', 'domain', 'admin_user',
'nodes', 'labels', 'number', 'vendor', 'model', 'sn',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity'
]
read_only_fields = (
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'created_by', 'date_created',
)
extra_kwargs = {
'hardware_info': {'label': _('Hardware info')},
'connectivity': {'label': _('Connectivity')},
'org_name': {'label': _('Org name')}
}
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):

View File

@ -1,5 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
@ -10,16 +12,36 @@ class SystemUserSerializer(serializers.ModelSerializer):
""" """
系统用户 系统用户
""" """
unreachable_amount = serializers.SerializerMethodField() password = serializers.CharField(
reachable_amount = serializers.SerializerMethodField() required=False, write_only=True, label=_('Password')
unreachable_assets = serializers.SerializerMethodField() )
reachable_assets = serializers.SerializerMethodField() unreachable_amount = serializers.SerializerMethodField(
assets_amount = serializers.SerializerMethodField() label=_('Unreachable')
)
unreachable_assets = serializers.SerializerMethodField(
label=_('Unreachable assets')
)
reachable_assets = serializers.SerializerMethodField(
label=_('Reachable assets')
)
reachable_amount = serializers.SerializerMethodField(label=_('Reachable'))
assets_amount = serializers.SerializerMethodField(label=_('Asset'))
class Meta: class Meta:
model = SystemUser model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'org_id', 'name', 'username', 'login_mode',
'login_mode_display', 'priority', 'protocol', 'auto_push',
'password', 'assets_amount', 'reachable_amount', 'reachable_assets',
'unreachable_amount', 'unreachable_assets', 'cmd_filters', 'sudo',
'shell', 'comment', 'nodes', 'assets'
]
extra_kwargs = {
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'nodes': {'read_only': True},
'assets': {'read_only': True}
}
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)

View File

@ -0,0 +1,6 @@
{% 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

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

View File

@ -1,29 +1,6 @@
{% extends '_modal.html' %} {% extends '_import_modal.html' %}
{% load i18n %} {% load i18n %}
{% block modal_id %}asset_import_modal{% endblock %}
{% block modal_title%}{% trans "Import asset" %}{% endblock %} {% block modal_title%}{% trans "Import assets" %}{% endblock %}
{% block modal_body %}
<form method="post" action="{% url 'assets:asset-import' %}" id="fm_asset_import" enctype="multipart/form-data"> {% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %}
{% csrf_token %}
<div class="form-group">
<label class="control-label" for="id_assets">{% trans "Template" %}</label>
<a href="{% url 'assets:asset-export' %}" style="display: block">{% trans 'Download' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Asset csv file" %}</label>
<input id="id_assets" type="file" name="file" />
<span class="help-block red-fonts">
{% trans 'If set id, will use this id update asset existed' %}
</span>
</div>
</form>
<p>
<p class="text-success" id="id_created"></p>
<p id="id_created_detail"></p>
<p class="text-warning" id="id_updated"></p>
<p id="id_updated_detail"></p>
<p class="text-danger" id="id_failed"></p>
<p id="id_failed_detail"></p>
</p>
{% endblock %}
{% block modal_confirm_id %}btn_asset_import{% endblock %}

View File

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

View File

@ -0,0 +1,140 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_id %}asset_user_auth_view{% endblock %}
{% block modal_title%}{% trans "Asset user auth" %}{% endblock %}
{% block modal_body %}
<style>
.inmodal .modal-body {
background: #fff;
}
</style>
<form class="form-horizontal" action="" style="padding-top: 20px">
<div class="form-group mfa-field">
<label for="mfa" class="col-sm-2 control-label">{% trans 'MFA' %}</label>
<div class="col-sm-8">
<input type="text" id="mfa" class="form-control input-sm" name="mfa">
<span id="mfa_error" class="help-block">{% trans "Need otp auth for view auth" %}</span>
</div>
<div class="col-sm-2">
<a class="btn btn-primary btn-sm btn-mfa">{% trans "Confirm" %}</a>
</div>
</div>
<div hidden class="auth-field">
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Hostname' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_hostname_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Username' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_username_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Password' %}</label>
<div class="col-sm-8">
<input id="id_password_view" type="password" class="form-control" value="" readonly style="border: none;padding-left: 0;background-color: #fff;width: 100%">
</div>
<div class="col-sm-2" style="padding-left: 2px">
<a class="btn btn-white btn-sm btn-show-password"><i class="fa fa-eye"></i></a>
<a class="btn btn-white btn-sm btn-copy-password"><i class="fa fa-copy"></i></a>
</div>
</div>
</div>
</form>
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
var showPassword = false;
var lastMFATime = "{{ request.session.OTP_LAST_VERIFY_TIME }}";
var asset_id = "";
var host = "";
var username = "";
function initClipboard() {
var clipboard = new Clipboard('.btn-copy-password', {
text: function (trigger) {
return $("#id_password_view").val()
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function showAuth() {
$(".mfa-field").hide();
$(".auth-field").show();
var url = "{% url "api-assets:asset-user-auth-info" %}?asset_id=" + asset_id + "&username=" + username;
$("#id_username_view").html(username);
$("#id_hostname_view").html(host);
var success = function (data) {
var password = data.password;
$("#id_password_view").val(password);
};
var error = function() {
var msg = "{% trans 'Get auth info error' %}";
toastr.error(msg)
};
APIUpdateAttr({
url: url,
method: "GET",
success: success,
flash_message: false,
error: error
})
}
function showMFA() {
$(".mfa-field").show();
$(".auth-field").hide();
}
$(document).ready(function () {
initClipboard();
}).on("click", ".btn-show-password", function () {
showPassword = !showPassword;
if (showPassword) {
$("#id_password_view").attr("type", "text")
} else {
$("#id_password_view").attr("type", "password")
}
}).on("show.bs.modal", "#asset_user_auth_view", function () {
var now = new Date();
if (lastMFATime === "") {
lastMFATime = 0
}
var nowTime = now.getTime() / 1000;
if (nowTime - lastMFATime < 60*10 ) {
showAuth();
}
}).on("click", ".btn-mfa", function () {
var url = "{% url 'api-auth:user-otp-verify' %}";
var data = {
code: $("#mfa").val()
};
var success = function () {
var now = new Date();
lastMFATime = now.getTime() / 1000;
showAuth()
};
var error = function () {
$("#mfa_error").addClass("text-danger").html("Code error");
};
APIUpdateAttr({
url: url,
method: "POST",
body: JSON.stringify(data),
success: success,
flash_message: false,
error: error
})
})
</script>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% 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

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

View File

@ -85,6 +85,7 @@
</div> </div>
</div> </div>
{% include 'assets/_asset_user_auth_modal.html' %} {% include 'assets/_asset_user_auth_modal.html' %}
{% include 'assets/_asset_user_view_auth_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
@ -112,9 +113,12 @@ function initTable() {
} }
}}, }},
{targets: 4, createdCell: function (td, cellData, rowData) { {targets: 4, createdCell: function (td, cellData, rowData) {
var btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
var view_btn = ' <a class="btn btn-xs btn-primary btn-view-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "View auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname); btn += view_btn;
$(td).html(test_btn + update_auth_btn); btn += test_btn;
$(td).html(btn);
}} }}
], ],
@ -201,5 +205,11 @@ $(document).ready(function () {
$('#id_password').parent().addClass('has-error'); $('#id_password').parent().addClass('has-error');
} }
}) })
.on("click", ".btn-view-auth", function (evt) {
asset_id = $(this).data("aid") ;
host = $(this).data("hostname");
username = "{{ admin_user.username }}";
$("#asset_user_auth_view").modal();
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,5 @@
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block table_search %}
{% endblock %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
{# 管理用户是资产被控服务器上的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#} {# 管理用户是资产被控服务器上的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
@ -12,6 +9,30 @@
{% trans 'You can set any one for Windows or other hardware.' %} {% trans 'You can set any one for Windows or other hardware.' %}
</div> </div>
{% endblock %} {% endblock %}
{% block table_search %}
<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>
{% endblock %}
{% block table_container %} {% block table_container %}
<div class="uc pull-left m-r-5"> <div class="uc pull-left m-r-5">
@ -36,11 +57,14 @@
<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 %}
<script> <script>
$(document).ready(function(){ var admin_user_table = 0;
function initTable() {
var options = { var options = {
ele: $('#admin_user_list_table'), ele: $('#admin_user_list_table'),
columnDefs: [ columnDefs: [
@ -93,7 +117,12 @@ $(document).ready(function(){
columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" }, columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}] {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}]
}; };
jumpserver.initServerSideDataTable(options) admin_user_table = jumpserver.initServerSideDataTable(options);
return admin_user_table
}
$(document).ready(function(){
initTable()
}) })
.on('click', '.btn_admin_user_delete', function () { .on('click', '.btn_admin_user_delete', function () {
@ -107,6 +136,70 @@ $(document).ready(function(){
$data_table.ajax.reload(); $data_table.ajax.reload();
}, 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

@ -2,10 +2,6 @@
{% load common_tags %} {% load common_tags %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block custom_head_css_js %}
{% 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">
@ -87,6 +83,7 @@
</div> </div>
</div> </div>
{% include 'assets/_asset_user_auth_modal.html' %} {% include 'assets/_asset_user_auth_modal.html' %}
{% include 'assets/_asset_user_view_auth_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
@ -117,14 +114,14 @@ function initAssetUserTable() {
$(td).html(cellData.slice(0, -6)); $(td).html(cellData.slice(0, -6));
}}, }},
{targets: 5, createdCell: function (td, cellData) { {targets: 5, createdCell: function (td, cellData) {
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData); var btn = '<a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
{% if asset.protocol == 'ssh' %} var view_btn = ' <a class="btn btn-xs btn-primary btn-view-auth" data-username="DEFAULT_USERNAME">{% trans "View auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
$(td).html(test_btn + update_auth_btn); btn += view_btn;
{% else %} {% if asset.protocol == 'ssh' %}
$(td).html(update_auth_btn); btn += test_btn;
{% endif %} {% endif %}
{#var check_btn = ' <a class="btn btn-xs btn-info btn-check-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Check auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);#} $(td).html(btn);
}} }}
], ],
@ -142,17 +139,6 @@ var username;
$(document).ready(function () { $(document).ready(function () {
initAssetUserTable(); initAssetUserTable();
}) })
{#.on('click', '.btn-check-asset-user-auth', function(){#}
{# var username = $(this).data('username');#}
{# var the_url = "{% url 'api-assets:asset-user-auth-info' %}" + '?asset_id={{ asset.id }}' + '&username=' + username;#}
{# $.ajax({#}
{# url: the_url,#}
{# method: 'GET',#}
{# success: function (data) {#}
{# alert("Password: " + data.password);#}
{# }#}
{# });#}
{# })#}
.on('click', '.btn-update-asset-user-auth', function() { .on('click', '.btn-update-asset-user-auth', function() {
username = $(this).data('username'); username = $(this).data('username');
var hostname = "{{ asset.hostname }}"; var hostname = "{{ asset.hostname }}";
@ -214,5 +200,11 @@ $(document).ready(function () {
flash_message: false flash_message: false
}); });
}) })
.on("click", ".btn-view-auth", function (evt) {
asset_id = "{{ asset.id }}" ;
host = "{{ asset.hostname }}";
username = $(this).data("username");
$("#asset_user_auth_view").modal();
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -67,14 +67,26 @@
</div> </div>
<div class="mail-box-header"> <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="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div>
<div class="html5buttons"> <div class="" style="float: right">
<div class="dt-buttons btn-group"> <div class=" btn-group">
<a class="btn btn-default btn_import" data-toggle="modal" data-target="#asset_import_modal" tabindex="0"> <button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<span>{% trans "Import" %}</span> <ul class="dropdown-menu">
</a> <li>
<a class="btn btn-default btn_export" tabindex="0"> <a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span> <span>{% trans "Export" %}</span>
</a> </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> </div>
<div class="btn-group" style="float: right"> <div class="btn-group" style="float: right">
@ -140,7 +152,7 @@
{# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#} {# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#}
</ul> </ul>
</div> </div>
{% include 'assets/_asset_update_modal.html' %}
{% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_import_modal.html' %}
{% include 'assets/_asset_list_modal.html' %} {% include 'assets/_asset_list_modal.html' %}
{% endblock %} {% endblock %}
@ -457,49 +469,78 @@ $(document).ready(function(){
asset_table.search(val).draw(); asset_table.search(val).draw();
}) })
.on('click', '.btn_export', function () { .on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable(); var assets = asset_table.selected;
var rows = $data_table.rows('.selected').data(); var data = {
'resources': assets
var assets = []; };
$.each(rows, function (index, obj) { var search = $("input[type='search']").val();
assets.push(obj.id) var props = {
}); method: "POST",
$.ajax({ body: JSON.stringify(data),
url: "{% url "assets:asset-export" %}", success_url: "{% url 'api-assets:asset-list' %}",
method: 'POST', format: 'csv',
data: JSON.stringify({assets_id: assets, node_id: current_node_id}), params: {
dataType: "json", search: search,
success: function (data, textStatus) { node_id: current_node_id || ''
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
} }
};
APIExportData(props);
}) })
}) .on('click', '#btn_import_confirm', function () {
.on('click', '#btn_asset_import', function () { var file = document.getElementById('id_file').files[0];
var $form = $('#fm_asset_import'); if(!file){
var action = $form.attr("action"); toastr.error("{% trans "Please select file" %}");
return
}
var url = "{% url 'api-assets:asset-list' %}";
if (current_node_id){ if (current_node_id){
action = setUrlParam(action, 'node_id', current_node_id); url = setUrlParam(url, 'node_id', current_node_id);
$form.attr("action", action)
} }
$form.find('.help-block').remove(); var data_table = $('#asset_list_table').DataTable();
function success (data) {
if (data.valid === false) { APIImportData({
$('<span />', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_assets')); url: url,
} else { method: "POST",
$('#id_created').html(data.created_info); body: file,
$('#id_created_detail').html(data.created.join(', ')); data_table: data_table
$('#id_updated').html(data.updated_info); });
$('#id_updated_detail').html(data.updated.join(', ')); })
$('#id_failed').html(data.failed_info); .on('click', '#download_update_template', function () {
$('#id_failed_detail').html(data.failed.join(', ')); var assets = asset_table.selected;
var $data_table = $('#asset_list_table').DataTable(); var data = {
$data_table.ajax.reload(); '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
} }
$form.ajaxSubmit({success: success}); 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' %}";
@ -584,15 +625,17 @@ $(document).ready(function(){
}) })
.on('click', '#btn_bulk_update', function () { .on('click', '#btn_bulk_update', function () {
var action = $('#slct_bulk_update').val(); var action = $('#slct_bulk_update').val();
var $data_table = $('#asset_list_table').DataTable(); var id_list = asset_table.selected;
var id_list = [];
$data_table.rows({selected: true}).every(function(){
id_list.push(this.data().id);
});
if (id_list.length === 0) { if (id_list.length === 0) {
return false; return false;
} }
var the_url = "{% url 'api-assets:asset-list' %}"; var the_url = "{% url 'api-assets:asset-list' %}";
var data = {
'resources': id_list
};
function refreshTag() {
$('#asset_list_table').DataTable().ajax.reload();
}
function doDeactive() { function doDeactive() {
var data = []; var data = [];
@ -601,7 +644,8 @@ $(document).ready(function(){
data.push(obj); data.push(obj);
}); });
function success() { function success() {
asset_table.ajax.reload() setTimeout( function () {
window.location.reload();}, 500);
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
@ -617,7 +661,8 @@ $(document).ready(function(){
data.push(obj); data.push(obj);
}); });
function success() { function success() {
asset_table.ajax.reload() setTimeout( function () {
window.location.reload();}, 300);
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
@ -637,43 +682,46 @@ $(document).ready(function(){
confirmButtonText: "{% trans 'Confirm' %}", confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false closeOnConfirm: false
},function () { },function () {
var success = function() { function success(data) {
url = setUrlParam(the_url, 'spm', data.spm);
APIUpdateAttr({
url:url,
method:'DELETE',
success:refreshTag,
flash_message:false,
});
var msg = "{% trans 'Asset Deleted.' %}"; var msg = "{% trans 'Asset Deleted.' %}";
swal("{% trans 'Asset Delete' %}", msg, "success"); swal("{% trans 'Asset Delete' %}", msg, "success");
$('#asset_list_table').DataTable().ajax.reload(); }
}; function fail() {
var fail = function() {
var msg = "{% trans 'Asset Deleting failed.' %}"; var msg = "{% trans 'Asset Deleting failed.' %}";
swal("{% trans 'Asset Delete' %}", msg, "error"); swal("{% trans 'Asset Delete' %}", msg, "error");
}; }
var url_delete = the_url + '?id__in=' + JSON.stringify(id_list);
APIUpdateAttr({ APIUpdateAttr({
url: url_delete, url: "{% url 'api-common:resources-cache' %}",
method: 'DELETE', method:'POST',
body:JSON.stringify(data),
success:success, success:success,
error:fail error:fail
}); })
$data_table.ajax.reload(); })
jumpserver.checked = false;
});
} }
function doUpdate() { function doUpdate() {
var data = { function fail(data) {
'assets_id':id_list toastr.error(JSON.parse(data))
};
function error(data) {
toastr.error(JSON.parse(data).error)
} }
function success(data) { function success(data) {
location.href = data.url; var url = "{% url 'assets:asset-bulk-update' %}";
location.href= setUrlParam(url, 'spm', data.spm);
} }
APIUpdateAttr({ APIUpdateAttr({
'url': "{% url 'api-assets:asset-bulk-update-select' %}", url: "{% url 'api-common:resources-cache' %}",
'method': 'POST', method:'POST',
'body': JSON.stringify(data), body:JSON.stringify(data),
'flash_message': false, flash_message:false,
'success': success, success:success,
'error': error, error:fail
}) })
} }
@ -698,6 +746,7 @@ $(document).ready(function(){
'success': success 'success': success
}) })
} }
switch(action) { switch(action) {
case 'deactive': case 'deactive':
doDeactive(); doDeactive();

View File

@ -133,6 +133,7 @@
</div> </div>
</div> </div>
{% include 'assets/_asset_user_auth_modal.html' %} {% include 'assets/_asset_user_auth_modal.html' %}
{% include 'assets/_asset_user_view_auth_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
@ -163,9 +164,10 @@ function initAssetsTable() {
push_btn = ' <a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); push_btn = ' <a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{% endif %} {% endif %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var view_btn = ' <a class="btn btn-xs btn-primary btn-view-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "View auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#} {#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname); var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
$(td).html(push_btn + test_btn + update_auth_btn); $(td).html(update_auth_btn + view_btn + push_btn + test_btn);
}} }}
], ],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}', ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
@ -360,5 +362,11 @@ $(document).ready(function () {
$('#id_password').parent().addClass('has-error'); $('#id_password').parent().addClass('has-error');
} }
}) })
.on("click", ".btn-view-auth", function (evt) {
asset_id = $(this).data("aid") ;
host = $(this).data("hostname");
username = "{{ system_user.username }}";
$("#asset_user_auth_view").modal();
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -14,6 +14,28 @@
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<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>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
@ -41,9 +63,12 @@
<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>
var system_user_table = 0;
function initTable() { function initTable() {
var options = { var options = {
ele: $('#system_user_list_table'), ele: $('#system_user_list_table'),
@ -101,7 +126,8 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initServerSideDataTable(options); system_user_table = jumpserver.initServerSideDataTable(options);
return system_user_table
} }
$(document).ready(function(){ $(document).ready(function(){
@ -173,6 +199,71 @@ $(document).ready(function(){
break; 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

@ -27,7 +27,9 @@ from django.contrib.messages.views import SuccessMessageMixin
from common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger from common.utils import get_object_or_none, get_logger
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg from common.const import (
create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID
)
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from orgs.utils import current_org from orgs.utils import current_org
from .. import forms from .. import forms
@ -122,7 +124,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
spm = request.GET.get('spm', '') spm = request.GET.get('spm', '')
assets_id = cache.get(CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)) assets_id = cache.get(KEY_CACHE_RESOURCES_ID.format(spm))
if kwargs.get('form'): if kwargs.get('form'):
self.form = kwargs['form'] self.form = kwargs['form']
elif assets_id: elif assets_id:

View File

@ -2,6 +2,7 @@
# #
import uuid import uuid
import time
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
@ -10,10 +11,11 @@ from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser from common.permissions import IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins import RootOrgViewMixin from orgs.mixins import RootOrgViewMixin
from users.serializers import UserSerializer from users.serializers import UserSerializer
from users.models import User from users.models import User
@ -23,12 +25,13 @@ from users.utils import (
check_user_valid, check_otp_code, increase_login_failed_count, check_user_valid, check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count is_block_login, clean_failed_count
) )
from ..serializers import OtpVerifySerializer
from ..signals import post_auth_success, post_auth_failed from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', 'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
'UserOtpVerifyApi',
] ]
@ -179,3 +182,20 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
sender=self.__class__, username=username, sender=self.__class__, username=username,
request=self.request, reason=reason request=self.request, reason=reason
) )
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
if request.user.check_otp(code):
request.session["OTP_LAST_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": "Code not valid"}, status=400)

View File

@ -7,6 +7,8 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend from django_auth_ldap.backend import _LDAPUser, LDAPBackend
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
logger = _LDAPConfig.get_logger() logger = _LDAPConfig.get_logger()
@ -86,13 +88,18 @@ class LDAPUser(_LDAPUser):
return user_dn return user_dn
def _populate_user_from_attributes(self): def _populate_user_from_attributes(self):
super()._populate_user_from_attributes() for field, attr in self.settings.USER_ATTR_MAP.items():
if not hasattr(self._user, 'email') or '@' not in self._user.email: try:
if '@' not in self._user.username: value = self.attrs[attr][0]
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX) except LookupError:
logger.warning("{} does not have a value for the attribute {}".format(self.dn, attr))
else: else:
email = self._user.username if not hasattr(self._user, field):
continue
if isinstance(getattr(self._user, field), bool):
value = value.lower() in ['true', '1']
setattr(self._user, field, value)
email = getattr(self._user, 'email', '')
email = construct_user_email(email, self._user.username)
setattr(self._user, 'email', email) setattr(self._user, 'email', email)

View File

@ -23,15 +23,12 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request): def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False # Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID: if not settings.AUTH_OPENID:
logger.debug("Not settings.AUTH_OPENID")
return return
# Don't need check single logout if user not authenticated # Don't need check single logout if user not authenticated
if not request.user.is_authenticated: if not request.user.is_authenticated:
logger.debug("User is not authenticated")
return return
elif not request.session[BACKEND_SESSION_KEY].endswith( elif not request.session[BACKEND_SESSION_KEY].endswith(
BACKEND_OPENID_AUTH_CODE): BACKEND_OPENID_AUTH_CODE):
logger.debug("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE")
return return
# Check openid user single logout or not with access_token # Check openid user single logout or not with access_token

View File

@ -14,3 +14,7 @@ class AccessKeySerializer(serializers.ModelSerializer):
model = AccessKey model = AccessKey
fields = ['id', 'secret'] fields = ['id', 'secret']
read_only_fields = ['id', 'secret'] read_only_fields = ['id', 'secret']
class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)

View File

@ -16,5 +16,6 @@ urlpatterns = [
path('connection-token/', path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
] ]

View File

@ -3,10 +3,18 @@
import os import os
import uuid import uuid
from rest_framework.views import Response
from rest_framework import generics, serializers
from django.core.cache import cache from django.core.cache import cache
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics, serializers
from .const import KEY_CACHE_RESOURCES_ID
__all__ = [
'LogTailApi', 'ResourcesIDCacheApi',
]
class OutputSerializer(serializers.Serializer): class OutputSerializer(serializers.Serializer):
output = serializers.CharField() output = serializers.CharField()
@ -68,3 +76,14 @@ class LogTailApi(generics.RetrieveAPIView):
data, end, new_mark = self.read_from_file() data, end, new_mark = self.read_from_file()
return Response({"data": data, 'end': end, 'mark': new_mark}) return Response({"data": data, 'end': end, 'mark': new_mark})
class ResourcesIDCacheApi(APIView):
def post(self, request, *args, **kwargs):
spm = str(uuid.uuid4())
resources_id = request.data.get('resources')
if resources_id:
cache_key = KEY_CACHE_RESOURCES_ID.format(spm)
cache.set(cache_key, resources_id, 300)
return Response({'spm': spm})

View File

@ -7,3 +7,4 @@ create_success_msg = _("%(name)s was created successfully")
update_success_msg = _("%(name)s was updated successfully") update_success_msg = _("%(name)s was updated successfully")
FILE_END_GUARD = ">>> Content End <<<" FILE_END_GUARD = ">>> Content End <<<"
celery_task_pre_key = "CELERY_" celery_task_pre_key = "CELERY_"
KEY_CACHE_RESOURCES_ID = "RESOURCES_ID_{}"

111
apps/common/drfmetadata.py Normal file
View File

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
#
from __future__ import unicode_literals
from collections import OrderedDict
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.encoding import force_text
from rest_framework.metadata import SimpleMetadata
from rest_framework import exceptions, serializers
from rest_framework.request import clone_request
class SimpleMetadataWithFilters(SimpleMetadata):
"""Override SimpleMetadata, adding info about filters"""
methods = {"PUT", "POST", "GET"}
attrs = [
'read_only', 'label', 'help_text',
'min_length', 'max_length',
'min_value', 'max_value', "write_only"
]
def determine_actions(self, request, view):
"""
For generic class based views we return information about
the fields that are accepted for 'PUT' and 'POST' methods.
"""
actions = {}
for method in self.methods & set(view.allowed_methods):
view.request = clone_request(request, method)
try:
# Test global permissions
if hasattr(view, 'check_permissions'):
view.check_permissions(view.request)
# Test object permissions
if method == 'PUT' and hasattr(view, 'get_object'):
view.get_object()
except (exceptions.APIException, PermissionDenied, Http404):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = view.get_serializer()
actions[method] = self.get_serializer_info(serializer)
finally:
view.request = request
return actions
def get_field_info(self, field):
"""
Given an instance of a serializer field, return a dictionary
of metadata about it.
"""
field_info = OrderedDict()
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
for attr in self.attrs:
value = getattr(field, attr, None)
if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True)
if getattr(field, 'child', None):
field_info['child'] = self.get_field_info(field.child)
elif getattr(field, 'fields', None):
field_info['children'] = self.get_serializer_info(field)
if (not field_info.get('read_only') and
not isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField)) and
hasattr(field, 'choices')):
field_info['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in field.choices.items()
]
return field_info
def get_filters_fields(self, request, view):
fields = []
if hasattr(view, 'get_filter_fields'):
fields = view.get_filter_fields(request)
elif hasattr(view, 'filter_fields'):
fields = view.filter_fields
return fields
def get_ordering_fields(self, request, view):
fields = []
if hasattr(view, 'get_ordering_fields'):
fields = view.get_filter_fields(request)
elif hasattr(view, 'ordering_fields'):
fields = view.filter_fields
return fields
def determine_metadata(self, request, view):
metadata = super(SimpleMetadataWithFilters, self).determine_metadata(request, view)
filter_fields = self.get_filters_fields(request, view)
order_fields = self.get_ordering_fields(request, view)
meta_get = metadata.get("actions", {}).get("GET", {})
for k, v in meta_get.items():
if k in filter_fields:
v["filter"] = True
if k in order_fields:
v["order"] = True
return metadata

View File

@ -11,7 +11,7 @@ __all__ = [
'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin', 'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin',
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', 'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField',
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', 'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField',
'EncryptTextField', 'EncryptMixin', 'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField',
] ]
signer = get_signer() signer = get_signer()
@ -129,4 +129,7 @@ class EncryptCharField(EncryptMixin, models.CharField):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
pass

View File

@ -3,12 +3,15 @@
from django.db import models from django.db import models
from django.http import JsonResponse from django.http import JsonResponse
from django.utils import timezone from django.utils import timezone
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.utils import html from rest_framework.utils import html
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import SkipField from rest_framework.fields import SkipField
from .const import KEY_CACHE_RESOURCES_ID
class NoDeleteQuerySet(models.query.QuerySet): class NoDeleteQuerySet(models.query.QuerySet):
@ -65,6 +68,27 @@ class IDInFilterMixin(object):
return queryset return queryset
class IDInCacheFilterMixin(object):
def filter_queryset(self, queryset):
queryset = super(IDInCacheFilterMixin, self).filter_queryset(queryset)
spm = self.request.query_params.get('spm')
cache_key = KEY_CACHE_RESOURCES_ID.format(spm)
resources_id = cache.get(cache_key)
if resources_id and isinstance(resources_id, list):
queryset = queryset.filter(id__in=resources_id)
return queryset
class IDExportFilterMixin(object):
def filter_queryset(self, queryset):
# 下载导入模版
if self.request.query_params.get('template') == 'import':
return []
else:
return super(IDExportFilterMixin, self).filter_queryset(queryset)
class BulkSerializerMixin(object): class BulkSerializerMixin(object):
""" """
Become rest_framework_bulk not support uuid as a primary key Become rest_framework_bulk not support uuid as a primary key
@ -131,7 +155,11 @@ class BulkListSerializerMixin(object):
for item in data: for item in data:
try: try:
# prepare child serializer to only handle one instance # prepare child serializer to only handle one instance
if 'id' in item.keys():
self.child.instance = self.instance.get(id=item['id']) if self.instance else None self.child.instance = self.instance.get(id=item['id']) if self.instance else None
if 'pk' in item.keys():
self.child.instance = self.instance.get(id=item['pk']) if self.instance else None
self.child.initial_data = item self.child.initial_data = item
# raw # raw
validated = self.child.run_validation(item) validated = self.child.run_validation(item)

View File

@ -0,0 +1 @@
from .csv import *

101
apps/common/parsers/csv.py Normal file
View File

@ -0,0 +1,101 @@
# ~*~ coding: utf-8 ~*~
#
import json
import unicodecsv
from rest_framework.parsers import BaseParser
from rest_framework.exceptions import ParseError
from ..utils import get_logger
logger = get_logger(__file__)
class JMSCSVParser(BaseParser):
"""
Parses CSV file to serializer data
"""
media_type = 'text/csv'
@staticmethod
def _universal_newlines(stream):
"""
保证在`通用换行模式`下打开文件
"""
for line in stream.splitlines():
yield line
@staticmethod
def _gen_rows(csv_data, charset='utf-8', **kwargs):
csv_reader = unicodecsv.reader(csv_data, encoding=charset, **kwargs)
for row in csv_reader:
if not any(row): # 空行
continue
yield row
@staticmethod
def _get_fields_map(serializer):
fields_map = {}
fields = serializer.get_fields()
fields_map.update({v.label: k for k, v in fields.items()})
fields_map.update({k: k for k, _ in fields.items()})
return fields_map
@staticmethod
def _process_row(row):
"""
构建json数据前的行处理
"""
_row = []
for col in row:
# 列表转换
if isinstance(col, str) and col.find("[") != -1 and col.find("]") != -1:
# 替换中文格式引号
col = col.replace("", '"').replace("", '"').\
replace("", '"').replace('', '"').replace("'", '"')
col = json.loads(col)
_row.append(col)
return _row
@staticmethod
def _process_row_data(row_data):
"""
构建json数据后的行数据处理
"""
_row_data = {}
for k, v in row_data.items():
if isinstance(v, list) \
or isinstance(v, str) and k.strip() and v.strip():
_row_data[k] = v
return _row_data
def parse(self, stream, media_type=None, parser_context=None):
parser_context = parser_context or {}
encoding = parser_context.get('encoding', 'utf-8')
try:
serializer = parser_context["view"].get_serializer()
except Exception as e:
logger.debug(e, exc_info=True)
raise ParseError('The resource does not support imports!')
try:
stream_data = stream.read()
binary = self._universal_newlines(stream_data)
rows = self._gen_rows(binary, charset=encoding)
header = next(rows)
fields_map = self._get_fields_map(serializer)
header = [fields_map.get(name, '') for name in header]
data = []
for row in rows:
row = self._process_row(row)
row_data = dict(zip(header, row))
row_data = self._process_row_data(row_data)
data.append(row_data)
return data
except Exception as e:
logger.debug(e, exc_info=True)
raise ParseError('CSV parse error!')

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from rest_framework import permissions from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin

View File

@ -0,0 +1 @@
from .csv import *

View File

@ -0,0 +1,83 @@
# ~*~ coding: utf-8 ~*~
#
import unicodecsv
from datetime import datetime
from six import BytesIO
from rest_framework.renderers import BaseRenderer
from rest_framework.utils import encoders, json
from ..utils import get_logger
logger = get_logger(__file__)
class JMSCSVRender(BaseRenderer):
media_type = 'text/csv'
format = 'csv'
@staticmethod
def _get_header(fields, template):
if template == 'import':
header = [
k for k, v in fields.items()
if not v.read_only and k != 'org_id'
]
elif template == 'update':
header = [k for k, v in fields.items() if not v.read_only]
else:
# template in ['export']
header = [k for k, v in fields.items() if not v.write_only]
return header
@staticmethod
def _gen_table(data, header, labels=None):
labels = labels or {}
yield [labels.get(k, k) for k in header]
for item in data:
row = [item.get(key) for key in header]
yield row
def set_response_disposition(self, serializer, context):
response = context.get('response')
if response and hasattr(serializer, 'Meta') and \
hasattr(serializer.Meta, "model"):
model_name = serializer.Meta.model.__name__.lower()
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = "{}_{}.csv".format(model_name, now)
disposition = 'attachment; filename="{}"'.format(filename)
response['Content-Disposition'] = disposition
def render(self, data, media_type=None, renderer_context=None):
renderer_context = renderer_context or {}
encoding = renderer_context.get('encoding', 'utf-8')
request = renderer_context['request']
template = request.query_params.get('template', 'export')
view = renderer_context['view']
data = json.loads(json.dumps(data, cls=encoders.JSONEncoder))
if template == 'import':
data = [data[0]] if data else data
try:
serializer = view.get_serializer()
self.set_response_disposition(serializer, renderer_context)
except Exception as e:
logger.debug(e, exc_info=True)
value = 'The resource not support export!'.encode('utf-8')
else:
fields = serializer.get_fields()
header = self._get_header(fields, template)
labels = {k: v.label for k, v in fields.items() if v.label}
table = self._gen_table(data, header, labels)
csv_buffer = BytesIO()
csv_writer = unicodecsv.writer(csv_buffer, encoding=encoding)
for row in table:
csv_writer.writerow(row)
value = csv_buffer.getvalue()
return value

View File

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from .. import api
app_name = 'common'
urlpatterns = [
path('resources/cache/',
api.ResourcesIDCacheApi.as_view(), name='resources-cache'),
]

View File

@ -144,6 +144,7 @@ def is_uuid(seq):
def get_request_ip(request): def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]: if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0] login_ip = x_forwarded_for[0]
else: else:

View File

@ -194,7 +194,7 @@ class Config(dict):
filename = os.path.join(self.root_path, filename) filename = os.path.join(self.root_path, filename)
try: try:
with open(filename, 'rt', encoding='utf8') as f: with open(filename, 'rt', encoding='utf8') as f:
obj = yaml.load(f) obj = yaml.safe_load(f)
except IOError as e: except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR): if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False return False
@ -273,6 +273,19 @@ class Config(dict):
if default_value is None: if default_value is None:
return v return v
tp = type(default_value) tp = type(default_value)
# 对bool特殊处理
if tp is bool and isinstance(v, str):
if v in ("true", "True", "1"):
return True
else:
return False
if tp in [list, dict] and isinstance(v, str):
try:
v = json.loads(v)
return v
except json.JSONDecodeError:
return v
try: try:
v = tp(v) v = tp(v)
except Exception: except Exception:
@ -289,14 +302,10 @@ class Config(dict):
except KeyError: except KeyError:
value = None value = None
if value is not None: if value is not None:
return self.convert_type(item, value) return value
# 其次从环境变量来 # 其次从环境变量来
value = os.environ.get(item, None) value = os.environ.get(item, None)
if value is not None: if value is not None:
if value.lower() == 'false':
value = False
elif value.lower() == 'true':
value = True
return self.convert_type(item, value) return self.convert_type(item, value)
return self.defaults.get(item) return self.defaults.get(item)
@ -343,6 +352,7 @@ defaults = {
'TERMINAL_SESSION_KEEP_DURATION': 9999, 'TERMINAL_SESSION_KEEP_DURATION': 9999,
'TERMINAL_HOST_KEY': '', 'TERMINAL_HOST_KEY': '',
'TERMINAL_TELNET_REGEX': '', 'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {},
'SECURITY_MFA_AUTH': False, 'SECURITY_MFA_AUTH': False,
'SECURITY_LOGIN_LIMIT_COUNT': 7, 'SECURITY_LOGIN_LIMIT_COUNT': 7,
'SECURITY_LOGIN_LIMIT_TIME': 30, 'SECURITY_LOGIN_LIMIT_TIME': 30,
@ -361,6 +371,7 @@ defaults = {
'HTTP_LISTEN_PORT': 8080, 'HTTP_LISTEN_PORT': 8080,
'LOGIN_LOG_KEEP_DAYS': 90, 'LOGIN_LOG_KEEP_DAYS': 90,
'ASSETS_PERM_CACHE_TIME': 3600, 'ASSETS_PERM_CACHE_TIME': 3600,
} }

View File

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
VERSION = '1.4.10' VERSION = '1.5.0'

View File

@ -67,6 +67,7 @@ INSTALLED_APPS = [
'terminal.apps.TerminalConfig', 'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig', 'audits.apps.AuditsConfig',
'authentication.apps.AuthenticationConfig', # authentication 'authentication.apps.AuthenticationConfig', # authentication
'applications.apps.ApplicationsConfig',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'drf_yasg', 'drf_yasg',
@ -172,7 +173,7 @@ DATABASES = {
'OPTIONS': DB_OPTIONS 'OPTIONS': DB_OPTIONS
} }
} }
DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'ca.pem') DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'certs', 'db_ca.pem')
if CONFIG.DB_ENGINE.lower() == 'mysql': if CONFIG.DB_ENGINE.lower() == 'mysql':
DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'" DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'"
if os.path.isfile(DB_CA_PATH): if os.path.isfile(DB_CA_PATH):
@ -356,12 +357,30 @@ EMAIL_USE_SSL = False
EMAIL_USE_TLS = False EMAIL_USE_TLS = False
EMAIL_SUBJECT_PREFIX = '[JMS] ' EMAIL_SUBJECT_PREFIX = '[JMS] '
#Email custom content
EMAIL_CUSTOM_USER_CREATED_SUBJECT = ''
EMAIL_CUSTOM_USER_CREATED_HONORIFIC = ''
EMAIL_CUSTOM_USER_CREATED_BODY = ''
EMAIL_CUSTOM_USER_CREATED_SIGNATURE = ''
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'common.permissions.IsOrgAdmin', 'common.permissions.IsOrgAdmin',
), ),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
'common.renders.JMSCSVRender',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
'common.parsers.JMSCSVParser',
'rest_framework.parsers.FileUploadParser',
),
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication', # 'rest_framework.authentication.BasicAuthentication',
'authentication.backends.api.AccessKeyAuthentication', 'authentication.backends.api.AccessKeyAuthentication',
@ -374,6 +393,7 @@ REST_FRAMEWORK = {
'rest_framework.filters.SearchFilter', 'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter', 'rest_framework.filters.OrderingFilter',
), ),
'DEFAULT_METADATA_CLASS': 'common.drfmetadata.SimpleMetadataWithFilters',
'ORDERING_PARAM': "order", 'ORDERING_PARAM': "order",
'SEARCH_PARAM': "search", 'SEARCH_PARAM': "search",
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
@ -406,6 +426,12 @@ AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"} AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
}
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
if os.path.isfile(LDAP_CERT_FILE):
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch( # AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
@ -507,12 +533,7 @@ DEFAULT_TERMINAL_COMMAND_STORAGE = {
}, },
} }
TERMINAL_COMMAND_STORAGE = { TERMINAL_COMMAND_STORAGE = CONFIG.TERMINAL_COMMAND_STORAGE
# 'ali-es': {
# 'TYPE': 'elasticsearch',
# 'HOSTS': ['http://elastic:changeme@localhost:9200'],
# },
}
DEFAULT_TERMINAL_REPLAY_STORAGE = { DEFAULT_TERMINAL_REPLAY_STORAGE = {
"default": { "default": {

View File

@ -20,6 +20,8 @@ api_v1 = [
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')), path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')), path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
path('applications/v1/', include('applications.urls.api_urls', namespace='api-applications')),
] ]
api_v2 = [ api_v2 = [
@ -37,6 +39,7 @@ app_view_patterns = [
path('audits/', include('audits.urls.view_urls', namespace='audits')), path('audits/', include('audits.urls.view_urls', namespace='audits')),
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('applications/', include('applications.urls.views_urls', namespace='applications')),
] ]

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-21 19:14+0800\n" "POT-Creation-Date: 2019-05-27 15:53+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,58 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:168 #: static/js/jumpserver.js:249
msgid "Update is successful!" msgid "Update is successful!"
msgstr "更新成功" msgstr "更新成功"
#: static/js/jumpserver.js:170 #: static/js/jumpserver.js:251
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误" msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:236 static/js/jumpserver.js:273 #: static/js/jumpserver.js:315 static/js/jumpserver.js:352
#: static/js/jumpserver.js:276 #: static/js/jumpserver.js:355
msgid "Error" msgid "Error"
msgstr "错误" msgstr "错误"
#: static/js/jumpserver.js:236 #: static/js/jumpserver.js:315
msgid "Being used by the asset, please unbind the asset first." msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被资产使用中,请先解除资产绑定" msgstr "正在被资产使用中,请先解除资产绑定"
#: static/js/jumpserver.js:242 static/js/jumpserver.js:283 #: static/js/jumpserver.js:321 static/js/jumpserver.js:362
msgid "Delete the success" msgid "Delete the success"
msgstr "删除成功" msgstr "删除成功"
#: static/js/jumpserver.js:248 #: static/js/jumpserver.js:327
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?" msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:252 static/js/jumpserver.js:293 #: static/js/jumpserver.js:331 static/js/jumpserver.js:372
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: static/js/jumpserver.js:254 static/js/jumpserver.js:295 #: static/js/jumpserver.js:333 static/js/jumpserver.js:374
msgid "Confirm" msgid "Confirm"
msgstr "确认" msgstr "确认"
#: static/js/jumpserver.js:273 #: static/js/jumpserver.js:352
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "组织中包含未删除信息,请删除后重试" msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:276 #: static/js/jumpserver.js:355
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:289 #: static/js/jumpserver.js:368
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "请确保组织内的以下信息已删除" msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:290 #: static/js/jumpserver.js:369
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
@ -76,52 +76,76 @@ msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权" "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"规则" "规则"
#: static/js/jumpserver.js:329 #: static/js/jumpserver.js:408
msgid "Loading ..." msgid "Loading ..."
msgstr "加载中 ..." msgstr "加载中 ..."
#: static/js/jumpserver.js:330 #: static/js/jumpserver.js:409
msgid "Search" msgid "Search"
msgstr "搜索" msgstr "搜索"
#: static/js/jumpserver.js:333 #: static/js/jumpserver.js:412
#, javascript-format #, javascript-format
msgid "Selected item %d" msgid "Selected item %d"
msgstr "选中 %d 项" msgstr "选中 %d 项"
#: static/js/jumpserver.js:337 #: static/js/jumpserver.js:416
msgid "Per page _MENU_" msgid "Per page _MENU_"
msgstr "每页 _MENU_" msgstr "每页 _MENU_"
#: static/js/jumpserver.js:338 #: static/js/jumpserver.js:417
msgid "" msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:341 #: static/js/jumpserver.js:420
msgid "No match" msgid "No match"
msgstr "没有匹配项" msgstr "没有匹配项"
#: static/js/jumpserver.js:342 #: static/js/jumpserver.js:421
msgid "No record" msgid "No record"
msgstr "没有记录" msgstr "没有记录"
#: static/js/jumpserver.js:701 #: static/js/jumpserver.js:563
msgid "Unknown error occur"
msgstr ""
#: static/js/jumpserver.js:800
msgid "Password minimum length {N} bits" msgid "Password minimum length {N} bits"
msgstr "密码最小长度 {N} 位" msgstr "密码最小长度 {N} 位"
#: static/js/jumpserver.js:702 #: static/js/jumpserver.js:801
msgid "Must contain capital letters" msgid "Must contain capital letters"
msgstr "必须包含大写字母" msgstr "必须包含大写字母"
#: static/js/jumpserver.js:703 #: static/js/jumpserver.js:802
msgid "Must contain lowercase letters" msgid "Must contain lowercase letters"
msgstr "必须包含小写字母" msgstr "必须包含小写字母"
#: static/js/jumpserver.js:704 #: static/js/jumpserver.js:803
msgid "Must contain numeric characters" msgid "Must contain numeric characters"
msgstr "必须包含数字字符" msgstr "必须包含数字字符"
#: static/js/jumpserver.js:705 #: static/js/jumpserver.js:804
msgid "Must contain special characters" msgid "Must contain special characters"
msgstr "必须包含特殊字符" msgstr "必须包含特殊字符"
#: static/js/jumpserver.js:976
msgid "Export failed"
msgstr "导出失败"
#: static/js/jumpserver.js:993
msgid "Import Success"
msgstr "导入成功"
#: static/js/jumpserver.js:998
msgid "Update Success"
msgstr "更新成功"
#: static/js/jumpserver.js:1028
msgid "Import failed"
msgstr "导入失败"
#: static/js/jumpserver.js:1033
msgid "Update failed"
msgstr "更新失败"

View File

@ -124,6 +124,9 @@ class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
def display_ok_hosts(self): def display_ok_hosts(self):
pass pass
def display_failed_stderr(self):
pass
class CommandResultCallback(AdHocResultCallback): class CommandResultCallback(AdHocResultCallback):
""" """

View File

@ -1,8 +1,11 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import os import os
import shutil
from collections import namedtuple from collections import namedtuple
from ansible import context
from ansible.module_utils.common.collections import ImmutableDict
from ansible.executor.task_queue_manager import TaskQueueManager from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.vars.manager import VariableManager from ansible.vars.manager import VariableManager
from ansible.parsing.dataloader import DataLoader from ansible.parsing.dataloader import DataLoader
@ -33,29 +36,18 @@ Options = namedtuple('Options', [
def get_default_options(): def get_default_options():
options = Options( options = dict(
listtags=False,
listtasks=False,
listhosts=False,
syntax=False, syntax=False,
timeout=30, timeout=30,
connection='ssh', connection='ssh',
module_path='',
forks=10, forks=10,
remote_user='root', remote_user='root',
private_key_file=None, private_key_file=None,
ssh_common_args="",
ssh_extra_args="",
sftp_extra_args="",
scp_extra_args="",
become=None, become=None,
become_method=None, become_method=None,
become_user=None, become_user=None,
verbosity=None, verbosity=1,
extra_vars=[],
check=False, check=False,
playbook_path='/etc/ansible/',
passwords=None,
diff=False, diff=False,
gathering='implicit', gathering='implicit',
remote_tmp='/tmp/.ansible' remote_tmp='/tmp/.ansible'
@ -108,9 +100,9 @@ class PlayBookRunner:
inventory=self.inventory, inventory=self.inventory,
variable_manager=self.variable_manager, variable_manager=self.variable_manager,
loader=self.loader, loader=self.loader,
options=self.options, passwords={"conn_pass": self.passwords}
passwords=self.passwords
) )
context.CLIARGS = ImmutableDict(self.options)
if executor._tqm: if executor._tqm:
executor._tqm._stdout_callback = self.results_callback executor._tqm._stdout_callback = self.results_callback
@ -185,11 +177,10 @@ class AdHocRunner:
return cleaned_tasks return cleaned_tasks
def update_options(self, options): def update_options(self, options):
_options = {k: v for k, v in self.default_options.items()}
if options and isinstance(options, dict): if options and isinstance(options, dict):
options = self.__class__.default_options._replace(**options) _options.update(options)
else: return _options
options = self.__class__.default_options
return options
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'): def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
""" """
@ -202,6 +193,7 @@ class AdHocRunner:
self.check_pattern(pattern) self.check_pattern(pattern)
self.results_callback = self.get_result_callback() self.results_callback = self.get_result_callback()
cleaned_tasks = self.clean_tasks(tasks) cleaned_tasks = self.clean_tasks(tasks)
context.CLIARGS = ImmutableDict(self.options)
play_source = dict( play_source = dict(
name=play_name, name=play_name,
@ -220,9 +212,8 @@ class AdHocRunner:
inventory=self.inventory, inventory=self.inventory,
variable_manager=self.variable_manager, variable_manager=self.variable_manager,
loader=self.loader, loader=self.loader,
options=self.options,
stdout_callback=self.results_callback, stdout_callback=self.results_callback,
passwords=self.options.passwords, passwords={"conn_pass": self.options.get("password", "")}
) )
try: try:
tqm.run(play) tqm.run(play)
@ -230,8 +221,9 @@ class AdHocRunner:
except Exception as e: except Exception as e:
raise AnsibleError(e) raise AnsibleError(e)
finally: finally:
if tqm is not None:
tqm.cleanup() tqm.cleanup()
self.loader.cleanup_all_tmp_files() shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
class CommandRunner(AdHocRunner): class CommandRunner(AdHocRunner):

View File

@ -15,7 +15,7 @@ class TestAdHocRunner(unittest.TestCase):
host_data = [ host_data = [
{ {
"hostname": "testserver", "hostname": "testserver",
"ip": "192.168.244.168", "ip": "192.168.244.185",
"port": 22, "port": 22,
"username": "root", "username": "root",
"password": "redhat", "password": "redhat",

View File

@ -35,7 +35,6 @@ class JMSBaseInventory(BaseInventory):
info["vars"].update({ info["vars"].update({
label.name: label.value label.name: label.value
}) })
info["groups"].append("{}:{}".format(label.name, label.value))
if asset.domain: if asset.domain:
info["vars"].update({ info["vars"].update({
"domain": asset.domain.name, "domain": asset.domain.name,

View File

@ -8,9 +8,12 @@ from django.shortcuts import redirect, get_object_or_404
from django.forms import ModelForm from django.forms import ModelForm
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from rest_framework import serializers
from common.utils import get_logger from common.utils import get_logger
from .utils import current_org, set_current_org, set_to_root_org from .utils import (
current_org, set_current_org, set_to_root_org, get_current_org_id
)
from .models import Organization from .models import Organization
logger = get_logger(__file__) logger = get_logger(__file__)
@ -18,7 +21,8 @@ tl = Local()
__all__ = [ __all__ = [
'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm',
'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin' 'RootOrgViewMixin', 'OrgMembershipSerializerMixin',
'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin',
] ]
@ -202,3 +206,11 @@ class OrgMembershipModelViewSetMixin:
def get_queryset(self): def get_queryset(self):
queryset = self.membership_class.objects.filter(organization=self.org) queryset = self.membership_class.objects.filter(organization=self.org)
return queryset return queryset
class OrgResourceSerializerMixin(serializers.Serializer):
"""
通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id
(同时为serializer.is_valid()对Model的unique_together校验做准备)
"""
org_id = serializers.HiddenField(default=get_current_org_id)

View File

@ -38,4 +38,10 @@ def get_current_org():
return _find('current_org') return _find('current_org')
def get_current_org_id():
org = get_current_org()
org_id = str(org.id) if org.is_real() else ''
return org_id
current_org = LocalProxy(partial(_find, 'current_org')) current_org = LocalProxy(partial(_find, 'current_org'))

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .permission import * from .asset_permission import *
from .user_permission import * from .user_permission import *
from .user_group_permission import * from .user_group_permission import *
from .remote_app_permission import *

View File

@ -0,0 +1,101 @@
# coding: utf-8
#
from rest_framework import viewsets, generics
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import Response
from common.permissions import IsOrgAdmin
from ..models import RemoteAppPermission
from ..serializers import (
RemoteAppPermissionSerializer,
RemoteAppPermissionUpdateUserSerializer,
RemoteAppPermissionUpdateRemoteAppSerializer,
)
__all__ = [
'RemoteAppPermissionViewSet',
'RemoteAppPermissionAddUserApi', 'RemoteAppPermissionAddRemoteAppApi',
'RemoteAppPermissionRemoveUserApi', 'RemoteAppPermissionRemoveRemoteAppApi',
]
class RemoteAppPermissionViewSet(viewsets.ModelViewSet):
filter_fields = ('name', )
search_fields = filter_fields
queryset = RemoteAppPermission.objects.all()
serializer_class = RemoteAppPermissionSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdmin,)
class RemoteAppPermissionAddUserApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppPermissionUpdateUserSerializer
queryset = RemoteAppPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.add(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class RemoteAppPermissionRemoveUserApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppPermissionUpdateUserSerializer
queryset = RemoteAppPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
users = serializer.validated_data.get('users')
if users:
perm.users.remove(*tuple(users))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class RemoteAppPermissionAddRemoteAppApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppPermissionUpdateRemoteAppSerializer
queryset = RemoteAppPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
remote_apps = serializer.validated_data.get('remote_apps')
if remote_apps:
perm.remote_apps.add(*tuple(remote_apps))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class RemoteAppPermissionRemoveRemoteAppApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppPermissionUpdateRemoteAppSerializer
queryset = RemoteAppPermission.objects.all()
def update(self, request, *args, **kwargs):
perm = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
remote_apps = serializer.validated_data.get('remote_apps')
if remote_apps:
perm.remote_apps.remove(*tuple(remote_apps))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})

View File

@ -10,10 +10,12 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
from orgs.utils import set_to_root_org from orgs.utils import set_to_root_org
from ..utils import ( from ..utils import (
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node,
RemoteAppPermissionUtil,
) )
from ..hands import ( from ..hands import (
AssetGrantedSerializer, UserGroup, Node, NodeSerializer AssetGrantedSerializer, UserGroup, Node, NodeSerializer,
RemoteAppSerializer,
) )
from .. import serializers from .. import serializers
@ -22,6 +24,7 @@ __all__ = [
'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi',
'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'UserGroupGrantedNodesWithAssetsAsTreeApi', 'UserGroupGrantedNodesWithAssetsAsTreeApi',
'UserGroupGrantedRemoteAppsApi',
] ]
@ -138,3 +141,20 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
for asset, system_users in assets.items(): for asset, system_users in assets.items():
asset.system_users_granted = system_users asset.system_users_granted = system_users
return assets return assets
# RemoteApp permission
class UserGroupGrantedRemoteAppsApi(ListAPIView):
permission_classes = (IsOrgAdmin, )
serializer_class = RemoteAppSerializer
def get_queryset(self):
queryset = []
user_group_id = self.kwargs.get('pk')
if not user_group_id:
return queryset
user_group = get_object_or_404(UserGroup, id=user_group_id)
util = RemoteAppPermissionUtil(user_group)
queryset = util.get_remote_apps()
return queryset

View File

@ -17,14 +17,15 @@ from common.utils import get_logger
from orgs.utils import set_to_root_org from orgs.utils import set_to_root_org
from ..utils import ( from ..utils import (
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node, AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node,
check_system_user_action check_system_user_action, RemoteAppPermissionUtil,
construct_remote_apps_tree_root, parse_remote_app_to_tree_node,
) )
from ..hands import ( from ..hands import (
AssetGrantedSerializer, User, Asset, Node, User, Asset, Node, SystemUser, RemoteApp, AssetGrantedSerializer,
SystemUser, NodeSerializer NodeSerializer, RemoteAppSerializer,
) )
from .. import serializers from .. import serializers
from ..mixins import AssetsFilterMixin from ..mixins import AssetsFilterMixin, RemoteAppFilterMixin
from ..models import Action from ..models import Action
logger = get_logger(__name__) logger = get_logger(__name__)
@ -34,6 +35,8 @@ __all__ = [
'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi', 'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi',
'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi', 'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi',
'UserGrantedNodesWithAssetsAsTreeApi', 'GetUserAssetPermissionActionsApi', 'UserGrantedNodesWithAssetsAsTreeApi', 'GetUserAssetPermissionActionsApi',
'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi',
'UserGrantedRemoteAppsAsTreeApi',
] ]
@ -447,3 +450,79 @@ class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, APIView):
actions = [action.name for action in getattr(_su, 'actions', [])] actions = [action.name for action in getattr(_su, 'actions', [])]
return Response({'actions': actions}, status=200) return Response({'actions': actions}, status=200)
# RemoteApp permission
class UserGrantedRemoteAppsApi(RemoteAppFilterMixin, ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = RemoteAppSerializer
pagination_class = LimitOffsetPagination
def get_object(self):
user_id = self.kwargs.get('pk', '')
if user_id:
user = get_object_or_404(User, id=user_id)
else:
user = self.request.user
return user
def get_queryset(self):
util = RemoteAppPermissionUtil(self.get_object())
queryset = util.get_remote_apps()
queryset = list(queryset)
return queryset
def get_permissions(self):
if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
class UserGrantedRemoteAppsAsTreeApi(ListAPIView):
serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,)
def get_object(self):
user_id = self.kwargs.get('pk', '')
if not user_id:
user = self.request.user
else:
user = get_object_or_404(User, id=user_id)
return user
def get_queryset(self):
queryset = []
tree_root = construct_remote_apps_tree_root()
queryset.append(tree_root)
util = RemoteAppPermissionUtil(self.get_object())
remote_apps = util.get_remote_apps()
for remote_app in remote_apps:
node = parse_remote_app_to_tree_node(tree_root, remote_app)
queryset.append(node)
queryset = sorted(queryset)
return queryset
def get_permissions(self):
if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
class ValidateUserRemoteAppPermissionApi(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
user_id = request.query_params.get('user_id', '')
remote_app_id = request.query_params.get('remote_app_id', '')
user = get_object_or_404(User, id=user_id)
remote_app = get_object_or_404(RemoteApp, id=remote_app_id)
util = RemoteAppPermissionUtil(user)
remote_apps = util.get_remote_apps()
if remote_app not in remote_apps:
return Response({'msg': False}, status=403)
return Response({'msg': True}, status=200)

View File

@ -0,0 +1,5 @@
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *

View File

@ -6,9 +6,13 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelForm from orgs.mixins import OrgModelForm
from orgs.utils import current_org from orgs.utils import current_org
from .models import AssetPermission from perms.models import AssetPermission
from assets.models import Asset from assets.models import Asset
__all__ = [
'AssetPermissionForm',
]
class AssetPermissionForm(OrgModelForm): class AssetPermissionForm(OrgModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -0,0 +1,49 @@
# coding: utf-8
#
from django.utils.translation import ugettext as _
from django import forms
from orgs.mixins import OrgModelForm
from orgs.utils import current_org
from ..models import RemoteAppPermission
__all__ = [
'RemoteAppPermissionCreateUpdateForm',
]
class RemoteAppPermissionCreateUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
users_field = self.fields.get('users')
if hasattr(users_field, 'queryset'):
users_field.queryset = current_org.get_org_users()
class Meta:
model = RemoteAppPermission
exclude = (
'id', 'date_created', 'created_by', 'org_id'
)
widgets = {
'users': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('User')}
),
'user_groups': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('User group')}
),
'remote_apps': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('RemoteApp')}
)
}
def clean_user_groups(self):
users = self.cleaned_data.get('users')
user_groups = self.cleaned_data.get('user_groups')
if not users and not user_groups:
raise forms.ValidationError(
_("User or group at least one required")
)
return self.cleaned_data['user_groups']

View File

@ -3,8 +3,11 @@
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, SystemUser, Node from assets.models import Asset, SystemUser, Node, RemoteApp
from assets.serializers import AssetGrantedSerializer, NodeSerializer from assets.serializers import (
AssetGrantedSerializer, NodeSerializer
)
from applications.serializers import RemoteAppSerializer

View File

@ -0,0 +1,55 @@
# Generated by Django 2.1.7 on 2019-05-21 08:19
import common.utils.django
from django.conf import settings
from django.db import migrations, models
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0019_auto_20190304_1459'),
('applications', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('perms', '0004_assetpermission_actions'),
]
operations = [
migrations.CreateModel(
name='RemoteAppPermission',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('date_start', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start')),
('date_expired', models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired')),
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('remote_apps', models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='applications.RemoteApp', verbose_name='RemoteApp')),
('user_groups', models.ManyToManyField(blank=True, to='users.UserGroup', verbose_name='User group')),
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'RemoteApp permission',
'ordering': ('name',),
},
),
migrations.AlterField(
model_name='assetpermission',
name='user_groups',
field=models.ManyToManyField(blank=True, to='users.UserGroup', verbose_name='User group'),
),
migrations.AlterField(
model_name='assetpermission',
name='users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterUniqueTogether(
name='remoteapppermission',
unique_together={('org_id', 'name')},
),
]

View File

@ -2,6 +2,11 @@
# #
__all__ = [
'AssetsFilterMixin', 'RemoteAppFilterMixin',
]
class AssetsFilterMixin(object): class AssetsFilterMixin(object):
""" """
对资产进行过滤(查询排序) 对资产进行过滤(查询排序)
@ -34,3 +39,38 @@ class AssetsFilterMixin(object):
queryset = sort_assets(queryset, order_by=order_by, reverse=reverse) queryset = sort_assets(queryset, order_by=order_by, reverse=reverse)
return queryset return queryset
class RemoteAppFilterMixin(object):
"""
对RemoteApp进行过滤(查询排序)
"""
def filter_queryset(self, queryset):
queryset = self.search_remote_apps(queryset)
queryset = self.sort_remote_apps(queryset)
return queryset
def search_remote_apps(self, queryset):
value = self.request.query_params.get('search')
if not value:
return queryset
queryset = [
remote_app for remote_app in queryset if value in remote_app.name
]
return queryset
def sort_remote_apps(self, queryset):
order_by = self.request.query_params.get('order')
if not order_by:
order_by = 'name'
if order_by.startswith('-'):
order_by = order_by.lstrip('-')
reverse = True
else:
reverse = False
queryset = sorted(
queryset, key=lambda x: getattr(x, order_by), reverse=reverse
)
return queryset

View File

@ -0,0 +1,5 @@
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *

View File

@ -2,12 +2,17 @@ import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from common.utils import date_expired_default, set_or_append_attr_bulk from common.utils import date_expired_default, set_or_append_attr_bulk
from orgs.mixins import OrgModelMixin, OrgManager from orgs.mixins import OrgModelMixin
from .const import PERMS_ACTION_NAME_CHOICES, PERMS_ACTION_NAME_ALL from ..const import PERMS_ACTION_NAME_CHOICES, PERMS_ACTION_NAME_ALL
from .base import BasePermission
__all__ = [
'Action', 'AssetPermission', 'NodePermission',
]
class Action(models.Model): class Action(models.Model):
@ -28,69 +33,16 @@ class Action(models.Model):
return cls.objects.get(name=PERMS_ACTION_NAME_ALL) return cls.objects.get(name=PERMS_ACTION_NAME_ALL)
class AssetPermissionQuerySet(models.QuerySet): class AssetPermission(BasePermission):
def active(self):
return self.filter(is_active=True)
def valid(self):
return self.active().filter(date_start__lt=timezone.now())\
.filter(date_expired__gt=timezone.now())
class AssetPermissionManager(OrgManager):
def valid(self):
return self.get_queryset().valid()
class AssetPermission(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
users = models.ManyToManyField('users.User', related_name='asset_permissions', blank=True, verbose_name=_("User"))
user_groups = models.ManyToManyField('users.UserGroup', related_name='asset_permissions', blank=True, verbose_name=_("User group"))
assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset"))
nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes"))
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user")) system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user"))
actions = models.ManyToManyField('Action', related_name='permissions', blank=True, verbose_name=_('Action')) actions = models.ManyToManyField('Action', related_name='permissions', blank=True, verbose_name=_('Action'))
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))
created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
comment = models.TextField(verbose_name=_('Comment'), blank=True)
objects = AssetPermissionManager.from_queryset(AssetPermissionQuerySet)()
class Meta: class Meta:
unique_together = [('org_id', 'name')] unique_together = [('org_id', 'name')]
verbose_name = _("Asset permission") verbose_name = _("Asset permission")
def __str__(self):
return self.name
@property
def id_str(self):
return str(self.id)
@property
def is_expired(self):
if self.date_expired > timezone.now() > self.date_start:
return False
return True
@property
def is_valid(self):
if not self.is_expired and self.is_active:
return True
return False
def get_all_users(self):
users = set(self.users.all())
for group in self.user_groups.all():
_users = group.users.all()
set_or_append_attr_bulk(_users, 'inherit', group.name)
users.update(set(_users))
return users
def get_all_assets(self): def get_all_assets(self):
assets = set(self.assets.all()) assets = set(self.assets.all())
for node in self.nodes.all(): for node in self.nodes.all():

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