Unverified Commit d46f5858 authored by BaiJiangJie's avatar BaiJiangJie Committed by GitHub

Merge pull request #2737 from jumpserver/dev

Dev
parents 74ae7d13 a8491eaf
...@@ -6,7 +6,8 @@ RUN useradd jumpserver ...@@ -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
......
from django.contrib import admin
# Register your models here.
from .remote_app import *
# 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
from __future__ import unicode_literals
from django.apps import AppConfig
class ApplicationsConfig(AppConfig):
name = 'applications'
# 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
}
from .remote_app import *
# 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
"""
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
# 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')},
),
]
# 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
}
# 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
{% 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 %}
\ No newline at end of file
{% 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 %}
\ No newline at end of file
{% 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 %}
\ No newline at end of file
{% 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 %}
from django.test import TestCase
# Create your tests here.
# coding: utf-8
#
__all__ = [
]
# 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
# 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')
]
from .remote_app import *
# 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)
...@@ -20,7 +20,7 @@ from rest_framework.response import Response ...@@ -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__ = [ ...@@ -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
""" """
......
...@@ -16,8 +16,9 @@ from django.urls import reverse_lazy ...@@ -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__ = [ ...@@ -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): ...@@ -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): ...@@ -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
""" """
......
...@@ -39,6 +39,8 @@ __all__ = [ ...@@ -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
......
...@@ -21,6 +21,7 @@ from rest_framework.pagination import LimitOffsetPagination ...@@ -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__ = [ ...@@ -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
""" """
......
...@@ -11,7 +11,7 @@ class AuthBookBackend(BaseBackend): ...@@ -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)
......
...@@ -27,7 +27,7 @@ class AdminUserBackend(BaseBackend): ...@@ -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)
......
...@@ -30,7 +30,7 @@ class SystemUserBackend(BaseBackend): ...@@ -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
......
# -*- 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 = { ...@@ -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_{}'
# 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'),
),
]
...@@ -8,3 +8,4 @@ from .asset import * ...@@ -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 *
...@@ -71,7 +71,7 @@ class Asset(OrgModelMixin): ...@@ -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'))
......
...@@ -78,7 +78,7 @@ class AuthBook(AssetUser): ...@@ -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
......
# -*- 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): ...@@ -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)
......
...@@ -2,6 +2,9 @@ ...@@ -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__ = [ ...@@ -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):
......
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): ...@@ -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)
......
{% 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 %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update admin user" %}{% endblock %}
\ No newline at end of file
{% 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 %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update assets" %}{% endblock %}
{% 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 %}
{% 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 %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update system user" %}{% endblock %}
\ No newline at end of file
...@@ -85,6 +85,7 @@ ...@@ -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() { ...@@ -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 () { ...@@ -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 %}
{% 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 @@ ...@@ -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 @@ ...@@ -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(){ ...@@ -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(){ ...@@ -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 %}
...@@ -2,10 +2,6 @@ ...@@ -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 @@ ...@@ -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() { ...@@ -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);
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);
btn += view_btn;
{% if asset.protocol == 'ssh' %} {% if asset.protocol == 'ssh' %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData); btn += test_btn;
$(td).html(test_btn + update_auth_btn);
{% else %}
$(td).html(update_auth_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; ...@@ -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 () { ...@@ -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 %}
\ No newline at end of file
...@@ -67,14 +67,26 @@ ...@@ -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 @@ ...@@ -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(){ ...@@ -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_asset_import', function () { .on('click', '#btn_import_confirm', function () {
var $form = $('#fm_asset_import'); var file = document.getElementById('id_file').files[0];
var action = $form.attr("action"); if(!file){
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
}
var url = "{% url 'api-assets:asset-list' %}";
if (current_node_id){
url = setUrlParam(url, 'node_id', current_node_id);
} }
$form.ajaxSubmit({success: success}); 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(){ ...@@ -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(){ ...@@ -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(){ ...@@ -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,
...@@ -636,68 +681,72 @@ $(document).ready(function(){ ...@@ -636,68 +681,72 @@ $(document).ready(function(){
confirmButtonColor: "#DD6B55", confirmButtonColor: "#DD6B55",
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',
success: success, body:JSON.stringify(data),
error: fail success:success,
}); 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
}) })
} }
function doRemove() { function doRemove() {
var nodes = zTree.getSelectedNodes(); var nodes = zTree.getSelectedNodes();
if (!current_node_id) { if (!current_node_id) {
return return
} }
var data = { var data = {
'assets': id_list 'assets': id_list
}; };
var success = function () { var success = function () {
asset_table.ajax.reload() asset_table.ajax.reload()
}; };
APIUpdateAttr({ APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/', 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'method': 'PUT', 'method': 'PUT',
'body': JSON.stringify(data), 'body': JSON.stringify(data),
'success': success 'success': success
}) })
} }
switch(action) { switch(action) {
case 'deactive': case 'deactive':
doDeactive(); doDeactive();
......
...@@ -133,6 +133,7 @@ ...@@ -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>
...@@ -160,12 +161,13 @@ function initAssetsTable() { ...@@ -160,12 +161,13 @@ function initAssetsTable() {
{targets: 4, createdCell: function (td, cellData, rowData) { {targets: 4, createdCell: function (td, cellData, rowData) {
var push_btn = ''; var push_btn = '';
{% if system_user.auto_push %} {% if system_user.auto_push %}
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 () { ...@@ -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 %}
...@@ -14,6 +14,28 @@ ...@@ -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 @@ ...@@ -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() { ...@@ -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(){ ...@@ -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 %}
......
...@@ -27,7 +27,9 @@ from django.contrib.messages.views import SuccessMessageMixin ...@@ -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): ...@@ -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:
......
...@@ -2,6 +2,7 @@ ...@@ -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 _ ...@@ -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 ( ...@@ -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): ...@@ -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)
...@@ -7,6 +7,8 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist ...@@ -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): ...@@ -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):
setattr(self._user, 'email', email) 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)
...@@ -23,15 +23,12 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): ...@@ -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
......
...@@ -14,3 +14,7 @@ class AccessKeySerializer(serializers.ModelSerializer): ...@@ -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)
...@@ -16,5 +16,6 @@ urlpatterns = [ ...@@ -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'),
] ]
...@@ -3,10 +3,18 @@ ...@@ -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): ...@@ -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})
...@@ -7,3 +7,4 @@ create_success_msg = _("%(name)s was created successfully") ...@@ -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_{}"
# -*- 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
...@@ -11,7 +11,7 @@ __all__ = [ ...@@ -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): ...@@ -129,4 +129,7 @@ class EncryptCharField(EncryptMixin, models.CharField):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
pass
...@@ -3,12 +3,15 @@ ...@@ -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): ...@@ -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): ...@@ -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
self.child.instance = self.instance.get(id=item['id']) if self.instance else None if 'id' in item.keys():
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)
......
from .csv import *
\ No newline at end of file
# ~*~ 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!')
# -*- 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
......
from .csv import *
\ No newline at end of file
# ~*~ 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
# -*- 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'),
]
...@@ -144,6 +144,7 @@ def is_uuid(seq): ...@@ -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:
......
...@@ -194,7 +194,7 @@ class Config(dict): ...@@ -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): ...@@ -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): ...@@ -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 = { ...@@ -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 = { ...@@ -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,
} }
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
VERSION = '1.4.10' VERSION = '1.5.0'
...@@ -67,6 +67,7 @@ INSTALLED_APPS = [ ...@@ -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 = { ...@@ -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 ...@@ -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 = { ...@@ -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' ...@@ -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 = { ...@@ -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": {
......
...@@ -20,6 +20,8 @@ api_v1 = [ ...@@ -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 = [ ...@@ -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')),
] ]
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -8,7 +8,7 @@ msgid "" ...@@ -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 "" ...@@ -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 "" ...@@ -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 "更新失败"
...@@ -124,6 +124,9 @@ class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule): ...@@ -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):
""" """
......
# ~*~ 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', [ ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -230,8 +221,9 @@ class AdHocRunner:
except Exception as e: except Exception as e:
raise AnsibleError(e) raise AnsibleError(e)
finally: finally:
tqm.cleanup() if tqm is not None:
self.loader.cleanup_all_tmp_files() tqm.cleanup()
shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
class CommandRunner(AdHocRunner): class CommandRunner(AdHocRunner):
......
...@@ -15,7 +15,7 @@ class TestAdHocRunner(unittest.TestCase): ...@@ -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",
......
...@@ -35,7 +35,6 @@ class JMSBaseInventory(BaseInventory): ...@@ -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,
......
...@@ -8,9 +8,12 @@ from django.shortcuts import redirect, get_object_or_404 ...@@ -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() ...@@ -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: ...@@ -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)
...@@ -38,4 +38,10 @@ def get_current_org(): ...@@ -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'))
# -*- 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 *
# 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})
...@@ -10,10 +10,12 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser ...@@ -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__ = [ ...@@ -22,6 +24,7 @@ __all__ = [
'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi',
'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'UserGroupGrantedNodesWithAssetsAsTreeApi', 'UserGroupGrantedNodesWithAssetsAsTreeApi',
'UserGroupGrantedRemoteAppsApi',
] ]
...@@ -138,3 +141,20 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): ...@@ -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
...@@ -17,14 +17,15 @@ from common.utils import get_logger ...@@ -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__ = [ ...@@ -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): ...@@ -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)
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *
...@@ -6,9 +6,13 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -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):
......
# 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']
...@@ -3,8 +3,11 @@ ...@@ -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
# 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')},
),
]
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
# #
__all__ = [
'AssetsFilterMixin', 'RemoteAppFilterMixin',
]
class AssetsFilterMixin(object): class AssetsFilterMixin(object):
""" """
对资产进行过滤(查询,排序) 对资产进行过滤(查询,排序)
...@@ -34,3 +39,38 @@ 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
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *
...@@ -2,12 +2,17 @@ import uuid ...@@ -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): ...@@ -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():
......
# coding: utf-8
#
import uuid
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.utils import timezone
from orgs.mixins import OrgModelMixin
from common.utils import date_expired_default, set_or_append_attr_bulk
from orgs.mixins import OrgManager
__all__ = [
'BasePermission',
]
class BasePermissionQuerySet(models.QuerySet):
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 BasePermissionManager(OrgManager):
def valid(self):
return self.get_queryset().valid()
class BasePermission(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"))
user_groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User group"))
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 = BasePermissionManager.from_queryset(BasePermissionQuerySet)()
class Meta:
abstract = True
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
# coding: utf-8
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import BasePermission
__all__ = [
'RemoteAppPermission',
]
class RemoteAppPermission(BasePermission):
remote_apps = models.ManyToManyField('applications.RemoteApp', related_name='granted_by_permissions', blank=True, verbose_name=_("RemoteApp"))
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _('RemoteApp permission')
ordering = ('name',)
def get_all_remote_apps(self):
return set(self.remote_apps.all())
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from rest_framework import serializers from rest_framework import serializers
from common.fields import StringManyToManyField from common.fields import StringManyToManyField
from .models import AssetPermission, Action from perms.models import AssetPermission, Action
from assets.models import Node, Asset, SystemUser from assets.models import Node, Asset, SystemUser
from assets.serializers import AssetGrantedSerializer from assets.serializers import AssetGrantedSerializer
...@@ -13,7 +13,7 @@ __all__ = [ ...@@ -13,7 +13,7 @@ __all__ = [
'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer',
'AssetPermissionNodeSerializer', 'GrantedNodeSerializer', 'AssetPermissionNodeSerializer', 'GrantedNodeSerializer',
'GrantedAssetSerializer', 'GrantedSystemUserSerializer', 'GrantedAssetSerializer', 'GrantedSystemUserSerializer',
'ActionSerializer', 'ActionSerializer', 'NodeGrantedSerializer',
] ]
......
# coding: utf-8
#
from rest_framework import serializers
from ..models import RemoteAppPermission
__all__ = [
'RemoteAppPermissionSerializer',
'RemoteAppPermissionUpdateUserSerializer',
'RemoteAppPermissionUpdateRemoteAppSerializer',
]
class RemoteAppPermissionSerializer(serializers.ModelSerializer):
class Meta:
model = RemoteAppPermission
fields = [
'id', 'name', 'users', 'user_groups', 'remote_apps', 'comment',
'is_active', 'date_start', 'date_expired', 'is_valid',
'created_by', 'date_created', 'org_id'
]
read_only_fields = ['created_by', 'date_created']
class RemoteAppPermissionUpdateUserSerializer(serializers.ModelSerializer):
class Meta:
model = RemoteAppPermission
fields = ['id', 'users']
class RemoteAppPermissionUpdateRemoteAppSerializer(serializers.ModelSerializer):
class Meta:
model = RemoteAppPermission
fields = ['id', 'remote_apps']
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
{% 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="ibox-title">
<h5>{{ action }}</h5>
<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>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<form method="post" class="form-horizontal" action="" >
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %}
<h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.name layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'User' %}</h3>
{% bootstrap_field form.users layout="horizontal" %}
{% bootstrap_field form.user_groups layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'RemoteApp' %}</h3>
{% bootstrap_field form.remote_apps layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Other' %}</h3>
<div class="form-group">
<label for="{{ form.is_active.id_for_label }}" class="col-sm-2 control-label">{% trans 'Active' %}</label>
<div class="col-sm-8">
{{ form.is_active }}
</div>
</div>
<div class="form-group {% if form.date_expired.errors or form.date_start.errors %} has-error {% endif %}" id="date_5">
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{% trans 'Validity period' %}</label>
<div class="col-sm-9">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
{% if form.errors %}
<input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value }}">
{% else %}
<input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value|date:'Y-m-d H:i' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}">
{% endif %}
</div>
<span class="help-block ">{{ form.date_expired.errors }}</span>
<span class="help-block ">{{ form.date_start.errors }}</span>
</div>
</div>
{% bootstrap_field form.comment layout="horizontal" %}
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/moment.min.js" %}'></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/daterangepicker.min.js" %}'></script>
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
<script>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () {
$('.select2').select2({
closeOnSelect: false
});
$('#date_start').daterangepicker(dateOptions);
$('#date_expired').daterangepicker(dateOptions);
})
</script>
{% endblock %}
\ No newline at end of file
{% 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 'perms:remote-app-permission-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li>
<a href="{% url 'perms:remote-app-permission-user-list' pk=object.id %}" class="text-center">
<i class="fa fa-group"></i> {% trans 'Users and user groups' %}
</a>
</li>
<li>
<a href="{% url 'perms:remote-app-permission-remote-app-list' pk=object.id %}" class="text-center">
<i class="fa fa-th"></i> {% trans 'RemoteApp' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'perms:remote-app-permission-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-delete">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-7" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ object.name }}</b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ object.name }}</b></td>
</tr>
<tr>
<td>{% trans 'User count' %}:</td>
<td><b>{{ object.users.count }}</b></td>
</tr>
<tr>
<td>{% trans 'User group count' %}:</td>
<td><b>{{ object.user_groups.count }}</b></td>
</tr>
<tr>
<td>{% trans 'RemoteApp count' %}:</td>
<td><b>{{ object.remote_apps.count }}</b></td>
</tr>
<tr>
<td>{% trans 'Date start' %}:</td>
<td><b>{{ object.date_start }}</b></td>
</tr>
<tr>
<td>{% trans 'Date expired' %}:</td>
<td><b>{{ object.date_expired }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ object.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ object.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td width="50%">{% trans 'Active' %} :</td>
<td><span style="float: right">
<div class="switch">
<div class="onoffswitch">
<input type="checkbox" {% if object.is_active %} checked {% endif %} class="onoffswitch-checkbox" id="is_active">
<label class="onoffswitch-label" for="is_active">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</div>
</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
$('.select2').select2()
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.system_users_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.system_users_selected[data.id]
})
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var name = "{{ object.name }}";
var rid = "{{ object.id }}";
var the_url = '{% url "api-perms:remote-app-permission-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
var redirect_url = "{% url 'perms:remote-app-permission-list' %}";
objectDelete($this, name, the_url, redirect_url);
}).on('click', '#is_active', function () {
var the_url = '{% url "api-perms:remote-app-permission-detail" pk=object.id %}';
var checked = $(this).prop('checked');
var body = {
'is_active': checked
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body)
});
})
</script>
{% endblock %}
\ No newline at end of file
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'perms:remote-app-permission-create' %}" class="btn btn-sm btn-primary"> {% trans "Create permission" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="remote_app_permission_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 'User' %}</th>
<th class="text-center">{% trans 'User group' %}</th>
<th class="text-center">{% trans 'RemoteApp' %}</th>
<th class="text-center">{% trans 'Validity' %}</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_permission_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'perms:remote-app-permission-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
var num = cellData.length;
$(td).html(num);
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var num = cellData.length;
$(td).html(num);
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var num = cellData.length;
$(td).html(num);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "perms:remote-app-permission-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-perms:remote-app-permission-list" %}',
columns: [
{data: "id"},
{data: "name" },
{data: "users", orderable: false},
{data: "user_groups", orderable: false},
{data: "remote_apps", orderable: false},
{data: "is_valid", orderable: false},
{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-perms:remote-app-permission-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'perms:remote-app-permission-detail' pk=remote_app_permission.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li>
<a href="{% url 'perms:remote-app-permission-user-list' pk=remote_app_permission.id %}" class="text-center">
<i class="fa fa-group"></i> {% trans 'Users and user groups' %}
</a>
</li>
<li class="active">
<a href="{% url 'perms:remote-app-permission-remote-app-list' pk=remote_app_permission.id %}" class="text-center">
<i class="fa fa-th"></i> {% trans 'RemoteApp' %}</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-7" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left">{% trans 'RemoteApp list of ' %} <b>{{ remote_app_permission.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 table-hover">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for remote_app in object_list %}
<tr>
<td>{{ remote_app.name }}</td>
<td>{{ remote_app.get_type_display }}</td>
<td>
<button data-aid="{{ remote_app.id }}" class="btn btn-danger btn-xs btn-remove-remote-app" type="button" style="float: right;"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
{% include '_pagination.html' %}
</div>
</div>
</div>
</div>
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Add RemoteApp to this permission' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<form>
<tr class="no-borders-tr">
<td colspan="2">
<select data-placeholder="{% trans 'Select RemoteApp' %}" class="select2" id="remote_app_select2" style="width: 100%" multiple="" tabindex="4">
{% for remote_app in remote_app_remain %}
<option value="{{ remote_app.id }}">{{ remote_app }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr class="no-borders-tr">
<td colspan="2">
<button type="button" class="btn btn-primary btn-sm btn-add-remote-app">{% trans 'Add' %}</button>
</td>
</tr>
</form>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
function addRemoteApps(remote_apps) {
var the_url = "{% url 'api-perms:remote-app-permission-add-remote-app' pk=remote_app_permission.id %}";
var body = {
remote_apps : remote_apps
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
function removeRemoteApps(remote_apps) {
var the_url = "{% url 'api-perms:remote-app-permission-remove-remote-app' pk=remote_app_permission.id %}";
var body = {
remote_apps: remote_apps
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
$(document).ready(function () {
$('.select2').select2();
})
.on('click', '.btn-add-remote-app', function () {
var remote_apps_selected = $("#remote_app_select2 option:selected").map(function () {
return $(this).attr('value');
}).get();
if (remote_apps_selected.length === 0) {
return false;
}
addRemoteApps(remote_apps_selected);
})
.on('click', '.btn-remove-remote-app', function () {
var remote_app_id= $(this).data("aid");
if (remote_app_id === "") {
return
}
var remote_apps = [remote_app_id];
removeRemoteApps(remote_apps)
})
</script>
{% endblock %}
\ No newline at end of file
{% 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>
<a href="{% url 'perms:remote-app-permission-detail' pk=remote_app_permission.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="active">
<a href="{% url 'perms:remote-app-permission-user-list' pk=remote_app_permission.id %}" class="text-center">
<i class="fa fa-group"></i> {% trans 'Users and user groups' %}
</a>
</li>
<li>
<a href="{% url 'perms:remote-app-permission-remote-app-list' pk=remote_app_permission.id %}" class="text-center">
<i class="fa fa-th"></i> {% trans 'RemoteApp' %}</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-7" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left">{% trans 'User list of ' %} <b>{{ remote_app_permission.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 table-hover">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Username' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for user in object_list %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.username }}</td>
<td>
<button class="btn btn-danger btn-xs btn-remove-user {% if user.inherit %} disabled {% endif %}" data-gid="{{ user.id }}" type="button" style="float: right;"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
{% include '_pagination.html' %}
</div>
</div>
</div>
</div>
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Add user to this permission' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<form>
<tr class="no-borders-tr">
<td colspan="2">
<select data-placeholder="{% trans 'Select user' %}" class="select2 user" style="width: 100%" multiple="" tabindex="4">
{% for user in users_remain %}
<option value="{{ user.id }}">{{ user }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr class="no-borders-tr">
<td colspan="2">
<button type="button" class="btn btn-primary btn-sm btn-add-user">{% trans 'Add' %}</button>
</td>
</tr>
</form>
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Add user group to this permission' %}
</div>
<div class="panel-body">
<table class="table group_edit">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select user groups' %}" class="select2 user-group" style="width: 100%" multiple="" tabindex="4">
{% for user_group in user_groups_remain %}
<option value="{{ user_group.id }}" id="opt_{{ user_group.id }}">{{ user_group }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-group">{% trans 'Add' %}</button>
</td>
</tr>
</form>
{% for user_group in remote_app_permission.user_groups.all %}
<tr>
<td ><b class="bdg_group" data-gid={{ user_group.id }}>{{ user_group }}</b></td>
<td>
<button class="btn btn-danger btn-xs btn-remove-group" type="button" data-gid="{{ user_group.id }}" style="float: right;"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
jumpserver.users_selected = {};
jumpserver.user_groups_selected = {};
function addUsers(users) {
var the_url = "{% url 'api-perms:remote-app-permission-add-user' pk=remote_app_permission.id %}";
var body = {
users: users
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
function removeUser(users) {
var the_url = "{% url 'api-perms:remote-app-permission-remove-user' pk=remote_app_permission.id %}";
var body = {
users: users
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
function updateGroup(groups) {
var the_url = "{% url 'api-perms:remote-app-permission-detail' pk=remote_app_permission.id %}";
var body = {
user_groups: groups
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body)
});
}
$(document).ready(function () {
$('.select2.user').select2()
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.users_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.users_selected[data.id]
});
$('.select2.user-group').select2()
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.user_groups_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.user_groups_selected[data.id]
})
}).on('click', '.btn-add-user', function () {
if (Object.keys(jumpserver.users_selected).length === 0) {
return false;
}
var users_id = [];
$.map(jumpserver.users_selected, function(value, index) {
users_id.push(index);
});
addUsers(users_id);
}).on('click', '.btn-remove-user', function () {
var user_id = $(this).data("gid");
if (user_id === "") {
return
}
var users = [user_id];
removeUser(users)
}).on('click', '#btn-add-group', function () {
if (Object.keys(jumpserver.user_groups_selected).length === 0) {
return false;
}
var groups = $('.bdg_group').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.user_groups_selected, function(group_name, index) {
groups.push(index);
$('#opt_' + index).remove();
$('.group_edit tbody').append(
'<tr>' +
'<td><b class="bdg_group" data-gid="' + index + '">' + group_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-leave-group" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>'
)
});
updateGroup(groups);
}).on('click', '.btn-remove-group', function () {
var $this = $(this);
var $tr = $this.closest('tr');
var groups = $('.bdg_group').map(function() {
if ($(this).data('gid') !== $this.data('gid')){
return $(this).data('gid');
}
}).get();
updateGroup(groups);
$tr.remove()
})
</script>
{% endblock %}
\ No newline at end of file
...@@ -9,8 +9,9 @@ app_name = 'perms' ...@@ -9,8 +9,9 @@ app_name = 'perms'
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register('actions', api.ActionViewSet, 'action') router.register('actions', api.ActionViewSet, 'action')
router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission')
urlpatterns = [ asset_permission_urlpatterns = [
# 查询某个用户授权的资产和资产组 # 查询某个用户授权的资产和资产组
path('user/<uuid:pk>/assets/', path('user/<uuid:pk>/assets/',
api.UserGrantedAssetsApi.as_view(), name='user-assets'), api.UserGrantedAssetsApi.as_view(), name='user-assets'),
...@@ -35,7 +36,6 @@ urlpatterns = [ ...@@ -35,7 +36,6 @@ urlpatterns = [
path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(),
name='my-nodes-assets-as-tree'), name='my-nodes-assets-as-tree'),
# 查询某个用户组授权的资产和资产组 # 查询某个用户组授权的资产和资产组
path('user-group/<uuid:pk>/assets/', path('user-group/<uuid:pk>/assets/',
api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
...@@ -72,5 +72,48 @@ urlpatterns = [ ...@@ -72,5 +72,48 @@ urlpatterns = [
name='get-user-asset-permission-actions'), name='get-user-asset-permission-actions'),
] ]
remote_app_permission_urlpatterns = [
# 查询用户授权的RemoteApp
path('user/<uuid:pk>/remote-apps/',
api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
path('user/remote-apps/',
api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
# 获取用户授权的RemoteApp树
path('user/<uuid:pk>/remote-apps/tree/',
api.UserGrantedRemoteAppsAsTreeApi.as_view(),
name='user-remote-apps-as-tree'),
path('user/remote-apps/tree/',
api.UserGrantedRemoteAppsAsTreeApi.as_view(),
name='my-remote-apps-as-tree'),
# 查询用户组授权的RemoteApp
path('user-group/<uuid:pk>/remote-apps/',
api.UserGroupGrantedRemoteAppsApi.as_view(),
name='user-group-remote-apps'),
# 校验用户对RemoteApp的权限
path('remote-app-permission/user/validate/',
api.ValidateUserRemoteAppPermissionApi.as_view(),
name='validate-user-remote-app-permission'),
# 用户和RemoteApp变更
path('remote-app-permissions/<uuid:pk>/user/add/',
api.RemoteAppPermissionAddUserApi.as_view(),
name='remote-app-permission-add-user'),
path('remote-app-permissions/<uuid:pk>/user/remove/',
api.RemoteAppPermissionRemoveUserApi.as_view(),
name='remote-app-permission-remove-user'),
path('remote-app-permissions/<uuid:pk>/remote-app/remove/',
api.RemoteAppPermissionRemoveRemoteAppApi.as_view(),
name='remote-app-permission-remove-remote-app'),
path('remote-app-permissions/<uuid:pk>/remote-app/add/',
api.RemoteAppPermissionAddRemoteAppApi.as_view(),
name='remote-app-permission-add-remote-app'),
]
urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns
urlpatterns += router.urls urlpatterns += router.urls
...@@ -7,6 +7,7 @@ from .. import views ...@@ -7,6 +7,7 @@ from .. import views
app_name = 'perms' app_name = 'perms'
urlpatterns = [ urlpatterns = [
# asset-permission
path('asset-permission/', views.AssetPermissionListView.as_view(), name='asset-permission-list'), path('asset-permission/', views.AssetPermissionListView.as_view(), name='asset-permission-list'),
path('asset-permission/create/', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'), path('asset-permission/create/', views.AssetPermissionCreateView.as_view(), name='asset-permission-create'),
path('asset-permission/<uuid:pk>/update/', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'), path('asset-permission/<uuid:pk>/update/', views.AssetPermissionUpdateView.as_view(), name='asset-permission-update'),
...@@ -14,4 +15,12 @@ urlpatterns = [ ...@@ -14,4 +15,12 @@ urlpatterns = [
path('asset-permission/<uuid:pk>/delete/', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'), path('asset-permission/<uuid:pk>/delete/', views.AssetPermissionDeleteView.as_view(), name='asset-permission-delete'),
path('asset-permission/<uuid:pk>/user/', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'), path('asset-permission/<uuid:pk>/user/', views.AssetPermissionUserView.as_view(), name='asset-permission-user-list'),
path('asset-permission/<uuid:pk>/asset/', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'), path('asset-permission/<uuid:pk>/asset/', views.AssetPermissionAssetView.as_view(), name='asset-permission-asset-list'),
# remote-app-permission
path('remote-app-permission/', views.RemoteAppPermissionListView.as_view(), name='remote-app-permission-list'),
path('remote-app-permission/create/', views.RemoteAppPermissionCreateView.as_view(), name='remote-app-permission-create'),
path('remote-app-permission/<uuid:pk>/update/', views.RemoteAppPermissionUpdateView.as_view(), name='remote-app-permission-update'),
path('remote-app-permission/<uuid:pk>/', views.RemoteAppPermissionDetailView.as_view(), name='remote-app-permission-detail'),
path('remote-app-permission/<uuid:pk>/user/', views.RemoteAppPermissionUserView.as_view(), name='remote-app-permission-user-list'),
path('remote-app-permission/<uuid:pk>/remote-app/', views.RemoteAppPermissionRemoteAppView.as_view(), name='remote-app-permission-remote-app-list'),
] ]
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *
...@@ -12,12 +12,19 @@ from django.conf import settings ...@@ -12,12 +12,19 @@ from django.conf import settings
from common.utils import get_logger from common.utils import get_logger
from common.tree import TreeNode from common.tree import TreeNode
from .models import AssetPermission, Action from perms.models import AssetPermission, Action
from .hands import Node from perms.hands import Node
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [
'AssetPermissionUtil', 'is_obj_attr_has', 'sort_assets',
'parse_asset_to_tree_node', 'parse_node_to_tree_node',
'check_system_user_action',
]
class GenerateTree: class GenerateTree:
def __init__(self): def __init__(self):
""" """
...@@ -153,51 +160,53 @@ class AssetPermissionUtil: ...@@ -153,51 +160,53 @@ class AssetPermissionUtil:
self._permissions = self.permissions.filter(**filters) self._permissions = self.permissions.filter(**filters)
self._filter_id = md5(filters_json.encode()).hexdigest() self._filter_id = md5(filters_json.encode()).hexdigest()
@staticmethod
def _structured_system_user(system_users, actions):
"""
结构化系统用户
:param system_users:
:param actions:
:return: {system_user1: {'actions': set(), }, }
"""
_attr = {'actions': set(actions)}
_system_users = {system_user: _attr for system_user in system_users}
return _system_users
def get_nodes_direct(self): def get_nodes_direct(self):
""" """
返回用户/组授权规则直接关联的节点 返回用户/组授权规则直接关联的节点
:return: {node1: set(system_user1,)} :return: {asset1: {system_user1: {'actions': set()},}}
""" """
nodes = defaultdict(set) nodes = defaultdict(dict)
permissions = self.permissions.prefetch_related('nodes', 'system_users') permissions = self.permissions.prefetch_related('nodes', 'system_users')
for perm in permissions: for perm in permissions:
actions = perm.actions.all()
for node in perm.nodes.all(): for node in perm.nodes.all():
nodes[node].update(perm.system_users.all()) system_users = perm.system_users.all()
system_users = self._structured_system_user(system_users, actions)
nodes[node].update(system_users)
return nodes return nodes
def get_assets_direct(self): def get_assets_direct(self):
""" """
返回用户授权规则直接关联的资产 返回用户授权规则直接关联的资产
:return: {asset1: set(system_user1,)} :return: {asset1: {system_user1: {'actions': set()},}}
""" """
assets = defaultdict(set) assets = defaultdict(dict)
permissions = self.permissions.prefetch_related('assets', 'system_users') permissions = self.permissions.prefetch_related('assets', 'system_users')
for perm in permissions: for perm in permissions:
actions = perm.actions.all()
for asset in perm.assets.all().valid().prefetch_related('nodes'): for asset in perm.assets.all().valid().prefetch_related('nodes'):
assets[asset].update( system_users = perm.system_users.filter(protocol=asset.protocol)
perm.system_users.filter(protocol=asset.protocol) system_users = self._structured_system_user(system_users, actions)
) assets[asset].update(system_users)
return assets return assets
def _setattr_actions_to_system_user(self): def get_assets_without_cache(self):
""" """
动态给system_use设置属性actions :return: {asset1: set(system_user1,)}
""" """
for asset, system_users in self._assets.items():
# 获取资产和资产的祖先节点的所有授权规则
perms = get_asset_permissions(asset, include_node=True)
# 过滤当前self.permission的授权规则
perms = perms.filter(id__in=[perm.id for perm in self.permissions])
for system_user in system_users:
actions = set()
_perms = perms.filter(system_users=system_user).\
prefetch_related('actions')
for _perm in _perms:
actions.update(_perm.actions.all())
setattr(system_user, 'actions', actions)
def get_assets_without_cache(self):
if self._assets: if self._assets:
return self._assets return self._assets
assets = self.get_assets_direct() assets = self.get_assets_direct()
...@@ -205,11 +214,22 @@ class AssetPermissionUtil: ...@@ -205,11 +214,22 @@ class AssetPermissionUtil:
for node, system_users in nodes.items(): for node, system_users in nodes.items():
_assets = node.get_all_assets().valid().prefetch_related('nodes') _assets = node.get_all_assets().valid().prefetch_related('nodes')
for asset in _assets: for asset in _assets:
assets[asset].update( for system_user, attr_dict in system_users.items():
[s for s in system_users if s.protocol == asset.protocol] if system_user.protocol != asset.protocol:
) continue
self._assets = assets if system_user in assets[asset]:
self._setattr_actions_to_system_user() actions = assets[asset][system_user]['actions']
attr_dict['actions'].update(actions)
system_users.update({system_user: attr_dict})
assets[asset].update(system_users)
__assets = defaultdict(set)
for asset, system_users in assets.items():
for system_user, attr_dict in system_users.items():
setattr(system_user, 'actions', attr_dict['actions'])
__assets[asset] = set(system_users.keys())
self._assets = __assets
return self._assets return self._assets
def get_cache_key(self, resource): def get_cache_key(self, resource):
...@@ -378,7 +398,7 @@ def sort_assets(assets, order_by='hostname', reverse=False): ...@@ -378,7 +398,7 @@ def sort_assets(assets, order_by='hostname', reverse=False):
def parse_node_to_tree_node(node): def parse_node_to_tree_node(node):
from . import serializers from .. import serializers
name = '{} ({})'.format(node.value, node.assets_amount) name = '{} ({})'.format(node.value, node.assets_amount)
node_serializer = serializers.GrantedNodeSerializer(node) node_serializer = serializers.GrantedNodeSerializer(node)
data = { data = {
...@@ -444,11 +464,6 @@ def parse_asset_to_tree_node(node, asset, system_users): ...@@ -444,11 +464,6 @@ def parse_asset_to_tree_node(node, asset, system_users):
return tree_node return tree_node
#
# actions
#
def check_system_user_action(system_user, action): def check_system_user_action(system_user, action):
""" """
:param system_user: SystemUser object (包含动态属性: actions) :param system_user: SystemUser object (包含动态属性: actions)
......
# coding: utf-8
#
from django.db.models import Q
from common.tree import TreeNode
from ..models import RemoteAppPermission
__all__ = [
'RemoteAppPermissionUtil',
'construct_remote_apps_tree_root',
'parse_remote_app_to_tree_node',
]
def get_user_remote_app_permissions(user, include_group=True):
if include_group:
groups = user.groups.all()
arg = Q(users=user) | Q(user_groups__in=groups)
else:
arg = Q(users=user)
return RemoteAppPermission.objects.all().valid().filter(arg)
def get_user_group_remote_app_permissions(user_group):
return RemoteAppPermission.objects.all().valid().filter(
user_groups=user_group
)
class RemoteAppPermissionUtil:
get_permissions_map = {
"User": get_user_remote_app_permissions,
"UserGroup": get_user_group_remote_app_permissions,
}
def __init__(self, obj):
self.object = obj
@property
def permissions(self):
obj_class = self.object.__class__.__name__
func = self.get_permissions_map[obj_class]
_permissions = func(self.object)
return _permissions
def get_remote_apps(self):
remote_apps = set()
for perm in self.permissions:
remote_apps.update(list(perm.remote_apps.all()))
return remote_apps
def construct_remote_apps_tree_root():
tree_root = {
'id': 'ID_REMOTE_APP_ROOT',
'name': 'RemoteApp',
'title': 'RemoteApp',
'pId': '',
'open': False,
'isParent': True,
'iconSkin': '',
'meta': {'type': 'remote_app'}
}
return TreeNode(**tree_root)
def parse_remote_app_to_tree_node(parent, remote_app):
tree_node = {
'id': remote_app.id,
'name': remote_app.name,
'title': remote_app.name,
'pId': parent.id,
'open': False,
'isParent': False,
'iconSkin': 'file',
'meta': {'type': 'remote_app'}
}
return TreeNode(**tree_node)
# coding: utf-8
#
from .asset_permission import *
from .remote_app_permission import *
...@@ -10,10 +10,19 @@ from django.conf import settings ...@@ -10,10 +10,19 @@ from django.conf import settings
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org from orgs.utils import current_org
from .hands import Node, Asset, SystemUser, User, UserGroup from perms.hands import Node, Asset, SystemUser, User, UserGroup
from .models import AssetPermission, Action from perms.models import AssetPermission, Action
from .forms import AssetPermissionForm from perms.forms import AssetPermissionForm
from .const import PERMS_ACTION_NAME_ALL from perms.const import PERMS_ACTION_NAME_ALL
__all__ = [
'AssetPermissionListView', 'AssetPermissionCreateView',
'AssetPermissionUpdateView', 'AssetPermissionDetailView',
'AssetPermissionDeleteView', 'AssetPermissionUserView',
'AssetPermissionAssetView',
]
class AssetPermissionListView(AdminUserRequiredMixin, TemplateView): class AssetPermissionListView(AdminUserRequiredMixin, TemplateView):
...@@ -84,7 +93,7 @@ class AssetPermissionDetailView(AdminUserRequiredMixin, DetailView): ...@@ -84,7 +93,7 @@ class AssetPermissionDetailView(AdminUserRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Perms'), 'app': _('Perms'),
'action': _('Update asset permission'), 'action': _('Asset permission detail'),
'system_users_remain': SystemUser.objects.exclude( 'system_users_remain': SystemUser.objects.exclude(
granted_by_permissions=self.object granted_by_permissions=self.object
), ),
...@@ -121,10 +130,10 @@ class AssetPermissionUserView(AdminUserRequiredMixin, ...@@ -121,10 +130,10 @@ class AssetPermissionUserView(AdminUserRequiredMixin,
'app': _('Perms'), 'app': _('Perms'),
'action': _('Asset permission user list'), 'action': _('Asset permission user list'),
'users_remain': current_org.get_org_users().exclude( 'users_remain': current_org.get_org_users().exclude(
asset_permissions=self.object assetpermission=self.object
), ),
'user_groups_remain': UserGroup.objects.exclude( 'user_groups_remain': UserGroup.objects.exclude(
asset_permissions=self.object assetpermission=self.object
) )
} }
kwargs.update(context) kwargs.update(context)
......
# coding: utf-8
#
from django.utils.translation import ugettext as _
from django.urls import reverse_lazy
from django.views.generic import (
TemplateView, CreateView, UpdateView, DetailView, ListView
)
from django.views.generic.edit import SingleObjectMixin
from django.conf import settings
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from users.models import UserGroup
from assets.models import RemoteApp
from ..models import RemoteAppPermission
from ..forms import RemoteAppPermissionCreateUpdateForm
__all__ = [
'RemoteAppPermissionListView', 'RemoteAppPermissionCreateView',
'RemoteAppPermissionUpdateView', 'RemoteAppPermissionDetailView',
'RemoteAppPermissionUserView', 'RemoteAppPermissionRemoteAppView'
]
class RemoteAppPermissionListView(AdminUserRequiredMixin, TemplateView):
template_name = 'perms/remote_app_permission_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('RemoteApp permission list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppPermissionCreateView(AdminUserRequiredMixin, CreateView):
template_name = 'perms/remote_app_permission_create_update.html'
model = RemoteAppPermission
form_class = RemoteAppPermissionCreateUpdateForm
success_url = reverse_lazy('perms:remote-app-permission-list')
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('Create RemoteApp permission'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppPermissionUpdateView(AdminUserRequiredMixin, UpdateView):
template_name = 'perms/remote_app_permission_create_update.html'
model = RemoteAppPermission
form_class = RemoteAppPermissionCreateUpdateForm
success_url = reverse_lazy('perms:remote-app-permission-list')
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('Update RemoteApp permission')
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppPermissionDetailView(AdminUserRequiredMixin, DetailView):
template_name = 'perms/remote_app_permission_detail.html'
model = RemoteAppPermission
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('RemoteApp permission detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppPermissionUserView(AdminUserRequiredMixin,
SingleObjectMixin,
ListView):
template_name = 'perms/remote_app_permission_user.html'
context_object_name = 'remote_app_permission'
paginate_by = settings.DISPLAY_PER_PAGE
object = None
def get(self, request, *args, **kwargs):
self.object = self.get_object(
queryset=RemoteAppPermission.objects.all())
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = list(self.object.get_all_users())
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('RemoteApp permission user list'),
'users_remain': current_org.get_org_users().exclude(
remoteapppermissions=self.object
),
'user_groups_remain': UserGroup.objects.exclude(
remoteapppermissions=self.object
)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppPermissionRemoteAppView(AdminUserRequiredMixin,
SingleObjectMixin,
ListView):
template_name = 'perms/remote_app_permission_remote_app.html'
context_object_name = 'remote_app_permission'
paginate_by = settings.DISPLAY_PER_PAGE
object = None
def get(self, request, *args, **kwargs):
self.object = self.get_object(
queryset=RemoteAppPermission.objects.all()
)
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = list(self.object.get_all_remote_apps())
return queryset
def get_context_data(self, **kwargs):
remote_app_granted = self.get_queryset()
remote_app_remain = RemoteApp.objects.exclude(
id__in=[a.id for a in remote_app_granted])
context = {
'app': _('Perms'),
'action': _('RemoteApp permission RemoteApp list'),
'remote_app_remain': remote_app_remain
}
kwargs.update(context)
return super().get_context_data(**kwargs)
...@@ -5,7 +5,9 @@ import os ...@@ -5,7 +5,9 @@ import os
import json import json
import jms_storage import jms_storage
from rest_framework import generics
from rest_framework.views import Response, APIView from rest_framework.views import Response, APIView
from rest_framework.pagination import LimitOffsetPagination
from django.conf import settings from django.conf import settings
from django.core.mail import send_mail from django.core.mail import send_mail
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -79,7 +81,7 @@ class LDAPTestingAPI(APIView): ...@@ -79,7 +81,7 @@ class LDAPTestingAPI(APIView):
util = self.get_ldap_util(serializer) util = self.get_ldap_util(serializer)
try: try:
users = util.get_search_user_items() users = util.search_user_items()
except Exception as e: except Exception as e:
return Response({"error": str(e)}, status=401) return Response({"error": str(e)}, status=401)
...@@ -89,30 +91,66 @@ class LDAPTestingAPI(APIView): ...@@ -89,30 +91,66 @@ class LDAPTestingAPI(APIView):
return Response({"error": "Have user but attr mapping error"}, status=401) return Response({"error": "Have user but attr mapping error"}, status=401)
class LDAPUserListApi(APIView): class LDAPUserListApi(generics.ListAPIView):
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def get(self, request): def get_queryset(self):
util = LDAPUtil() util = LDAPUtil()
try: try:
users = util.get_search_user_items() users = util.search_user_items()
except Exception as e: except Exception as e:
users = [] users = []
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
# 前端data_table会根据row.id对table.selected值进行操作
for user in users:
user['id'] = user['username']
return users
def filter_queryset(self, queryset):
search = self.request.query_params.get('search')
if not search:
return queryset
search = search.lower()
queryset = [
q for q in queryset
if
search in q['username'].lower()
or search in q['name'].lower()
or search in q['email'].lower()
]
return queryset
def sort_queryset(self, queryset):
order_by = self.request.query_params.get('order')
if not order_by:
order_by = 'existing'
if order_by.startswith('-'):
order_by = order_by.lstrip('-')
reverse = True
else: else:
users = sorted(users, key=lambda u: (u['existing'], u['username'])) reverse = False
return Response(users) queryset = sorted(queryset, key=lambda x: x[order_by], reverse=reverse)
return queryset
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
queryset = self.sort_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None:
return self.get_paginated_response(page)
return Response(queryset)
class LDAPUserSyncAPI(APIView): class LDAPUserSyncAPI(APIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def post(self, request): def post(self, request):
user_names = request.data.get('user_names', '') username_list = request.data.get('username_list', [])
util = LDAPUtil() util = LDAPUtil()
try: try:
result = util.sync_users(username_set=user_names) result = util.sync_users(username_list)
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return Response({'error': str(e)}, status=401) return Response({'error': str(e)}, status=401)
...@@ -220,7 +258,4 @@ class DjangoSettingsAPI(APIView): ...@@ -220,7 +258,4 @@ class DjangoSettingsAPI(APIView):
data[k] = v data[k] = v
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
data[k] = str(v) data[k] = str(v)
return Response(data) return Response(data)
\ No newline at end of file
...@@ -121,9 +121,9 @@ class LDAPSettingForm(BaseForm): ...@@ -121,9 +121,9 @@ class LDAPSettingForm(BaseForm):
) )
# 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_START_TLS = forms.BooleanField( # AUTH_LDAP_START_TLS = forms.BooleanField(
label=_("Use SSL"), required=False # label=_("Use SSL"), required=False
) # )
AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False)
...@@ -147,7 +147,7 @@ class TerminalSettingForm(BaseForm): ...@@ -147,7 +147,7 @@ class TerminalSettingForm(BaseForm):
required=False, label=_("Public key auth") required=False, label=_("Public key auth")
) )
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
min_value=5, label=_("Heartbeat interval"), min_value=5, max_value=99999, label=_("Heartbeat interval"),
help_text=_("Units: seconds") help_text=_("Units: seconds")
) )
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
...@@ -157,7 +157,7 @@ class TerminalSettingForm(BaseForm): ...@@ -157,7 +157,7 @@ class TerminalSettingForm(BaseForm):
choices=PAGE_SIZE_CHOICES, label=_("List page size"), choices=PAGE_SIZE_CHOICES, label=_("List page size"),
) )
TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField(
min_value=1, label=_("Session keep duration"), min_value=1, max_value=99999, label=_("Session keep duration"),
help_text=_("Units: days, Session, record, command will be delete " help_text=_("Units: days, Session, record, command will be delete "
"if more than duration, only in database") "if more than duration, only in database")
) )
...@@ -182,11 +182,12 @@ class SecuritySettingForm(BaseForm): ...@@ -182,11 +182,12 @@ class SecuritySettingForm(BaseForm):
) )
# limit login count # limit login count
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
min_value=3, label=_("Limit the number of login failures") min_value=3, max_value=99999,
label=_("Limit the number of login failures")
) )
# limit login time # limit login time
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
min_value=5, label=_("No logon interval"), min_value=5, max_value=99999, label=_("No logon interval"),
help_text=_( help_text=_(
"Tip: (unit/minute) if the user has failed to log in for a limited " "Tip: (unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval." "number of times, no login is allowed during this time interval."
...@@ -194,7 +195,8 @@ class SecuritySettingForm(BaseForm): ...@@ -194,7 +195,8 @@ class SecuritySettingForm(BaseForm):
) )
# ssh max idle time # ssh max idle time
SECURITY_MAX_IDLE_TIME = forms.IntegerField( SECURITY_MAX_IDLE_TIME = forms.IntegerField(
required=False, label=_("Connection max idle time"), min_value=1, max_value=99999, required=False,
label=_("Connection max idle time"),
help_text=_( help_text=_(
'If idle time more than it, disconnect connection(only ssh now) ' 'If idle time more than it, disconnect connection(only ssh now) '
'Unit: minute' 'Unit: minute'
...@@ -202,8 +204,7 @@ class SecuritySettingForm(BaseForm): ...@@ -202,8 +204,7 @@ class SecuritySettingForm(BaseForm):
) )
# password expiration time # password expiration time
SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
label=_("Password expiration time"), min_value=1, max_value=99999, label=_("Password expiration time"),
min_value=1, max_value=99999,
help_text=_( help_text=_(
"Tip: (unit: day) " "Tip: (unit: day) "
"If the user does not update the password during the time, " "If the user does not update the password during the time, "
...@@ -214,7 +215,7 @@ class SecuritySettingForm(BaseForm): ...@@ -214,7 +215,7 @@ class SecuritySettingForm(BaseForm):
) )
# min length # min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
min_value=6, label=_("Password minimum length"), min_value=6, max_value=30, label=_("Password minimum length"),
) )
# upper case # upper case
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
...@@ -242,3 +243,26 @@ class SecuritySettingForm(BaseForm): ...@@ -242,3 +243,26 @@ class SecuritySettingForm(BaseForm):
'and resets must contain special characters') 'and resets must contain special characters')
) )
class EmailContentSettingForm(BaseForm):
EMAIL_CUSTOM_USER_CREATED_SUBJECT = forms.CharField(
max_length=1024, required=False, label=_("Create user email subject"),
help_text=_("Tips: When creating a user, send the subject of the email"
" (eg:Create account successfully)")
)
EMAIL_CUSTOM_USER_CREATED_HONORIFIC = forms.CharField(
max_length=1024, required=False, label=_("Create user honorific"),
help_text=_("Tips: When creating a user, send the honorific of the "
"email (eg:Hello)")
)
EMAIL_CUSTOM_USER_CREATED_BODY = forms.CharField(
max_length=4096, required=False, widget=forms.Textarea(),
label=_('Create user email content'),
help_text=_('Tips:When creating a user, send the content of the email')
)
EMAIL_CUSTOM_USER_CREATED_SIGNATURE = forms.CharField(
max_length=512, required=False, label=_("Signature"),
help_text=_("Tips: Email signature (eg:jumpserver)")
)
...@@ -52,12 +52,15 @@ ...@@ -52,12 +52,15 @@
var ldap_users_table = 0; var ldap_users_table = 0;
function initLdapUsersTable() { function initLdapUsersTable() {
if(ldap_users_table){ if(ldap_users_table){
return return ldap_users_table
} }
var options = { var options = {
ele: $('#ldap_list_users_table'), ele: $('#ldap_list_users_table'),
ajax_url: '{% url "api-settings:ldap-user-list" %}', ajax_url: '{% url "api-settings:ldap-user-list" %}',
columnDefs: [ columnDefs: [
{targets: 0, createdCell: function (td, cellData, rowData) {
$(td).html("<input type='checkbox' class='text-center ipt_check' id='ID_USERNAME'>".replace("ID_USERNAME", cellData))
}},
{targets: 4, createdCell: function (td, cellData, rowData) { {targets: 4, createdCell: function (td, cellData, rowData) {
if(cellData){ if(cellData){
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
...@@ -70,10 +73,10 @@ function initLdapUsersTable() { ...@@ -70,10 +73,10 @@ function initLdapUsersTable() {
{data: "username" },{data: "username" }, {data: "name" }, {data: "username" },{data: "username" }, {data: "name" },
{data:"email"}, {data:'existing'} {data:"email"}, {data:'existing'}
], ],
pageLength: 10 pageLength: 15
}; };
ldap_users_table = jumpserver.initDataTable(options); ldap_users_table = jumpserver.initServerSideDataTable(options);
return ldap_users_table return ldap_users_table
} }
......
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
<li> <li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a> <a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li>
<li> <li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a> <a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
</li> </li>
......
{% extends 'base.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% load common_tags %}
{% 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>
<a href="{% url 'settings:basic-setting' %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Basic setting' %}</a>
</li>
<li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li>
<li class="active">
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li>
<li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
</li>
<li>
<a href="{% url 'settings:terminal-setting' %}" class="text-center"><i class="fa fa-hdd-o"></i> {% trans 'Terminal setting' %} </a>
</li>
<li>
<a href="{% url 'settings:security-setting' %}" class="text-center"><i class="fa fa-lock"></i> {% trans 'Security setting' %} </a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-12" style="padding-left:0">
<div class="ibox-content" style="border-width: 0;padding-top: 40px;">
<form action="" method="post" class="form-horizontal">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %}
<h3>{% trans "Create User setting" %}</h3>
{% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SUBJECT layout="horizontal" %}
{% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_HONORIFIC layout="horizontal" %}
{% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_BODY layout="horizontal" %}
{% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SIGNATURE 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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
</script>
{% endblock %}
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
<li class="active"> <li class="active">
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a> <a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li>
<li> <li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a> <a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
</li> </li>
......
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
<li> <li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a> <a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li> </li>
<li>
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li>
<li class="active"> <li class="active">
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a> <a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
</li> </li>
...@@ -107,13 +110,10 @@ $(document).ready(function () { ...@@ -107,13 +110,10 @@ $(document).ready(function () {
}); });
}) })
.on("click","#btn_ldap_modal_confirm",function () { .on("click","#btn_ldap_modal_confirm",function () {
var user_names=[]; var username_list = ldap_users_table.selected;
$("tbody input[type='checkbox']:checked").each(function () {
user_names.push($(this).attr('id'));
});
if (user_names.length === 0){ if (username_list.length === 0){
var msg = "{% trans 'User is not currently selected, please check the user you want to import'%}" var msg = "{% trans 'User is not currently selected, please check the user you want to import'%}";
toastr.error(msg); toastr.error(msg);
return return
} }
...@@ -129,7 +129,7 @@ $(document).ready(function () { ...@@ -129,7 +129,7 @@ $(document).ready(function () {
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
body: JSON.stringify({'user_names':user_names}), body: JSON.stringify({'username_list':username_list}),
method: "POST", method: "POST",
flash_message: false, flash_message: false,
success: success, success: success,
......
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
</li> </li>
<li> <li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a> <a href="{% url 'settings:email-setting' %}" class="text-center"><i class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li>
<li>
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li> </li>
<li> <li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a> <a href="{% url 'settings:ldap-setting' %}" class="text-center"><i class="fa fa-archive"></i> {% trans 'LDAP setting' %} </a>
...@@ -30,8 +33,8 @@ ...@@ -30,8 +33,8 @@
</div> </div>
<div class="tab-content"> <div class="tab-content">
<div class="col-sm-12" style="padding-left:0"> <div class="col-sm-12" style="padding-left:0">
<div class="ibox-content" style="border-width: 0;padding-top: 40px;"> <div class="ibox-content" style="border-width: 0;padding-top: 40px;">
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{{ form.non_field_errors }} {{ form.non_field_errors }}
......
...@@ -18,6 +18,9 @@ ...@@ -18,6 +18,9 @@
<li> <li>
<a href="{% url 'settings:email-setting' %}" class="text-center"><i <a href="{% url 'settings:email-setting' %}" class="text-center"><i
class="fa fa-envelope"></i> {% trans 'Email setting' %} </a> class="fa fa-envelope"></i> {% trans 'Email setting' %} </a>
</li>
<li>
<a href="{% url 'settings:email-content-setting' %}" class="text-center"><i class="fa fa-file-text"></i> {% trans 'Email content setting' %} </a>
</li> </li>
<li> <li>
<a href="{% url 'settings:ldap-setting' %}" class="text-center"><i <a href="{% url 'settings:ldap-setting' %}" class="text-center"><i
......
...@@ -9,6 +9,7 @@ app_name = 'common' ...@@ -9,6 +9,7 @@ app_name = 'common'
urlpatterns = [ urlpatterns = [
url(r'^$', views.BasicSettingView.as_view(), name='basic-setting'), url(r'^$', views.BasicSettingView.as_view(), name='basic-setting'),
url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'),
url(r'^email-content/$', views.EmailContentSettingView.as_view(), name='email-content-setting'),
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'),
url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'),
url(r'^terminal/replay-storage/create$', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'), url(r'^terminal/replay-storage/create$', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'),
......
...@@ -5,7 +5,9 @@ from ldap3 import Server, Connection ...@@ -5,7 +5,9 @@ from ldap3 import Server, Connection
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from users.models import User from users.models import User
from users.utils import construct_user_email
from common.utils import get_logger from common.utils import get_logger
from .models import settings from .models import settings
...@@ -17,11 +19,11 @@ class LDAPOUGroupException(Exception): ...@@ -17,11 +19,11 @@ class LDAPOUGroupException(Exception):
class LDAPUtil: class LDAPUtil:
_conn = None
def __init__(self, use_settings_config=True, server_uri=None, bind_dn=None, def __init__(self, use_settings_config=True, server_uri=None, bind_dn=None,
password=None, use_ssl=None, search_ougroup=None, password=None, use_ssl=None, search_ougroup=None,
search_filter=None, attr_map=None, auth_ldap=None): search_filter=None, attr_map=None, auth_ldap=None):
# config # config
if use_settings_config: if use_settings_config:
self._load_config_from_settings() self._load_config_from_settings()
...@@ -45,46 +47,82 @@ class LDAPUtil: ...@@ -45,46 +47,82 @@ class LDAPUtil:
self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP
self.auth_ldap = settings.AUTH_LDAP self.auth_ldap = settings.AUTH_LDAP
@property
def connection(self):
if self._conn is None:
server = Server(self.server_uri, use_ssl=self.use_ssl)
conn = Connection(server, self.bind_dn, self.password)
conn.bind()
self._conn = conn
return self._conn
@staticmethod @staticmethod
def get_user_by_username(username): def get_user_by_username(username):
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
except Exception as e: except Exception as e:
logger.info(e)
return None return None
else: else:
return user return user
def _ldap_entry_to_user_item(self, entry):
user_item = {}
for attr, mapping in self.attr_map.items():
if not hasattr(entry, mapping):
continue
user_item[attr] = getattr(entry, mapping).value or ''
return user_item
def search_user_items(self):
user_items = []
for search_ou in str(self.search_ougroup).split("|"):
ok = self.connection.search(
search_ou, self.search_filter % ({"user": "*"}),
attributes=list(self.attr_map.values())
)
if not ok:
error = _("Search no entry matched in ou {}".format(search_ou))
raise LDAPOUGroupException(error)
for entry in self.connection.entries:
user_item = self._ldap_entry_to_user_item(entry)
user = self.get_user_by_username(user_item['username'])
user_item['existing'] = bool(user)
user_items.append(user_item)
return user_items
def search_filter_user_items(self, username_list):
user_items = self.search_user_items()
if username_list:
user_items = [u for u in user_items if u['username'] in username_list]
return user_items
@staticmethod @staticmethod
def _update_user(user, user_item): def save_user(user, user_item):
for field, value in user_item.items(): for field, value in user_item.items():
if not hasattr(user, field): if not hasattr(user, field):
continue continue
if isinstance(getattr(user, field), bool):
value = value.lower() in ['true', 1]
setattr(user, field, value) setattr(user, field, value)
user.save() user.save()
def update_user(self, user_item): def update_user(self, user_item):
user = self.get_user_by_username(user_item['username']) user = self.get_user_by_username(user_item['username'])
if not user:
msg = _('User does not exist')
return False, msg
if user.source != User.SOURCE_LDAP: if user.source != User.SOURCE_LDAP:
msg = _('The user source is not LDAP') msg = _('The user source is not LDAP')
return False, msg return False, msg
try: try:
self._update_user(user, user_item) self.save_user(user, user_item)
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return False, str(e) return False, str(e)
else: else:
return True, None return True, None
@staticmethod def create_user(self, user_item):
def create_user(user_item): user = User(source=User.SOURCE_LDAP)
user_item['source'] = User.SOURCE_LDAP
try: try:
User.objects.create(**user_item) self.save_user(user, user_item)
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return False, str(e) return False, str(e)
...@@ -92,26 +130,21 @@ class LDAPUtil: ...@@ -92,26 +130,21 @@ class LDAPUtil:
return True, None return True, None
@staticmethod @staticmethod
def get_or_construct_email(user_item): def construct_user_email(user_item):
if not user_item.get('email', None): username = user_item['username']
if '@' in user_item['username']: email = user_item.get('email', '')
email = user_item['username'] email = construct_user_email(username, email)
else:
email = '{}@{}'.format(
user_item['username'], settings.EMAIL_SUFFIX)
else:
email = user_item['email']
return email return email
def create_or_update_users(self, user_items, force_update=True): def create_or_update_users(self, user_items, force_update=True):
succeed = failed = 0 succeed = failed = 0
for user_item in user_items: for user_item in user_items:
user_item['email'] = self.get_or_construct_email(user_item) exist = user_item.pop('existing', False)
exist = user_item.pop('existing', None) user_item['email'] = self.construct_user_email(user_item)
if exist: if not exist:
ok, error = self.update_user(user_item)
else:
ok, error = self.create_user(user_item) ok, error = self.create_user(user_item)
else:
ok, error = self.update_user(user_item)
if not ok: if not ok:
failed += 1 failed += 1
else: else:
...@@ -119,44 +152,7 @@ class LDAPUtil: ...@@ -119,44 +152,7 @@ class LDAPUtil:
result = {'total': len(user_items), 'succeed': succeed, 'failed': failed} result = {'total': len(user_items), 'succeed': succeed, 'failed': failed}
return result return result
def _ldap_entry_to_user_item(self, entry): def sync_users(self, username_list):
user_item = {} user_items = self.search_filter_user_items(username_list)
for attr, mapping in self.attr_map.items():
if not hasattr(entry, mapping):
continue
user_item[attr] = getattr(entry, mapping).value or ''
return user_item
def get_connection(self):
server = Server(self.server_uri, use_ssl=self.use_ssl)
conn = Connection(server, self.bind_dn, self.password)
conn.bind()
return conn
def get_search_user_items(self):
conn = self.get_connection()
user_items = []
search_ougroup = str(self.search_ougroup).split("|")
for search_ou in search_ougroup:
ok = conn.search(
search_ou, self.search_filter % ({"user": "*"}),
attributes=list(self.attr_map.values())
)
if not ok:
error = _("Search no entry matched in ou {}".format(search_ou))
raise LDAPOUGroupException(error)
for entry in conn.entries:
user_item = self._ldap_entry_to_user_item(entry)
user = self.get_user_by_username(user_item['username'])
user_item['existing'] = bool(user)
user_items.append(user_item)
return user_items
def sync_users(self, username_set):
user_items = self.get_search_user_items()
if username_set:
user_items = [u for u in user_items if u['username'] in username_set]
result = self.create_or_update_users(user_items) result = self.create_or_update_users(user_items)
return result return result
...@@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _ ...@@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _
from common.permissions import SuperUserRequiredMixin from common.permissions import SuperUserRequiredMixin
from common import utils from common import utils
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm
class BasicSettingView(SuperUserRequiredMixin, TemplateView): class BasicSettingView(SuperUserRequiredMixin, TemplateView):
...@@ -166,3 +166,29 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView): ...@@ -166,3 +166,29 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
context = self.get_context_data() context = self.get_context_data()
context.update({"form": form}) context.update({"form": form})
return render(request, self.template_name, context) return render(request, self.template_name, context)
class EmailContentSettingView(SuperUserRequiredMixin, TemplateView):
template_name = "settings/email_content_setting.html"
form_class = EmailContentSettingForm
def get_context_data(self, **kwargs):
context = {
'app': _('Settings'),
'action': _('Email content setting'),
'form': self.form_class(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def post(self, request):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
msg = _("Update setting successfully")
messages.success(request, msg)
return redirect('settings:email-content-setting')
else:
context = self.get_context_data()
context.update({"form": form})
return render(request, self.template_name, context)
...@@ -715,9 +715,12 @@ String.prototype.format = function(args) { ...@@ -715,9 +715,12 @@ String.prototype.format = function(args) {
return result; return result;
}; };
function setCookie(key, value) { function setCookie(key, value, time) {
var expires = new Date(); var expires = new Date();
expires.setTime(expires.getTime() + (24 * 60 * 60 * 1000)); if (!time) {
time = expires.getTime() + (24 * 60 * 60 * 1000);
}
expires.setTime(time);
document.cookie = key + '=' + value + ';expires=' + expires.toUTCString() + ';path=/'; document.cookie = key + '=' + value + ';expires=' + expires.toUTCString() + ';path=/';
} }
...@@ -951,9 +954,92 @@ function rootNodeAddDom(ztree, callback) { ...@@ -951,9 +954,92 @@ function rootNodeAddDom(ztree, callback) {
}) })
} }
function APIExportData(props) {
props = props || {};
$.ajax({
url: '/api/common/v1/resources/cache/',
type: props.method || "POST",
data: props.body,
contentType: props.content_type || "application/json; charset=utf-8",
dataType: props.data_type || "json",
success: function (data) {
var export_url = props.success_url;
var params = props.params || {};
params['format'] = props.format;
params['spm'] = data.spm;
for (var k in params){
export_url = setUrlParam(export_url, k, params[k])
}
window.open(export_url);
},
error: function () {
toastr.error(gettext('Export failed'));
}
})
}
function APIImportData(props){
props = props || {};
$.ajax({
url: props.url,
type: props.method || "POST",
processData: false,
data: props.body,
contentType: props.content_type || 'text/csv',
success: function (data) {
if(props.method === 'POST'){
$('#created_failed').html('');
$('#created_failed_detail').html('');
$('#success_created').html(gettext("Import Success"));
$('#success_created_detail').html("Count" + ": " + data.length);
}else{
$('#updated_failed').html('');
$('#updated_failed_detail').html('');
$('#success_updated').html(gettext("Update Success"));
$('#success_updated_detail').html("Count" + ": " + data.length);
}
props.data_table.ajax.reload()
},
error: function (error) {
var data = error.responseJSON;
if (data instanceof Array){
var html = '';
var li = '';
var err = '';
$.each(data, function (index, item){
err = '';
for (var prop in item) {
err += prop + ": " + item[prop][0] + " "
}
if (err) {
li = "<li>" + "Line " + (++index) + ". " + err + "</li>";
html += li
}
});
html = "<ul>" + html + "</ul>"
}
else {
html = error.responseText
}
if(props.method === 'POST'){
$('#success_created').html('');
$('#success_created_detail').html('');
$('#created_failed').html(gettext("Import failed"));
$('#created_failed_detail').html(html);
}else{
$('#success_updated').html('');
$('#success_updated_detail').html('');
$('#updated_failed').html(gettext("Update failed"));
$('#updated_failed_detail').html(html);
}
}
})
}
function htmlEscape ( d ) { function htmlEscape ( d ) {
return typeof d === 'string' ? return typeof d === 'string' ?
d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') : d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
d; d;
} }
\ No newline at end of file
/*!
* clipboard.js v1.5.5
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/
!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,r){function o(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return o(n?n:t)},l,l.exports,t,e,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;a<r.length;a++)o(r[a]);return o}({1:[function(t,e,n){var r=t("matches-selector");e.exports=function(t,e,n){for(var o=n?t:t.parentNode;o&&o!==document;){if(r(o,e))return o;o=o.parentNode}}},{"matches-selector":2}],2:[function(t,e,n){function r(t,e){if(i)return i.call(t,e);for(var n=t.parentNode.querySelectorAll(e),r=0;r<n.length;++r)if(n[r]==t)return!0;return!1}var o=Element.prototype,i=o.matchesSelector||o.webkitMatchesSelector||o.mozMatchesSelector||o.msMatchesSelector||o.oMatchesSelector;e.exports=r},{}],3:[function(t,e,n){function r(t,e,n,r){var i=o.apply(this,arguments);return t.addEventListener(n,i),{destroy:function(){t.removeEventListener(n,i)}}}function o(t,e,n,r){return function(n){n.delegateTarget=i(n.target,e,!0),n.delegateTarget&&r.call(t,n)}}var i=t("closest");e.exports=r},{closest:1}],4:[function(t,e,n){n.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},n.nodeList=function(t){var e=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===e||"[object HTMLCollection]"===e)&&"length"in t&&(0===t.length||n.node(t[0]))},n.string=function(t){return"string"==typeof t||t instanceof String},n.function=function(t){var e=Object.prototype.toString.call(t);return"[object Function]"===e}},{}],5:[function(t,e,n){function r(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.function(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return o(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function o(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return s(document.body,t,e,n)}var c=t("./is"),s=t("delegate");e.exports=r},{"./is":4,delegate:3}],6:[function(t,e,n){function r(t){var e;if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName)t.focus(),t.setSelectionRange(0,t.value.length),e=t.value;else{t.hasAttribute("contenteditable")&&t.focus();var n=window.getSelection(),r=document.createRange();r.selectNodeContents(t),n.removeAllRanges(),n.addRange(r),e=n.toString()}return e}e.exports=r},{}],7:[function(t,e,n){function r(){}r.prototype={on:function(t,e,n){var r=this.e||(this.e={});return(r[t]||(r[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function r(){o.off(t,r),e.apply(n,arguments)}var o=this;return r._=e,this.on(t,r,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),r=0,o=n.length;for(r;o>r;r++)n[r].fn.apply(n[r].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),r=n[t],o=[];if(r&&e)for(var i=0,a=r.length;a>i;i++)r[i].fn!==e&&r[i].fn._!==e&&o.push(r[i]);return o.length?n[t]=o:delete n[t],this}},e.exports=r},{}],8:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}n.__esModule=!0;var i=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),a=t("select"),c=r(a),s=function(){function t(e){o(this,t),this.resolveOptions(e),this.initSelection()}return t.prototype.resolveOptions=function t(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];this.action=e.action,this.emitter=e.emitter,this.target=e.target,this.text=e.text,this.trigger=e.trigger,this.selectedText=""},t.prototype.initSelection=function t(){if(this.text&&this.target)throw new Error('Multiple attributes declared, use either "target" or "text"');if(this.text)this.selectFake();else{if(!this.target)throw new Error('Missing required attributes, use either "target" or "text"');this.selectTarget()}},t.prototype.selectFake=function t(){var e=this;this.removeFake(),this.fakeHandler=document.body.addEventListener("click",function(){return e.removeFake()}),this.fakeElem=document.createElement("textarea"),this.fakeElem.style.position="absolute",this.fakeElem.style.left="-9999px",this.fakeElem.style.top=(window.pageYOffset||document.documentElement.scrollTop)+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,document.body.appendChild(this.fakeElem),this.selectedText=c.default(this.fakeElem),this.copyText()},t.prototype.removeFake=function t(){this.fakeHandler&&(document.body.removeEventListener("click"),this.fakeHandler=null),this.fakeElem&&(document.body.removeChild(this.fakeElem),this.fakeElem=null)},t.prototype.selectTarget=function t(){this.selectedText=c.default(this.target),this.copyText()},t.prototype.copyText=function t(){var e=void 0;try{e=document.execCommand(this.action)}catch(n){e=!1}this.handleResult(e)},t.prototype.handleResult=function t(e){e?this.emitter.emit("success",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)}):this.emitter.emit("error",{action:this.action,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})},t.prototype.clearSelection=function t(){this.target&&this.target.blur(),window.getSelection().removeAllRanges()},t.prototype.destroy=function t(){this.removeFake()},i(t,[{key:"action",set:function t(){var e=arguments.length<=0||void 0===arguments[0]?"copy":arguments[0];if(this._action=e,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function t(){return this._action}},{key:"target",set:function t(e){if(void 0!==e){if(!e||"object"!=typeof e||1!==e.nodeType)throw new Error('Invalid "target" value, use a valid Element');this._target=e}},get:function t(){return this._target}}]),t}();n.default=s,e.exports=n.default},{select:6}],9:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function a(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}n.__esModule=!0;var c=t("./clipboard-action"),s=r(c),u=t("tiny-emitter"),l=r(u),f=t("good-listener"),d=r(f),h=function(t){function e(n,r){o(this,e),t.call(this),this.resolveOptions(r),this.listenClick(n)}return i(e,t),e.prototype.resolveOptions=function t(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];this.action="function"==typeof e.action?e.action:this.defaultAction,this.target="function"==typeof e.target?e.target:this.defaultTarget,this.text="function"==typeof e.text?e.text:this.defaultText},e.prototype.listenClick=function t(e){var n=this;this.listener=d.default(e,"click",function(t){return n.onClick(t)})},e.prototype.onClick=function t(e){var n=e.delegateTarget||e.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new s.default({action:this.action(n),target:this.target(n),text:this.text(n),trigger:n,emitter:this})},e.prototype.defaultAction=function t(e){return a("action",e)},e.prototype.defaultTarget=function t(e){var n=a("target",e);return n?document.querySelector(n):void 0},e.prototype.defaultText=function t(e){return a("text",e)},e.prototype.destroy=function t(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)},e}(l.default);n.default=h,e.exports=n.default},{"./clipboard-action":8,"good-listener":5,"tiny-emitter":7}]},{},[9])(9)});
\ No newline at end of file
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}import_modal{% endblock %}
{% block modal_confirm_id %}btn_import_confirm{% endblock %}
{% block modal_body %}
<form method="post" id="fm_import">
{% csrf_token %}
<div class="form-group">
<label class="control-label">{% trans "Download the imported template or use the exported CSV file format" %}</label>
<a href="{% block import_modal_download_template_url %}{% endblock %}?format=csv&template=import" style="display: block">{% trans 'Download the import template' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_file">{% trans "Select the CSV file to import" %}</label>
<input id="id_file" type="file" name="file" />
</div>
</form>
<div>
<p class="text-success" id="success_created"></p>
<p id="success_created_detail"></p>
<p class="text-danger" id="created_failed"></p>
<p id="created_failed_detail"></p>
</div>
{% endblock %}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="modal-dialog {% block modal_class %}{% endblock %}"> <div class="modal-dialog {% block modal_class %}{% endblock %}">
<div class="modal-content animated fadeIn"> <div class="modal-content animated fadeIn">
<div class="modal-header"> <div class="modal-header">
<button data-dismiss="modal" class="close close_btn1" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> <button data-dismiss="modal" id="close_button1" class="close close_btn1 " type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">{% block modal_title %}{% endblock %}</h4> <h4 class="modal-title">{% block modal_title %}{% endblock %}</h4>
<small>{% block modal_comment %}{% endblock %}</small> <small>{% block modal_comment %}{% endblock %}</small>
</div> </div>
...@@ -19,10 +19,32 @@ ...@@ -19,10 +19,32 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{% block modal_button %} {% block modal_button %}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button> <button id="close_button2" data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
<button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button> <button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button>
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
$(document).ready(function(){
})
.on('click', '#close_button1', function () {
SetMessageLabelEmpty()
})
.on('click', '#close_button2', function () {
SetMessageLabelEmpty()
});
function SetMessageLabelEmpty() {
$('#success_created').html('');
$('#success_created_detail').html('');
$('#created_failed').html('');
$('#created_failed_detail').html('');
$('#success_updated').html('');
$('#success_updated_detail').html('');
$('#updated_failed').html('');
$('#updated_failed_detail').html('');
}
</script>
...@@ -27,12 +27,27 @@ ...@@ -27,12 +27,27 @@
<li id="cmd-filter"><a href="{% url 'assets:cmd-filter-list' %}">{% trans 'Command filters' %}</a></li> <li id="cmd-filter"><a href="{% url 'assets:cmd-filter-list' %}">{% trans 'Command filters' %}</a></li>
</ul> </ul>
</li> </li>
{% if LICENSE_VALID %}
<li id="applications">
<a>
<i class="fa fa-th" style="width: 14px"></i> <span class="nav-label">{% trans 'Applications' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="remote-app"><a href="{% url 'applications:remote-app-list' %}">{% trans 'RemoteApp' %}</a></li>
</ul>
</li>
{% endif %}
<li id="perms"> <li id="perms">
<a href="#"><i class="fa fa-edit" style="width: 14px"></i> <span class="nav-label">{% trans 'Perms' %}</span><span class="fa arrow"></span></a> <a href="#"><i class="fa fa-edit" style="width: 14px"></i> <span class="nav-label">{% trans 'Perms' %}</span><span class="fa arrow"></span></a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="asset-permission"> <li id="asset-permission">
<a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a> <a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a>
</li> </li>
{% if LICENSE_VALID %}
<li id="remote-app-permission">
<a href="{% url 'perms:remote-app-permission-list' %}">{% trans 'RemoteApp' %}</a>
</li>
{% endif %}
</ul> </ul>
</li> </li>
<li id="terminal"> <li id="terminal">
......
...@@ -4,6 +4,18 @@ ...@@ -4,6 +4,18 @@
<i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span> <i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span>
</a> </a>
</li> </li>
<li id="applications">
<a>
<i class="fa fa-th" style="width: 14px"></i> <span class="nav-label">{% trans 'My Applications' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="user-remote-app">
<a href="{% url 'applications:user-remote-app-list' %}">
<i class="" style="width: 14px"></i><span class="nav-label">{% trans 'RemoteApp' %}</span><span class="label label-info pull-right"></span>
</a>
</li>
</ul>
</li>
<li id="ops"> <li id="ops">
<a href="{% url 'ops:command-execution-start' %}"> <a href="{% url 'ops:command-execution-start' %}">
<i class="fa fa-terminal" style="width: 14px"></i> <span class="nav-label">{% trans 'Command execution' %}</span><span class="label label-info pull-right"></span> <i class="fa fa-terminal" style="width: 14px"></i> <span class="nav-label">{% trans 'Command execution' %}</span><span class="label label-info pull-right"></span>
......
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}update_modal{% endblock %}
{% block modal_confirm_id %}btn_update_confirm{% endblock %}
{% block modal_body %}
<form method="post" id="fm_import">
{% csrf_token %}
<div class="form-group">
<label class="control-label">{% trans "Download the update template or use the exported CSV file format" %}</label>
<a id="download_update_template" style="display: block">{% trans 'Download the update template' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="update_file">{% trans "Select the CSV file to import" %}</label>
<input id="update_file" type="file" name="file" />
</div>
</form>
<div>
<p class="text-warning" id="success_updated"></p>
<p id="success_updated_detail"></p>
<p class="text-danger" id="updated_failed"></p>
<p id="updated_failed_detail"></p>
</div>
{% endblock %}
...@@ -67,7 +67,6 @@ class CommandViewSet(viewsets.ViewSet): ...@@ -67,7 +67,6 @@ class CommandViewSet(viewsets.ViewSet):
""" """
command_store = get_command_storage() command_store = get_command_storage()
multi_command_storage = get_multi_command_storage()
serializer_class = SessionCommandSerializer serializer_class = SessionCommandSerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
...@@ -88,7 +87,8 @@ class CommandViewSet(viewsets.ViewSet): ...@@ -88,7 +87,8 @@ class CommandViewSet(viewsets.ViewSet):
return Response({"msg": msg}, status=401) return Response({"msg": msg}, status=401)
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
queryset = self.multi_command_storage.filter() multi_command_storage = get_multi_command_storage()
queryset = multi_command_storage.filter()
serializer = self.serializer_class(queryset, many=True) serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
......
...@@ -5,10 +5,10 @@ from django import template ...@@ -5,10 +5,10 @@ from django import template
from ..backends import get_multi_command_storage from ..backends import get_multi_command_storage
register = template.Library() register = template.Library()
command_store = get_multi_command_storage()
@register.filter @register.filter
def get_session_command_amount(session_id): def get_session_command_amount(session_id):
command_store = get_multi_command_storage()
return command_store.count(session=session_id) return command_store.count(session=session_id)
...@@ -19,7 +19,6 @@ __all__ = [ ...@@ -19,7 +19,6 @@ __all__ = [
'SessionDetailView', 'SessionDetailView',
] ]
command_store = get_multi_command_storage()
class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
...@@ -108,6 +107,7 @@ class SessionDetailView(SingleObjectMixin, AdminUserRequiredMixin, ListView): ...@@ -108,6 +107,7 @@ class SessionDetailView(SingleObjectMixin, AdminUserRequiredMixin, ListView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
command_store = get_multi_command_storage()
return command_store.filter(session=self.object.id) return command_store.filter(session=self.object.id)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
......
...@@ -9,13 +9,13 @@ from ..serializers import UserGroupSerializer, \ ...@@ -9,13 +9,13 @@ from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemberSerializer UserGroupUpdateMemberSerializer
from ..models import UserGroup from ..models import UserGroup
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi'] __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): class UserGroupViewSet(IDInCacheFilterMixin, BulkModelViewSet):
filter_fields = ("name",) filter_fields = ("name",)
search_fields = filter_fields search_fields = filter_fields
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
......
...@@ -15,7 +15,7 @@ from rest_framework.pagination import LimitOffsetPagination ...@@ -15,7 +15,7 @@ from rest_framework.pagination import LimitOffsetPagination
from common.permissions import ( from common.permissions import (
IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
) )
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import current_org from orgs.utils import current_org
from ..serializers import UserSerializer, UserPKUpdateSerializer, \ from ..serializers import UserSerializer, UserPKUpdateSerializer, \
...@@ -32,7 +32,7 @@ __all__ = [ ...@@ -32,7 +32,7 @@ __all__ = [
] ]
class UserViewSet(IDInFilterMixin, BulkModelViewSet): class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
filter_fields = ('username', 'email', 'name', 'id') filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields search_fields = filter_fields
queryset = User.objects.exclude(role=User.ROLE_APP) queryset = User.objects.exclude(role=User.ROLE_APP)
...@@ -40,9 +40,15 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -40,9 +40,15 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
def send_created_signal(self, users):
if not isinstance(users, list):
users = [users]
for user in users:
post_user_create.send(self.__class__, user=user)
def perform_create(self, serializer): def perform_create(self, serializer):
user = serializer.save() users = serializer.save()
post_user_create.send(self.__class__, user=user) self.send_created_signal(users)
def get_queryset(self): def get_queryset(self):
queryset = current_org.get_org_users() queryset = current_org.get_org_users()
...@@ -213,4 +219,4 @@ class UserResetOTPApi(generics.RetrieveAPIView): ...@@ -213,4 +219,4 @@ class UserResetOTPApi(generics.RetrieveAPIView):
user.otp_secret_key = '' user.otp_secret_key = ''
user.save() user.save()
logout(request) logout(request)
return Response({"msg": "success"}) return Response({"msg": "success"})
\ No newline at end of file
...@@ -21,7 +21,7 @@ class UserCheckOtpCodeForm(forms.Form): ...@@ -21,7 +21,7 @@ class UserCheckOtpCodeForm(forms.Form):
otp_code = forms.CharField(label=_('MFA code'), max_length=6) otp_code = forms.CharField(label=_('MFA code'), max_length=6)
class UserCreateUpdateForm(OrgModelForm): class UserCreateUpdateFormMixin(OrgModelForm):
role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP)
password = forms.CharField( password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
...@@ -55,7 +55,7 @@ class UserCreateUpdateForm(OrgModelForm): ...@@ -55,7 +55,7 @@ class UserCreateUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None) self.request = kwargs.pop("request", None)
super(UserCreateUpdateForm, self).__init__(*args, **kwargs) super(UserCreateUpdateFormMixin, self).__init__(*args, **kwargs)
roles = [] roles = []
# Super admin user # Super admin user
...@@ -105,6 +105,23 @@ class UserCreateUpdateForm(OrgModelForm): ...@@ -105,6 +105,23 @@ class UserCreateUpdateForm(OrgModelForm):
return user return user
class UserCreateForm(UserCreateUpdateFormMixin):
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
CUSTOM_PASSWORD = _('Set password')
PASSWORD_STRATEGY_CHOICES = (
(0, EMAIL_SET_PASSWORD),
(1, CUSTOM_PASSWORD)
)
password_strategy = forms.ChoiceField(
choices=PASSWORD_STRATEGY_CHOICES, required=True, initial=0,
widget=forms.RadioSelect(), label=_('Password strategy')
)
class UserUpdateForm(UserCreateUpdateFormMixin):
pass
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
username = forms.CharField(disabled=True) username = forms.CharField(disabled=True)
name = forms.CharField(disabled=True) name = forms.CharField(disabled=True)
......
...@@ -147,6 +147,10 @@ class User(AbstractUser): ...@@ -147,6 +147,10 @@ class User(AbstractUser):
def otp_secret_key(self, item): def otp_secret_key(self, item):
self._otp_secret_key = signer.sign(item) self._otp_secret_key = signer.sign(item)
def check_otp(self, code):
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('users:user-detail', args=(self.id,)) return reverse('users:user-detail', args=(self.id,))
......
...@@ -19,12 +19,21 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -19,12 +19,21 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ fields = [
'id', 'name', 'username', 'email', 'groups', 'groups_display', 'id', 'name', 'username', 'email', 'groups', 'groups_display',
'role', 'role_display', 'avatar_url', 'wechat', 'phone', 'role', 'role_display', 'wechat', 'phone', 'otp_level',
'otp_level', 'comment', 'source', 'source_display', 'comment', 'source', 'source_display', 'is_valid', 'is_expired',
'is_valid', 'is_expired', 'is_active', 'is_active', 'created_by', 'is_first_login',
'created_by', 'is_first_login', 'date_password_last_updated', 'date_expired', 'avatar_url',
'date_password_last_updated', 'date_expired',
] ]
extra_kwargs = {
'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source name')},
'is_first_login': {'label': _('Is first login'), 'read_only': True},
'role_display': {'label': _('Role name')},
'is_valid': {'label': _('Is valid')},
'is_expired': {'label': _('Is expired')},
'avatar_url': {'label': _('Avatar url')},
'created_by': {'read_only': True}, 'source': {'read_only': True}
}
class UserPKUpdateSerializer(serializers.ModelSerializer): class UserPKUpdateSerializer(serializers.ModelSerializer):
...@@ -48,17 +57,20 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer): ...@@ -48,17 +57,20 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer):
class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
users = serializers.SerializerMethodField() users = serializers.PrimaryKeyRelatedField(
required=False, many=True, queryset=User.objects.all(), label=_('User')
)
class Meta: class Meta:
model = UserGroup model = UserGroup
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' fields = [
read_only_fields = ['created_by'] 'id', 'org_id', 'name', 'users', 'comment', 'date_created',
'created_by',
@staticmethod ]
def get_users(obj): extra_kwargs = {
return [user.name for user in obj.users.all()] 'created_by': {'label': _('Created by'), 'read_only': True}
}
class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
......
...@@ -28,4 +28,3 @@ def on_user_create(sender, user=None, **kwargs): ...@@ -28,4 +28,3 @@ def on_user_create(sender, user=None, **kwargs):
logger.info(" - Sending welcome mail ...".format(user.name)) logger.info(" - Sending welcome mail ...".format(user.name))
if user.email: if user.email:
send_user_created_mail(user) send_user_created_mail(user)
...@@ -74,6 +74,9 @@ ...@@ -74,6 +74,9 @@
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
$('#id_date_expired').daterangepicker(dateOptions); $('#id_date_expired').daterangepicker(dateOptions);
var mfa_radio = $('#id_otp_level');
mfa_radio.addClass("form-inline");
mfa_radio.children().css("margin-right","15px")
}) })
</script> </script>
{% endblock %} {% endblock %}
{% extends '_import_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Import user groups" %}{% endblock %}
{% block import_modal_download_template_url %}{% url "api-users:user-group-list" %}{% endblock %}
\ No newline at end of file
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update user group" %}{% endblock %}
\ No newline at end of file
{% extends '_modal.html' %} {% extends '_import_modal.html' %}
{% load i18n %} {% load i18n %}
{% block modal_id %}user_import_modal{% endblock %}
{% block modal_title%}{% trans "Import user" %}{% endblock %} {% block modal_title%}{% trans "Import users" %}{% endblock %}
{% block modal_body %}
<p class="text-success">{% trans "Download template or use export csv format" %}</p> {% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %}
<form method="post" action="{% url 'users:user-import' %}" id="fm_user_import" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Template" %}</label>
<a href="{% url 'users:user-export' %}" style="display: block">{% trans 'Download' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Users csv file" %}</label>
<input id="id_users" type="file" name="file" />
<span class="help-block red-fonts">{% trans 'If set id, will use this id update user 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_user_import{% endblock %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update user" %}{% endblock %}
\ No newline at end of file
...@@ -2,15 +2,83 @@ ...@@ -2,15 +2,83 @@
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block user_template_title %}{% trans "Create user" %}{% endblock %} {% block user_template_title %}{% trans "Create user" %}{% endblock %}
{#{% block username %}#}
{# {% bootstrap_field form.username layout="horizontal" %}#}
{#{% endblock %}#}
{% block password %} {% block password %}
<div class="form-group"> {% bootstrap_field form.password_strategy layout="horizontal" %}
<label class="col-sm-2 control-label">{% trans 'Password' %}</label> <div class="form-group" id="custom_password">
<div class="col-sm-8 controls" style="margin-top: 8px;"> {% bootstrap_field form.password layout="horizontal" %}
{% trans 'Reset link will be generated and sent to the user. ' %} </div>
{# 密码popover #}
<div id="container">
<div class="popover fade bottom in" role="tooltip" id="popover777" style=" display: none; width:260px;">
<div class="arrow" style="left: 50%;"></div>
<h3 class="popover-title" style="display: none;"></h3>
<h4>{% trans 'Your password must satisfy' %}</h4><div id="id_password_rules" style="color: #908a8a; margin-left:20px; font-size:15px;"></div>
<h4 style="margin-top: 10px;">{% trans 'Password strength' %}</h4><div id="id_progress"></div>
<div class="popover-content"></div>
</div> </div>
</div> </div>
<script>
function passwordCheck() {
if ($('#id_password').length != 1) {
return
}
var el = $('#id_password_rules'),
idPassword = $('#id_password'),
idPopover = $('#popover777'),
container = $('#container'),
progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }},
minLength = 6,
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
left = 377,
i18n_fallback = {
"veryWeak": "{% trans 'Very weak' %}",
"weak": "{% trans 'Weak' %}",
"normal": "{% trans 'Normal' %}",
"medium": "{% trans 'Medium' %}",
"strong": "{% trans 'Strong' %}",
"veryStrong": "{% trans 'Very strong' %}"
};
$.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
// 监听事件
idPassword.on('focus', function () {
idPopover.css('top', top);
idPopover.css('left', left);
idPopover.css('display', 'block');
});
idPassword.on('blur', function () {
idPopover.css('display', 'none');
});
idPassword.on('keyup', function(){
var password = idPassword.val();
checkPasswordRules(password, minLength);
});
}
var password_strategy_radio_input = 'input[type=radio][name=password_strategy]';
function passwordStrategyFieldsDisplay(){
var val = $('input:radio[name="password_strategy"]:checked').val();
if(val === '0'){
$('#custom_password').addClass('hidden')
}else {
$('#custom_password').removeClass('hidden')
}
}
$(document).ready(function () {
passwordCheck();
passwordStrategyFieldsDisplay()
}).on('change', password_strategy_radio_input, function(){
passwordStrategyFieldsDisplay()
})
</script>
{% endblock %} {% endblock %}
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block table_search %}{% 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="pull-left m-r-5"><a href="{% url 'users:user-group-create' %}" class="btn btn-sm btn-primary ">{% trans "Create user group" %}</a></div> <div class="pull-left m-r-5"><a href="{% url 'users:user-group-create' %}" class="btn btn-sm btn-primary ">{% trans "Create user group" %}</a></div>
<table class="table table-striped table-bordered table-hover " id="group_list_table" > <table class="table table-striped table-bordered table-hover " id="group_list_table" >
...@@ -16,13 +39,15 @@ ...@@ -16,13 +39,15 @@
</tr> </tr>
</thead> </thead>
</table> </table>
{% include "users/_user_groups_import_modal.html" %}
{% include "users/_user_groups_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 groups_table = 0;
function initTable() {
var options = { var options = {
ele: $('#group_list_table'), ele: $('#group_list_table'),
buttons: [], buttons: [],
...@@ -60,7 +85,11 @@ $(document).ready(function() { ...@@ -60,7 +85,11 @@ $(document).ready(function() {
order: [], order: [],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initServerSideDataTable(options); groups_table = jumpserver.initServerSideDataTable(options);
return groups_table
}
$(document).ready(function() {
initTable()
}).on('click', '.btn_delete_user_group', function(){ }).on('click', '.btn_delete_user_group', function(){
var $this = $(this); var $this = $(this);
...@@ -111,6 +140,68 @@ $(document).ready(function() { ...@@ -111,6 +140,68 @@ $(document).ready(function() {
default: default:
break; break;
} }
}).on('click', '.btn_export', function(){
var groups = groups_table.selected;
var data = {
'resources': groups
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-users:user-group-list' %}",
format: "csv",
params: {
search: search
}
};
APIExportData(props);
}).on('click', '#btn_import_confirm',function () {
var url = "{% url 'api-users:user-group-list' %}";
var file = document.getElementById('id_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var data_table = $('#group_list_table').DataTable();
APIImportData({
url: url,
method: "POST",
body: file,
data_table: data_table
});
})
.on('click', '#download_update_template', function(){
var groups = groups_table.selected;
var data = {
'resources': groups
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-users:user-group-list' %}?format=csv&template=update",
format: "csv",
params: {
search: search
}
};
APIExportData(props);
}).on('click', '#btn_update_confirm',function () {
var url = "{% url 'api-users:user-group-list' %}";
var file = document.getElementById('update_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
}
var data_table = $('#group_list_table').DataTable();
APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
}) })
</script> </script>
{% endblock %} {% endblock %}
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block table_search %} {% block table_search %}
<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="#user_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>
</div> </li>
</div> <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 %}
<div class="uc pull-left m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div> <div class="uc pull-left m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
...@@ -48,12 +60,13 @@ ...@@ -48,12 +60,13 @@
</div> </div>
</div> </div>
{% include "users/_user_import_modal.html" %} {% include "users/_user_import_modal.html" %}
{% include "users/_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 src="{% static 'js/jquery.form.min.js' %}"></script> <script src="{% static 'js/jquery.form.min.js' %}"></script>
<script> <script>
var users_table = 0;
function initTable() { function initTable() {
var options = { var options = {
ele: $('#user_list_table'), ele: $('#user_list_table'),
...@@ -109,98 +122,127 @@ function initTable() { ...@@ -109,98 +122,127 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
var table = jumpserver.initServerSideDataTable(options); users_table = jumpserver.initServerSideDataTable(options);
return table return users_table
} }
$(document).ready(function(){ $(document).ready(function(){
var table = initTable(); initTable();
var fields = $('#fm_user_bulk_update .form-group'); var fields = $('#fm_user_bulk_update .form-group');
$.each(fields, function (index, value) { $.each(fields, function (index, value) {
console.log(value) console.log(value)
}); });
$('.btn_export').click(function () { $('.btn_export').click(function () {
var users = []; var users = users_table.selected;
var rows = table.rows('.selected').data(); var data = {
if(rows.length===0){ 'resources': users
rows = table.rows().data(); };
} var search = $("input[type='search']").val();
$.each(rows, function (index, obj) { var props = {
users.push(obj.id) method: "POST",
}); body: JSON.stringify(data),
$.ajax({ success_url: "{% url 'api-users:user-list' %}",
url: "{% url 'users:user-export' %}", format: 'csv',
method: 'POST', params: {
data: JSON.stringify({users_id: users}), search: search
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
} }
}) };
APIExportData(props);
}); });
$('#btn_user_import').click(function() { $('#btn_import_confirm').click(function() {
var $form = $('#fm_user_import'); var url = "{% url 'api-users:user-list' %}";
$form.find('.help-block').remove(); var file = document.getElementById('id_file').files[0];
function success (data) { if(!file){
if (data.valid === false) { toastr.error("{% trans "Please select file" %}");
$('<span />', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_users')); return
} else { }
$('#id_created').html(data.created_info); var data_table = $('#user_list_table').DataTable();
$('#id_created_detail').html(data.created.join(', ')); APIImportData({
$('#id_updated').html(data.updated_info); url: url,
$('#id_updated_detail').html(data.updated.join(', ')); method: "POST",
$('#id_failed').html(data.failed_info); body: file,
$('#id_failed_detail').html(data.failed.join(', ')); data_table: data_table
var $data_table = $('#user_list_table').DataTable(); });
$data_table.ajax.reload(); });
$('#download_update_template').click(function () {
var users = users_table.selected;
var data = {
'resources': users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-users:user-list' %}?format=csv&template=update",
format: 'csv',
params: {
search: search
} }
};
APIExportData(props);
});
$('#btn_update_confirm').click(function() {
var url = "{% url 'api-users:user-list' %}";
var file = document.getElementById('update_file').files[0];
if(!file){
toastr.error("{% trans "Please select file" %}");
return
} }
$form.ajaxSubmit({success: success}); var data_table = $('#user_list_table').DataTable();
}) APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
});
}).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 = $('#user_list_table').DataTable(); var id_list = users_table.selected;
var id_list = []; if (id_list.length === 0) {
var plain_id_list = [];
$data_table.rows({selected: true}).every(function(){
id_list.push({pk: this.data().id});
plain_id_list.push(this.data().id);
});
if (id_list === []) {
return false; return false;
} }
var the_url = "{% url 'api-users:user-list' %}"; var the_url = "{% url 'api-users:user-list' %}";
var data = {
'resources': id_list
};
function refreshTag() {
$('#user_list_table').DataTable().ajax.reload()
}
function doDeactive() { function doDeactive() {
var body = $.each(id_list, function(index, user_object) { var data = [];
user_object['is_active'] = false; $.each(id_list, function(index, object_id) {
var obj = {"pk": object_id, "is_active": false};
data.push(obj);
}); });
function success() { function success() {
location.reload(); setTimeout( function () {
window.location.reload();}, 300);
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(body), body: JSON.stringify(data),
success: success success: success
}); });
location.reload();
} }
function doActive() { function doActive() {
var body = $.each(id_list, function(index, user_object) { var data = [];
user_object['is_active'] = true; $.each(id_list, function(index, object_id) {
var obj = {"pk": object_id, "is_active": true};
data.push(obj);
}); });
function success() { function success() {
location.reload(); setTimeout( function () {
window.location.reload();}, 300);
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(body), body: JSON.stringify(data),
success: success success: success
}); });
} }
...@@ -214,26 +256,49 @@ $(document).ready(function(){ ...@@ -214,26 +256,49 @@ $(document).ready(function(){
confirmButtonColor: "#DD6B55", confirmButtonColor: "#DD6B55",
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 'User Deleted.' %}"; var msg = "{% trans 'User Deleted.' %}";
swal("{% trans 'User Delete' %}", msg, "success"); swal("{% trans 'User Delete' %}", msg, "success");
$('#user_list_table').DataTable().ajax.reload(); }
}; function fail() {
var fail = function() {
var msg = "{% trans 'User Deleting failed.' %}"; var msg = "{% trans 'User Deleting failed.' %}";
swal("{% trans 'User Delete' %}", msg, "error"); swal("{% trans 'User Delete' %}", msg, "error");
}; }
var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list); APIUpdateAttr({
APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail}); url: "{% url 'api-common:resources-cache' %}",
jumpserver.checked = false; method:'POST',
}); body:JSON.stringify(data),
success:success,
error:fail
})
})
} }
function doUpdate() { function doUpdate() {
var users_id = plain_id_list.join(','); function fail(data) {
var url = "{% url 'users:user-bulk-update' %}?users_id=" + users_id; toastr.error(JSON.parse(data))
location.href = url }
} function success(data) {
var url = "{% url 'users:user-bulk-update' %}";
location.href= setUrlParam(url, 'spm', data.spm);
}
APIUpdateAttr({
url: "{% url 'api-common:resources-cache' %}",
method:'POST',
body:JSON.stringify(data),
flash_message:false,
success:success,
error:fail
})
}
switch(action) { switch(action) {
case 'deactive': case 'deactive':
doDeactive(); doDeactive();
......
...@@ -34,29 +34,24 @@ class AdminUserRequiredMixin(UserPassesTestMixin): ...@@ -34,29 +34,24 @@ class AdminUserRequiredMixin(UserPassesTestMixin):
return True return True
def send_user_created_mail(user): def construct_user_created_email_body(user):
subject = _('Create account successfully') default_body = _("""
recipient_list = [user.email] <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
message = _(""" <p style="text-indent:2em;">
Hello %(name)s: <span>
</br> Username: %(username)s.
Your account has been created successfully </span>
</br> <span>
Username: %(username)s <a href="%(rest_password_url)s?token=%(rest_password_token)s">click here to set your password</a>
</br> </span>
<a href="%(rest_password_url)s?token=%(rest_password_token)s">click here to set your password</a> <span>
</br> This link is valid for 1 hour. After it expires, <a href="%(forget_password_url)s?email=%(email)s">request new one</a>
This link is valid for 1 hour. After it expires, <a href="%(forget_password_url)s?email=%(email)s">request new one</a> </span>
<span>
</br> <a href="%(login_url)s">Login direct</a>
--- </span>
</p>
</br> """) % {
<a href="%(login_url)s">Login direct</a>
</br>
""") % {
'name': user.name,
'username': user.username, 'username': user.username,
'rest_password_url': reverse('users:reset-password', external=True), 'rest_password_url': reverse('users:reset-password', external=True),
'rest_password_token': user.generate_reset_token(), 'rest_password_token': user.generate_reset_token(),
...@@ -64,6 +59,32 @@ def send_user_created_mail(user): ...@@ -64,6 +59,32 @@ def send_user_created_mail(user):
'email': user.email, 'email': user.email,
'login_url': reverse('authentication:login', external=True), 'login_url': reverse('authentication:login', external=True),
} }
if settings.EMAIL_CUSTOM_USER_CREATED_BODY:
custom_body = '<p style="text-indent:2em">' + settings.EMAIL_CUSTOM_USER_CREATED_BODY + '</p>'
else:
custom_body = ''
body = custom_body + default_body
return body
def send_user_created_mail(user):
recipient_list = [user.email]
subject = _('Create account successfully')
if settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT:
subject = settings.EMAIL_CUSTOM_USER_CREATED_SUBJECT
honorific = '<p>' + _('Hello %(name)s') % {'name': user.name} + ':</p>'
if settings.EMAIL_CUSTOM_USER_CREATED_HONORIFIC:
honorific = '<p>' + settings.EMAIL_CUSTOM_USER_CREATED_HONORIFIC + ':</p>'
body = construct_user_created_email_body(user)
signature = '<p style="float:right">jumpserver</p>'
if settings.EMAIL_CUSTOM_USER_CREATED_SIGNATURE:
signature = '<p style="float:right">' + settings.EMAIL_CUSTOM_USER_CREATED_SIGNATURE + '</p>'
message = honorific + body + signature
if settings.DEBUG: if settings.DEBUG:
try: try:
print(message) print(message)
...@@ -313,3 +334,13 @@ def is_need_unblock(key_block): ...@@ -313,3 +334,13 @@ def is_need_unblock(key_block):
if not cache.get(key_block): if not cache.get(key_block):
return False return False
return True return True
def construct_user_email(username, email):
if '@' not in email:
if '@' in username:
email = username
else:
email = '{}@{}'.format(username, settings.EMAIL_SUFFIX)
return email
...@@ -31,7 +31,9 @@ from django.views.generic.detail import DetailView ...@@ -31,7 +31,9 @@ from django.views.generic.detail import DetailView
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import logout as auth_logout from django.contrib.auth import logout as auth_logout
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 common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
...@@ -73,15 +75,20 @@ class UserListView(AdminUserRequiredMixin, TemplateView): ...@@ -73,15 +75,20 @@ class UserListView(AdminUserRequiredMixin, TemplateView):
class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
model = User model = User
form_class = forms.UserCreateUpdateForm form_class = forms.UserCreateForm
template_name = 'users/user_create.html' template_name = 'users/user_create.html'
success_url = reverse_lazy('users:user-list') success_url = reverse_lazy('users:user-list')
success_message = create_success_msg success_message = create_success_msg
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) check_rules = get_password_check_rules()
context.update({'app': _('Users'), 'action': _('Create user')}) context = {
return context 'app': _('Users'),
'action': _('Create user'),
'password_check_rules': check_rules,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
user = form.save(commit=False) user = form.save(commit=False)
...@@ -101,7 +108,7 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): ...@@ -101,7 +108,7 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
model = User model = User
form_class = forms.UserCreateUpdateForm form_class = forms.UserUpdateForm
template_name = 'users/user_update.html' template_name = 'users/user_update.html'
context_object_name = 'user_object' context_object_name = 'user_object'
success_url = reverse_lazy('users:user-list') success_url = reverse_lazy('users:user-list')
...@@ -156,15 +163,12 @@ class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView): ...@@ -156,15 +163,12 @@ class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView):
id_list = None id_list = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
users_id = self.request.GET.get('users_id', '') spm = request.GET.get('spm', '')
self.id_list = [i for i in users_id.split(',')] users_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 users_id: elif users_id:
self.form = self.form_class( self.form = self.form_class(initial={'users': users_id})
initial={'users': self.id_list}
)
else: else:
self.form = self.form_class() self.form = self.form_class()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
......
amqp==2.1.4 amqp==2.1.4
ansible==2.4.2.0 ansible==2.8.0
asn1crypto==0.24.0 asn1crypto==0.24.0
bcrypt==3.1.4 bcrypt==3.1.4
billiard==3.5.0.3 billiard==3.5.0.3
...@@ -24,7 +24,7 @@ django-ranged-response==0.2.0 ...@@ -24,7 +24,7 @@ django-ranged-response==0.2.0
django-redis-cache==1.7.1 django-redis-cache==1.7.1
django-rest-swagger==2.1.2 django-rest-swagger==2.1.2
django-simple-captcha==0.5.6 django-simple-captcha==0.5.6
djangorestframework==3.8.2 djangorestframework==3.9.4
djangorestframework-bulk==0.2.1 djangorestframework-bulk==0.2.1
docutils==0.14 docutils==0.14
ecdsa==0.13 ecdsa==0.13
...@@ -38,7 +38,7 @@ gunicorn==19.9.0 ...@@ -38,7 +38,7 @@ gunicorn==19.9.0
idna==2.6 idna==2.6
itsdangerous==0.24 itsdangerous==0.24
itypes==1.1.0 itypes==1.1.0
Jinja2==2.10 Jinja2==2.10.1
jmespath==0.9.3 jmespath==0.9.3
kombu==4.0.2 kombu==4.0.2
ldap3==2.4 ldap3==2.4
...@@ -46,7 +46,7 @@ MarkupSafe==1.0 ...@@ -46,7 +46,7 @@ MarkupSafe==1.0
mysqlclient==1.3.14 mysqlclient==1.3.14
olefile==0.44 olefile==0.44
openapi-codec==1.3.2 openapi-codec==1.3.2
paramiko==2.4.1 paramiko==2.4.2
passlib==1.7.1 passlib==1.7.1
Pillow==4.3.0 Pillow==4.3.0
pyasn1==0.4.2 pyasn1==0.4.2
...@@ -57,20 +57,20 @@ PyNaCl==1.2.1 ...@@ -57,20 +57,20 @@ PyNaCl==1.2.1
python-dateutil==2.6.1 python-dateutil==2.6.1
python-gssapi==0.6.4 python-gssapi==0.6.4
pytz==2018.3 pytz==2018.3
PyYAML==3.12 PyYAML==5.1
redis==2.10.6 redis==2.10.6
requests==2.18.4 requests==2.22.0
jms-storage==0.0.22 jms-storage==0.0.22
s3transfer==0.1.13 s3transfer==0.1.13
simplejson==3.13.2 simplejson==3.13.2
six==1.11.0 six==1.11.0
sshpubkeys==3.1.0 sshpubkeys==3.1.0
uritemplate==3.0.0 uritemplate==3.0.0
urllib3==1.22 urllib3==1.25.2
vine==1.1.4 vine==1.1.4
drf-yasg==1.9.1 drf-yasg==1.9.1
Werkzeug==0.14.1 Werkzeug==0.14.1
drf-nested-routers==0.90.2 drf-nested-routers==0.91
aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-core-v3==2.9.1
aliyun-python-sdk-ecs==4.10.1 aliyun-python-sdk-ecs==4.10.1
python-keycloak==0.13.3 python-keycloak==0.13.3
...@@ -81,3 +81,4 @@ tencentcloud-sdk-python==3.0.40 ...@@ -81,3 +81,4 @@ tencentcloud-sdk-python==3.0.40
django-radius==1.3.3 django-radius==1.3.3
ipip-ipdb==1.2.1 ipip-ipdb==1.2.1
django-redis-sessions==0.6.1 django-redis-sessions==0.6.1
unicodecsv==0.14.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment